DEV Community

Cover image for Simplifying WordPress's functions.php with OOP
Tyler Smith
Tyler Smith

Posted on

Simplifying WordPress's functions.php with OOP

I love WordPress, and I use it for most of my clients' sites. Between its built-in features, rich plugin ecosystem and endless learning resources/documentation, I can spend more time building features unique to each project and less time reinventing the wheel.

That being said, WordPress isn't perfect. Like PHP itself, the WordPress API often feels clunky and inconsistent. I spend lots of time Googling functions and action hooks that I frequently use because I can't remember their names. This part of WordPress is less than ideal.

When I learned Laravel, I discovered how much better coding could feel when using a simple and elegant object-oriented API. That feeling was like a drug: once I had a taste I was hooked. I wanted to lace my WordPress development with a little bit of that sweet object-oriented programming (OOP for short).

Enter functions.php.

I maintain my own Underscores-based starter theme that I use for most of my projects, and its functions.php felt clunky. My functions file was fairly typical and close to the stock Underscores functions.php. Here's a rundown of what it was doing:

  1. Inside the after_setup_theme hook:
    • Add theme support for title-tag, custom-logo, post-thumbnails, customize-selective-refresh-widgets and an array of html5 components using add_theme_support().
    • Register navigation menus using register_nav_menus().
    • Add image size using add_image_size().
  2. Inside the wp_enqueue_scripts hook:
    • Enqueue styles and scripts with their respective wp_enqueue_style() and wp_enqueue_script() functions.
  3. Include related files.

Again, this is a fairly typical functions.php, and there's nothing wrong with it. However, I have a few issues with how it's setup:

  1. Memorization isn't my biggest strength, and remembering which functions start with the word add, register and wp_enqueue just isn't going to happen for me.
  2. Action hooks fail silently, and I can't tell you how many times I've typed after_theme_setup instead of after_setup_theme.
  3. I literally add theme support for the exact same things in every project, and I don't really want that identical boiler plate cluttering my functions.php code.

Let's take a step back and consider what the functions.php code is actually doing here.

When you think about it, everything that's happening is performing some kind of action on the theme itself. What if we had a theme object that we could perform these actions on with a simple, object-oriented API?

A Simple, Object-Oriented API

I don't want to remember which functions start with the word add, register or wp_enqueue. In fact, all of these essentially do the same thing: they add something to the theme. So I'm going to use the word add for all of these. I want to add theme support, add nav menus, add image sizes, and add scripts.

I'm lazy. Fight me.

I want my functions.php to look more or less like this:

<?php // functions.php

require get_template_directory() . '/classes/theme-class.php';

$theme = new MyTheme;

$theme->addNavMenus([
    'menu-1' => 'Primary',
]);

$theme->addSupport('post-thumbnails');

$theme->addImageSize('full-width', 1600);

$theme->addStyle('theme-styles',  get_stylesheet_uri())
      ->addScript('theme-script', get_template_directory_uri() . '/js/custom.js');

Enter fullscreen mode Exit fullscreen mode

No more memorizing hook names and function prefixes. Rejoice!

It is also abundantly clear that all of these methods are performing an action on the theme itself.

So let's build this.

We'll start by defining a theme class in a new file and building a addNavMenus() method. Essentially, we're just building wrappers around the existing WordPress hooks and functions, so this shouldn't be too complicated.

<?php // theme-class.php

