abstracting-wordpress-code-to-reuse-with-other-cms

About The Author

Leonardo Losoviz is a freelance developer and writer, with an ongoing quest to integrate innovative paradigms (Serverless PHP, server-side components, GraphQL) …
More about
Leonardo

Making our code CMS-agnostic, as much as possible, enables us to easily port our application to another CMS if the need arises. In this article we will learn how to abstract a WordPress application, making its code readily available for other frameworks or CMSs.

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 proceed to abstract a WordPress application, making its code ready to be used with Symfony components, Laravel framework, and October CMS (which is based on Laravel).

Accessing Services

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 Symfony’s DependencyInjection component. To access the defined services, we create a class ContainerBuilderFactory which simply stores a static instance of the component’s ContainerBuilder object:

use SymfonyComponentDependencyInjectionContainerBuilder;

class ContainerBuilderFactory {
  private static $instance;
  public static function init()
  {
    self::$instance = new ContainerBuilder();
  }
  public static function getInstance()
  {
    return self::$instance;
  }
}

Then, to access a service called "cache", the application requests it like this:

$cacheService = ContainerBuilderFactory::getInstance()->get('cache');
// Do something with the service
// $cacheService->...

Abstracting WordPress Code

We have identified the following pieces of code and concepts from a WordPress application that need be abstracted away from WordPress’s opinionatedness:

  • accessing functions
  • function names
  • function parameters
  • states (and other constant values)
  • CMS helper functions
  • user permissions
  • application options
  • database column names
  • errors
  • hooks
  • routing
  • object properties
  • global state
  • entity models (meta, post types, pages being posts, and taxonomies —tags and categories—)
  • translation
  • media.

Let’s proceed to abstract them, one by one.

Note: 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 PSR-4, is a must! Among other advantages, the application can then benefit from autoloading, and Symfony’s dependency injection can rely on automatic service loading as to reduce its configuration to the bare minimum.

Accessing functions

The mantra “code against interfaces, not implementations” 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.

For instance, if our application accesses function get_posts:

$posts = get_posts($args);

We must then abstract this function under some contract:

interface PostAPIInterface
{
  public function getPosts($args);
}

The contract must be implemented for WordPress:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args) {
    return get_posts($args);
  }
}

A service "posts_api" must be added to the dependency injection services.yaml configuration file, indicating which class resolves the service:

services:
  posts_api:
    class: WPPostAPI

And finally, the application can reference the function through service "posts_api":

$postsAPIService = ContainerBuilderFactory::getInstance()->get('posts_api');
$posts = $postsAPIService->getPosts($args);

Function names

If you have noticed from the code demonstrated above, function get_posts is abstracted as getPosts. There are a couple of reasons why this is a good idea:

  • By calling the function differently, it helps identify which code belongs to WordPress and which code belongs to our abstracted application.
  • Function names must be camelCased to comply with PSR-2, which attempts to define a standard for writing PHP code.

Certain functions can be redefined, making more sense in an abstract context. For instance, WordPress function get_user_by($field, $value) uses parameter $field with values "id", "ID", "slug", "email" or "login" to know how to get the user. Instead of replicating this methodology, we can explicitly define a separate function for each of them:

interface UsersAPIInterface
{
  public function getUserById($value);
  public function getUserByEmail($value);
  public function getUserBySlug($value);
  public function getUserByLogin($value);
}

And these are resolved for WordPress:

class WPUsersAPI implements UsersAPIInterface
{
  public function getUserById($value)
  {
    return get_user_by('id', $value);
  }
  public function getUserByEmail($value)
  {
    return get_user_by('email', $value);
  }
  public function getUserBySlug($value)
  {
    return get_user_by('slug', $value);
  }
  public function getUserByLogin($value)
  {
    return get_user_by('login', $value);
  }
}

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 get_the_author_meta can receive parameter "user_lastname", indicating that the user’s lastname is stored as a “meta” 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 "lastname" in the user table, so it doesn’t apply as a meta value. (The actual definition of “meta” value is actually inconsistent in WordPress: function get_the_author_meta also accepts value "user_email", even though the email is stored on the user table. Hence, I’d rather stick to my definition of “meta” value, and remove all inconsistencies from the abstracted code.)

Then, our contract will implement the following functions:

interface UsersAPIInterface
{
  public function getUserDisplayName($user_id);
  public function getUserEmail($user_id);
  public function getUserFirstname($user_id);
  public function getUserLastname($user_id);
  ...
}

Which are resolved for WordPress:

class WPUsersAPI implements UsersAPIInterface
{
  public function getUserDisplayName($user_id)
  {
    return get_the_author_meta('display_name', $user_id);
  }
  public function getUserEmail($user_id)
  {
    return get_the_author_meta('user_email', $user_id);
  }
  public function getUserFirstname($user_id)
  {
    return get_the_author_meta('user_firstname', $user_id);
  }
  public function getUserLastname($user_id)
  {
    return get_the_author_meta('user_lastname', $user_id);
  }
  ...
}

Our functions could also be re-defined as to remove the limitations from WordPress. For instance, function update_user_meta($user_id, $meta_key, $meta_value) 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 updateUserMetaAttributes($user_id, $meta) which can update several meta values at the same time:

interface UserMetaInterface
{
  public function updateUserMetaAttributes($user_id, $meta);
}

Which is resolved for WordPress like this:

class WPUsersAPI implements UsersAPIInterface
{
  public function updateUserMetaAttributes($user_id, $meta)
  {
    foreach ($meta as $meta_key => $meta_value) {
      update_user_meta($user_id, $meta_key, $meta_value);
    }
  }
}

Finally, we may want to re-define a function to remove its ambiguities. For instance, WordPress function add_query_arg can receive parameters in two different ways:

  1. Using a single key and value: add_query_arg('key', 'value', 'http://example.com');
  2. Using an associative array: add_query_arg(['key1' => 'value1', 'key2' => 'value2'], 'http://example.com');

This becomes difficult to keep consistent across CMSs. Hence, our contract can define functions addQueryArg (singular) and addQueryArgs (plural) as to remove the ambiguity:

public function addQueryArg(string $key, string $value, string $url);
public function addQueryArgs(array $key_values, string $url);

Function parameters

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:

  • renaming and/or re-defining the parameters;
  • renaming and/or re-defining the attributes passed on array parameters.

For instance, WordPress function get_posts receives a unique parameter $args, which is an array of attributes. One of its attributes is fields which, when given the value "ids", 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’d prefer a different solution: Convey this information through a separate parameter called $options, under attribute "return-type".

To accomplish this, we add parameter $options to the function in our contract:

interface PostAPIInterface
{
  public function getPosts($args, $options = []);
}

Instead of referencing WordPress constant value "ids" (which we can’t guarantee will be the one used in all other CMSs), we create a corresponding constant value for our abstracted application:

class Constants
{
  const RETURNTYPE_IDS = 'ids';
}

The WordPress implementation must map and recreate the parameters between the contract and the implementation:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args, $options = []) {
    if ($options['return-type'] == Constants::RETURNTYPE_IDS) {
      $args['fields'] = 'ids';
    }
    return get_posts($args);
  }
}

