Using the Symfony Options Resolver in WordPress

I’ve written a lot about object-oriented PHP and more specifically dependency injection and using classes to encapsulate functionality with a simple public API. By using the object-oriented programming features of PHP this way, you can write unit and integration tests so that you whenever you make changes to the class, you know that given the same inputs, the output of the class is consistent.

That all sounds great, but dealing with inputs is tricky. This is especially true when taking an object-oriented approach to working with WordPress. In this article, we’ll look at a common problem when writing WordPress code — preparing arguments for a WordPress query class, in this case WP_Query.

We’ll borrow a component from the Symfony framework. If you’re not already using Symfony components in your WordPress code, you really should consider it. Laravel, Slim, Drupal and many other PHP frameworks do. Symfony components are all totally decoupled from the Symfony framework, are easily integrated with WordPress using have excellent documentation.

The Problem

Let’s start by defining the problem we’re solving. Let’s play with a requirement for a system that generates WP_Query objects that have specific collections of posts. We’ll want to have some default arguments that we can override. For example, think about a class that uses WP_Query for finding popular posts. A PopularPosts class will need to merge some default WP_Query arguments with additional arguments passed in. A very basic version of this class would look like this:

<?php
/**
 * Class PopularPost
 *
 * Query for popular posts
 */
class PopularPost {
    /** @var array  */
    protected $args;
    /**
     * PopularPost constructor.
     * @param array $args Arguments for WP_Query
     */
    public function __construct( array $args = [] )
    {
        $this->args = wp_parse_args( $args, [
            'orderby' => 'comment_count',
            'page' => 1,
            'posts_per_page' => 15,
        ] );
    }
    /**
     * Create a WP_Query with our args
     *
     * @return WP_Query
     */
    public function doQuery()
    {
        return new WP_Query( $this->args );
    }
}

This class’ constructor is using wp_parse_args to merge WP_Query arguments with defaults. That works. But if we need to get more complex, we start running into problems with typing — many arguments can be integers, strings, arrays, unparsed query variables… This is where we start coding defensively and checking if array keys are set and if they are arrays or strings or, well it goes on.

Using A Class Didn’t Help

This is how code bloat happens. Every class is now going to be responsible for its primary responsibility and managing complex type checking and validation of inputs. That’s a violation of the single responsibility principle. We could just assume the input is correct — transferring responsibility to one or more other locations, which probably leads to code repeating. Or we could put the logic inside of a factory class. Except if the factory class is only there to solve this problem that’s just adding an extra layer of complication, that might get skipped. This is how lasagna code happens.

This is getting boring and stupid. The problem we’re looking at is not unique to WordPress.  Let’s just use the Symfony component that solves this problem. That component is the OptionsResolver component. OptionsResolver is billed as array_merge() on steroids.

Using OptionsResolver

Default Options

The most basic usage of OptionsResolver does everything wp_parse_args does. This means that we can basically just use it as a drop in replacement. Here is a new version of our popular posts, that replaces wp_parse_args with OptionsResolver to do the same thing:

<?php
/**
 * Get popular posts
 *
 * @param array $args Optional. WP_Query arguments
 * @return WP_Query
 */
function PopularPosts( array $args = [] ) {
    //Create new options resolver
    $optionsResolver = new SymfonyComponentOptionsResolverOptionsResolver();
    //Set default options
    $optionsResolver->setDefaults([
            'orderby' => 'comment_count',
            'page' => 1,
            'posts_per_page' => 15,
        ]
    );
    //Return query that uses parse args
    return new WP_Query( $optionsResolver->resolve( $args ) );
}

In this code, the inline comments walk you through each step. First, a new instance of OptionsResolver is created. Then the default values are registered. Finally, the arguments were merged and used to create a new WP_Query instance.

Right now, this is academic. If that code did everything I needed and I was totally happy, I wouldn’t introduce a new dependency. I’d just use wp_parse_args :

/**
 * Get popular posts
 *
 * @param array $args Optional. WP_Query arguments
 * @return WP_Query
 */
function PopularPosts( array $args = [] ) {
    //Return query that uses parse args
    return new WP_Query( wp_parse_args( $args, [
        'orderby' => 'comment_count',
        'page' => 1,
        'posts_per_page' => 15,
    ]) );

}

The point of using the OptionsResolver is it can handle required options, option types and option validation. Instead of writing our own wall of conditionals. We can declare the rules we expect the options to observe and let OptionsResolver throw an Exception when the rules are violated. I could write that all myself without using a third-party dependency. I’m bored with doing it myself, over and over again, because it’s stupid to repeat myself when there is a perfectly good Symfony component that can do it for me.

Required Options

Let’s start looking at how that all works in more detail. Let’s start over and use proper object oriented PHP and separation of concerns.

I’ll trust you can install the component using Composer. There is a complete WordPress plugin you can see on my Github here. It also uses Composer. If you don’t know how to use Composer, I wrote everything you need to know here.

Instead of just creating PopularPosts, let’s create a system for creating WP_Query instances according to well-defined rules. Let’s start by creating an abstract class called Posts. We want the subclass of Posts to only be responsible for setting up the rules of the WP_Query args.

We’ll want subclass of Posts to have an initOptions method. That method will be marked abstract in the base class so that subclasses will be required to provide that method. Posts will handle setting up object properties through the constructor and running the query. Arguments for WP_Query will be built using the OptionsResolver instance defined in the subclasses, such as Popular posts.