class MyTheme
{
    public function addNavMenus($locations = array())
    {
        add_action('after_setup_theme',function() use ($locations){
            register_nav_menus($locations);
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

Let's unpack what's going on here.

We define our class MyTheme, make a public method for addNavMenus() and give it the same arguments as the register_nav_menus() WordPress function.

Inside the method, we add an action to the after_setup_theme hook, and create a closure (PHP's flavor of an anonymous function) where we call the WordPress register_nav_menu() function. The $locations variable is passed into the closure using PHP's use keyword, otherwise the variable would be outside of the closure's scope.

Side note: closures are supported as of PHP 5.3, and they are how I interact with WordPress hooks 90% of the time to avoid cluttering up the global namespace. However, despite WordPress's adoption of modern technologies like React.js, WordPress officially maintains its PHP backwards compatibility to PHP 5.2, which reached its end-of-life in January 2011 🤷‍♂️

Reduce Hook Failures

In the addNavMenus() method, we've solved the first problem we defined above: we've simplified the API (no more remembering prefixes like register). We still have our second problem though: misspelled hooks fail silently. As I build out my theme class's methods, at some point I'm probably going to write after_theme_setup instead of after_setup_theme somewhere and not notice.

Let's fix that by creating a private method that fires the after_setup_theme action hook, then calling that method within the addNavMenus() method instead of the add_action() function.

<?php // theme-class.php

class MyTheme
{
    private function actionAfterSetup($function)
    {
        add_action('after_setup_theme', function() use ($function) {
            $function();
        });
    }

    public function addNavMenus($locations = array())
    {
        $this->actionAfterSetup(function() use ($locations){
            register_nav_menus($locations);
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

So this is kind of cool: we're passing in the closure within addNavMenus() to our actionAfterSetup() method, then passing it to the closure via the use keyword, then calling the code from its variable name from within the closure. Wonderful witchcraft!

...if that description didn't make sense at all, just study the code and it isn't too bad.

I've prefixed the method with the word "action" to tell me this is an action hook, and I've made it private because this should only be used within the class.

This solves our second problem: if we type the actionAfterSetup() method incorrectly, it will no longer fail silently. Now we only need the hook name to be correct in one place.

Let's add some more methods!

Abstracting Code Shared Between Projects

I add theme support for the same features on almost every project: title-tag, custom-logo, post-thumbnails, customize-selective-refresh-widgets and an array of html5 components.

Let's add a wrapper for the add_theme_support() function. We'll simply call it addSupport() as including the word "theme" feels redundant on the theme class. Once implemented, we'll see how we can abstract some of the repeated code.

The code that powers the add_theme_support() function is kind of wonky: it counts the number of arguments passed into it to determine what it should do. Because of this, we're going to set up a conditional within our wrapper to see if a second argument is set and only pass in the the second argument if it has a value.

<?php // theme-class.php

class MyTheme
{
    /** Previous code truncated for clarity */

    public function addSupport($feature, $options = null)
    {
        $this->actionAfterSetup(function() use ($feature, $options) {
            if ($options){
                add_theme_support($feature, $options);
            } else {
                add_theme_support($feature);
            }
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

Additionally, I'd like to be able to chain several of these methods together, so I'm going to have addSupport() and all other public methods return $this.

<?php // theme-class.php

class MyTheme
{
    /** Previous code truncated for clarity */

    public function addSupport($feature, $options = null)
    {
        $this->actionAfterSetup(function() use ($feature, $options) {
            if ($options){
                add_theme_support($feature, $options);
            } else {
                add_theme_support($feature);
            }
        });
        return $this;
    }
}

Enter fullscreen mode Exit fullscreen mode

Now that we have implemented a way to add theme support, let's abstract away the settings we're going to want on every project by creating a usable set of defaults for the theme. We can do this by using the class constructor.

<?php // theme-class.php

class MyTheme
{
    /** Previous code truncated for clarity */

    public function __construct()
    {
        $this->addSupport('title-tag')
             ->addSupport('custom-logo')
             ->addSupport('post-thumbnails')
             ->addSupport('customize-selective-refresh-widgets')
             ->addSupport('html5', [
                 'search-form',
                 'comment-form',
                 'comment-list',
                 'gallery',
                 'caption'
             ]);
    }
}

Enter fullscreen mode Exit fullscreen mode

Because these are now in the class constructor, this code will execute immediately once a theme object is instantiated, meaning we no longer have to clutter the functions file with boiler plate that is used in every site we build with this theme.

Instead, the functions file only defines the parts of the theme that are different across each project.

Going Further

This is just the beginning, but there's much more to be done! You can create wrappers to add scripts and stylesheets. You can add style.css and main.js to your theme automatically through the theme class constructor, streamlining functions.php even further. You can create methods to remove theme support and remove styles/scripts. This approach is very flexible and ultimately leads to less spaghetti code.

If you want to see where I landed with my own theme class, look at my GitHub gist.

I hope this was helpful and showed you some nifty object-oriented options for WordPress. If you're interested in learning more about object-oriented programming for WordPress, I strongly recommend checking out the Object-Oriented Theme Development WordCamp Talk by Kevin Fodness. For those who are already using OOP in WordPress, I've love to hear your opinions about camelCase vs snake_case for method names, PHP namespaces and more. Cheers!

Top comments (26)

Collapse
 
luc45 profile image
Random Dev

Hi Tyler,

Always good to see some OOP in WP. Looks similar to a project I was developing a month ago, called Modern WordPress Website (github.com/Luc45/ModernWordPressWe...). I follow PSR-2.

Best,
Lucas

Collapse
 
tylerlwsmith profile image
Tyler Smith

Interesting! I'll have to check Modern WordPress Website out, it looks pretty spiffy!

Collapse
 
theking2 profile image
theking2

the link died unfortunately

Collapse
 
vanaf1979 profile image
Stephan Nijman

Nice article. WordPress may not have a lot of modern php, but that doesn't mean my/our code cant. I may take some of your ideas and apply them to my own OOP functions.php boilerplate: github.com/vanaf1979/functionsphp Thanks.

Collapse
 
tylerlwsmith profile image
Tyler Smith

I'm glad you potentially found some of these ideas helpful! I've been building out this class since I wrote this article. At some point I want to turn it into a Composer package so I can share easily among my themes and potentially with anyone else who is interested in it.

Collapse
 
zimaben profile image
Benny T

I really liked this post and played around with changing my construction process around it. Ultimately I didn't want to invest the time to properly figure out how to tackle filters or large $arg functions (I'm sure it's possible), but holy hell this option is elegant as hell declaring wp_ajax_ handlers. Just throw the name of the handler function in and you're set:

private function __construct()
{
$this->addAjaxHandler('feature_post' )
->addAjaxHandler('unfeature_post');

}

private function addAjaxHandler( $function_name ){

\add_action( 'wp_ajax_' . $function_name, function() use( $function_name){
self::$function_name();
} );
return $this;

}

Collapse
 
tylerlwsmith profile image
Tyler Smith

I'm glad you got some value out of the post! I hadn't even considered using this for ajax, but I'll have to try it out at some point.

Collapse
 
zimaben profile image
Benny T

Yeah it works in this case since wp_ajax follows a standard naming convention with your function and PHP will natively try to execute a variable as a function if you put the () parens after, so declaring the function name pulls double duty here.

Collapse
 
mathiu profile image
Mathiu • Edited

Action hooks fail silently, and I can't tell you how many times I've typed after_theme_setup instead of after_setup_theme.

I have created a simple theme generator years ago, its purpose was mostly to prefix some functions and create all the standard files. I gave it to one of my newly hired colleagues once and as he was finishing his first project, translations were not working exactly because of this. It was fun looking for misspelling.

Collapse
 
tylerlwsmith profile image
Tyler Smith

That's the worst! To some extent these kinds of mistakes are unavoidable, and probably a good a good argument for test driven development. I've got methods wrapping this functionality in this theme class because I'm using the same hooks over and over, but in most cases this approach is less than practical because of the sheer volume of hooks available.

Building a theme generator like the one you built sounds super cool. That's something I should definitely focus on learning soon.

Collapse
 
thomas_grgl profile image
Thomas G. • Edited

Hey, thanks for your post !

I've been using it in a little library i'm currently building, it's time to treat wordpress as an object !

I hope it doesn't bother you 😇

gitlab.com/tgeorgel/object-press

Collapse
 
tylerlwsmith profile image
Tyler Smith

I'm glad you found it useful. If you wanted to link to this article in your readme in would be appreciated but it isn't required. Best of luck with your library!

Collapse
 
thomas_grgl profile image
Thomas G.

Of course, will do ;)

Collapse
 
theking2 profile image
theking2

I am in the process of actually creating something very similar . what for wordpress is imperative is the possibility to dequeue or remove actions in a child theme. So each "add" method should have a mirroring "disable" or "remove" method.

Collapse
 
codingmindfully profile image
Daragh Byrne

You might be interested in Sage from roots.io - it’s a starter theme that makes WordPress MVC and uses Laravels blade engine

Collapse
 
tylerlwsmith profile image
Tyler Smith

I'm a fan of Sage! I'm launching a site that I built with Sage next month, and I especially love having Blade in WordPress.

It's great for big data-heavy sites, though I tend to avoid it for smaller sites because it ends up feeling like too much tool for the job. Its folder structure can overwhelm me, and I sometimes feel like using PHP namespaces with WordPress feels like a "square block in a round hole" kind of thing.

The Blade templating though; I'll definitely be using Sage again in the future for that alone!

Collapse
 
codingmindfully profile image
Daragh Byrne

Yes - I’ve made similar observations - the templating is gold, and if you hook it up to advanced custom fields life gets so much easier!

Collapse
 
grandemayta profile image
Gabriel Mayta

Awesome!

Collapse
 
tylerlwsmith profile image
Tyler Smith

Thanks Gabriel, I'm glad you liked it!

Collapse
 
lboneluv profile image
José Manuel Ramírez

Nice article. I will apply some in my projects 😉

Collapse
 
tylerlwsmith profile image
Tyler Smith

I'm glad you like it! I hope it's helpful in your projects.

Collapse
 
devellopah profile image
Islam Ibakaev • Edited

Thanks Tyler for the post and for sharing classy gist!

Collapse
 
tylerlwsmith profile image
Tyler Smith

Thanks for reading! I hope you got some value out of it.