And finally, we can execute the code through our contract:

$options = [
  'return-type' => Constants::RETURNTYPE_IDS,
];
$post_ids = $postsAPIService->getPosts($args, $options);

While abstracting the parameters, we should avoid transferring WordPress’s technical debt to our abstracted code, whenever possible. For instance, parameter $args from function get_posts can contain attribute 'post_type'. This attribute name is somewhat misleading, since it can receive one element (post_type => "post") but also a list of them (post_type => "post, event"), so this name should be in plural instead: post_types. When abstracting this piece of code, we can set our interface to expect attribute post_types instead, which will be mapped to WordPress’s post_type.

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 $args, WordPress function get_posts accepts attribute posts_per_page, and function get_users accepts attribute number. These attribute names can perfectly be replaced with the more generic attribute name limit.

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 "_" with "-", so our newly-defined argument post_types becomes post-types.

Applying these prior considerations, our abstracted code will look like this:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args, $options = []) {
    ...
    if (isset($args['post-types'])) {
      $args['post_type'] = $args['post-types'];
      unset($args['post-types']);
    }
    if (isset($args['limit'])) { 
      $args['posts_per_page'] = $args['limit'];
      unset($args['limit']);
    }
    return get_posts($args);
  }
}

We can also re-define attributes to modify the shape of their values. For instance, WordPress parameter $args in function get_posts can receive attribute date_query, whose properties ("after", "inclusive", etc) can be considered specific to WordPress:

$date = current_time('timestamp');
$args['date_query'] = array(
  array(
    'after' => date('Y-m-d H:i:s', $date),
    'inclusive' => true,
  )
);

To unify the shape of this value into something more generic, we can re-implement it using other arguments, such as "date-from" and "date-from-inclusive" (this solution is not 100% convincing though, since it is more verbose than WordPress’s):

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args, $options = []) {
    ...
    if (isset($args['date-from'])) {
      $args['date_args'][] = [
        'after' => $args['date-from'],
        'inclusive' => false,
      ];
      unset($args['date-from']);
    }
    if (isset($args['date-from-inclusive'])) {
      $args['date_args'][] = [
        'after' => $args['date-from-inclusive'],
        'inclusive' => true,
      ];
      unset($args['date-from-inclusive']);
    }
    return get_posts($args);
  }
}

In addition, we need to consider if to abstract or not those parameters which are too specific to WordPress. For instance, function get_posts allows to order posts by attribute menu_order, which I don’t think it works in a generic context. Then, I’d rather not abstract this code and keep it on the CMS-specific package for WordPress.

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:

interface PostAPIInterface
{
  public function getPosts(array $args, array $options = []): array;
}

States (and other constant values)

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: "publish", "pending", "draft" or "trash". 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:

class PostStates {
  const PUBLISHED = 'published';
  const PENDING = 'pending';
  const DRAFT = 'draft';
  const TRASH = 'trash';
}

As it can be seen, the actual constant values may or may not be the same as in WordPress: while "publish" was renamed as "published", the other ones remain the same.

For the implementation for WordPress, we convert from the agnostic value to the WordPress-specific one:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args, $options = []) {
    ...
    if (isset($args['post-status'])) {
      $conversion = [
        PostStates::PUBLISHED => 'publish',
        PostStates::PENDING => 'pending',
        PostStates::DRAFT => 'draft',
        PostStates::TRASH => 'trash',
      ];
      $args['post_status'] = $conversion[$args['post-status']];
      unset($args['post-status']);
    }
    return get_posts($args);
  }
}

Finally, we can reference these constants throughout our CMS-agnostic application:

$args = [
  'post-status' => PostStates::PUBLISHED,
];
$posts = $postsAPIService->getPosts($args);

This strategy works under the assumption that all CMSs will support these states. If any CMS does not support a particular state (eg: "pending") then it should throw an exception whenever a corresponding functionality is invoked.

CMS helper functions

WordPress implements several helper functions that must also abstracted, such as make_clickable. 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.

We first define the contract:

interface HelperAPIInterface
{
  public function makeClickable(string $text);
}

And provide a default behaviour for the helper functions through an abstract class:

abstract class AbstractHelperAPI implements HelperAPIInterface
{
  public function makeClickable(string $text) {
    return preg_replace('!(((f|ht)tp(s)?://)[-a-zA-Zа-яА-Я()0-9@:%_ .~#?&;//=] )!i', '$1', $text);
  }
}

Now, our application can either use this functionality or, if it runs on WordPress, use the WordPress-specific implementation:

class WPHelperAPI extends AbstractHelperAPI
{
  public function makeClickable(string $text) {
    return make_clickable($text);
  }
}

User permissions

For all CMSs which support user management, in addition to abstracting the corresponding functions (such as current_user_can and user_can 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’t satisfy it. For instance, if the application needs to validate if the user can edit posts, it can represent it through a capability called "capability:editPosts", which is satisfied for WordPress through its capability "edit_posts".

This is still an instance of the “code against interfaces, not implementations” 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’t 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 "capability:editPosts") has been satisfied through a capability by the CMS. This concept, which I call a “loose contract”, will need to be handled by our application, on runtime.

To deal with “loose contracts”, I have created a service LooseContractService through which:

  • the application can define what “contract names” must be implemented, through function requireNames.
  • the CMS-specific implementations can satisfy those names, through function implementNames.
  • the application can get the implementation of a name through function getImplementedName.
  • the application can also inquire for all non-satisfied required names through function getNotImplementedRequiredNames, as to throw an exception or log the error if needed.

The service looks like this:

class LooseContractService
{
  protected $requiredNames = [];
  protected $nameImplementations = [];

  public function requireNames(array $names): void
  {
    $this->requiredNames = array_merge(
      $this->requiredNames,
      $names
    );
  }

  public function implementNames(array $nameImplementations): void
  {
    $this->nameImplementations = array_merge(
      $this->nameImplementations,
      $nameImplementations
    );
  }

  public function getImplementedName(string $name): ?string {
    return $this->nameImplementations[$name];
  }

  public function getNotImplementedRequiredNames(): array {
    return array_diff(
      $this->requiredNames,
      array_keys($this->nameImplementations)
    );
  }
}

The application, when initialized, can then establish loose contracts by requiring names:

$looseContractService = ContainerBuilderFactory::getInstance()->get('loose_contracts');
$looseContractService->requireNames([
  'capability:editPosts',
]);

And the CMS-specific implementation can satisfy these:

$looseContractService->implementNames([
  'capability:editPosts' => 'edit_posts',
]);

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:

$cmsCapabilityName = $looseContractService->getImplementedName('capability:editPosts');
if (!$cmsCapabilityName) {
  throw new Exception(sprintf(
    "The CMS has no support for capability "%s"",
    'capability:editPosts'
  ));
}
// Now can use the capability to check for permissions
$userManagementAPIService = ContainerBuilderFactory::getInstance()->get('user_management_api');
if ($userManagementAPIService->userCan($user_id, $cmsCapabilityName)) {
  ...
}

