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:
- In advance of WordPress 5.6 with support for PHP 8.0, a report by Yoast described how running PHPStan on WordPress core would produce thousands of issues.
- Due to its still supporting PHP 5.6, WordPress test suites currently only support PHPUnit up to version 7.5, which has reached the end of life.
- Scoping WordPress plugins via PHP-Scoper is very challenging.
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
The following code uses Symfony’s contract CacheInterface
and the PHP Standard Recommendation (PSR) contract CacheItemInterface
to implement caching functionality:
use Psr\Cache\CacheItemInterface;
use Symfony\Contracts\Cache\CacheInterface;
$value = $cache->get('my_cache_key', function (CacheItemInterface $item) {
$item->expiresAfter(3600);
return 'foobar';
});
$cache
implements 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 Owner\MyApp\Contracts\PostsAPIInterface
:
namespace Owner\MyApp\Contracts;
interface PostAPIInterface
{
public function get_posts(array $args = null): PostInterface[]|int[];
}
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 WP_Post
:
namespace Owner\MyApp\Contracts;
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 PostAPIInterface
and 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 WP_Post
object:
namespace Owner\MyAppForWP\ContractImplementations;
use Owner\MyApp\Contracts\PostInterface;
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;
}
// ...
}
When implementing PostAPI
, since method get_posts
returns PostInterface[]
, we must convert objects from WP_Post
to PostWrapper
:
namespace Owner\MyAppForWP\ContractImplementations;
use Owner\MyApp\Contracts\PostAPIInterface;
use WP_Post;
class PostAPI implements PostAPIInterface
{
public function get_posts(array $args = null): PostInterface[]|int[]
{
// This var will contain WP_Post[] or int[]
$wpPosts = \get_posts($args);
// Convert WP_Post[] to PostWrapper[]
return array_map(
function (WP_Post|int $post) {
if ($post instanceof WP_Post) {
return new PostWrapper($post);
}
return $post
},
$wpPosts
);
}
}
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:
- Symfony’s DependencyInjection
- PHP-DI
- Aura.Di
- Container (Dependency Injection)
- Yii Dependency Injection
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 Owner\MyApp\Contracts\PostAPIInterface;
// Obtain the service container, as specified by the library we use
$serviceContainer = ContainerBuilderFactory::getInstance();
// The obtained service will be of class Owner\MyAppForWP\ContractImplementations\PostAPI
$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:
Owner\MyApp\Contracts\PostAPIInterface:
class: \Owner\MyAppForWP\ContractImplementations\PostAPI
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
GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistryInterface:
class: '\GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistry'
GraphQLAPI\GraphQLAPI\Security\UserAuthorizationInterface:
class: '\GraphQLAPI\GraphQLAPI\Security\UserAuthorization'
GraphQLAPI\GraphQLAPI\Security\UserAuthorizationSchemes\:
resource: '../src/Security/UserAuthorizationSchemes/*'
This configuration defines the following:
- Contract
UserAuthorizationSchemeRegistryInterface
is satisfied via classUserAuthorizationSchemeRegistry
- Contract
UserAuthorizationInterface
is satisfied via classUserAuthorization
- All classes under the folder
UserAuthorizationSchemes/
are an implementation of themselves - Services must be automatically injected into one another (
autowire: true
)
Let’s see how autowiring works. The class UserAuthorization
depends on service with contract UserAuthorizationSchemeRegistryInterface
:
class UserAuthorization implements UserAuthorizationInterface
{
public function __construct(
protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry
) {
}
// ...
}
Thanks to autowire: true
, the DependencyInjection component will automatically have the service UserAuthorization
receive its required dependency, which is an instance of UserAuthorizationSchemeRegistry
.
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
As mentioned earlier, running PHP-Scoper on WordPress is difficult. By decoupling the WordPress code into distinctive packages, it becomes feasible to scope a WordPress plugin directly.
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
If we need to migrate a project from Drupal to WordPress, WordPress to Laravel, or any other combination, then only 10% of the code must be rewritten — a significant saving.
Best Practices
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 getPosts
:
interface PostAPIInterface
{
public function getPosts(array $args = null): PostInterface[]|int[];
}
…and:
class PostAPI implements PostAPIInterface
{
public function getPosts(array $args = null): PostInterface[]|int[]
{
// This var will contain WP_Post[] or int[]
$wpPosts = \get_posts($args);
// Rest of the code
// ...
}
}
Split Methods
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 "id"
, "ID"
, "slug"
, "email"
or "login"
. This design has a few issues:
- It will not fail at compilation time if we pass a wrong string
- Parameter
$value
needs to accept all different types for all options, even though when passing"ID"
it expects anint
, when passing"email"
it can only receive astring
We can improve this situation by splitting the function into several ones:
namespace Owner\MyApp\Contracts;
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 UserWrapper
and UserInterface
, as explained earlier on):
namespace Owner\MyAppForWP\ContractImplementations;
use Owner\MyApp\Contracts\UserAPIInterface;
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|string $value): ?UserInterface
{
if ($user = \get_user_by($prop, $value)) {
return new UserWrapper($user);
}
return null;
}
}
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 wp_usermeta
):
$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:
getPosts
returns only postsgetPages
returns only pagesgetCustomPosts
returns 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.
1. WPide
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.
Summary
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!
Leave a Reply