WordPress Abstraction: Best Practices and WordPress Abstraction Plugins
WordPress is an old CMS, but also the most used one. Thanks to its history of supporting outdated PHP versions and legacy code, it still lacks in implementing modern coding practices — WordPress abstraction is one example.
For instance, it’ll be so much better to split the WordPress core codebase into packages managed by Composer. Or perhaps, to autoload WordPress classes from file paths.
This article will teach you how to abstract WordPress code manually and use abstract WordPress plugin capabilities.
Issues With Integrating WordPress and PHP Tools
Due to its ancient architecture, we occasionally encounter problems when integrating WordPress with tooling for PHP codebases, such as the static analyzer PHPStan, the unit test library PHPUnit, and the namespace-scoping library PHP-Scoper. For instance, consider the following cases:
The WordPress code within our projects will only be a fraction of the total; the project will also contain business code agnostic of the underlying CMS. Yet, just by having some WordPress code, the project may not integrate with tooling properly.
Because of this, it could make sense to split the project into packages, some of them containing WordPress code and others having only business code using “vanilla” PHP and no WordPress code. This way, these latter packages won’t be affected by the issues described above but can be perfectly integrated with tooling.
What Is Code Abstraction?
Code abstraction removes fixed dependencies from the code, producing packages that interact with each other via contracts. These packages can then be added to different applications with different stacks, maximizing their usability. The result of code abstraction is a cleanly decoupled codebase based on the following pillars:
- Code against interfaces, not implementations.
- Create packages and distribute them via Composer.
- Glue all parts together via dependency injection.
Coding Against Interfaces, Not Implementations
Coding against interfaces is the practice of using contracts to have pieces of code interact with each other. A contract is simply a PHP interface (or any different language) that defines what functions are available and their signatures, i.e., what inputs they receive and their output.
An interface declares the intent of the functionality without explaining how the functionality will be implemented. By accessing functionality via interfaces, our application can rely on autonomous pieces of code that accomplish a specific goal without knowing, or caring about, how they do it. This way, the application doesn’t need to be adapted to switch to another piece of code that accomplishes the same goal — for instance, from a different provider.
Example of Contracts
use PsrCacheCacheItemInterface; use SymfonyContractsCacheCacheInterface; $value = $cache->get('my_cache_key', function (CacheItemInterface $item) $item->expiresAfter(3600); return 'foobar'; );
CacheInterface, which defines the method
get to retrieve an object from the cache. By accessing this functionality via the contract, the application can be oblivious to where the cache is. Whether it’s in memory, disk, database, network, or anywhere else. Still, it has to perform the function.
CacheItemInterface defines method
expiresAfter to declare how long the item must be kept in the cache. The application can invoke this method without caring what the cached object is; it only cares how long it must be cached.
Coding Against Interfaces in WordPress
Because we’re abstracting WordPress code, the result will be that the application won’t reference WordPress code directly, but always via an interface. For instance, the WordPress function
get_posts has this signature:
/** * @param array $args * @return WP_Post|int Array of post objects or post IDs. */ function get_posts( $args = null )
Instead of invoking this method directly, we can access it via the contract
namespace OwnerMyAppContracts; interface PostAPIInterface public function get_posts(array $args = null): PostInterface
Note that the WordPress function
get_posts can return objects of the class
WP_Post, which is specific to WordPress. When abstracting the code, we need to remove this kind of fixed dependency. The method
get_posts in the contract returns objects of the type
PostInterface, allowing you to reference the class
WP_Post without being explicit about it. The class
PostInterface will need to provide access to all methods and attributes from
namespace OwnerMyAppContracts; interface PostInterface public function get_ID(): int; public function get_post_author(): string; public function get_post_date(): string; // ...
Executing this strategy can change our understanding of where WordPress fits in our stack. Instead of thinking of WordPress as the application itself (over which we install themes and plugins), we can think of it simply as another dependency within the application, replaceable as any other component. (Even though we won’t replace WordPress in practice, it is replaceable from a conceptual point of view.)
Creating and Distributing Packages
Composer is a package manager for PHP. It allows PHP applications to retrieve packages (i.e. pieces of code) from a repository and install them as dependencies. To decouple the application from WordPress, we must distribute its code into packages of two different types: those containing WordPress code and the others containing business logic (i.e. no WordPress code).
Finally, we add all packages as dependencies in the application, and we install them via Composer. Since tooling will be applied to the business code packages, these must contain most of the code of the application; the higher the percentage, the better. Having them manage around 90% of the overall code is a good goal.
Extracting WordPress Code Into Packages
Following the example from earlier on, contracts
PostInterface will be added to the package containing business code, and another package will include the WordPress implementation of these contracts. To satisfy
PostInterface, we create a
PostWrapper class that will retrieve all attributes from a
namespace OwnerMyAppForWPContractImplementations; use OwnerMyAppContractsPostInterface; use WP_Post; class PostWrapper implements PostInterface private WP_Post $post; public function __construct(WP_Post $post) $this->post = $post; public function get_ID(): int return $this->post->ID; public function get_post_author(): string return $this->post->post_author; public function get_post_date(): string return $this->post->post_date; // ...
PostAPI, since method
PostInterface, we must convert objects from
namespace OwnerMyAppForWPContractImplementations; use OwnerMyAppContractsPostAPIInterface; use WP_Post; class PostAPI implements PostAPIInterface public function get_posts(array $args = null): PostInterface
Using Dependency Injection
Dependency injection is a design pattern that lets you glue all application parts together in a loosely coupled manner. With dependency injection, the application accesses services via their contracts, and the contract implementations are “injected” into the application via configuration.
Simply by changing the configuration, we can easily switch from one contract provider to another one. There are several dependency injection libraries we can choose from. We advise selecting one that adheres to the PHP Standard Recommendations (often referred to as “PSR”), so we can easily replace the library with another one if the need arises. Concerning dependency injection, the library must satisfy PSR-11, which provides the specification for a “container interface.” Among others, the following libraries comply with PSR-11:
Accessing Services via the Service Container
The dependency injection library will make available a “service container,” which resolves a contract into its corresponding implementing class. The application must rely on the service container to access all functionality. For instance, while we would typically invoke WordPress functions directly:
$posts = get_posts();
…with the service container, we must first obtain the service that satisfies
PostAPIInterface and execute the functionality through it:
use OwnerMyAppContractsPostAPIInterface; // Obtain the service container, as specified by the library we use $serviceContainer = ContainerBuilderFactory::getInstance(); // The obtained service will be of class OwnerMyAppForWPContractImplementationsPostAPI $postAPI = $serviceContainer->get(PostAPIInterface::class); // Now we can invoke the WordPress functionality $posts = $postAPI->get_posts();
Using Symfony’s DependencyInjection
Symfony’s DependencyInjection component is currently the most popular dependency injection library. It allows you to configure the service container via PHP, YAML, or XML code. For instance, to define that contract
PostAPIInterface is satisfied via class
PostAPI is configured in YAML like this:
services: OwnerMyAppContractsPostAPIInterface: class: OwnerMyAppForWPContractImplementationsPostAPI
Symfony’s DependencyInjection also allows for instances from one service to be automatically injected (or “autowired”) into any other service that depends on it. In addition, it makes it easy to define that a class is an implementation of its own service. For instance, consider the following YAML configuration:
services: _defaults: public: true autowire: true GraphQLAPIGraphQLAPIRegistriesUserAuthorizationSchemeRegistryInterface: class: 'GraphQLAPIGraphQLAPIRegistriesUserAuthorizationSchemeRegistry' GraphQLAPIGraphQLAPISecurityUserAuthorizationInterface: class: 'GraphQLAPIGraphQLAPISecurityUserAuthorization' GraphQLAPIGraphQLAPISecurityUserAuthorizationSchemes: resource: '../src/Security/UserAuthorizationSchemes/*'
This configuration defines the following:
UserAuthorizationSchemeRegistryInterfaceis satisfied via class
UserAuthorizationInterfaceis satisfied via class
- All classes under the folder
UserAuthorizationSchemes/are an implementation of themselves
- Services must be automatically injected into one another (
Let’s see how autowiring works. The class
UserAuthorization depends on service with contract
class UserAuthorization implements UserAuthorizationInterface public function __construct( protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry ) // ...
autowire: true, the DependencyInjection component will automatically have the service
UserAuthorization receive its required dependency, which is an instance of
When To Abstract
Abstracting code could consume considerable time and effort, so we should only undertake it when its benefits outweigh its costs. The following are suggestions of when abstracting the code may be worth it. You can do this by using code snippets in this article or the suggested abstract WordPress plugins below.
Gaining Access to Tooling
Reducing Tooling Time and Cost
Running a PHPUnit test suite takes longer when it needs to initialize and run WordPress than when it doesn’t. Less time can also translate into less money spent running the tests — for example, GitHub Actions charges for GitHub-hosted runners based on time spent using them.
Heavy Refactoring Not Needed
An existing project may require heavy refactoring to introduce the required architecture (dependency injection, splitting code into packages, etc.), making it difficult to pull out. Abstracting code when creating a project from scratch makes it much more manageable.
Producing Code for Multiple Platforms
By extracting 90% of the code into a CMS-agnostic package, we can produce a library version that works for a different CMS or framework by only replacing 10% of the overall codebase.
Migrating to a Different Platform
While designing the contracts to abstract our code, there are several improvements we can apply to the codebase.
Adhere to PSR-12
When defining the interface to access the WordPress methods, we should adhere to PSR-12. This recent specification aims to reduce cognitive friction when scanning code from different authors. Adhering to PSR-12 implies renaming the WordPress functions.
WordPress names functions using snake_case, while PSR-12 uses camelCase. Hence, function
get_posts will become
interface PostAPIInterface public function getPosts(array $args = null): PostInterface
class PostAPI implements PostAPIInterface int // This var will contain WP_Post or int $wpPosts = get_posts($args); // Rest of the code // ...
Methods in the interface do not need to be a replica of the ones from WordPress. We can transform them whenever it makes sense. For instance, the WordPress function
get_user_by($field, $value) knows how to retrieve the user from the database via parameter
$field, which accepts values
"login". This design has a few issues:
- It will not fail at compilation time if we pass a wrong string
$valueneeds to accept all different types for all options, even though when passing
"ID"it expects an
int, when passing
"email"it can only receive a
We can improve this situation by splitting the function into several ones:
namespace OwnerMyAppContracts; interface UserAPIInterface public function getUserById(int $id): ?UserInterface; public function getUserByEmail(string $email): ?UserInterface; public function getUserBySlug(string $slug): ?UserInterface; public function getUserByLogin(string $login): ?UserInterface;
The contract is resolved for WordPress like this (assuming we have created
UserInterface, as explained earlier on):
namespace OwnerMyAppForWPContractImplementations; use OwnerMyAppContractsUserAPIInterface; class UserAPI implements UserAPIInterface public function getUserById(int $id): ?UserInterface return $this->getUserByProp('id', $id); public function getUserByEmail(string $email): ?UserInterface return $this->getUserByProp('email', $email); public function getUserBySlug(string $slug): ?UserInterface return $this->getUserByProp('slug', $slug); public function getUserByLogin(string $login): ?UserInterface return $this->getUserByProp('login', $login); private function getUserByProp(string $prop, int
Remove Implementation Details from Function Signature
Functions in WordPress may provide information on how they are implemented in their own signature. This information can be removed when appraising the function from an abstract perspective. For example, obtaining the user’s last name in WordPress is done by calling
get_the_author_meta, making it explicit that a user’s last name is stored as a “meta” value (on table
$userLastname = get_the_author_meta("user_lastname", $user_id);
You don’t have to convey this information to the contract. Interfaces only care about the what, not the how. Hence, the contract can instead have a method
getUserLastname, which does not provide any information on how it’s implemented:
interface UserAPIInterface public function getUserLastname(UserWrapper $userWrapper): string; ...
Add Stricter Types
Some WordPress functions can receive parameters in different ways, leading to ambiguity. For instance, function
add_query_arg can either receive a single key and value:
$url = add_query_arg('id', 5, $url);
… or an array of
key => value:
$url = add_query_arg(['id' => 5], $url);
Our interface can define a more comprehensible intent by splitting such functions into several separate ones, each of them accepting a unique combination of inputs:
public function addQueryArg(string $key, string $value, string $url); public function addQueryArgs(array $keyValues, string $url);
Wipe Out Technical Debt
The WordPress function
get_posts returns not only “posts” but also “pages” or any entity of type “custom posts,” and these entities are not interchangeable. Both posts and pages are custom posts, but a page is not a post and not a page. Therefore, executing
get_posts can return pages. This behavior is a conceptual discrepancy.
To make it proper,
get_posts should instead be called
get_customposts, but it was never renamed in WordPress core. It’s a common issue with most long-lasting software and is called “technical debt” — code that has problems, but is never fixed because it introduces breaking changes.
When creating our contracts, though, we have the opportunity to avoid this type of technical debt. In this case, we can create a new interface
ModelAPIInterface which can deal with entities of different types, and we make several methods, each to deal with a different type:
interface ModelAPIInterface public function getPosts(array $args): array; public function getPages(array $args): array; public function getCustomPosts(array $args): array;
This way, the discrepancy won’t occur anymore, and you’ll see these results:
getPostsreturns only posts
getPagesreturns only pages
getCustomPostsreturns both posts and pages
Benefits of Abstracting Code
The main advantages of abstracting an application’s code are:
- Tooling running on packages containing only business code is easier to set up and will take less time (and less money) to run.
- We can use tooling that doesn’t work with WordPress, such as scoping a plugin with PHP-Scoper.
- The packages we produce can be autonomous to use in other applications easily.
- Migrating an application to other platforms becomes easier.
- We can shift our mindset from WordPress thinking to think in terms of our business logic.
- The contracts describe the intent of the application, making it more understandable.
- The application gets organized through packages, creating a lean application containing the bare minimum and progressively enhancing it as needed.
- We can clear up technical debt.
Issues With Abstracting Code
The disadvantages of abstracting an application’s code are:
- It involves a considerable amount of work initially.
- Code becomes more verbose; add extra layers of code to achieve the same outcome.
- You may end up producing dozens of packages which must then be managed and maintained.
- You may require a monorepo to manage all packages together.
- Dependency injection could be overkill for simple applications (diminishing returns).
- Abstracting the code will never be fully accomplished since there’s typically a general preference implicit in the CMS’s architecture.
Abstract WordPress Plugin Options
Although it’s generally wisest to extract your code to a local environment before working on it, some WordPress plugins can help you toward your abstraction goals. These are our top picks.
Produced by WebFactory Ltd, the popular WPide plugin dramatically extends WordPress’s default code editor’s functionality. It serves as an abstract WordPress plugin by allowing you to view your code in situ to visualize better what needs attention.
WPide also has a search-and-replace function for quickly locating outdated or expired code and replacing it with a refactored rendition.
On top of this, WPide provides loads of extra features, including:
- Syntax and block highlighting
- Automatic backups
- File and folder creation
- Comprehensive file tree browser
- Access to the WordPress filesystem API
2. Ultimate DB Manager
The Ultimate WP DB Manager plugin from WPHobby gives you a quick way to download your databases in full for extraction and refactoring.
Of course, plugins of this type aren’t necessary for Kinsta users, as Kinsta offers direct database access to all customers. However, if you don’t have sufficient database access through your hosting provider, Ultimate DB Manager could come in handy as an abstract WordPress plugin.
3. Your Own Custom Abstract WordPress Plugin
In the end, the best choice for abstraction will always be to create your plugin. It may seem like a big undertaking, but if you have limited ability to manage your WordPress core files directly, this offers an abstraction-friendly workaround.
Doing so has clear benefits:
- Abstracts your functions from your theme files
- Preserves your code through theme changes and database updates
You can learn how to create your abstract WordPress plugin through WordPress’ Plugin Developer Handbook.
Should we abstract the code in our applications? As with everything, there is no predefined “right answer” as it depends on a project-by-project basis. Those projects requiring a tremendous amount of time to analyze with PHPUnit or PHPStan can benefit the most, but the effort needed to pull it off may not always be worth it.
You’ve learned everything you need to know to get started abstracting WordPress code.
Do you plan to implement this strategy in your project? If so, will you use an abstract WordPress plugin? Let us know in the comments section!
Save time, costs and maximize site performance with:
- Instant help from WordPress hosting experts, 24/7.
- Cloudflare Enterprise integration.
- Global audience reach with 28 data centers worldwide.
- Optimization with our built-in Application Performance Monitoring.