Alternatively, the application can also fail when first initialized if any one required name is not satisfied:

if ($notImplementedNames = $looseContractService->getNotImplementedRequiredNames()) {
  throw new Exception(sprintf(
    "The CMS has not implemented loose contract names %s",
    implode(', ', $notImplementedNames)
  ));
}

Application options

WordPress ships with several application options, such as those stored in table wp_options under entries "blogname", "blogdescription", "admin_email", "date_format" and many others. Abstracting application options involves:

  • abstraction the function getOption;
  • abstracting each of the required options, aiming to make the CMS satisfy the notion of this option (eg: if a CMS doesn’t have an option for the site’s description, it can’t return the site’s name instead).

Let’s solve these 2 actions in turn. Concerning function getOption, I believe that we can expect all CMSs to support storing and retrieving options, so we can place the corresponding function under a CMSCoreInterface contract:

interface CMSCoreInterface
{
  public function getOption($option, $default = false);
}

As it can be observed from the function signature above, I’m making the assumption that each option will also have a default value. However, I don’t know if every CMS allows setting default values for options. But it doesn’t matter since the implementation can simply return NULL then.

This function is resolved for WordPress like this:

class WPCMSCore implements CMSCoreInterface
{
  public function getOption($option, $default = false)
  {
    return get_option($option, $default);
  }
}

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 getOption, we can’t expect it to implement each single option used by WordPress, such as "use_smiles" or "default_ping_status". Hence, we must first filter all options, and abstract only those that make sense in a generic context, such as "siteName" or "dateFormat".

Then, having the list of options to abstract, we can use a “loose contract” (as explained earlier on) and require a corresponding option name for each, such as "option:siteName" (resolved for WordPress as "blogname") or "option:dateFormat" (resolved as "date_format").

Database column names

In WordPress, when we are requesting data from function get_posts we can set attribute "orderby" in $args to order the results, which can be based on a column from the posts table (such as values "ID", "title", "date", "comment_count", etc), a meta value (through values "meta_value" and "meta_value_num") or other values (such as "post__in" and "rand").

Whenever the value corresponds to the table column name, we can abstract them using a “loose contract”, as explained earlier on. Then, the application can reference a loose contract name:

$args = [
  'orderby' => $looseContractService->getImplementedName('dbcolumn:orderby:posts:date'),
];
$posts = $postsAPIService->getPosts($args);

And this name is resolved for WordPress:

$looseContractService->implementNames([
  'dbcolumn:orderby:posts:date' => 'date',
]);

Now, let’s say that in our WordPress application we have created a meta value "likes_count" (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 "meta_key", like this:

$args = [
  'orderby' => 'meta_value',
  'meta_key' => 'likes_count',
];

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.

Let’s do that. First, I create a helper class to retrieve the CMS-agnostic query:

class QueryHelper
{
  public function getOrderByQuery()
  {
    return array(
      'orderby' => $looseContractService->getImplementedName('dbcolumn:orderby:posts:likesCount'),
    );
  }
}

The OctoberCMS-specific package can add a column "likes_count" to the posts table, and resolve name "dbcolumn:orderby:posts:likesCount" to "like_count" and it will work. The WordPress-specific package, though, must resolve "dbcolumn:orderby:posts:likesCount" as "meta_value" and then override the helper function to add the additional property "meta_key":

class WPQueryHelper extends QueryHelper
{
  public function getOrderByQuery()
  {
    $query = parent::getOrderByQuery();
    $query['meta_key'] = 'likes_count';
    return $query;
  }
}

Finally, we set-up the helper query class as a service in the ContainerBuilder, configure it to be resolved to the WordPress-specific class, and we obtain the query for ordering results:

$queryHelperService = ContainerBuilderFactory::getInstance()->get('query_helper');
$args = $queryHelperService->getOrderByQuery();
$posts = $postsAPIService->getPosts($args);

Abstracting the values for ordering results that do not correspond to column names or meta properties (such as "post__in" and "rand") seems to be more difficult. Because my application doesn’t use them, I haven’t 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.

Errors

When dealing with errors, we must consider abstracting the following elements:

  • the definition of an error;
  • error codes and messages.

Let’s review these in turn.

Definition of an error:

An Error is a special object, different than an Exception, used to indicate that some operation has failed, and why it failed. WordPress represents errors through class WP_Error, and allows to check if some returned value is an error through function is_wp_error.

We can abstract checking for an error:

interface CMSCoreInterface
{
  public function isError($object);
}

Which is resolved for WordPress like this:

class WPCMSCore implements CMSCoreInterface
{
  public function isError($object)
  {
    return is_wp_error($object);
  }
}

However, to deal with errors in our abstracted code, we can’t expect all CMSs to have an error class with the same properties and methods as WordPress’s WP_Error 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.

The abstract error class Error is simply a slightly modified version from WordPress’s WP_Error class:

class Error {

  protected $errors = array();
  protected $error_data = array();

  public function __construct($code = null, $message = null, $data = null) 
  {
    if ($code) {
      $this->errors[$code][] = $message;
      if ($data) {
        $this->error_data[$code] = $data;
      }
    }
  }

  public function getErrorCodes()
  {
    return array_keys($this->errors);
  }

  public function getErrorCode()
  {    
    if ($codes = $this->getErrorCodes()) {
      return $codes[0];
    }

    return null;
  }

  public function getErrorMessages($code = null)
  {    
    if ($code) {
      return $this->errors[$code] ?? [];
    }

    // Return all messages if no code specified.
    return array_reduce($this->errors, 'array_merge', array());
  }

  public function getErrorMessage($code = null)
  {
    if (!$code) {
      $code = $this->getErrorCode();
    }
    $messages = $this->getErrorMessages($code);
    return $messages[0] ?? '';
  }

  public function getErrorData($code = null)
  {
    if (!$code) {
      $code = $this->getErrorCode();
    }

    return $this->error_data[$code];
  }

  public function add($code, $message, $data = null)
  {
    $this->errors[$code][] = $message;
    if ($data) {
      $this->error_data[$code] = $data;
    }
  }

  public function addData($data, $code = null)
  {
    if (!$code) {
      $code = $this->getErrorCode();
    }

    $this->error_data[$code] = $data;
  }

  public function remove($code)
  {
    unset($this->errors[$code]);
    unset($this->error_data[$code]);
  }
}

We implement a function to convert from the CMS to the abstract error through a helper class:

class WPHelpers
{
  public static function returnResultOrConvertError($result)
  {
    if (is_wp_error($result)) {
      // Create a new instance of the abstracted error class
      $error = new Error();
      foreach ($result->get_error_codes() as $code) {
        $error->add($code, $result->get_error_message($code), $result->get_error_data($code));
      }
      return $error;
    }
    return $result;
  }
}

And we finally invoke this method for all functions that may return an error:

class UserManagementService implements UserManagementInterface
{
  public function getPasswordResetKey($user_id)
  {
    $result = get_password_reset_key($user_id);
    return WPHelpers::returnResultOrConvertError($result);
  }
}
Error codes and messages:

Every CMS will have its own set of error codes and corresponding explanatory messages. For instance, WordPress function get_password_reset_key can fail due to the following reasons, as represented by their error codes and messages:

  1. "no_password_reset": Password reset is not allowed for this user.
  2. "no_password_key_update": Could not save password reset key to database.

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 returnResultOrConvertError explained above).

