For the last few years many people, including me, have been talking about how WordPress has evolved to where it can power applications, including SaaS platforms. One of the reasons for this is the WordPress REST API. And while I completely agree with this, one of the reasons I think it’s a great choice is that we have several full-featured eCommerce solutions.
I wrote about implementing my own subscription billing system using Laravel Cashier and comparing it to working with a WordPress eCommerce platform a while ago. Since then I’ve thought alot about what an eCommerce system like WooCommerce or Easy Digital Downloads (EDD) provides. It’s more than just checking out and charging credit cards. It’s account management, invoicing, and integration with marketing automation platforms.
When I wrote that post, it was after we launched Caldera Forms PDF, a small SaaS product for turning Caldera Forms entries into PDFs. That is a Laravel app that handles its own billing. Our new SaaS product Caldera Forms Pro is a hybrid WordPress and Laravel app, with all of the eCommerce handled by Easy Digital Downloads.
In this article, I’m going to go over a few ways to use EDD to power a SaaS product. I’ll cover using it to limit access to REST API endpoints two ways; when WordPress also powers the app and also with an app built outside of WordPress.
I’ll be using EDD because it’s the eCommerce plugin I know best and have used to implement these concepts. My code examples are simplified versions of plugins I have open-sourced and will provide links to.
A Hybrid Approach
For a lot of reasons, we ended up using WordPress for eCommerce, marketing automation, and documentation but also using Laravel and VueJS for the app. As a result, API requests to the app don’t pass through WordPress. Authentication for the app’s UI does involve WordPress, I’ll cover that in a different article.
What I do want to cover is an approach to integrating EDD and EDD Recurring with external apps. What we required was to send an API request to the app whenever a subscription is created, updated, or created. Also, it was important to limit this logic to only act on one specific subscription, since we have many other types of subscription-based products.
This section of the article is based on a plugin that I wrote for handling the interaction for EDD running on CalderaForms.com and Caldera Forms Pro. While it’s a site-specific plugin that probably can’t be reused as-is, I hope others can learn from reading it and copying from it.
Separating Logic From The Plugins API
EDD Recurring provides all of the hooks that are needed to make this work. But before tying into those events, I created the logic I needed in classes that are totally decoupled from those hooks. This makes my code more testable and also means it will be easier to use this code when I needed other ways to work with subscriptions.
The first step was creating a subscription class to handle all of the logic of this subscription. In a recent article for Torque, I discussed the decorator pattern as an alternative to extending classes. Because I felt that this subscription object had fundamentally different responsibilities than the EDD_Subscription class, I chose to decorate the EDD_Subscription class instead of extending it.
I say “fundamentally different responsibilities” because the job of the EDD_Subscription class is to describe the state of the subscription in the database and update it accordingly. This new class describes the relationship between WordPress and the app. I also don’t want to rewrite what EDD_Subscription already does, so I use it in the class.
Here is a very simple example. EDD_Subscription has a get_status() method. But what I actually need to know if that status represents an active subscription or not.
<?php namespace example; class Subscription { /** * @var \EDD_Subscription */ protected $subscription; /** * * @param \EDD_Subscription $subscription */ public function __construct( \EDD_Subscription $subscription ){ $this->subscription = $subscription; } /** * @return bool */ public function statusActive() : bool { return in_array( $this->subscription->get_status(), [ 'active', 'trialing' ]); } }
This method can be used during the various actions that happen when subscriptions change. But before looking at that, I think it’s useful to have a factory for these objects, that can determine if the EDD Subscription is for the SaaS product. I discussed the factory pattern in a previous article for Torque if you’re not familiar with this pattern.
This code assumes that there is an environmental variable that provides the product ID for the subscription product. You could use a constant instead if you’re WordPress configurations doesn’t use environment variables.
<?php namespace example; /** * Class Factory * @package example */ class Factory { /** * @param int $id * * @return Subscription|null */ public static function subscription( $id ){ $subscription = new \EDD_Subscription( $id ); if ( static::is_correct_product( $id ) ){ return new Subscription( $subscription ); } return null; } /*** * @param \EDD_Subscription $subscription * * @return bool */ public static function is_correct_product( \EDD_Subscription $subscription ) : bool { return $subscription->id && (int) $subscription->product_id === (int) $_ENV[ 'SAAS_PRODUCT_ID' ]; } }
This provides us a way to get the subscription object while checking if it’s for the right product.
Hooking It Up
As I said earlier, it was important to me to keep all of my logic decoupled from the plugins API, but that doesn’t mean that it doesn’t need to be hooked into the actions that EDD fires when subscriptions are created, updated, or renewed.
I used a Hooks class to handle the interaction with the plugins API. In each callback, the factory method is called to get the subscription class, if it’s the right product. Since that method returns null, a simple conditional prevents further code from running when these hooks are not needed.
Here is an outline for that class:
<?php namespace example; class Hooks { /** * Add hooks for interacting with EDD */ public function addHooks(){ add_action( 'edd_subscription_post_create', [ $this, 'subscription_created' ] ); add_action( 'edd_recurring_update_subscription', [ $this, 'subscription_updated' ] ); add_action( 'edd_subscription_cancelled', [ $this, 'subscription_cancelled' ] ); } /** * When subscription is created, run our logic * * @uses "edd_subscription_post_create" * * @param int $id Subscription ID */ public function subscription_created( $id ){ $subscription = Factory::subscription( $id ); if( $subscription && $subscription->statusActive() ){ //Call remote API to create } } /** * When subscription is updated, maybe cancel or create or something in app * * @uses "edd_recurring_update_subscription" * * @param $id */ public function subscription_updated( $id ){ $subscription = Factory::subscription( $id ); if( $subscription && $subscription->statusActive() ){ //Call remote API to update }elseif( $subscription && ! $subscription->statusActive() ){ //Call remote API to cancel } } /** * When subscription is canceled, run our logic * * @uses "edd_subscription_cancelled" * * @param int $id Subscription ID */ public function subscription_cancelled( $id ){ $subscription = Factory::subscription( $id ); if( $subscription ){ //Call remote API to cancel } } }
What you do inside of these conditionals will depend on how your app works. If you want to see how I handled it for my app, you can read the source here.
Selling Access To WordPress REST API Endpoints
Many SaaS products sell API access. That might be the entire product, Google Maps API is an example – they rate-limit their API and you can buy additional requests. In other cases the SaaS product includes the interface, with a limited number of times per month that you can use the service, which on a technical level, requires rate-limiting API endpoints.
Even if a SaaS product isn’t based on rate-limiting, IE it’s a month to month or year to year subscription, if you’re using the REST API, you still need to control access based on subscription.
In this section of the article, I will discuss ways to control access to a WordPress REST API based on a subscription and provide meaningful error responses you can use in your app.
I actually built a pretty fully-featured plugin to do all of this using EDD’s Recurring Payments add-on. It’s not strongly-tied to EDD though and could be easily made to work with a membership plugin or WooCommerce. Originally my plan for Caldera Forms Pro involved using the WordPress REST API essentially as a proxy for the app. I built this plugin before changing the architecture of the app so this wasn’t needed, but my plugin is there if you want to use it or copy from it.
I want to show you how you could expand on what I showed in the last section to control access to a WordPress REST API route. I’m imagining this would be the route that provides your service. IE If your product is a pet walking service, walks would be scheduled by having your app making POST requests to this route. Or, if your product was providing information, your app or your API client would make GET requests to this app.
WordPress provides a rest_pre_dispatch filter that runs before core responds to the request. This filter is an example of an early entry point. If nothing is returned on this filter, everything proceeds as normal. But if something is returned there, that is used as the result. This is perfect for caching requests, implementing authentication systems that don’t use WordPress users or returning errors because a rate limit is exceeded.
Here is a starter class to use that filter to check if the current request is to a specific route with a placeholder for our logic:
<?php namespace example; /** * Class Route * @package example */ class Route { /** @var string */ protected $route; public function __construct( $route ){ $this->route = $route; add_filter( 'rest_pre_dispatch', [ $this, 'check' ], 10, 3 ); } /** * Before WordPress responds to request check if we should block, and if so block * * @param bool $serve Whether to serve request * @param \WP_REST_Server $server Server instance. * @param \WP_REST_Request $request Request used to generate the response. * * @return bool */ public function check( $serve, $server, $request ){ //make sure route property starts with a / if( 0 !== strpos( $this->route, '/' ) ){ $this->route = '/' . $this->route; } //check if route is route if ( 0 === strpos( $request->get_route(), $this->route ) ) { //Assuming some logic that returns a WP_Error using code 429 too many requests $check = Something(); if( is_wp_error( $check ) ){ return $this->generate_error_response($check ); }else{ //authorized so return unaffected return $serve; } }else{ //not our route, return unaffected return $serve; } } /** * Generate an error response * * @param \WP_Error $error * @param int $status * * @return \WP_REST_Response */ protected function generate_error_response( \WP_Error $error, $status = 429 ){ return new \WP_REST_Response( [ 'message' => $error->get_error_message( $status ) ], $status ); } }
Right now, this doesn’t do anything, but I wanted to show this as a start point to understand the concept. Also your implementation of the rate limiting or account logic might be totally different, but this can be a start point for other approaches.
Notice that this code calls a function called “something()” — we need to replace that, but the key point here is that this class is designed to call some other logic and interpret WP_Errors.
First, let’s add one method to the factory class from before to get all active subscriptions of our product belonging to the current user:
<?php namespace example; class Factory { /** * @param int $id * * @return Subscription|null */ public static function subscription( $id ){ $subscription = new \EDD_Subscription( $id ); if ( static::is_correct_product( $id ) ){ return new Subscription( $subscription ); } return null; } /*** * @param \EDD_Subscription $subscription * * @return bool */ public static function is_correct_product( \EDD_Subscription $subscription ) : bool { return $subscription->id && (int) $subscription->product_id === (int) $_ENV[ 'SAAS_PRODUCT_ID' ]; } /** * Get all subscriptions of our product that are active or in free-trial * * @param $user_id * * @return array */ public static function get_user_subscriptions( $user_id ){ return ( new \EDD_Recurring_Subscriber( $user_id, true ) ) ->get_subscriptions( $_ENV[ 'SAAS_PRODUCT_ID' ], [ 'active', 'trialing' ] ); } }
Now, let’s start a class to handle our logic. One complication I ran into is that EDD_Subscriptions uses a custom table with no meta table. I came up with a few solutions. In the API rate-limiting plugin, I decoupled rate-limiting from subscriptions and counted the usages in a custom table. In this example code, I’m going to use a hidden custom post type called ‘_limit_tracker’ with two meta fields — ‘_subscription_id’ and ‘_uses’.
<?php class Limiter { /** @var array */ protected $subscriptions; /** @var int */ protected $error_code = 0; /** @var int */ protected $max_uses = 5000; public function __construct( $user_id ){ $this->subscriptions= Factory::get_user_subscriptions( $user_id ); } /** * Check if request should be allowed * * @return bool|\WP_Error */ public function allowed(){ if( empty( $this->subscriptions ) ){ $this->error_code = 403; return new \WP_Error( $this->error_code, __( 'No subscription found', 'text-domain' ) ); } if( true === $this->check_uses( $this->subscriptions[0] ) ){ return true; }else{ $this->error_code = 429; return new \WP_Error( $this->error_code, __( 'Rate limit exceeded', 'text-domain' ) ); } } /** * @return int|null */ public function get_error_code(){ return $this->error_code; } /** * @param int $subscription_id */ protected function check_uses( $subscription_id ){ $wp_query = new \WP_Query([ 'post_type' => '_limit_tracker', 'meta_query' => [ 'key' => '_subscription_id', 'value' => $subscription_id, 'compare' => '=' ] ] ); if( empty( $wp_query->posts[0] ) ){ $id = wp_insert_post([ 'post_type' => '_limit_tracker', 'post_content' => '...', ]); update_post_meta( $id, '_subscription_id', $subscription_id ); $uses = 0; }else{ $id = $wp_query->posts[0]->ID; $uses = get_post_meta( $id, '_uses', true ); } if( $uses > $this->max_uses ){ return false; }else{ update_post_meta( $id, '_uses', $uses + 1 ); return true; } } }
This class, which I will use inside the route class shortly checks that hidden post type to count uses. As you can see in the check_uses() method, every time you do the check another use is counted and a comparison versus max allowed uses is performed. You will probably need to expand on this logic for uses per month, or reseting. My goal here is to give you a start point and illustrate how to make this type of class.
With that in place, we can finish ouf the route class that was started earlier using this logic. That class was already prepared to format a WP_Error as a REST Response or allow the request to pass. So we just need to call that logic in the check method, which is hooked to rest_pre_dispatch so it will interrupt unauthorized requests.
<?php namespace example; /** * Class Route * @package example */ class Route { /** @var string */ protected $route; public function __construct( $route ){ $this->route = $route; add_filter( 'rest_pre_dispatch', [ $this, 'check' ], 10, 3 ); } /** * Before WordPress responds to request check if we should block, and if so block * * @param bool $serve Whether to serve request * @param \WP_REST_Server $server Server instance. * @param \WP_REST_Request $request Request used to generate the response. * * @return bool */ public function check( $serve, $server, $request ){ //make sure route property starts with a / if( 0 !== strpos( $this->route, '/' ) ){ $this->route = '/' . $this->route; } //check if route is route if ( 0 === strpos( $request->get_route(), $this->route ) ) { //Assuming some logic that returns a WP_Error using code 429 too many requests $checker = new Limter( get_current_user_id() ); $allowed = $checker->allowed(); if( is_wp_error( $allowed ) ){ return $this->generate_error_response($ allowed, $checker->get_error_code() ); }else{ //authorized so return unaffected return $serve; } }else{ //not our route, return unaffected return $serve; } } /** * Generate an error response * * @param \WP_Error $error * @param int $status * * @return \WP_REST_Response */ protected function generate_error_response( \WP_Error $error, $status = 429 ){ return new \WP_REST_Response( [ 'message' => $error->get_error_message( $status ) ], $status ); } }
WaaS – WordPress As A SaaS
I hope this article has given you a lot of ideas and the beginning of the code you need to make a WordPress-powered SaaS. Whether you use WordPress just for the eCommerce — an approach I will be talking more about in the future or for the whole application, WordPress is a great tool for these projects because no matter how much or how little you choose to use them.
No Comments