<?php
namespace Example;
use SymfonyComponentOptionsResolverOptionsResolver;
abstract class Posts
{
    /**
     * @var array
     */
    protected $args;
    /**
     * @var OptionsResolver
     */
    protected $optionsResolver;
    /**
     * PopularPosts constructor.
     * @param array $args Optional. WP_Query arguments
     */
    public function __construct( array $args = []  )
    {
        $this->args = $args;
        $this->optionsResolver = new OptionsResolver();
        $this->initOptions();
    }
    /**
     * Init options resolver
     */
    abstract protected function initOptions();
    /**
     * Get query
     *
     * Causes query to run and args to be parsed
     *
     * @return WP_Query
     */
    public function getQuery()
    {
        return new WP_Query( $this->args );
    }
    /**
     * Get args
     *
     * @return array
     */
    protected function getArgs()
    {
        return $this->optionsResolver->resolve($this->args);
    }
}

Now that we have that, let’s create a rule. This will be our PopularPosts. Let’s make post_type a required rule. Instead of providing a default, which WordPress also provides, let’s require that the post type be chosen.

We’ve already seen how to add a default argument. Now let’s look at an argument without a default and make it required. OptionsResolver’s API is pretty clearly written. We define and argument with the method setDefined() and we make it required with the method setRequired. Here is our new PopularPosts:

<?php
namespace Example;
/**
 * Class PopularPosts
 *
 * Get popular posts
 *
 * @package Example
 */
class PopularPosts extends Posts
{
    /**
     * Init options resolver
     */
    protected function initOptions()
    {
        //Add default, non-required args
        $this->optionsResolver->setDefaults([
            'orderby' => 'comment_count',
            'page' => 1,
            'posts_per_page' => 15,
        ]);
        //Add post_type as a required arg with no default
        $this->optionsResolver->setDefined( 'post_type' );
        $this->optionsResolver->setRequired( 'post_type' );
    }
}

Here is how you would use this class:

<?php
$popular = new ExamplePopularPosts( [ 'post_type' => 'post' ] );
$posts = $popular->doQuery()->getQuery()->get_posts();

Type and Value Validation

Let’s do one more to look at more ways we can use OptionsResolver. For this one, we’ll create AuthorPosts — a collection of posts by an author, selected by user ID represented as an integer, in one of two post types, defined by a string.

For the author requirements, first we set author as defined using setDefined() and mark it required using setRequired(). This is the same thing we did before. But there is one more requirement here. We want the author to be represented by an integer — the user ID. This is a strict requirement, but saves us from writing extra code to deal with a username as a string, a user ID as a string, a WP_User that is is valid, a WP_User that is not valid or a standard class object that has the property ID that is the a valid user ID or whatever.

For that last requirement instead of dealing with all of that insanity, we can use the setAllowedTypes() method to specify that an integer and an integer only can be used.

<?php
//Make author a required option with no default
$this->optionsResolver->setDefined( 'author' );
$this->optionsResolver->setRequired( 'author' );
//Require author argument to be an integer
$this->optionsResolver->setAllowedTypes( 'author', array(
    'integer'
));

If we want to allow a numeric string, or a WP_User object later, we can do that. We just need to change one thing.

The other requirement we made up was that the post type had to be a single post type that was either “post” or “article”. We can force the type of “post_type” to be a string, as we’ve already seen. But we can also add an array of allowed values:

//Force post type to be a string -- IE one post type only
$this->optionsResolver->setAllowedTypes('post_type', 'string');
//Force post type to be "post" or "article"
$this->optionsResolver->setAllowedValues('post_type', [
    'post',
    'article'
]);

Here is the whole class:

<?php
namespace Example;
/**
 * Class AuthorPosts
 *
 * Get posts by author
 *
 * @package Example
 */
class AuthorPosts extends Posts
{
    /**
     * Init options resolver
     */
    protected function initOptions()
    {
        //Add default, non-required args
        $this->optionsResolver->setDefaults([
            'orderby' => 'comment_count',
            'page' => 1,
            'posts_per_page' => 15,
            'post_type' => 'post'
        ]);
        //Make author a required option with no default
        $this->optionsResolver->setDefined( 'author' );
        $this->optionsResolver->setRequired( 'author' );
        //Require author argument to be an integer
        $this->optionsResolver->setAllowedTypes( 'author', array(
            'integer'
        ));
        //Force post type to be a string -- IE one post type only
        $this->optionsResolver->setAllowedTypes('post_type', 'string');
        //Force post type to be "post" or "article"
        $this->optionsResolver->setAllowedValues('post_type', [
            'post',
            'article'
        ]);
    }
}

Here is how you would use this class:

<?php
$author = new ExampleAuthorPosts( [ 'author' => 2 ]  );
$posts = $author->getQuery()->get_posts();

Don’t Repeat Someone Else

There is an alternate universe version of this article where I have a goatee — over my beard? — where I walked you through building my own system for this. It would have been longer, more boring, and also you couldn’t have gone to Symfony’s excellent documentation to learn more.

What we actually created wasn’t a ton of code. It’s testable, and it’s super reusable. It has a ton of complexity in terms of what it can do, most of which we got

Josh is a WordPress developer and educator. He is the founder of Caldera Labs, makers of awesome WordPress tools including Caldera Forms — a drag and drop, responsive WordPress form builder. He teaches WordPress development at Caldera Learn.

The post Using the Symfony Options Resolver in WordPress appeared first on Torque.