Hooks

Abstracting hooks involves:

  • the hook functionality;
  • the hooks themselves.

Let’s analyze these in turn.

Abstracting the hook functionality

WordPress offers the concept of “hooks”: a mechanism through which we can change a default behavior or value (through “filters”) and execute related functionality (through “actions”). Both Symfony and Laravel offer mechanisms somewhat related to hooks: Symfony provides an event dispatcher component, and Laravel’s mechanism is called events; these 2 mechanisms are similar, sending notifications of events that have already taken place, to be processed by the application through listeners.

When comparing these 3 mechanisms (hooks, event dispatcher and events) we find that WordPress’s 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’s component requires to instantiate a new object to pass additional information, and Laravel’s solution suggests to run a command in Artisan (Laravel’s 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 $value = apply_filters("modifyValue", $value, $post_id); is as simple as it can get.

In the first part of this series, 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 “hook” concept in the CMS-agnostic application, and this solution is the one implemented by WordPress.

In order to decouple the CMS-agnostic hooks from those from WordPress, once again we must “code against interfaces, not implementations”: We define a contract with the corresponding hook functions:

interface HooksAPIInterface
{
  public function addFilter(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void;
  public function removeFilter(string $tag, $function_to_remove, int $priority = 10): bool;
  public function applyFilters(string $tag, $value, ...$args);
  public function addAction(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void;
  public function removeAction(string $tag, $function_to_remove, int $priority = 10): bool;
  public function doAction(string $tag, ...$args): void;
}

Please notice that functions applyFilters and doAction are variadic, i.e. they can receive a variable amount of arguments through parameter ...$args. 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 ...$args to a function, we can easily provide the implementation for WordPress:

class WPHooksAPI implements HooksAPIInterface
{
  public function addFilter(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void
  {
    add_filter($tag, $function_to_add, $priority, $accepted_args);
  }

  public function removeFilter(string $tag, $function_to_remove, int $priority = 10): bool
  {
    return remove_filter($tag, $function_to_remove, $priority);
  }

  public function applyFilters(string $tag, $value, ...$args)
  {
    return apply_filters($tag, $value, ...$args);
  }

  public function addAction(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void
  {
    add_action($tag, $function_to_add, $priority, $accepted_args);
  }

  public function removeAction(string $tag, $function_to_remove, int $priority = 10): bool
  {
    return remove_action($tag, $function_to_remove, $priority);
  }

  public function doAction(string $tag, ...$args): void
  {
    do_action($tag, ...$args);
  }
}

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 this one, this one or this one.

Finally, whenever we need to execute a hook, we do it through the corresponding service:

$hooksAPIService = ContainerBuilderFactory::getInstance()->get('hooks_api');
$title = $hooksAPIService->applyFilters("modifyTitle", $title, $post_id);
Abstracting the hooks themselves

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 "init" (triggered when the system has been initialized) or filter "the_title" (triggered to modify a post’s 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 "init"; certain other hooks can be considered too specific to WordPress, such as filter "rest_{$this->post_type}_query" from a REST controller, so we don’t need to abstract them.)

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 "init" (as defined in WordPress), any code in our application must add an action on hook "cms:init", and then we implement the bridge in the WordPress-specific package from "init" to "cms:init":

$hooksAPIService->addAction('init', function() use($hooksAPIService) {
  $hooksAPIService->doAction('cms:init');
});

Finally, the application can add a “loose contract” name for "cms:init", and the CMS-specific package must implement it (as demonstrated earlier on).

Routing

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:

  • In WordPress, URLs map to database queries, not to routes.
  • Symfony provides a Routing component 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.
  • Laravel’s routing builds on top of Symfony’s routing component to adapt it to the Laravel framework.

As it can be seen, WordPress’s solution is the outlier here: the concept of mapping URLs to database queries is tightly coupled to WordPress’s 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’t use a database). Instead, it makes more sense to use Symfony’s approach as its default behavior, and allow WordPress to override this behavior with its own routing mechanism.

(Indeed, while WordPress’s 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 "[contact_form]" to it as content, which is not as clean as simply mapping the route to its corresponding controller directly.)

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’s configuration.

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 Cortex. Making use of Cortex, the application running on WordPress can have it both ways:

  • if there is a custom-defined route matching the requested URL, use its corresponding controller.
  • 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).

To implement this functionality, I have designed the contract CMSRoutingInterface to, given the requested URL, calculate two pieces of information:

  • the actual route, such as contact, posts or posts/my-first-post.
  • the nature of the route: core nature values "standard", "home" and "404", and additional nature values added through packages such as "post" through a “Posts” package or "user" through a “Users” package.

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 global $post. It also helps identify which case we want to handle, to avoid inconsistencies. For instance, we could have defined a custom route contact handled by a controller, which will have nature "standard", and also a page in WordPress with slug "contact", which will have nature "page" (added through a package called “Pages”). Then, our application can prioritize which way to handle the request, either through the controller or through a database query.

Let’s implement it. We first define the service’s contract:

interface CMSRoutingInterface
{
  public function getNature();
  public function getRoute();
}

We can then define an abstract class which provides a base implementation of these functions:

abstract class AbstractCMSRouting implements CMSRoutingInterface
{
  const NATURE_STANDARD = 'standard';
  const NATURE_HOME = 'home';
  const NATURE_404 = '404';

  public function getNature()
  {
    return self::NATURE_STANDARD;
  }

  public function getRoute()
  {
    // By default, the URI path is already the route (minus parameters and trailing slashes)
    $route = $_SERVER['REQUEST_URI'];
    $params_pos = strpos($route, '?');
    if ($params_pos !== false) {
       $route = substr($route, 0, $params_pos);
    }
    return trim($route, '/');
  }
}

And the implementation is overriden for WordPress:

class WPCMSRouting extends AbstractCMSRouting
{
  const ROUTE_QUERY = [
    'custom_route_key' => 'custom_route_value',
  ];
  private $query;
  private function init()
  {
    if (is_null($this->query)) {
      global $wp_query;
      $this->query = $wp_query;
    }
  }

  private function isStandardRoute() {
    return !empty(array_intersect($this->query->query_vars, self::ROUTE_QUERY));
  }

  public function getNature()
  {
    $this->init();
    if ($this->isStandardRoute()) {
      return self::NATURE_STANDARD;
    } elseif ($this->query->is_home() || $this->query->is_front_page()) {
      return self::NATURE_HOME;
    } elseif ($this->query->is_404()) {
      return self::NATURE_404;
    }

    // Allow components to implement their own natures
    $hooksAPIService = ContainerBuilderFactory::getInstance()->get('hooks_api');
    return $hooksAPIService->applyFilters(
      "nature",
      parent::getNature(),
      $this->query
    );
  }
}

In the code above, please notice how constant ROUTE_QUERY is used by the service to know if the route is a custom-defined one, as configured through Cortex:

$hooksAPIService->addAction(
  'cortex.routes', 
  function(RouteCollectionInterface $routes) {  
    // Hook into filter "routes" to provide custom-defined routes
    $appRoutes = $hooksAPIService->applyFilters("routes", []);
    foreach ($appRoutes as $route) {
      $routes->addRoute(new QueryRoute(
        $route,
        function (array $matches) {
          return WPCMSRouting::ROUTE_QUERY;
        }
      ));
    }
  }
);

Finally, we add our routes through hook "routes":

$hooksAPIService->addFilter(
  'routes',
  function($routes) {
    return array_merge(
      $routes,
      [
        'contact',
        'posts',
      ]
    );
  }
);

Now, the application can find out the route and its nature, and proceed accordingly (for instance, for a "standard" nature invoke its controller, or for a "post" nature invoke WordPress’s templating system):

$cmsRoutingService = ContainerBuilderFactory::getInstance()->get('routing');
$nature = $cmsRoutingService->getNature();
$route = $cmsRoutingService->getRoute();
// Process the requested route, as appropriate
// ...

Object properties

A rather inconvenient consequence of abstracting our code is that we can’t 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 $post contains its ID under $post->ID in WordPress and under $post->id in October CMS. To resolve this property, our contract PostObjectPropertyResolverInterface will contain function getId:

interface PostObjectPropertyResolverInterface {
  public function getId($post);
}

Which is resolved for WordPress like this:

class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {
  public function getId($post)
  {
    return $post->ID;
  }
}

Similarly, the post content property is $post->post_content in WordPress and $post->content in October CMS. Our contract will then allow to access this property through function getContent:

interface PostObjectPropertyResolverInterface {
  public function getContent($post);
}

Which is resolved for WordPress like this:

class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {
  public function getContent($post)
  {
    return $post->post_content;
  }
}

Please notice that function getContent receives the object itself through parameter $post. 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’t want to make the previous assumption, then it makes more sense for function getContent to receive the post’s ID instead:

interface PostObjectPropertyResolverInterface {
  public function getContent($post_id);
}

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:

class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {
  public function getContent($post_id)
  {
    $post = get_post($post_id);
    return $post->post_content;
  }
}

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 apply_filters('the_content', $post->post_content) in WordPress, or directly through property $post->content_html in October CMS. Hence, our contract may have 2 functions to resolve the content property:

interface PostObjectPropertyResolverInterface {
  public function getContent($post_id); // = raw content
  public function getHTMLContent($post_id);
}

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 comment_approved has the value "1". However, other CMSs may have a similar property with value true. Hence, the contract should remove any potential inconsistency or ambiguity:

interface CommentObjectPropertyResolverInterface {
  public function isApproved($comment);
}

Which is implemented for WordPress like this:

class WPCommentObjectPropertyResolver implements CommentObjectPropertyResolverInterface {
  public function isApproved($comment)
  {
    return $comment->comment_approved == "1";
  }
}

Global state

WordPress sets several variables in the global context, such as global $post 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.

An approach we can take is to create a corresponding class AppState 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:

class AppState
{
  public static $vars = [];

  public static function getVars()
  {
    return self::$vars;
  }

  public static function initialize()
  {
    // Initialize core variables
    self::$vars['nature'] = $cmsRoutingService->getNature();
    self::$vars['route'] = $cmsRoutingService->getRoute();

    // Initialize $vars through hooks
    self::$vars = $hooksAPIService->applyFilters("AppState:init", self::$vars);

    return self::$vars;
  }
}

To replace global $post, a hook from WordPress can then set this data through a hook. A first step would be to set the data under "post-id":

$hooksAPIService->addFilter(
  "AppState:init", 
  function($vars) {
    if (is_single()) {
      global $post;
      $vars['post-id'] => $post->ID;
    }
    return $vars;
  }
);

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 "object-id", and we obtain its properties by inquiring the nature of the requested route:

$hooksAPIService->addFilter(
  "AppState:init", 
  function($vars) {
    if ($vars['nature'] == 'post') {
      global $post;
      $vars['object-id'] => $post->ID;
    }
    return $vars;
  }
);

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:

$vars = AppState::getVars();
$object_id = $vars['object-id'];
// Do something with it
// ...

Entity models (meta, post types, pages being posts, and taxonomies —tags and categories—)

We must abstract those decisions made for WordPress concerning how its entities are modeled. Whenever we consider that WordPress’s opinionatedness makes sense in a generic context too, we can then replicate such a decision for our CMS-agnostic code.

Meta:

As mentioned earlier, the concept of “meta” must be decoupled from the model entity (such as “post meta” from “posts”), so if a CMS doesn’t provide support for meta, it can then discard only this functionality.

Then, package “Post Meta” (decoupled from, but dependent on, package “Posts”) defines the following contract:

interface PostMetaAPIInterface
{
  public function getMetaKey($meta_key);
  public function getPostMeta($post_id, $key, $single = false);
  public function deletePostMeta($post_id, $meta_key, $meta_value = '');
  public function addPostMeta($post_id, $meta_key, $meta_value, $unique = false);
  public function updatePostMeta($post_id, $meta_key, $meta_value);
}

Which is resolved for WordPress like this:

class WPPostMetaAPI implements PostMetaAPIInterface
{
  public function getMetaKey($meta_key)
  {
    return '_'.$meta_key;
  }
  public function getPostMeta($post_id, $key, $single = false)
  {
    return get_post_meta($post_id, $key, $single);
  }
  public function deletePostMeta($post_id, $meta_key, $meta_value = '')
  {
    return delete_post_meta($post_id, $meta_key, $meta_value);
  }
  public function addPostMeta($post_id, $meta_key, $meta_value, $unique = false)
  {
    return add_post_meta($post_id, $meta_key, $meta_value, $unique);
  }
  public function updatePostMeta($post_id, $meta_key, $meta_value)
  {
    return update_post_meta($post_id, $meta_key, $meta_value);
  }
}
Post types:

I have decided that WordPress’s 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).

What happens when the CMS doesn’t support this concept? Well, nothing serious happens: a post will still indicate its custom post type to be a “post”, 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.

To support custom post types, we simply add a function getPostType in our contract:

interface PostAPIInterface
{
  public function getPostType($post_id);
}

Which is resolved for WordPress like this:

class WPPostAPI implements PostAPIInterface
{
  public function getPostType($post_id) {
    return get_post_type($post_id);
  }
}
Pages being posts:

While I justify keeping custom post types in order to extend posts, I don’t 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.

Hence we must create separate contracts for posts and pages, even though they may contain the same functions:

interface PostAPIInterface
{
  public function getTitle($post_id);
}

interface PageAPIInterface
{
  public function getTitle($page_id);
}

To resolve these contracts for WordPress and avoid duplicating code, we can implement the common functionality through a trait:

trait WPCommonPostFunctions
{
  public function getTitle($post_id)
  {
    return get_the_title($post_id);
  }
}

class WPPostAPI implements PostAPIInterface
{
  use WPCommonPostFunctions;
}

class WPPageAPI implements PageAPIInterface
{
  use WPCommonPostFunctions;
}
Taxonomies (tags and categories):

Once again, we can’t expect all CMSs to support what is called taxonomies in WordPress: tags and categories. Hence, we must implement this functionality through a package “Taxonomies”, and, assuming that tags and categories are added to posts, make this package dependent on package “Posts”.

interface TaxonomyAPIInterface
{
  public function getPostCategories($post_id, $options = []);
  public function getPostTags($post_id, $options = []);
  public function getCategories($query, $options = []);
  public function getTags($query, $options = []);
  public function getCategory($cat_id);
  public function getTag($tag_id);
  ...
}

We could have decided to create two separate packages “Categories” and “Tags” instead of “Taxonomies”, 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 “Taxonomies”.

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 "politics" by executing get_posts(['tag' => "politics"]). In this case, while function getPosts must be implemented in package “Posts”, filtering by tags must be implemented in package “Taxonomies”. To accomplish this separation, we can simply execute a hook in the implementation of function getPosts for WordPress, allowing any component to modify the arguments before executing get_posts:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args) {
    $args = $hooksAPIService->applyFilters("modifyArgs", $args);
    return get_posts($args);
  }
}

