{"id":73433,"date":"2019-11-28T04:18:33","date_gmt":"2019-11-28T11:18:33","guid":{"rendered":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/"},"modified":"2019-11-28T04:18:33","modified_gmt":"2019-11-28T11:18:33","slug":"abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2","status":"publish","type":"post","link":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/","title":{"rendered":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)"},"content":{"rendered":"<link rel=\"canonical\" href=\"https:\/\/www.smashingmagazine.com\/2019\/11\/abstracting-wordpress-code-reuse-with-other-cms-implementation\/\"><title>Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)<\/title><\/p>\n<article>\n<header>\n<h1>Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)<\/h1>\n<address>Leonardo Losoviz<\/address>\n<p>                  <time datetime=\"2019-11-28T11:00:00+00:00\">2019-11-28T11:00:00+00:00<\/time><time datetime=\"2019-11-28T11:00:00+00:00\">2019-11-28T11:07:08+00:00<\/time><\/header>\n<p>In the <a href=\"https:\/\/www.smashingmagazine.com\/2019\/11\/abstracting-wordpress-code-cms-concepts\/\">first part of this series<\/a>, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will proceed to abstract a WordPress application, making its code ready to be used with <a href=\"https:\/\/symfony.com\/\">Symfony components<\/a>, <a href=\"https:\/\/laravel.com\/\">Laravel framework<\/a>, and <a href=\"https:\/\/octobercms.com\/\">October CMS<\/a> (which is based on Laravel).<\/p>\n<h3>Accessing Services<\/h3>\n<p>Before we start abstracting the code, we need to provide the layer of dependency injection to the application. As described in the first part of this series, this layer is satisfied through <a href=\"https:\/\/symfony.com\/components\/DependencyInjection\">Symfony\u2019s DependencyInjection component<\/a>. To access the defined services, we create a class <code>ContainerBuilderFactory<\/code> which simply stores a static instance of the component\u2019s <code>ContainerBuilder<\/code> object:<\/p>\n<pre><code>use SymfonyComponentDependencyInjectionContainerBuilder;\n\nclass ContainerBuilderFactory {\n  private static $instance;\n  public static function init()\n  {\n    self::$instance = new ContainerBuilder();\n  }\n  public static function getInstance()\n  {\n    return self::$instance;\n  }\n}<\/code><\/pre>\n<div data-component=\"FeaturePanel\" data-audience=\"non-subscriber\" data-remove=\"true\"><\/div>\n<p>Then, to access a service called <code>\"cache\"<\/code>, the application requests it like this:<\/p>\n<div>\n<pre><code>$cacheService = ContainerBuilderFactory::getInstance()->get('cache');\n\/\/ Do something with the service\n\/\/ $cacheService->...<\/code><\/pre>\n<\/div>\n<h3>Abstracting WordPress Code<\/h3>\n<p>We have identified the following pieces of code and concepts from a WordPress application that need be abstracted away from WordPress\u2019s opinionatedness:<\/p>\n<ul>\n<li>accessing functions<\/li>\n<li>function names<\/li>\n<li>function parameters<\/li>\n<li>states (and other constant values)<\/li>\n<li>CMS helper functions<\/li>\n<li>user permissions<\/li>\n<li>application options<\/li>\n<li>database column names<\/li>\n<li>errors<\/li>\n<li>hooks<\/li>\n<li>routing<\/li>\n<li>object properties<\/li>\n<li>global state<\/li>\n<li>entity models (meta, post types, pages being posts, and taxonomies \u2014tags and categories\u2014)<\/li>\n<li>translation<\/li>\n<li>media.<\/li>\n<\/ul>\n<p>Let\u2019s proceed to abstract them, one by one.<\/p>\n<p><strong>Note:<\/strong> For ease of reading, I have omitted adding namespaces to all classes and interfaces throughout this article. However, adding namespaces, as specified in PHP Standards Recommendation <a href=\"https:\/\/www.php-fig.org\/psr\/psr-4\/\">PSR-4<\/a>, is a must! Among other advantages, the application can then benefit from <a href=\"https:\/\/getcomposer.org\/doc\/01-basic-usage.md#autoloading\">autoloading<\/a>, and Symfony\u2019s dependency injection can rely on <a href=\"https:\/\/symfony.com\/doc\/current\/service_container.html#service-psr4-loader\">automatic service loading<\/a> as to reduce its configuration to the bare minimum.<\/p>\n<h4>Accessing functions<\/h4>\n<p>The mantra \u201ccode against interfaces, not implementations\u201d means that all those functions provided by the CMS cannot be accessed directly anymore. Instead, we must access the function from a contract (an interface), on which the CMS function will simply be the implementation. By the end of the abstraction, since no WordPress code will be referenced directly anymore, we can then swap WordPress with a different CMS.<\/p>\n<p>For instance, if our application accesses function <code>get_posts<\/code>:<\/p>\n<pre><code>$posts = get_posts($args);<\/code><\/pre>\n<p>We must then abstract this function under some contract:<\/p>\n<pre><code>interface PostAPIInterface\n{\n  public function getPosts($args);\n}<\/code><\/pre>\n<p>The contract must be implemented for WordPress:<\/p>\n<pre><code>class WPPostAPI implements PostAPIInterface\n{\n  public function getPosts($args) {\n    return get_posts($args);\n  }\n}<\/code><\/pre>\n<p>A service <code>\"posts_api\"<\/code> must be added to the dependency injection <code>services.yaml<\/code> configuration file, indicating which class resolves the service:<\/p>\n<pre><code>services:\n  posts_api:\n    class: WPPostAPI<\/code><\/pre>\n<p>And finally, the application can reference the function through service <code>\"posts_api\"<\/code>:<\/p>\n<div>\n<pre><code>$postsAPIService = ContainerBuilderFactory::getInstance()->get('posts_api');\n$posts = $postsAPIService->getPosts($args);<\/code><\/pre>\n<\/div>\n<h4>Function names<\/h4>\n<p>If you have noticed from the code demonstrated above, function <code>get_posts<\/code> is abstracted as <code>getPosts<\/code>. There are a couple of reasons why this is a good idea:<\/p>\n<ul>\n<li>By calling the function differently, it helps identify which code belongs to WordPress and which code belongs to our abstracted application.<\/li>\n<li>Function names must be camelCased to comply with <a href=\"https:\/\/www.php-fig.org\/psr\/psr-2\/\">PSR-2<\/a>, which attempts to define a standard for writing PHP code.<\/li>\n<\/ul>\n<p>Certain functions can be redefined, making more sense in an abstract context. For instance, WordPress function <code>get_user_by($field, $value)<\/code> uses parameter <code>$field<\/code> with values <code>\"id\"<\/code>, <code>\"ID\"<\/code>, <code>\"slug\"<\/code>, <code>\"email\"<\/code> or <code>\"login\"<\/code> to know how to get the user. Instead of replicating this methodology, we can explicitly define a separate function for each of them:<\/p>\n<pre><code>interface UsersAPIInterface\n{\n  public function getUserById($value);\n  public function getUserByEmail($value);\n  public function getUserBySlug($value);\n  public function getUserByLogin($value);\n}<\/code><\/pre>\n<p>And these are resolved for WordPress:<\/p>\n<pre><code>class WPUsersAPI implements UsersAPIInterface\n{\n  public function getUserById($value)\n  {\n    return get_user_by('id', $value);\n  }\n  public function getUserByEmail($value)\n  {\n    return get_user_by('email', $value);\n  }\n  public function getUserBySlug($value)\n  {\n    return get_user_by('slug', $value);\n  }\n  public function getUserByLogin($value)\n  {\n    return get_user_by('login', $value);\n  }\n}<\/code><\/pre>\n<p>Certain other functions should be renamed because their names convey information about their implementation, which may not apply for a different CMS. For instance, WordPress function <code>get_the_author_meta<\/code> can receive parameter <code>\"user_lastname\"<\/code>, indicating that the user\u2019s lastname is stored as a \u201cmeta\u201d value (which is defined as an additional property for an object, not originally mapped in the database model). However, other CMSs may have a column <code>\"lastname\"<\/code> in the user table, so it doesn\u2019t apply as a meta value. (The actual definition of \u201cmeta\u201d value is actually inconsistent in WordPress: function <code>get_the_author_meta<\/code> also accepts value <code>\"user_email\"<\/code>, even though the email is stored on the user table. Hence, I\u2019d rather stick to my definition of \u201cmeta\u201d value, and remove all inconsistencies from the abstracted code.)<\/p>\n<p>Then, our contract will implement the following functions:<\/p>\n<pre><code>interface UsersAPIInterface\n{\n  public function getUserDisplayName($user_id);\n  public function getUserEmail($user_id);\n  public function getUserFirstname($user_id);\n  public function getUserLastname($user_id);\n  ...\n}<\/code><\/pre>\n<p>Which are resolved for WordPress:<\/p>\n<pre><code>class WPUsersAPI implements UsersAPIInterface\n{\n  public function getUserDisplayName($user_id)\n  {\n    return get_the_author_meta('display_name', $user_id);\n  }\n  public function getUserEmail($user_id)\n  {\n    return get_the_author_meta('user_email', $user_id);\n  }\n  public function getUserFirstname($user_id)\n  {\n    return get_the_author_meta('user_firstname', $user_id);\n  }\n  public function getUserLastname($user_id)\n  {\n    return get_the_author_meta('user_lastname', $user_id);\n  }\n  ...\n}<\/code><\/pre>\n<p>Our functions could also be re-defined as to remove the limitations from WordPress. For instance, function <code>update_user_meta($user_id, $meta_key, $meta_value)<\/code> can receive one meta attribute at a time, which makes sense since each of these is updated on its own database query. However, October CMS maps all meta attributes together on a single database column, so it makes more sense to update all values together on a single database operation. Then, our contract can include an operation <code>updateUserMetaAttributes($user_id, $meta)<\/code> which can update several meta values at the same time:<\/p>\n<pre><code>interface UserMetaInterface\n{\n  public function updateUserMetaAttributes($user_id, $meta);\n}<\/code><\/pre>\n<p>Which is resolved for WordPress like this:<\/p>\n<pre><code>class WPUsersAPI implements UsersAPIInterface\n{\n  public function updateUserMetaAttributes($user_id, $meta)\n  {\n    foreach ($meta as $meta_key => $meta_value) {\n      update_user_meta($user_id, $meta_key, $meta_value);\n    }\n  }\n}<\/code><\/pre>\n<p>Finally, we may want to re-define a function to remove its ambiguities. For instance, WordPress function <code>add_query_arg<\/code> can receive parameters in two different ways:<\/p>\n<ol>\n<li>Using a single key and value: <code>add_query_arg('key', 'value', 'http:\/\/example.com');<\/code><\/li>\n<li>Using an associative array: <code>add_query_arg(['key1' => 'value1', 'key2' => 'value2'], 'http:\/\/example.com');<\/code><\/li>\n<\/ol>\n<p>This becomes difficult to keep consistent across CMSs. Hence, our contract can define functions <code>addQueryArg<\/code> (singular) and <code>addQueryArgs<\/code> (plural) as to remove the ambiguity:<\/p>\n<div>\n<pre><code>public function addQueryArg(string $key, string $value, string $url);\npublic function addQueryArgs(array $key_values, string $url);<\/code><\/pre>\n<\/div>\n<h4>Function parameters<\/h4>\n<p>We must also abstract the parameters to the function, making sure they make sense in a generic context. For each function to abstract, we must consider:<\/p>\n<ul>\n<li>renaming and\/or re-defining the parameters;<\/li>\n<li>renaming and\/or re-defining the attributes passed on array parameters.<\/li>\n<\/ul>\n<p>For instance, WordPress function <code>get_posts<\/code> receives a unique parameter <code>$args<\/code>, which is an array of attributes. One of its attributes is <code>fields<\/code> which, when given the value <code>\"ids\"<\/code>, makes the function return an array of IDs instead of an array of objects. However, I deem this implementation too specific for WordPress, and for a generic context I\u2019d prefer a different solution: Convey this information through a separate parameter called <code>$options<\/code>, under attribute <code>\"return-type\"<\/code>.<\/p>\n<p>To accomplish this, we add parameter <code>$options<\/code> to the function in our contract:<\/p>\n<pre><code>interface PostAPIInterface\n{\n  public function getPosts($args, $options = []);\n}<\/code><\/pre>\n<p>Instead of referencing WordPress constant value <code>\"ids\"<\/code> (which we can\u2019t guarantee will be the one used in all other CMSs), we create a corresponding constant value for our abstracted application:<\/p>\n<pre><code>class Constants\n{\n  const RETURNTYPE_IDS = 'ids';\n}<\/code><\/pre>\n<p>The WordPress implementation must map and recreate the parameters between the contract and the implementation:<\/p>\n<pre><code>class WPPostAPI implements PostAPIInterface\n{\n  public function getPosts($args, $options = []) {\n    if ($options['return-type'] == Constants::RETURNTYPE_IDS) {\n      $args['fields'] = 'ids';\n    }\n    return get_posts($args);\n  }\n}<\/code><\/pre>\n<p>And finally, we can execute the code through our contract:<\/p>\n<pre><code>$options = [\n  'return-type' => Constants::RETURNTYPE_IDS,\n];\n$post_ids = $postsAPIService->getPosts($args, $options);<\/code><\/pre>\n<p>While abstracting the parameters, we should avoid transferring WordPress\u2019s technical debt to our abstracted code, whenever possible. For instance, parameter <code>$args<\/code> from function <code>get_posts<\/code> can contain attribute <code>'post_type'<\/code>. This attribute name is somewhat misleading, since it can receive one element (<code>post_type => \"post\"<\/code>) but also a list of them (<code>post_type => \"post, event\"<\/code>), so this name should be in plural instead: <code>post_types<\/code>. When abstracting this piece of code, we can set our interface to expect attribute <code>post_types<\/code> instead, which will be mapped to WordPress\u2019s <code>post_type<\/code>.<\/p>\n<p>Similarly, different functions accept arguments with different names, even though these have the same objective, so their names can be unified. For instance, through parameter <code>$args<\/code>, WordPress function <code>get_posts<\/code> accepts attribute <code>posts_per_page<\/code>, and function <code>get_users<\/code> accepts attribute <code>number<\/code>. These attribute names can perfectly be replaced with the more generic attribute name <code>limit<\/code>.<\/p>\n<p>It is also a good idea to rename parameters to make it easy to understand which ones belong to WordPress and which ones have been abstracted. For instance, we can decide to replace all <code>\"_\"<\/code> with <code>\"-\"<\/code>, so our newly-defined argument <code>post_types<\/code> becomes <code>post-types<\/code>.<\/p>\n<p>Applying these prior considerations, our abstracted code will look like this:<\/p>\n<pre><code>class WPPostAPI implements PostAPIInterface\n{\n  public function getPosts($args, $options = []) {\n    ...\n    if (isset($args['post-types'])) {\n      $args['post_type'] = $args['post-types'];\n      unset($args['post-types']);\n    }\n    if (isset($args['limit'])) { \n      $args['posts_per_page'] = $args['limit'];\n      unset($args['limit']);\n    }\n    return get_posts($args);\n  }\n}<\/code><\/pre>\n<p>We can also re-define attributes to modify the shape of their values. For instance, WordPress parameter <code>$args<\/code> in function <code>get_posts<\/code> can receive attribute <code>date_query<\/code>, whose properties (<code>\"after\"<\/code>, <code>\"inclusive\"<\/code>, etc) can be considered specific to WordPress:<\/p>\n<pre><code>$date = current_time('timestamp');\n$args['date_query'] = array(\n  array(\n    'after' => date('Y-m-d H:i:s', $date),\n    'inclusive' => true,\n  )\n);<\/code><\/pre>\n<p>To unify the shape of this value into something more generic, we can re-implement it using other arguments, such as <code>\"date-from\"<\/code> and <code>\"date-from-inclusive\"<\/code> (this solution is not 100% convincing though, since it is more verbose than WordPress\u2019s):<\/p>\n<pre><code>class WPPostAPI implements PostAPIInterface\n{\n  public function getPosts($args, $options = []) {\n    ...\n    if (isset($args['date-from'])) {\n      $args['date_args'][] = [\n        'after' => $args['date-from'],\n        'inclusive' => false,\n      ];\n      unset($args['date-from']);\n    }\n    if (isset($args['date-from-inclusive'])) {\n      $args['date_args'][] = [\n        'after' => $args['date-from-inclusive'],\n        'inclusive' => true,\n      ];\n      unset($args['date-from-inclusive']);\n    }\n    return get_posts($args);\n  }\n}<\/code><\/pre>\n<p>In addition, we need to consider if to abstract or not those parameters which are too specific to WordPress. For instance, function <code>get_posts<\/code> allows to order posts by attribute <code>menu_order<\/code>, which I don\u2019t think it works in a generic context. Then, I\u2019d rather not abstract this code and keep it on the CMS-specific package for WordPress.<\/p>\n<p>Finally, we can also add argument types (and, since here we are, also return types) to our contract fuction, making it more understandable and allowing the code to fail in compilation time instead of during runtime:<\/p>\n<div>\n<pre><code>interface PostAPIInterface\n{\n  public function getPosts(array $args, array $options = []): array;\n}<\/code><\/pre>\n<\/div>\n<h4>States (and other constant values)<\/h4>\n<p>We need to make sure that all states have the same meaning in all CMSs. For instance, posts in WordPress can have one among the following states: <code>\"publish\"<\/code>, <code>\"pending\"<\/code>, <code>\"draft\"<\/code> or <code>\"trash\"<\/code>. To make sure that the application references the abstracted version of the states and not the CMS-specific one, we can simply define a constant value for each of them:<\/p>\n<pre><code>class PostStates {\n  const PUBLISHED = 'published';\n  const PENDING = 'pending';\n  const DRAFT = 'draft';\n  const TRASH = 'trash';\n}<\/code><\/pre>\n<p>As it can be seen, the actual constant values may or may not be the same as in WordPress: while <code>\"publish\"<\/code> was renamed as <code>\"published\"<\/code>, the other ones remain the same.<\/p>\n<p>For the implementation for WordPress, we convert from the agnostic value to the WordPress-specific one:<\/p>\n<pre><code>class WPPostAPI implements PostAPIInterface\n{\n  public function getPosts($args, $options = []) {\n    ...\n    if (isset($args['post-status'])) {\n      $conversion = [\n        PostStates::PUBLISHED => 'publish',\n        PostStates::PENDING => 'pending',\n        PostStates::DRAFT => 'draft',\n        PostStates::TRASH => 'trash',\n      ];\n      $args['post_status'] = $conversion[$args['post-status']];\n      unset($args['post-status']);\n    }\n    return get_posts($args);\n  }\n}<\/code><\/pre>\n<p>Finally, we can reference these constants throughout our CMS-agnostic application:<\/p>\n<pre><code>$args = [\n  'post-status' => PostStates::PUBLISHED,\n];\n$posts = $postsAPIService->getPosts($args);<\/code><\/pre>\n<p>This strategy works under the assumption that all CMSs will support these states. If any CMS does not support a particular state (eg: <code>\"pending\"<\/code>) then it should throw an exception whenever a corresponding functionality is invoked.<\/p>\n<h4>CMS helper functions<\/h4>\n<p>WordPress implements several helper functions that must also abstracted, such as <code>make_clickable<\/code>. Because these functions are very generic, we can implement a default behavior for them that works well in an abstract context, and which can be overridden if the CMS implements a better solution.<\/p>\n<p>We first define the contract:<\/p>\n<pre><code>interface HelperAPIInterface\n{\n  public function makeClickable(string $text);\n}<\/code><\/pre>\n<p>And provide a default behaviour for the helper functions through an abstract class:<\/p>\n<div>\n<pre><code>abstract class AbstractHelperAPI implements HelperAPIInterface\n{\n  public function makeClickable(string $text) {\n    return preg_replace('!(((f|ht)tp(s)?:\/\/)[-a-zA-Z?-??-?()0-9@:%_+.~#?&;\/\/=]+)!i', '<a href=\"$1\">$1<\/a>', $text);\n  }\n}<\/code><\/pre>\n<\/div>\n<p>Now, our application can either use this functionality or, if it runs on WordPress, use the WordPress-specific implementation:<\/p>\n<pre><code>class WPHelperAPI extends AbstractHelperAPI\n{\n  public function makeClickable(string $text) {\n    return make_clickable($text);\n  }\n}<\/code><\/pre>\n<div><\/div>\n<h4>User permissions<\/h4>\n<p>For all CMSs which support user management, in addition to abstracting the corresponding functions (such as <code>current_user_can<\/code> and <code>user_can<\/code> in WordPress), we must also make sure that the user permissions (or capabilities) have the same effect across all CMSs. To achieve this, our abstracted application needs to explicitly state what is expected from the capability, and the implementation for each CMS must either satisfy it through one of its own capabilities or throw an exception if it can\u2019t satisfy it. For instance, if the application needs to validate if the user can edit posts, it can represent it through a capability called <code>\"capability:editPosts\"<\/code>, which is satisfied for WordPress through its capability <code>\"edit_posts\"<\/code>.<\/p>\n<p>This is still an instance of the \u201ccode against interfaces, not implementations\u201d principle, however here we run against a problem: Whereas in PHP we can define interfaces and classes to model contracts and service providers (which works in compilation time, so that the code doesn\u2019t compile if a class implementing an interface does not implement all functions defined in the interface), PHP offers no similar construct to validate that a contract capability (which is simply a string, such as <code>\"capability:editPosts\"<\/code>) has been satisfied through a capability by the CMS. This concept, which I call a \u201cloose contract\u201d, will need to be handled by our application, on runtime.<\/p>\n<p>To deal with \u201cloose contracts\u201d, I have created a service <code>LooseContractService<\/code> through which:<\/p>\n<ul>\n<li>the application can define what \u201ccontract names\u201d must be implemented, through function <code>requireNames<\/code>.<\/li>\n<li>the CMS-specific implementations can satisfy those names, through function <code>implementNames<\/code>.<\/li>\n<li>the application can get the implementation of a name through function <code>getImplementedName<\/code>.<\/li>\n<li>the application can also inquire for all non-satisfied required names through function <code>getNotImplementedRequiredNames<\/code>, as to throw an exception or log the error if needed.<\/li>\n<\/ul>\n<p>The service looks like this:<\/p>\n<div>\n<pre><code>class LooseContractService\n{\n  protected $requiredNames = [];\n  protected $nameImplementations = [];\n\n  public function requireNames(array $names): void\n  {\n    $this->requiredNames = array_merge(\n      $this->requiredNames,\n      $names\n    );\n  }\n\n  public function implementNames(array $nameImplementations): void\n  {\n    $this->nameImplementations = array_merge(\n      $this->nameImplementations,\n      $nameImplementations\n    );\n  }\n\n  public function getImplementedName(string $name): ?string {\n    return $this->nameImplementations[$name];\n  }\n\n  public function getNotImplementedRequiredNames(): array {\n    return array_diff(\n      $this->requiredNames,\n      array_keys($this->nameImplementations)\n    );\n  }\n}<\/code><\/pre>\n<\/div>\n<p>The application, when initialized, can then establish loose contracts by requiring names:<\/p>\n<div>\n<pre><code>$looseContractService = ContainerBuilderFactory::getInstance()->get('loose_contracts');\n$looseContractService->requireNames([\n  'capability:editPosts',\n]);<\/code><\/pre>\n<\/div>\n<p>And the CMS-specific implementation can satisfy these:<\/p>\n<pre><code>$looseContractService->implementNames([\n  'capability:editPosts' => 'edit_posts',\n]);<\/code><\/pre>\n<p>The application can then resolve the required name to the implementation from the CMS. If this required name (in this case, a capability) is not implemented, then the application may throw an exception:<\/p>\n<div>\n<pre><code>$cmsCapabilityName = $looseContractService->getImplementedName('capability:editPosts');\nif (!$cmsCapabilityName) {\n  throw new Exception(sprintf(\n    \"The CMS has no support for capability \"%s\"\",\n    'capability:editPosts'\n  ));\n}\n\/\/ Now can use the capability to check for permissions\n$userManagementAPIService = ContainerBuilderFactory::getInstance()->get('user_management_api');\nif ($userManagementAPIService->userCan($user_id, $cmsCapabilityName)) {\n  ...\n}<\/code><\/pre>\n<\/div>\n<p>Alternatively, the application can also fail when first initialized if any one required name is not satisfied:<\/p>\n<div>\n<pre><code>if ($notImplementedNames = $looseContractService->getNotImplementedRequiredNames()) {\n  throw new Exception(sprintf(\n    \"The CMS has not implemented loose contract names %s\",\n    implode(', ', $notImplementedNames)\n  ));\n}<\/code><\/pre>\n<\/div>\n<h4>Application options<\/h4>\n<p>WordPress ships with several application options, such as those stored in table <code>wp_options<\/code> under entries <code>\"blogname\"<\/code>, <code>\"blogdescription\"<\/code>, <code>\"admin_email\"<\/code>, <code>\"date_format\"<\/code> and many others. Abstracting application options involves:<\/p>\n<ul>\n<li>abstraction the function <code>getOption<\/code>;<\/li>\n<li>abstracting each of the required options, aiming to make the CMS satisfy the notion of this option (eg: if a CMS doesn\u2019t have an option for the site\u2019s description, it can\u2019t return the site\u2019s name instead).<\/li>\n<\/ul>\n<p>Let\u2019s solve these 2 actions in turn. Concerning function <code>getOption<\/code>, I believe that we can expect all CMSs to support storing and retrieving options, so we can place the corresponding function under a <code>CMSCoreInterface<\/code> contract:<\/p>\n<pre><code>interface CMSCoreInterface\n{\n  public function getOption($option, $default = false);\n}<\/code><\/pre>\n<p>As it can be observed from the function signature above, I\u2019m making the assumption that each option will also have a default value. However, I don\u2019t know if every CMS allows setting default values for options. But it doesn\u2019t matter since the implementation can simply return <code>NULL<\/code> then.<\/p>\n<p>This function is resolved for WordPress like this:<\/p>\n<pre><code>class WPCMSCore implements CMSCoreInterface\n{\n  public function getOption($option, $default = false)\n  {\n    return get_option($option, $default);\n  }\n}<\/code><\/pre>\n<p>To solve the 2nd action, which is abstracting each needed option, it is important to notice that even though we can always expect the CMS to support <code>getOption<\/code>, we can\u2019t expect it to implement each single option used by WordPress, such as <code>\"use_smiles\"<\/code> or <code>\"default_ping_status\"<\/code>. Hence, we must first filter all options, and abstract only those that make sense in a generic context, such as <code>\"siteName\"<\/code> or <code>\"dateFormat\"<\/code>.<\/p>\n<p>Then, having the list of options to abstract, we can use a \u201cloose contract\u201d (as explained earlier on) and require a corresponding option name for each, such as <code>\"option:siteName\"<\/code> (resolved for WordPress as <code>\"blogname\"<\/code>) or <code>\"option:dateFormat\"<\/code> (resolved as <code>\"date_format\"<\/code>).<\/p>\n<h4>Database column names<\/h4>\n<p>In WordPress, when we are requesting data from function <code>get_posts<\/code> we can set attribute <code>\"orderby\"<\/code> in <code>$args<\/code> to order the results, which can be based on a column from the posts table (such as values <code>\"ID\"<\/code>, <code>\"title\"<\/code>, <code>\"date\"<\/code>, <code>\"comment_count\"<\/code>, etc), a meta value (through values <code>\"meta_value\"<\/code> and <code>\"meta_value_num\"<\/code>) or other values (such as <code>\"post__in\"<\/code> and <code>\"rand\"<\/code>).<\/p>\n<p>Whenever the value corresponds to the table column name, we can abstract them using a \u201cloose contract\u201d, as explained earlier on. Then, the application can reference a loose contract name:<\/p>\n<div>\n<pre><code>$args = [\n  'orderby' => $looseContractService->getImplementedName('dbcolumn:orderby:posts:date'),\n];\n$posts = $postsAPIService->getPosts($args);<\/code><\/pre>\n<\/div>\n<p>And this name is resolved for WordPress:<\/p>\n<pre><code>$looseContractService->implementNames([\n  'dbcolumn:orderby:posts:date' => 'date',\n]);<\/code><\/pre>\n<p>Now, let\u2019s say that in our WordPress application we have created a meta value <code>\"likes_count\"<\/code> (which stores how many likes a post has) to order posts by popularity, and we want to abstract this functionality too. To order results by some meta property, WordPress expects an additional attribute <code>\"meta_key\"<\/code>, like this:<\/p>\n<pre><code>$args = [\n  'orderby' => 'meta_value',\n  'meta_key' => 'likes_count',\n];<\/code><\/pre>\n<p>Because of this additional attribute, I consider this implementation WordPress-specific and very difficult to abstract to make it work everywhere. Then, instead of generalizing this functionality, I can simply expect every CMS to add their own, specific implementation.<\/p>\n<p>Let\u2019s do that. First, I create a helper class to retrieve the CMS-agnostic query:<\/p>\n<div>\n<pre><code>class QueryHelper\n{\n  public function getOrderByQuery()\n  {\n    return array(\n      'orderby' => $looseContractService->getImplementedName('dbcolumn:orderby:posts:likesCount'),\n    );\n  }\n}<\/code><\/pre>\n<\/div>\n<p>The OctoberCMS-specific package can add a column <code>\"likes_count\"<\/code> to the posts table, and resolve name <code>\"dbcolumn:orderby:posts:likesCount\"<\/code> to <code>\"like_count\"<\/code> and it will work. The WordPress-specific package, though, must resolve <code>\"dbcolumn:orderby:posts:likesCount\"<\/code> as <code>\"meta_value\"<\/code> and then override the helper function to add the additional property <code>\"meta_key\"<\/code>:<\/p>\n<pre><code>class WPQueryHelper extends QueryHelper\n{\n  public function getOrderByQuery()\n  {\n    $query = parent::getOrderByQuery();\n    $query['meta_key'] = 'likes_count';\n    return $query;\n  }\n}<\/code><\/pre>\n<p>Finally, we set-up the helper query class as a service in the <code>ContainerBuilder<\/code>, configure it to be resolved to the WordPress-specific class, and we obtain the query for ordering results:<\/p>\n<div>\n<pre><code>$queryHelperService = ContainerBuilderFactory::getInstance()->get('query_helper');\n$args = $queryHelperService->getOrderByQuery();\n$posts = $postsAPIService->getPosts($args);<\/code><\/pre>\n<\/div>\n<p>Abstracting the values for ordering results that do not correspond to column names or meta properties (such as <code>\"post__in\"<\/code> and <code>\"rand\"<\/code>) seems to be more difficult. Because my application doesn\u2019t use them, I haven\u2019t considered how to do it, or even if it is possible. Then I took the easy way out: I have considered these to be WordPress-specific, hence the application makes them available only when running on WordPress.<\/p>\n<h4>Errors<\/h4>\n<p>When dealing with errors, we must consider abstracting the following elements:<\/p>\n<ul>\n<li>the definition of an error;<\/li>\n<li>error codes and messages.<\/li>\n<\/ul>\n<p>Let\u2019s review these in turn.<\/p>\n<h5>Definition of an error:<\/h5>\n<p>An <code>Error<\/code> is a special object, different than an <code>Exception<\/code>, used to indicate that some operation has failed, and why it failed. WordPress represents errors through class <code>WP_Error<\/code>, and allows to check if some returned value is an error through function <code>is_wp_error<\/code>.<\/p>\n<p>We can abstract checking for an error:<\/p>\n<pre><code>interface CMSCoreInterface\n{\n  public function isError($object);\n}<\/code><\/pre>\n<p>Which is resolved for WordPress like this:<\/p>\n<pre><code>class WPCMSCore implements CMSCoreInterface\n{\n  public function isError($object)\n  {\n    return is_wp_error($object);\n  }\n}<\/code><\/pre>\n<p>However, to deal with errors in our abstracted code, we can\u2019t expect all CMSs to have an error class with the same properties and methods as WordPress\u2019s <code>WP_Error<\/code> class. Hence, we must abstract this class too, and convert from the CMS error to the abstracted error after executing a function from the CMS.<\/p>\n<p>The abstract error class <code>Error<\/code> is simply a slightly modified version from WordPress\u2019s <code>WP_Error<\/code> class:<\/p>\n<div>\n<pre><code>class Error {\n\n  protected $errors = array();\n  protected $error_data = array();\n\n  public function __construct($code = null, $message = null, $data = null) \n  {\n    if ($code) {\n      $this->errors[$code][] = $message;\n      if ($data) {\n        $this->error_data[$code] = $data;\n      }\n    }\n  }\n\n  public function getErrorCodes()\n  {\n    return array_keys($this->errors);\n  }\n\n  public function getErrorCode()\n  {    \n    if ($codes = $this->getErrorCodes()) {\n      return $codes[0];\n    }\n\n    return null;\n  }\n\n  public function getErrorMessages($code = null)\n  {    \n    if ($code) {\n      return $this->errors[$code] ?? [];\n    }\n\n    \/\/ Return all messages if no code specified.\n    return array_reduce($this->errors, 'array_merge', array());\n  }\n\n  public function getErrorMessage($code = null)\n  {\n    if (!$code) {\n      $code = $this->getErrorCode();\n    }\n    $messages = $this->getErrorMessages($code);\n    return $messages[0] ?? '';\n  }\n\n  public function getErrorData($code = null)\n  {\n    if (!$code) {\n      $code = $this->getErrorCode();\n    }\n\n    return $this->error_data[$code];\n  }\n\n  public function add($code, $message, $data = null)\n  {\n    $this->errors[$code][] = $message;\n    if ($data) {\n      $this->error_data[$code] = $data;\n    }\n  }\n\n  public function addData($data, $code = null)\n  {\n    if (!$code) {\n      $code = $this->getErrorCode();\n    }\n\n    $this->error_data[$code] = $data;\n  }\n\n  public function remove($code)\n  {\n    unset($this->errors[$code]);\n    unset($this->error_data[$code]);\n  }\n}<\/code><\/pre>\n<\/div>\n<p>We implement a function to convert from the CMS to the abstract error through a helper class:<\/p>\n<div>\n<pre><code>class WPHelpers\n{\n  public static function returnResultOrConvertError($result)\n  {\n    if (is_wp_error($result)) {\n      \/\/ Create a new instance of the abstracted error class\n      $error = new Error();\n      foreach ($result->get_error_codes() as $code) {\n        $error->add($code, $result->get_error_message($code), $result->get_error_data($code));\n      }\n      return $error;\n    }\n    return $result;\n  }\n}<\/code><\/pre>\n<\/div>\n<p>And we finally invoke this method for all functions that may return an error:<\/p>\n<pre><code>class UserManagementService implements UserManagementInterface\n{\n  public function getPasswordResetKey($user_id)\n  {\n    $result = get_password_reset_key($user_id);\n    return WPHelpers::returnResultOrConvertError($result);\n  }\n}<\/code><\/pre>\n<h5>Error codes and messages:<\/h5>\n<p>Every CMS will have its own set of error codes and corresponding explanatory messages. For instance, WordPress function <code>get_password_reset_key<\/code> can fail due to the following reasons, as represented by their error codes and messages:<\/p>\n<ol>\n<li><code>\"no_password_reset\"<\/code>: Password reset is not allowed for this user.<\/li>\n<li><code>\"no_password_key_update\"<\/code>: Could not save password reset key to database.<\/li>\n<\/ol>\n<p>In order to unify errors so that an error code and message is consistent across CMSs, we will need to inspect these and replace them with our custom ones (possibly in function <code>returnResultOrConvertError<\/code> explained above).<\/p>\n<h4>Hooks<\/h4>\n<p>Abstracting hooks involves:<\/p>\n<ul>\n<li>the hook functionality;<\/li>\n<li>the hooks themselves.<\/li>\n<\/ul>\n<p>Let\u2019s analyze these in turn.<\/p>\n<h5>Abstracting the hook functionality<\/h5>\n<p>WordPress offers the concept of \u201chooks\u201d: a mechanism through which we can change a default behavior or value (through \u201cfilters\u201d) and execute related functionality (through \u201cactions\u201d). Both Symfony and Laravel offer mechanisms somewhat related to hooks: Symfony provides an <a href=\"https:\/\/symfony.com\/doc\/current\/components\/event_dispatcher.html\">event dispatcher<\/a> component, and Laravel\u2019s mechanism is called <a href=\"https:\/\/laravel.com\/docs\/6.x\/events\">events<\/a>; these 2 mechanisms are similar, sending notifications of events that have already taken place, to be processed by the application through listeners.<\/p>\n<p>When comparing these 3 mechanisms (hooks, event dispatcher and events) we find that WordPress\u2019s solution is the simpler one to set-up and use: Whereas WordPress hooks enable to pass an unlimited number of parameters in the hook itself and to directly modify a value as a response from a filter, Symfony\u2019s component requires to instantiate a new object to pass additional information, and Laravel\u2019s solution suggests to run a command in Artisan (Laravel\u2019s CLI) to generate the files containing the event and listener objects. If all we desire is to modify some value in the application, executing a hook such as <code>$value = apply_filters(\"modifyValue\", $value, $post_id);<\/code> is as simple as it can get.<\/p>\n<p>In the <a href=\"https:\/\/www.smashingmagazine.com\/2019\/11\/abstracting-wordpress-code-cms-concepts\/\">first part of this series<\/a>, I explained that the CMS-agnostic application already establishes a particular solution for dependency injection instead of relying on the solution by the CMS, because the application itself needs this functionality to glue its parts together. Something similar happens with hooks: they are such a powerful concept that the application can greatly benefit by making it available to the different CMS-agnostic packages (allowing them to interact with each other) and not leave this wiring-up to be implemented only at the CMS level. Hence, I have decided to already ship a solution for the \u201chook\u201d concept in the CMS-agnostic application, and this solution is the one implemented by WordPress.<\/p>\n<p>In order to decouple the CMS-agnostic hooks from those from WordPress, once again we must \u201ccode against interfaces, not implementations\u201d: We define a contract with the corresponding hook functions:<\/p>\n<div>\n<pre><code>interface HooksAPIInterface\n{\n  public function addFilter(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void;\n  public function removeFilter(string $tag, $function_to_remove, int $priority = 10): bool;\n  public function applyFilters(string $tag, $value, ...$args);\n  public function addAction(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void;\n  public function removeAction(string $tag, $function_to_remove, int $priority = 10): bool;\n  public function doAction(string $tag, ...$args): void;\n}<\/code><\/pre>\n<\/div>\n<p>Please notice that functions <code>applyFilters<\/code> and <code>doAction<\/code> are variadic, i.e. they can receive a variable amount of arguments through parameter <code>...$args<\/code>. By combining this feature (which was added to PHP in version 5.6, hence it was unavailable to WordPress until very recently) with argument unpacking, i.e. passing a variable amount of parameters <code>...$args<\/code> to a function, we can easily provide the implementation for WordPress:<\/p>\n<div>\n<pre><code>class WPHooksAPI implements HooksAPIInterface\n{\n  public function addFilter(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void\n  {\n    add_filter($tag, $function_to_add, $priority, $accepted_args);\n  }\n\n  public function removeFilter(string $tag, $function_to_remove, int $priority = 10): bool\n  {\n    return remove_filter($tag, $function_to_remove, $priority);\n  }\n\n  public function applyFilters(string $tag, $value, ...$args)\n  {\n    return apply_filters($tag, $value, ...$args);\n  }\n\n  public function addAction(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void\n  {\n    add_action($tag, $function_to_add, $priority, $accepted_args);\n  }\n\n  public function removeAction(string $tag, $function_to_remove, int $priority = 10): bool\n  {\n    return remove_action($tag, $function_to_remove, $priority);\n  }\n\n  public function doAction(string $tag, ...$args): void\n  {\n    do_action($tag, ...$args);\n  }\n}<\/code><\/pre>\n<\/div>\n<p>As for an application running on Symfony or Laravel, this contract can be satisfied by installing a CMS-agnostic package implementing WordPress-like hooks, such as <a href=\"https:\/\/github.com\/tormjens\/eventy\">this one<\/a>, <a href=\"https:\/\/github.com\/bainternet\/PHP-Hooks\">this one<\/a> or <a href=\"https:\/\/github.com\/voku\/php-hooks\">this one<\/a>.<\/p>\n<p>Finally, whenever we need to execute a hook, we do it through the corresponding service:<\/p>\n<div>\n<pre><code>$hooksAPIService = ContainerBuilderFactory::getInstance()->get('hooks_api');\n$title = $hooksAPIService->applyFilters(\"modifyTitle\", $title, $post_id);<\/code><\/pre>\n<\/div>\n<div><\/div>\n<h5>Abstracting the hooks themselves<\/h5>\n<p>We need to make sure that, whenever a hook is executed, a consistent action will be executed no matter which is the CMS. For hooks defined inside of our application that is no problem, since we can resolve them ourselves, most likely in our CMS-agnostic package. However, when the hook is provided by the CMS, such as action <code>\"init\"<\/code> (triggered when the system has been initialized) or filter <code>\"the_title\"<\/code> (triggered to modify a post\u2019s title) in WordPress, and we invoke these hooks, we must make sure that all other CMSs will process them correctly and consistently. (Please notice that this concerns hooks that make sense in every CMS, such as <code>\"init\"<\/code>; certain other hooks can be considered too specific to WordPress, such as filter <code>\"rest_{$this->post_type}_query\"<\/code> from a REST controller, so we don\u2019t need to abstract them.)<\/p>\n<p>The solution I found is to hook into actions or filters defined exclusively in the application (i.e. not in the CMS), and to bridge from CMS hooks to application hooks whenever needed. For instance, instead of adding an action for hook <code>\"init\"<\/code> (as defined in WordPress), any code in our application must add an action on hook <code>\"cms:init\"<\/code>, and then we implement the bridge in the WordPress-specific package from <code>\"init\"<\/code> to <code>\"cms:init\"<\/code>:<\/p>\n<div>\n<pre><code>$hooksAPIService->addAction('init', function() use($hooksAPIService) {\n  $hooksAPIService->doAction('cms:init');\n});<\/code><\/pre>\n<\/div>\n<p>Finally, the application can add a \u201cloose contract\u201d name for <code>\"cms:init\"<\/code>, and the CMS-specific package must implement it (as demonstrated earlier on).<\/p>\n<h4>Routing<\/h4>\n<p>Different frameworks will provide different solutions for routing (i.e. the mechanism of identifying how the requested URL will be handled by the application), which reflect the architecture of the framework:<\/p>\n<ul>\n<li>In WordPress, <a href=\"https:\/\/wordpress.stackexchange.com\/a\/141087\">URLs map to database queries<\/a>, not to routes.<\/li>\n<li>Symfony provides a <a href=\"https:\/\/symfony.com\/doc\/current\/components\/routing.html\">Routing component<\/a> which is independent (any PHP application can install it and use it), and which enables to define custom routes and which controller will process them.<\/li>\n<li><a href=\"https:\/\/laravel.com\/docs\/6.x\/routing\">Laravel\u2019s routing<\/a> builds on top of Symfony\u2019s routing component to adapt it to the Laravel framework.<\/li>\n<\/ul>\n<p>As it can be seen, WordPress\u2019s solution is the outlier here: the concept of mapping URLs to database queries is tightly coupled to WordPress\u2019s architecture, and we would not want to restrict our abstracted application to this methodology (for instance, October CMS can be set-up as a flat-file CMS, in which case it doesn\u2019t use a database). Instead, it makes more sense to use Symfony\u2019s approach as its default behavior, and allow WordPress to override this behavior with its own routing mechanism.<\/p>\n<p>(Indeed, while WordPress\u2019s approach works well for retrieving content, it is rather inappropriate when we need to access some functionality, such as displaying a contact form. In this case, before the launch of Gutenberg, we were forced to create a page and add a shortcode <code>\"[contact_form]\"<\/code> to it as content, which is not as clean as simply mapping the route to its corresponding controller directly.)<\/p>\n<p>Hence, the routing for our abstracted application will not be based around the modeled entities (post, page, category, tag, author) but purely on custom-defined routes. This should already work perfectly for Symfony and Laravel, using their own solutions, and there is not much for us to do other than injecting the routes with the corresponding controllers into the application\u2019s configuration.<\/p>\n<p>To make it work in WordPress, though, we need to take some extra steps: We must introduce an external library to handle routing, such as <a href=\"https:\/\/github.com\/Brain-WP\/Cortex\">Cortex<\/a>. Making use of Cortex, the application running on WordPress can have it both ways:<\/p>\n<ul>\n<li>if there is a custom-defined route matching the requested URL, use its corresponding controller.<\/li>\n<li>if not, let WordPress handle the request in its own way (i.e. retrieving the matched database entity or returning a 404 if no match is successful).<\/li>\n<\/ul>\n<p>To implement this functionality, I have designed the contract <code>CMSRoutingInterface<\/code> to, given the requested URL, calculate two pieces of information:<\/p>\n<ul>\n<li>the actual route, such as <code>contact<\/code>, <code>posts<\/code> or <code>posts\/my-first-post<\/code>.<\/li>\n<li>the nature of the route: core nature values <code>\"standard\"<\/code>, <code>\"home\"<\/code> and <code>\"404\"<\/code>, and additional nature values added through packages such as <code>\"post\"<\/code> through a \u201cPosts\u201d package or <code>\"user\"<\/code> through a \u201cUsers\u201d package.<\/li>\n<\/ul>\n<p>The nature of the route is an artificial construction that enables the CMS-agnostic application to identify if the route has extra qualities attached to it. For instance, when requesting the URL for a single post in WordPress, the corresponding database object post is loaded into the global state, under <code>global $post<\/code>. It also helps identify which case we want to handle, to avoid inconsistencies. For instance, we could have defined a custom route <code>contact<\/code> handled by a controller, which will have nature <code>\"standard\"<\/code>, and also a page in WordPress with slug <code>\"contact\"<\/code>, which will have nature <code>\"page\"<\/code> (added through a package called \u201cPages\u201d). Then, our application can prioritize which way to handle the request, either through the controller or through a database query.<\/p>\n<p>Let\u2019s implement it. We first define the service\u2019s contract:<\/p>\n<pre><code>interface CMSRoutingInterface\n{\n  public function getNature();\n  public function getRoute();\n}<\/code><\/pre>\n<p>We can then define an abstract class which provides a base implementation of these functions:<\/p>\n<div>\n<pre><code>abstract class AbstractCMSRouting implements CMSRoutingInterface\n{\n  const NATURE_STANDARD = 'standard';\n  const NATURE_HOME = 'home';\n  const NATURE_404 = '404';\n\n  public function getNature()\n  {\n    return self::NATURE_STANDARD;\n  }\n\n  public function getRoute()\n  {\n    \/\/ By default, the URI path is already the route (minus parameters and trailing slashes)\n    $route = $_SERVER['REQUEST_URI'];\n    $params_pos = strpos($route, '?');\n    if ($params_pos !== false) {\n       $route = substr($route, 0, $params_pos);\n    }\n    return trim($route, '\/');\n  }\n}<\/code><\/pre>\n<\/div>\n<p>And the implementation is overriden for WordPress:<\/p>\n<div>\n<pre><code>class WPCMSRouting extends AbstractCMSRouting\n{\n  const ROUTE_QUERY = [\n    'custom_route_key' => 'custom_route_value',\n  ];\n  private $query;\n  private function init()\n  {\n    if (is_null($this->query)) {\n      global $wp_query;\n      $this->query = $wp_query;\n    }\n  }\n\n  private function isStandardRoute() {\n    return !empty(array_intersect($this->query->query_vars, self::ROUTE_QUERY));\n  }\n\n  public function getNature()\n  {\n    $this->init();\n    if ($this->isStandardRoute()) {\n      return self::NATURE_STANDARD;\n    } elseif ($this->query->is_home() || $this->query->is_front_page()) {\n      return self::NATURE_HOME;\n    } elseif ($this->query->is_404()) {\n      return self::NATURE_404;\n    }\n\n    \/\/ Allow components to implement their own natures\n    $hooksAPIService = ContainerBuilderFactory::getInstance()->get('hooks_api');\n    return $hooksAPIService->applyFilters(\n      \"nature\",\n      parent::getNature(),\n      $this->query\n    );\n  }\n}<\/code><\/pre>\n<\/div>\n<p>In the code above, please notice how constant <code>ROUTE_QUERY<\/code> is used by the service to know if the route is a custom-defined one, as configured through Cortex:<\/p>\n<div>\n<pre><code>$hooksAPIService->addAction(\n  'cortex.routes', \n  function(RouteCollectionInterface $routes) {  \n    \/\/ Hook into filter \"routes\" to provide custom-defined routes\n    $appRoutes = $hooksAPIService->applyFilters(\"routes\", []);\n    foreach ($appRoutes as $route) {\n      $routes->addRoute(new QueryRoute(\n        $route,\n        function (array $matches) {\n          return WPCMSRouting::ROUTE_QUERY;\n        }\n      ));\n    }\n  }\n);<\/code><\/pre>\n<\/div>\n<p>Finally, we add our routes through hook <code>\"routes\"<\/code>:<\/p>\n<pre><code>$hooksAPIService->addFilter(\n  'routes',\n  function($routes) {\n    return array_merge(\n      $routes,\n      [\n        'contact',\n        'posts',\n      ]\n    );\n  }\n);<\/code><\/pre>\n<p>Now, the application can find out the route and its nature, and proceed accordingly (for instance, for a <code>\"standard\"<\/code> nature invoke its controller, or for a <code>\"post\"<\/code> nature invoke WordPress\u2019s templating system):<\/p>\n<div>\n<pre><code>$cmsRoutingService = ContainerBuilderFactory::getInstance()->get('routing');\n$nature = $cmsRoutingService->getNature();\n$route = $cmsRoutingService->getRoute();\n\/\/ Process the requested route, as appropriate\n\/\/ ...<\/code><\/pre>\n<\/div>\n<h4>Object properties<\/h4>\n<p>A rather inconvenient consequence of abstracting our code is that we can\u2019t reference the properties from an object directly, and we must do it through a function instead. This is because different CMSs will represent the same object as containing different properties, and it is easier to abstract a function to access the object properties than to abstract the object itself (in which case, among other disadvantages, we may have to reproduce the object caching mechanism from the CMS). For instance, a post object <code>$post<\/code> contains its ID under <code>$post->ID<\/code> in WordPress and under <code>$post->id<\/code> in October CMS. To resolve this property, our contract <code>PostObjectPropertyResolverInterface<\/code> will contain function <code>getId<\/code>:<\/p>\n<pre><code>interface PostObjectPropertyResolverInterface {\n  public function getId($post);\n}<\/code><\/pre>\n<p>Which is resolved for WordPress like this:<\/p>\n<div>\n<pre><code>class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {\n  public function getId($post)\n  {\n    return $post->ID;\n  }\n}<\/code><\/pre>\n<\/div>\n<p>Similarly, the post content property is <code>$post->post_content<\/code> in WordPress and <code>$post->content<\/code> in October CMS. Our contract will then allow to access this property through function <code>getContent<\/code>:<\/p>\n<pre><code>interface PostObjectPropertyResolverInterface {\n  public function getContent($post);\n}<\/code><\/pre>\n<p>Which is resolved for WordPress like this:<\/p>\n<div>\n<pre><code>class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {\n  public function getContent($post)\n  {\n    return $post->post_content;\n  }\n}<\/code><\/pre>\n<\/div>\n<p>Please notice that function <code>getContent<\/code> receives the object itself through parameter <code>$post<\/code>. This is because we are assuming the content will be a property of the post object in all CMSs. However, we should be cautious on making this assumption, and decide on a property by property basis. If we don\u2019t want to make the previous assumption, then it makes more sense for function <code>getContent<\/code> to receive the post\u2019s ID instead:<\/p>\n<pre><code>interface PostObjectPropertyResolverInterface {\n  public function getContent($post_id);\n}<\/code><\/pre>\n<p>Being more conservative, the latter function signature makes the code potentially more reusable, however it is also less efficient, because the implementation will still need to retrieve the post object:<\/p>\n<div>\n<pre><code>class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {\n  public function getContent($post_id)\n  {\n    $post = get_post($post_id);\n    return $post->post_content;\n  }\n}<\/code><\/pre>\n<\/div>\n<p>In addition, some properties may be needed in their original value and also after applying some processing; for these cases, we will need to implement a corresponding extra function in our contract. For instance, the post content needs be accessed also as HTML, which is done through executing <code>apply_filters('the_content', $post->post_content)<\/code> in WordPress, or directly through property <code>$post->content_html<\/code> in October CMS. Hence, our contract may have 2 functions to resolve the content property:<\/p>\n<pre><code>interface PostObjectPropertyResolverInterface {\n  public function getContent($post_id); \/\/ = raw content\n  public function getHTMLContent($post_id);\n}<\/code><\/pre>\n<p>We must also be concerned with abstracting the value that the property can have. For instance, a comment is approved in WordPress if its property <code>comment_approved<\/code> has the value <code>\"1\"<\/code>. However, other CMSs may have a similar property with value <code>true<\/code>. Hence, the contract should remove any potential inconsistency or ambiguity:<\/p>\n<pre><code>interface CommentObjectPropertyResolverInterface {\n  public function isApproved($comment);\n}<\/code><\/pre>\n<p>Which is implemented for WordPress like this:<\/p>\n<div>\n<pre><code>class WPCommentObjectPropertyResolver implements CommentObjectPropertyResolverInterface {\n  public function isApproved($comment)\n  {\n    return $comment->comment_approved == \"1\";\n  }\n}<\/code><\/pre>\n<\/div>\n<h4>Global state<\/h4>\n<p>WordPress sets several variables in the global context, such as <code>global $post<\/code> when querying a single post. Keeping variables in the global context is considered an anti-pattern, since the developer could unintentionally override their values, producing bugs that are difficult to track down. Hence, abstracting our code gives us the chance to implement a better solution.<\/p>\n<p>An approach we can take is to create a corresponding class <code>AppState<\/code> which simply contains a property to store all variables that our application will need. In addition to initializing all core variables, we enable components to initialize their own ones through hooks:<\/p>\n<div>\n<pre><code>class AppState\n{\n  public static $vars = [];\n\n  public static function getVars()\n  {\n    return self::$vars;\n  }\n\n  public static function initialize()\n  {\n    \/\/ Initialize core variables\n    self::$vars['nature'] = $cmsRoutingService->getNature();\n    self::$vars['route'] = $cmsRoutingService->getRoute();\n\n    \/\/ Initialize $vars through hooks\n    self::$vars = $hooksAPIService->applyFilters(\"AppState:init\", self::$vars);\n\n    return self::$vars;\n  }\n}<\/code><\/pre>\n<\/div>\n<p>To replace <code>global $post<\/code>, a hook from WordPress can then set this data through a hook. A first step would be to set the data under <code>\"post-id\"<\/code>:<\/p>\n<pre><code>$hooksAPIService->addFilter(\n  \"AppState:init\", \n  function($vars) {\n    if (is_single()) {\n      global $post;\n      $vars['post-id'] => $post->ID;\n    }\n    return $vars;\n  }\n);<\/code><\/pre>\n<p>However, we can also abstract the global variables: instead of dealing with fixed entities (such as posts, users, comments, etc), we can deal with the entity in a generic way through <code>\"object-id\"<\/code>, and we obtain its properties by inquiring the nature of the requested route:<\/p>\n<pre><code>$hooksAPIService->addFilter(\n  \"AppState:init\", \n  function($vars) {\n    if ($vars['nature'] == 'post') {\n      global $post;\n      $vars['object-id'] => $post->ID;\n    }\n    return $vars;\n  }\n);<\/code><\/pre>\n<p>From now own, if we need to display a property of the current post, we access it from the newly defined class instead of the global context:<\/p>\n<pre><code>$vars = AppState::getVars();\n$object_id = $vars['object-id'];\n\/\/ Do something with it\n\/\/ ...<\/code><\/pre>\n<h4>Entity models (meta, post types, pages being posts, and taxonomies \u2014tags and categories\u2014)<\/h4>\n<p>We must abstract those decisions made for WordPress concerning how its entities are modeled. Whenever we consider that WordPress\u2019s opinionatedness makes sense in a generic context too, we can then replicate such a decision for our CMS-agnostic code.<\/p>\n<h5>Meta:<\/h5>\n<p>As mentioned earlier, the concept of \u201cmeta\u201d must be decoupled from the model entity (such as \u201cpost meta\u201d from \u201cposts\u201d), so if a CMS doesn\u2019t provide support for meta, it can then discard only this functionality.<\/p>\n<p>Then, package \u201cPost Meta\u201d (decoupled from, but dependent on, package \u201cPosts\u201d) defines the following contract:<\/p>\n<div>\n<pre><code>interface PostMetaAPIInterface\n{\n  public function getMetaKey($meta_key);\n  public function getPostMeta($post_id, $key, $single = false);\n  public function deletePostMeta($post_id, $meta_key, $meta_value = '');\n  public function addPostMeta($post_id, $meta_key, $meta_value, $unique = false);\n  public function updatePostMeta($post_id, $meta_key, $meta_value);\n}<\/code><\/pre>\n<\/div>\n<p>Which is resolved for WordPress like this:<\/p>\n<div>\n<pre><code>class WPPostMetaAPI implements PostMetaAPIInterface\n{\n  public function getMetaKey($meta_key)\n  {\n    return '_'.$meta_key;\n  }\n  public function getPostMeta($post_id, $key, $single = false)\n  {\n    return get_post_meta($post_id, $key, $single);\n  }\n  public function deletePostMeta($post_id, $meta_key, $meta_value = '')\n  {\n    return delete_post_meta($post_id, $meta_key, $meta_value);\n  }\n  public function addPostMeta($post_id, $meta_key, $meta_value, $unique = false)\n  {\n    return add_post_meta($post_id, $meta_key, $meta_value, $unique);\n  }\n  public function updatePostMeta($post_id, $meta_key, $meta_value)\n  {\n    return update_post_meta($post_id, $meta_key, $meta_value);\n  }\n}<\/code><\/pre>\n<\/div>\n<h5>Post types:<\/h5>\n<p>I have decided that WordPress\u2019s concept of a custom post type, which allows to model entities (such as an event or a portfolio) as extensions of posts, can apply in a generic context, and as such, I have replicated this functionality in the CMS-agnostic code. This decision is controversial, however, I justify it because the application may need to display a feed of entries of different types (such as posts, events, etc) and custom post types make such implementation feasible. Without custom post types, I would expect the application to need to execute several queries to bring the data for every entity type, and the logic would get all muddled up (for instance, if fetching 12 entries, should we fetch 6 posts and 6 events? but what if the events were posted much earlier than the last 12 posts? and so on).<\/p>\n<p>What happens when the CMS doesn\u2019t support this concept? Well, nothing serious happens: a post will still indicate its custom post type to be a \u201cpost\u201d, and no other entities will inherit from the post. The application will still work properly, just with some slight overhead from the unneeded code. This is a trade-off that, I believe, is more than worth it.<\/p>\n<p>To support custom post types, we simply add a function <code>getPostType<\/code> in our contract:<\/p>\n<pre><code>interface PostAPIInterface\n{\n  public function getPostType($post_id);\n}<\/code><\/pre>\n<p>Which is resolved for WordPress like this:<\/p>\n<pre><code>class WPPostAPI implements PostAPIInterface\n{\n  public function getPostType($post_id) {\n    return get_post_type($post_id);\n  }\n}<\/code><\/pre>\n<h5>Pages being posts:<\/h5>\n<p>While I justify keeping custom post types in order to extend posts, I don\u2019t justify a page being a post, as it happens in WordPress, because in other CMSs these entities are completely decoupled and, more importantly, a page may have higher rank than a post, so making a page extend from a post would make no sense. For instance, October CMS ships pages in its core functionality, but posts must be installed through plugins.<\/p>\n<p>Hence we must create separate contracts for posts and pages, even though they may contain the same functions:<\/p>\n<pre><code>interface PostAPIInterface\n{\n  public function getTitle($post_id);\n}\n\ninterface PageAPIInterface\n{\n  public function getTitle($page_id);\n}<\/code><\/pre>\n<p>To resolve these contracts for WordPress and avoid duplicating code, we can implement the common functionality through a trait:<\/p>\n<pre><code>trait WPCommonPostFunctions\n{\n  public function getTitle($post_id)\n  {\n    return get_the_title($post_id);\n  }\n}\n\nclass WPPostAPI implements PostAPIInterface\n{\n  use WPCommonPostFunctions;\n}\n\nclass WPPageAPI implements PageAPIInterface\n{\n  use WPCommonPostFunctions;\n}<\/code><\/pre>\n<h5>Taxonomies (tags and categories):<\/h5>\n<p>Once again, we can\u2019t expect all CMSs to support what is called taxonomies in WordPress: tags and categories. Hence, we must implement this functionality through a package \u201cTaxonomies\u201d, and, assuming that tags and categories are added to posts, make this package dependent on package \u201cPosts\u201d.<\/p>\n<pre><code>interface TaxonomyAPIInterface\n{\n  public function getPostCategories($post_id, $options = []);\n  public function getPostTags($post_id, $options = []);\n  public function getCategories($query, $options = []);\n  public function getTags($query, $options = []);\n  public function getCategory($cat_id);\n  public function getTag($tag_id);\n  ...\n}<\/code><\/pre>\n<p>We could have decided to create two separate packages \u201cCategories\u201d and \u201cTags\u201d instead of \u201cTaxonomies\u201d, however, as the implementation in WordPress makes evident, a tag and a category are basically the same concept of entity with only a tiny difference: categories are hierarchical (i.e. a category can have a parent category), but tags are not. Then, I consider that it makes sense to keep this concept for a generic context, and shipped under a single package \u201cTaxonomies\u201d.<\/p>\n<p>We must pay attention that certain functionalities involve both posts and taxonomies, and these must be appropriately decoupled. For instance, in WordPress we can retrieve posts that were tagged <code>\"politics\"<\/code> by executing <code>get_posts(['tag' => \"politics\"])<\/code>. In this case, while function <code>getPosts<\/code> must be implemented in package \u201cPosts\u201d, filtering by tags must be implemented in package \u201cTaxonomies\u201d. To accomplish this separation, we can simply execute a hook in the implementation of function <code>getPosts<\/code> for WordPress, allowing any component to modify the arguments before executing <code>get_posts<\/code>:<\/p>\n<pre><code>class WPPostAPI implements PostAPIInterface\n{\n  public function getPosts($args) {\n    $args = $hooksAPIService->applyFilters(\"modifyArgs\", $args);\n    return get_posts($args);\n  }\n}<\/code><\/pre>\n<p>And finally we implement the hook in package \u201cTaxonomies for WordPress\u201d:<\/p>\n<pre><code>$hooksAPIService->addFilter(\n  'modifyArgs',\n  function($args) {\n    if (isset($args['tags'])) {\n      $args['tag'] = implode(',', $args['tags']);\n      unset($args['tags']);\n    }\n    if (isset($args['categories'])) {\n      $args['cat'] = implode(',', $args['categories']);\n      unset($args['categories']);\n    }\n    return $args;\n  }\n);<\/code><\/pre>\n<p>Please notice that in the abstracted code the attributes were re-defined (following the recommendations for abstracting function parameters, explained earlier on): <code>\"tag\"<\/code> must be provided as <code>\"tags\"<\/code> and <code>\"cat\"<\/code> must be provided as <code>\"categories\"<\/code> (shifting the connotation from singular to plural), and these values must be passed as arrays (i.e. removed accepting comma-separated strings as in WordPress, to add consistency).<\/p>\n<h4>Translation<\/h4>\n<p>Because calls to translate strings are spread all over the application code, translation is not a functionality that we can opt out from, and we should make sure that the other frameworks are compatible with our chosen translation mechanism.<\/p>\n<p>In WordPress, which <a href=\"https:\/\/codex.wordpress.org\/I18n_for_WordPress_Developers#Introduction_to_Gettext\">implements internationalization through gettext<\/a>, we are required to set-up translation files for each locale code (such as \u2018fr_FR\u2019, which is the code for <strong>fr<\/strong>ench language from <strong>FR<\/strong>ance), and these can be set under a text domain (which allows themes or plugins to define their own translations without fear of collision with the translations from other pieces of code). We don\u2019t need to check for support for placeholders in the string to translate (such as when doing <code>sprintf(__(\"Welcome %s\"), $user_name)<\/code>), because function <code>sprintf<\/code> belongs to PHP and not to the CMS, so it will always work.<\/p>\n<p>Let\u2019s check if the other frameworks support the required two properties, i.e. getting the translation data for a specific locale composed of language and country, and under a specific text domain:<\/p>\n<ul>\n<li><a href=\"https:\/\/symfony.com\/doc\/current\/components\/translation.html\">Symfony\u2019s translation component<\/a> supports these two properties.<\/li>\n<li>The locale used in <a href=\"https:\/\/laravel.com\/docs\/6.x\/localization\">Laravel\u2019s localization<\/a> involves the language but not the country, and text domains are not supported (they could be replicated through overriding package language files, but the domain is not explicitly set, so the contract and the implementation would be inconsistent with each other).<\/li>\n<\/ul>\n<p>However, luckily there is library <a href=\"https:\/\/github.com\/zerospam\/laravel-gettext\">Laravel Gettext<\/a> which can replace Laravel\u2019s native implementation with Symfony\u2019s translation component. Hence, we got support for all frameworks, and we can rely on a WordPress-like solution.<\/p>\n<p>We can then define our contract mirroring the WordPress function signatures:<\/p>\n<pre><code>interface TranslationAPIInterface\n{\n  public function __($text, $domain = 'default');\n  public function _e($text, $domain = 'default');\n}<\/code><\/pre>\n<p>The implementation of the contract for WordPress is like this:<\/p>\n<pre><code>class WPTranslationAPI implements TranslationAPIInterface\n{\n  public function __($text, $domain = 'default')\n  {\n    return __($text, $domain);\n  }\n  public function _e($text, $domain = 'default')\n  {\n    _e($text, $domain);\n  }\n}<\/code><\/pre>\n<p>And to use it in our application, we do:<\/p>\n<div>\n<pre><code>$translationAPI = ContainerBuilderFactory::getInstance()->get('translation_api');\n$text = $translationAPI->__(\"translate this\", \"my-domain\");<\/code><\/pre>\n<\/div>\n<h4>Media<\/h4>\n<p>WordPress has media management as part of its core functionality, which represents a media element as an entity all by itself, and allows to manipulate the media element (such as cropping or resizing images), but we can\u2019t expect all CMSs to have similar functionality. Hence, media management must be decoupled from the CMS core functionality.<\/p>\n<p>For the corresponding contract, we can mirror the WordPress media functions, but removing WordPress\u2019s opinionatedness. For instance, in WordPress, a media element is a post (with post type <code>\"attachment\"<\/code>), but for the CMS-agnostic code it is not, hence the parameter must be <code>$media_id<\/code> (or <code>$image_id<\/code>) instead of <code>$post_id<\/code>. Similarly, WordPress treats media as attachments to posts, but this doesn\u2019t need to be the case everywhere, hence we can remove the word \u201cattachment\u201d from the function signatures. Finally, we can decide to keep the <code>$size<\/code> of the image in the contract; if the CMS doesn\u2019t support creating multiple image sizes for an image, then it can just fall back on its default value <code>NULL<\/code> and nothing grave happens:<\/p>\n<div>\n<pre><code>interface MediaAPIInterface\n{\n  public function getImageSrcAndDimensions($image_id, $size = null): array;\n  public function getImageURL($image_id, $size = null): string;\n}<\/code><\/pre>\n<\/div>\n<p>The response by function <code>getImageSrcAndDimensions<\/code> can be asbtracted too, returning an array of our own design instead of simply re-using the one from the WordPress function <code>wp_get_attachment_image_src<\/code>:<\/p>\n<div>\n<pre><code>class WPMediaAPI implements MediaAPIInterface\n{\n  public function getImageSrcAndDimensions($image_id, $size = null): array\n  {\n    $img_data = wp_get_attachment_image_src($image_id, $size);\n    return [\n      'src' => $img_data[0],\n      'width' => $img_data[1],\n      'height' => $img_data[2],\n    ];\n  }\n  public function getImageURL($image_id, $size = null): string\n  {\n    return wp_get_attachment_image_url($image_id, $size);\n  }\n}<\/code><\/pre>\n<\/div>\n<h3>Conclusion<\/h3>\n<p>Setting-up a CMS-agnostic architecture for our application can be a painful endeavor. As it was demonstrated in this article, abstracting all the code was a lengthy process, taking plenty of time and energy to achieve, and it is not even finished yet. I wouldn\u2019t be surprised if the reader is intimidated by the idea of going through this process in order to convert a WordPress application into a CMS-agnostic one. If I hadn\u2019t done the abstraction myself, I would certainly be intimidated too.<\/p>\n<p>My suggestion is for the reader is to analyze if going through this process makes sense based on a project-by-project basis. If there is no need whatsoever to port an application to a different CMS, then you will be right to stay away from this process and stick to the WordPress way. However, if you do need to migrate an application away from WordPress and want to reduce the effort required, or if you already need to maintain several codebases which would benefit from code reusability, or even if you may migrate the application sometime in the future and you have just started a new project, then this process is for you. It may be painful to implement, but well worth it. I know because I\u2019ve been there. But I\u2019ve survived, and I\u2019d certainly do it again. Thanks for reading.<\/p>\n<div>\n  <img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/www.taterboy.com\/blog\/wp-content\/uploads\/2019\/11\/logo-red-23.png?w=900&#038;ssl=1\" alt=\"Smashing Editorial\"><span>(dm, yk, il)<\/span>\n<\/div>\n<\/article>\n<p class=\"wpematico_credit\"><small>Powered by <a href=\"http:\/\/www.wpematico.com\" target=\"_blank\" rel=\"noopener noreferrer\">WPeMatico<\/a><\/small><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Leonardo Losoviz 2019-11-28T11:00:00+00:002019-11-28T11:07:08+00:00 In the first part of this series, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will&#8230;<a class=\"moretag\" href=\"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/\"> Read the full article&#8230;<\/a><\/p>\n","protected":false},"author":4,"featured_media":73434,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[371],"tags":[],"class_list":["post-73433","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-user-experience"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.0.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Leonardo Losoviz 2019-11-28T11:00:00+00:002019-11-28T11:07:08+00:00In the first part of this series, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will proceed to\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Guest Contribution\"\/>\n\t<meta name=\"keywords\" content=\"user experience\" \/>\n\t<link rel=\"canonical\" href=\"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.0.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"Design for Immersive Technologies | User Experience for Games, Virtual Reality (VR) and Augmented\/Mixed Reality (AR\/MR)\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) | Design for Immersive Technologies\" \/>\n\t\t<meta property=\"og:description\" content=\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Leonardo Losoviz 2019-11-28T11:00:00+00:002019-11-28T11:07:08+00:00In the first part of this series, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will proceed to\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2019-11-28T11:18:33+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2019-11-28T11:18:33+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary\" \/>\n\t\t<meta name=\"twitter:title\" content=\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) | Design for Immersive Technologies\" \/>\n\t\t<meta name=\"twitter:description\" content=\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Leonardo Losoviz 2019-11-28T11:00:00+00:002019-11-28T11:07:08+00:00In the first part of this series, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will proceed to\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#article\",\"name\":\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) | Design for Immersive Technologies\",\"headline\":\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)\",\"author\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/author\\\/guestcontribution\\\/#author\"},\"publisher\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/#organization\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/i0.wp.com\\\/www.taterboy.com\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/logo-red-23.png?fit=60%2C80&ssl=1\",\"width\":60,\"height\":80},\"datePublished\":\"2019-11-28T04:18:33-07:00\",\"dateModified\":\"2019-11-28T04:18:33-07:00\",\"inLanguage\":\"en-US\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#webpage\"},\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#webpage\"},\"articleSection\":\"User Experience\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.taterboy.com\\\/blog\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/category\\\/user-experience\\\/#listItem\",\"name\":\"User Experience\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/category\\\/user-experience\\\/#listItem\",\"position\":2,\"name\":\"User Experience\",\"item\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/category\\\/user-experience\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#listItem\",\"name\":\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog#listItem\",\"name\":\"Home\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#listItem\",\"position\":3,\"name\":\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/category\\\/user-experience\\\/#listItem\",\"name\":\"User Experience\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/#organization\",\"name\":\"Design for Immersive Technologies\",\"description\":\"User Experience for Games, Virtual Reality (VR) and Augmented\\\/Mixed Reality (AR\\\/MR)\",\"url\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/author\\\/guestcontribution\\\/#author\",\"url\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/author\\\/guestcontribution\\\/\",\"name\":\"Guest Contribution\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#authorImage\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/d2aad73eb0f48c67b141e9fc978a0e498969791ed623346e361d02d19775b0bb?s=96&d=mm&r=g\",\"width\":96,\"height\":96,\"caption\":\"Guest Contribution\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#webpage\",\"url\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/\",\"name\":\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) | Design for Immersive Technologies\",\"description\":\"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Leonardo Losoviz 2019-11-28T11:00:00+00:002019-11-28T11:07:08+00:00In the first part of this series, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will proceed to\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#breadcrumblist\"},\"author\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/author\\\/guestcontribution\\\/#author\"},\"creator\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/author\\\/guestcontribution\\\/#author\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/i0.wp.com\\\/www.taterboy.com\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/logo-red-23.png?fit=60%2C80&ssl=1\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#mainImage\",\"width\":60,\"height\":80},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/2019\\\/11\\\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\\\/#mainImage\"},\"datePublished\":\"2019-11-28T04:18:33-07:00\",\"dateModified\":\"2019-11-28T04:18:33-07:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/\",\"name\":\"Design for Immersive Technologies\",\"description\":\"User Experience for Games, Virtual Reality (VR) and Augmented\\\/Mixed Reality (AR\\\/MR)\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.taterboy.com\\\/blog\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) | Design for Immersive Technologies","description":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Leonardo Losoviz 2019-11-28T11:00:00+00:002019-11-28T11:07:08+00:00In the first part of this series, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will proceed to","canonical_url":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/","robots":"max-image-preview:large","keywords":"user experience","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#article","name":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) | Design for Immersive Technologies","headline":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)","author":{"@id":"https:\/\/www.taterboy.com\/blog\/author\/guestcontribution\/#author"},"publisher":{"@id":"https:\/\/www.taterboy.com\/blog\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/i0.wp.com\/www.taterboy.com\/blog\/wp-content\/uploads\/2019\/11\/logo-red-23.png?fit=60%2C80&ssl=1","width":60,"height":80},"datePublished":"2019-11-28T04:18:33-07:00","dateModified":"2019-11-28T04:18:33-07:00","inLanguage":"en-US","mainEntityOfPage":{"@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#webpage"},"isPartOf":{"@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#webpage"},"articleSection":"User Experience"},{"@type":"BreadcrumbList","@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/www.taterboy.com\/blog#listItem","position":1,"name":"Home","item":"https:\/\/www.taterboy.com\/blog","nextItem":{"@type":"ListItem","@id":"https:\/\/www.taterboy.com\/blog\/category\/user-experience\/#listItem","name":"User Experience"}},{"@type":"ListItem","@id":"https:\/\/www.taterboy.com\/blog\/category\/user-experience\/#listItem","position":2,"name":"User Experience","item":"https:\/\/www.taterboy.com\/blog\/category\/user-experience\/","nextItem":{"@type":"ListItem","@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#listItem","name":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)"},"previousItem":{"@type":"ListItem","@id":"https:\/\/www.taterboy.com\/blog#listItem","name":"Home"}},{"@type":"ListItem","@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#listItem","position":3,"name":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)","previousItem":{"@type":"ListItem","@id":"https:\/\/www.taterboy.com\/blog\/category\/user-experience\/#listItem","name":"User Experience"}}]},{"@type":"Organization","@id":"https:\/\/www.taterboy.com\/blog\/#organization","name":"Design for Immersive Technologies","description":"User Experience for Games, Virtual Reality (VR) and Augmented\/Mixed Reality (AR\/MR)","url":"https:\/\/www.taterboy.com\/blog\/"},{"@type":"Person","@id":"https:\/\/www.taterboy.com\/blog\/author\/guestcontribution\/#author","url":"https:\/\/www.taterboy.com\/blog\/author\/guestcontribution\/","name":"Guest Contribution","image":{"@type":"ImageObject","@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/d2aad73eb0f48c67b141e9fc978a0e498969791ed623346e361d02d19775b0bb?s=96&d=mm&r=g","width":96,"height":96,"caption":"Guest Contribution"}},{"@type":"WebPage","@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#webpage","url":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/","name":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) | Design for Immersive Technologies","description":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Leonardo Losoviz 2019-11-28T11:00:00+00:002019-11-28T11:07:08+00:00In the first part of this series, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will proceed to","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/www.taterboy.com\/blog\/#website"},"breadcrumb":{"@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#breadcrumblist"},"author":{"@id":"https:\/\/www.taterboy.com\/blog\/author\/guestcontribution\/#author"},"creator":{"@id":"https:\/\/www.taterboy.com\/blog\/author\/guestcontribution\/#author"},"image":{"@type":"ImageObject","url":"https:\/\/i0.wp.com\/www.taterboy.com\/blog\/wp-content\/uploads\/2019\/11\/logo-red-23.png?fit=60%2C80&ssl=1","@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#mainImage","width":60,"height":80},"primaryImageOfPage":{"@id":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/#mainImage"},"datePublished":"2019-11-28T04:18:33-07:00","dateModified":"2019-11-28T04:18:33-07:00"},{"@type":"WebSite","@id":"https:\/\/www.taterboy.com\/blog\/#website","url":"https:\/\/www.taterboy.com\/blog\/","name":"Design for Immersive Technologies","description":"User Experience for Games, Virtual Reality (VR) and Augmented\/Mixed Reality (AR\/MR)","inLanguage":"en-US","publisher":{"@id":"https:\/\/www.taterboy.com\/blog\/#organization"}}]},"og:locale":"en_US","og:site_name":"Design for Immersive Technologies | User Experience for Games, Virtual Reality (VR) and Augmented\/Mixed Reality (AR\/MR)","og:type":"article","og:title":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) | Design for Immersive Technologies","og:description":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Leonardo Losoviz 2019-11-28T11:00:00+00:002019-11-28T11:07:08+00:00In the first part of this series, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will proceed to","og:url":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/","article:published_time":"2019-11-28T11:18:33+00:00","article:modified_time":"2019-11-28T11:18:33+00:00","twitter:card":"summary","twitter:title":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) | Design for Immersive Technologies","twitter:description":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2) Leonardo Losoviz 2019-11-28T11:00:00+00:002019-11-28T11:07:08+00:00In the first part of this series, we learned the key concepts to build an application that is as CMS-agnostic as possible. In this second and final part, we will proceed to"},"aioseo_meta_data":{"post_id":"73433","title":null,"description":null,"keywords":null,"keyphrases":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_custom_url":null,"og_image_custom_fields":null,"og_custom_image_width":null,"og_custom_image_height":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"location":null,"local_seo":null,"created":"2021-02-07 16:36:31","updated":"2026-09-02 14:12:14","focus_keyword":null,"additional_keywords":null,"truseo_locale":null,"primary_term":null,"og_image_url":null,"og_image_width":null,"og_image_height":null,"twitter_image_url":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"Article","isEnabled":true},"graphs":[]},"limit_modified_date":false,"ai":null,"breadcrumb_settings":null,"seo_analyzer_scan_date":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/www.taterboy.com\/blog\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">&raquo;<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/www.taterboy.com\/blog\/category\/user-experience\/\" title=\"User Experience\">User Experience<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">&raquo;<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tAbstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/www.taterboy.com\/blog"},{"label":"User Experience","link":"https:\/\/www.taterboy.com\/blog\/category\/user-experience\/"},{"label":"Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)","link":"https:\/\/www.taterboy.com\/blog\/2019\/11\/abstracting-wordpress-code-to-reuse-with-other-cmss-implementation-part-2\/"}],"jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p8wzr5-j6p","jetpack_featured_media_url":"https:\/\/i0.wp.com\/www.taterboy.com\/blog\/wp-content\/uploads\/2019\/11\/logo-red-23.png?fit=60%2C80&ssl=1","_links":{"self":[{"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/posts\/73433","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/comments?post=73433"}],"version-history":[{"count":0,"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/posts\/73433\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/media\/73434"}],"wp:attachment":[{"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/media?parent=73433"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/categories?post=73433"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.taterboy.com\/blog\/wp-json\/wp\/v2\/tags?post=73433"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}