And finally we implement the hook in package “Taxonomies for WordPress”:

$hooksAPIService->addFilter(
  'modifyArgs',
  function($args) {
    if (isset($args['tags'])) {
      $args['tag'] = implode(',', $args['tags']);
      unset($args['tags']);
    }
    if (isset($args['categories'])) {
      $args['cat'] = implode(',', $args['categories']);
      unset($args['categories']);
    }
    return $args;
  }
);

Please notice that in the abstracted code the attributes were re-defined (following the recommendations for abstracting function parameters, explained earlier on): "tag" must be provided as "tags" and "cat" must be provided as "categories" (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).

Translation

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.

In WordPress, which implements internationalization through gettext, we are required to set-up translation files for each locale code (such as ‘fr_FR’, which is the code for french language from FRance), 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’t need to check for support for placeholders in the string to translate (such as when doing sprintf(__("Welcome %s"), $user_name)), because function sprintf belongs to PHP and not to the CMS, so it will always work.

Let’s 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:

  • Symfony’s translation component supports these two properties.
  • The locale used in Laravel’s localization 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).

However, luckily there is library Laravel Gettext which can replace Laravel’s native implementation with Symfony’s translation component. Hence, we got support for all frameworks, and we can rely on a WordPress-like solution.

We can then define our contract mirroring the WordPress function signatures:

interface TranslationAPIInterface
{
  public function __($text, $domain = 'default');
  public function _e($text, $domain = 'default');
}

The implementation of the contract for WordPress is like this:

class WPTranslationAPI implements TranslationAPIInterface
{
  public function __($text, $domain = 'default')
  {
    return __($text, $domain);
  }
  public function _e($text, $domain = 'default')
  {
    _e($text, $domain);
  }
}

And to use it in our application, we do:

$translationAPI = ContainerBuilderFactory::getInstance()->get('translation_api');
$text = $translationAPI->__("translate this", "my-domain");

Media

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’t expect all CMSs to have similar functionality. Hence, media management must be decoupled from the CMS core functionality.

For the corresponding contract, we can mirror the WordPress media functions, but removing WordPress’s opinionatedness. For instance, in WordPress, a media element is a post (with post type "attachment"), but for the CMS-agnostic code it is not, hence the parameter must be $media_id (or $image_id) instead of $post_id. Similarly, WordPress treats media as attachments to posts, but this doesn’t need to be the case everywhere, hence we can remove the word “attachment” from the function signatures. Finally, we can decide to keep the $size of the image in the contract; if the CMS doesn’t support creating multiple image sizes for an image, then it can just fall back on its default value NULL and nothing grave happens:

interface MediaAPIInterface
{
  public function getImageSrcAndDimensions($image_id, $size = null): array;
  public function getImageURL($image_id, $size = null): string;
}

The response by function getImageSrcAndDimensions can be asbtracted too, returning an array of our own design instead of simply re-using the one from the WordPress function wp_get_attachment_image_src:

class WPMediaAPI implements MediaAPIInterface
{
  public function getImageSrcAndDimensions($image_id, $size = null): array
  {
    $img_data = wp_get_attachment_image_src($image_id, $size);
    return [
      'src' => $img_data[0],
      'width' => $img_data[1],
      'height' => $img_data[2],
    ];
  }
  public function getImageURL($image_id, $size = null): string
  {
    return wp_get_attachment_image_url($image_id, $size);
  }
}

Conclusion

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’t 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’t done the abstraction myself, I would certainly be intimidated too.

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’ve been there. But I’ve survived, and I’d certainly do it again. Thanks for reading.

Smashing Editorial(dm, yk, il)

184 comments

  1. I am really impressed with your writing abilities
    and also with the structure in your weblog. Is this a paid subject matter or did you modify it your self?

    Either way keep up the excellent high quality writing, it is uncommon to look a nice blog like this
    one today..

  2. After I originally left a comment I appear to have clicked on the -Notify me when new comments
    are added- checkbox and from now on each time a comment is added I recieve four emails with the
    same comment. Is there a means you can remove me from that service?
    Cheers!

  3. fantastic points altogether, you just won a logo new reader.
    What would you recommend about your publish that you simply made some days ago?

    Any certain?

  4. Hey there! This is kind of off topic but I need some
    help from an established blog. Is it difficult to set up your own blog?

    I’m not very techincal but I can figure things out pretty quick.
    I’m thinking about creating my own but I’m not sure where to
    start. Do you have any tips or suggestions? With thanks

  5. Magnificent goods from you, man. I’ve understand your stuff previous to and you’re
    just too great. I actually like what you have acquired here,
    really like what you’re saying and the way in which you say it.
    You make it enjoyable and you still care for to
    keep it smart. I can’t wait to read far more from you.
    This is actually a wonderful website.

  6. Hello to every body, it’s my first pay a quick visit
    of this webpage; this webpage consists of amazing and actually fine material
    for visitors.

  7. First off I would like to say terrific blog!
    I had a quick question in which I’d like to ask if
    you don’t mind. I was curious to know how you center yourself
    and clear your thoughts prior to writing. I’ve had difficulty clearing my thoughts in getting my thoughts out there.
    I do take pleasure in writing however it just seems like the first 10 to 15
    minutes are lost simply just trying to figure out how to begin. Any ideas or hints?
    Appreciate it!

  8. hi!,I like your writing so a lot! percentage we be in contact extra approximately your post on AOL?
    I need a specialist in this house to resolve my problem.
    Maybe that is you! Having a look ahead to look you.

  9. May I just say what a comfort to find someone who genuinely knows what
    they’re discussing on the net. You actually know how to bring a problem to light and make it important.
    More people must read this and understand this side of your story.
    It’s surprising you aren’t more popular given that you definitely have the
    gift.

  10. Hi there! Do you use Twitter? I’d like to follow you if that would be ok.
    I’m absolutely enjoying your blog and look forward to new
    posts.

  11. My partner and I stumbled over here different website and thought I should check things out.
    I like what I see so i am just following you. Look forward to looking into your web page repeatedly.

  12. What i do not realize is in fact how you’re no longer actually much more well-liked than you might be right
    now. You are very intelligent. You already know therefore considerably in relation to this matter, produced
    me in my opinion consider it from so many numerous angles.
    Its like women and men are not involved except
    it’s something to do with Girl gaga! Your own stuffs great.
    At all times take care of it up!

  13. Unquestionably believe that that you stated. Your favorite reason appeared to be on the web the easiest factor to take note of.

    I say to you, I certainly get annoyed whilst people think about
    worries that they plainly don’t know about. You controlled to hit the
    nail upon the top and defined out the whole thing without having side effect ,
    other people can take a signal. Will likely be back to
    get more. Thanks

  14. If you would like to grow your know-how just keep visiting
    this web page and be updated with the newest information posted here.

  15. Hi, i read your blog from time to time and i own a similar one and i was just curious if you get a lot of spam
    remarks? If so how do you reduce it, any plugin or anything you can advise?

    I get so much lately it’s driving me insane so any help is very
    much appreciated.

  16. Heya i am for the first time here. I found this board and
    I in finding It really useful & it helped me out a lot.

    I am hoping to offer something back and help others such as you helped me.

  17. My brother suggested I might like this web site. He used to
    be entirely right. This publish actually made my day. You
    cann’t imagine just how a lot time I had spent
    for this info! Thanks!

  18. Nice blog right here! Also your site a lot up very
    fast! What host are you the usage of? Can I get your associate link on your host?
    I desire my website loaded up as quickly as yours lol

  19. Howdy! I know this is kind of off topic but I was wondering which blog platform are you using for this site?
    I’m getting fed up of WordPress because I’ve had problems with hackers and I’m looking at options for another platform.
    I would be awesome if you could point me in the direction of a good platform.

  20. We stumbled over here by a different website and thought I should check things out.
    I like what I see so i am just following you.
    Look forward to finding out about your web page yet again.

  21. you are actually a good webmaster. The site loading pace is amazing.
    It sort of feels that you’re doing any unique trick. Furthermore, The
    contents are masterwork. you’ve done a great process in this
    matter!

  22. I just like the helpful info you supply in your articles.

    I will bookmark your blog and take a look at once more here frequently.
    I’m fairly sure I’ll be told many new stuff proper here! Good
    luck for the following!

  23. Hey I know this is off topic but I was wondering if you knew of any widgets
    I could add to my blog that automatically tweet my newest twitter updates.
    I’ve been looking for a plug-in like this for quite some time and was hoping
    maybe you would have some experience with something
    like this. Please let me know if you run into anything.
    I truly enjoy reading your blog and I look forward to your new updates.

  24. Hi I am so delighted I found your blog, I really found you by
    error, while I was browsing on Bing for something else, Anyways I am here now and would just like
    to say thank you for a remarkable post and a all round enjoyable blog (I also
    love the theme/design), I don’t have time to read it all at the minute but I have saved it and also
    added in your RSS feeds, so when I have time I will be back to read much more, Please do keep
    up the awesome b.

  25. I loved as much as you’ll receive carried out right
    here. The sketch is tasteful, your authored subject matter stylish.
    nonetheless, you command get got an nervousness over that
    you wish be delivering the following. unwell unquestionably come more formerly again as exactly the same nearly a lot often inside case you shield this
    hike.

  26. No matter if some one searches for his essential thing, so he/she wishes to be available that in detail, so that thing
    is maintained over here.

  27. My coder is trying to convince me to move to .net from PHP.
    I have always disliked the idea because of the expenses. But he’s tryiong none the less.
    I’ve been using Movable-type on a number of websites for about a year and am worried
    about switching to another platform. I have heard good things about blogengine.net.
    Is there a way I can transfer all my wordpress content into it?

    Any kind of help would be greatly appreciated!

  28. Wonderful beat ! I would like to apprentice whilst you amend your site,
    how could i subscribe for a blog web site? The account helped me a appropriate deal.

    I have been a little bit acquainted of this your broadcast
    offered vivid clear idea

  29. I really like what you guys are usually up too.
    This type of clever work and reporting! Keep up the fantastic
    works guys I’ve included you guys to my own blogroll.

  30. Hola! I’ve been following your web site for some time now and finally got the
    courage to go ahead and give you a shout out from Lubbock Tx!
    Just wanted to tell you keep up the excellent job!

  31. Wow, incredible blog layout! How long have you been blogging for?
    you made blogging look easy. The overall look of your site is fantastic, let alone
    the content!

  32. I’ve been exploring for a bit for any high-quality articles or weblog posts in this
    kind of area . Exploring in Yahoo I eventually stumbled upon this web site.
    Studying this information So i am glad to exhibit that
    I have an incredibly excellent uncanny feeling I came upon just what I needed.

    I such a lot for sure will make certain to do not
    fail to remember this website and provides it a look on a relentless basis.

  33. Have you ever thought about adding a little bit more than just your articles?

    I mean, what you say is fundamental and all. However just imagine if you added some great pictures or videos to give your
    posts more, “pop”! Your content is excellent but with pics and videos, this website could
    certainly be one of the very best in its field. Awesome blog!

  34. Hi this is somewhat of off topic but I was wondering if blogs use WYSIWYG editors or if you have to manually
    code with HTML. I’m starting a blog soon but have
    no coding know-how so I wanted to get advice from someone with experience.
    Any help would be greatly appreciated!

  35. I like the helpful information you provide in your articles.
    I will bookmark your weblog and check again here regularly.
    I am quite sure I will learn a lot of new
    stuff right here! Good luck for the next!

  36. Hi, I do think your website might be having web browser compatibility problems.
    Whenever I look at your blog in Safari, it looks fine
    however, when opening in I.E., it has some overlapping issues.
    I just wanted to provide you with a quick heads up! Other than that,
    great blog!

  37. After I originally commented I appear to have clicked
    on the -Notify me when new comments are added- checkbox and now each time
    a comment is added I receive four emails with the exact same comment.
    Perhaps there is a way you can remove me from that service?

    Kudos!

  38. Greetings! I know this is somewhat off topic but
    I was wondering if you knew where I could find a captcha plugin for my comment
    form? I’m using the same blog platform as yours and I’m having difficulty
    finding one? Thanks a lot!

  39. Hi there! I just wanted to ask if you ever have any
    issues with hackers? My last blog (wordpress) was hacked and I ended up losing a few months of hard work due to no backup.
    Do you have any methods to stop hackers?

  40. I was curious if you ever considered changing the structure of your blog?
    Its very well written; I love what youve got to say.
    But maybe you could a little more in the way
    of content so people could connect with it better. Youve got an awful lot of text for only having
    one or two pictures. Maybe you could space it out better?

  41. Does your blog have a contact page? I’m having a tough time locating it but, I’d like to send you an email.
    I’ve got some suggestions for your blog you might
    be interested in hearing. Either way, great site and
    I look forward to seeing it improve over time.

  42. I’ve been surfing on-line more than 3 hours as of late, yet I never discovered
    any attention-grabbing article like yours. It is pretty price enough for me.

    In my opinion, if all site owners and bloggers made excellent content material as
    you did, the web will be much more helpful than ever before.

  43. You are so interesting! I do not suppose I have read anything like this before.
    So nice to discover another person with a few original thoughts on this topic.
    Seriously.. many thanks for starting this up. This
    web site is one thing that’s needed on the web, someone with some originality!

  44. Have you ever considered writing an ebook or guest
    authoring on other blogs? I have a blog based on the same subjects you discuss
    and would love to have you share some stories/information. I know
    my visitors would appreciate your work. If you are even remotely interested, feel free
    to shoot me an email.

  45. I’m not sure where you are getting your information, but great topic.
    I needs to spend some time learning more or understanding more.
    Thanks for wonderful information I was looking for this information for my mission.

  46. Hey would you mind sharing which blog platform you’re working
    with? I’m going to start my own blog in the near future but
    I’m having a tough time choosing between BlogEngine/Wordpress/B2evolution and
    Drupal. The reason I ask is because your design seems different then most blogs and I’m looking for something completely unique.

    P.S My apologies for getting off-topic but I had to ask!

  47. My relatives all the time say that I am killing my time here at net,
    but I know I am getting experience every day by reading such pleasant articles or reviews.

  48. Today, I went to the beachfront with my kids. I found a sea shell and
    gave it to my 4 year old daughter and said “You can hear the ocean if you put this to your ear.” She placed the shell to her ear and screamed.

    There was a hermit crab inside and it pinched
    her ear. She never wants to go back! LoL I know this is totally off topic but I had to tell someone!

  49. Its like you read my mind! You appear to know a lot about this, like you wrote the book in it or something.
    I think that you could do with a few pics to drive the message home a
    little bit, but other than that, this is fantastic blog.
    An excellent read. I’ll certainly be back.

  50. Greetings! I know this is kind of off topic but I was wondering which blog platform are you using for
    this site? I’m getting sick and tired of WordPress because I’ve
    had issues with hackers and I’m looking at alternatives for
    another platform. I would be fantastic if you could point me in the direction of a good platform.

  51. I’m very pleased to uncover this web site. I wanted to thank you for your
    time for this wonderful read!! I definitely savored every bit of it and i also
    have you book-marked to check out new stuff in your website.

  52. I’m not that much of a internet reader to be
    honest but your sites really nice, keep it up! I’ll go ahead and bookmark your
    website to come back in the future. Cheers

  53. I wanted to thank you for this great read!!
    I certainly enjoyed every bit of it. I have you saved as
    a favorite to look at new things you post…

  54. Undeniably believe that which you stated.
    Your favorite reason seemed to be on the internet the simplest thing to be aware of.
    I say to you, I definitely get irked while people
    think about worries that they plainly don’t know about.
    You managed to hit the nail upon the top as well as defined out
    the whole thing without having side effect , people could take a signal.
    Will probably be back to get more. Thanks

  55. Nice post. I learn something new and challenging on websites I stumbleupon every day.

    It’s always interesting to read through content from other
    writers and use something from their web sites.

  56. My brother recommended I might like this website. He was
    totally right. This post actually made my day. You cann’t imagine simply
    how much time I had spent for this info! Thanks!

  57. Have you ever considered about adding a little bit more than just your articles?
    I mean, what you say is fundamental and all. Nevertheless think of if you added some great visuals or videos to give your
    posts more, “pop”! Your content is excellent but with images and clips, this blog could definitely be one of the greatest in its niche.

    Awesome blog!

  58. Hi! I just wish to give you a huge thumbs up for your excellent info
    you have here on this post. I will be returning to your blog for more soon.

  59. Very good site you have here but I was wanting to know if
    you knew of any discussion boards that cover the same topics talked about here?
    I’d really like to be a part of online community where I can get advice from other knowledgeable individuals that share the same interest.
    If you have any recommendations, please let me know. Thanks!

  60. Hi there Dear, are you truly visiting this site regularly,
    if so then you will definitely take pleasant knowledge.

  61. fantastic post, very informative. I ponder why the opposite
    specialists of this sector do not realize this. You must continue your writing.
    I am sure, you have a great readers’ base already!

  62. After going over a few of the articles on your blog,
    I seriously like your technique of blogging.
    I saved it to my bookmark website list and will be checking back soon. Take a look
    at my website too and let me know your opinion.

  63. Wow, wonderful blog layout! How long have you been blogging for?

    you made blogging look easy. The overall look of your web site is magnificent, let alone the content!

  64. When someone writes an paragraph he/she maintains
    the thought of a user in his/her mind that how a user can be aware
    of it. Therefore that’s why this piece of writing is great.
    Thanks!

  65. Have you ever thought about publishing an e-book or guest authoring on other websites?
    I have a blog based on the same topics you discuss and would
    love to have you share some stories/information. I know my visitors
    would value your work. If you’re even remotely interested, feel free to
    send me an e mail.

  66. I do not know if it’s just me or if perhaps everybody else experiencing problems with your site.
    It looks like some of the written text in your posts are running off the screen. Can someone else please provide feedback and let me
    know if this is happening to them too? This could be a problem with my web browser because I’ve had this happen previously.
    Appreciate it

  67. I’m no longer certain the place you are getting your
    info, but good topic. I must spend a while studying more
    or working out more. Thank you for fantastic information I was looking for this information for my mission.

  68. Hello there I am so delighted I found your website,
    I really found you by error, while I was researching on Aol for something else, Regardless I am here now and
    would just like to say kudos for a fantastic post
    and a all round enjoyable blog (I also love the theme/design), I don’t have time to read through it all at the minute but I have bookmarked
    it and also added your RSS feeds, so when I have time I will be back to read a
    great deal more, Please do keep up the fantastic b.

  69. I was suggested this web site via my cousin. I’m now not certain whether or not this publish is written via him as no one
    else recognise such distinct about my problem. You are amazing!
    Thank you!

  70. I’m really enjoying the design and layout of your website.
    It’s a very easy on the eyes which makes it much more enjoyable for me to come here and
    visit more often. Did you hire out a designer to create your theme?
    Superb work!

  71. Do you mind if I quote a few of your articles as long as
    I provide credit and sources back to your website?
    My website is in the very same niche as yours and my users would genuinely benefit
    from some of the information you provide here. Please let me know if
    this alright with you. Thanks a lot!

  72. Just wish to say your article is as astonishing. The clearness in your post is simply excellent and i could
    assume you’re an expert on this subject. Well with your permission let
    me to grab your RSS feed to keep updated with forthcoming post.
    Thanks a million and please continue the rewarding work.

  73. I believe this is among the so much vital info for me.

    And i’m satisfied reading your article. However should remark on some normal issues,
    The web site style is perfect, the articles is truly excellent :
    D. Excellent job, cheers

Leave a Reply

Your email address will not be published.