How to properly use classes in a sage 10 theme

I am a bit confused about using classes in sage 10. The last project I did was very complex, in a sense that I had to use lots of hooks to customise the functionality of woocommerce, so I ended up with a woocommerce.php file with over 1000 lines, in the app folder. My next project will be even more complex, and I don’t want to create a plugin or write everything in a single file, I would like to use classes, as in a plugin. For example, a class in my Controllers folder would look like this :

<?php

namespace app\Controllers;

class WooController
{
    public function __construct()
    {
        add_action('init', [$this, 'register_roles']);
        add_action('wc_hook_1', [$this, 'something_1']);
        add_action('wc_hook_2', [$this, 'something_2']);
        add_action('wc_hook_3', [$this, 'something_3']);
        // ...
    }

    public function register_roles()
    {
        //  ...
    }
}


I would use this class to contain every hook that is woocommerce related. In a separate Utilities folder, I’d create a WooUtility class with static methods to keep my class cleaner. However for this to work, I need to create an instance of the WooController class. For now, inspired from another post, I added this function, but it feels wrong to do it this way:

function load_controllers($ns = 'App\Controllers') {
    foreach(glob(__dir__ . '/Controllers/*.php') as $fn) {
        $class = $ns . '\\' . basename($fn, '.php');
        new $class();
    }
}
load_controllers();

I was going to use Service Providers, but I asked a Laravel dev, and they told me I shouldn’t use a Service Provider for this, so now I’m a bit confused about the role of Service Providers in wordpress.

Where should I add the hooks? How should I structure the theme if I don’t want to write a separate plugin?

I’d do this in a mu-plugin.

I did it like that in my previous projects, but since the theme comes with so many useful stuff like collections, array methods, blade templates, etc. it would be a waste not to use them. I feel like only using the view composers is a waste, but this is just my personal opinion, which is probably wrong.

Since wordpress hooks are similar to Laravel events, I decided to use a combination of Service Providers and custom classes to achieve what I want. Here’s what I did so far:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Controllers\UserController;

class UserAccountServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register(): void
    {
        $this->app->singleton('user', function ($app) {
            return new UserController();
        });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot(): void
    {
        $userController = $this->app->make('user');

        add_action('init', [$userController, 'register_custom_user_roles']);
    }
}

I’m still not sure this is the best way, but maybe at least I will learn something new

1 Like

I use all of those in my mu-plugins, too

That looks great :+1:

1 Like

This question is pretty open-ended: Very rarely is there an “objectively-best” way to do things, usually the best way is going to be the best way for your specific situation. How can you narrow down this question? How can you make it more specific? If you’re trying to optimize, what are you optimizing for? If you want to reduce complexity, why? What type of complexity are you trying to reduce?

In my experience, if you’re doing something you don’t like (i.e. in this case a single huge file with tons of logic) it’s helpful to think through why it’s making you uncomfortable, and what alternative(s) would “feel” better to you–generally your instincts will be a pretty good guide. Looking around for someone else’s solution can sometimes keep you from engaging deeply with the problem, and instead of getting something better you just get something different.

Personally, though, I’d recommend against doing this sort of thing:

It’ll make it a lot more difficult for any static analysis tools (i.e. your IDE) to find and analyze things.

2 Likes

@tbd, thanks for this post and your idea of loading classes in Service Providers. It helped point me in the right direction.

We’re redesigning gravitywiz.com, and we have an incredible amount of custom logic around Easy Digital Downloads, Gravity Forms, an arsenal of shortcodes, and so on.

We previously used a site plugin, but it feels much better to just move it into Sage, that way we can [more] easily take advantage of autoloading, using methods in Composers for Views, etc.

All that said, we’ve landed on the following pattern for organizing logic, and it feels pretty good so far, without creating a ton of boilerplate.

Three benefits we’ve found:

  • By utilizing dependency injection/auto injection, we should hypothetically not run into any issues concerning the order of which we instantiate classes in the service container.
  • It also allows for easily accessing any of the singleton instances using app()
  • This helps reduce the number of service providers which reduces the amount of times we need to update composer.json
// app/Providers/ExampleServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class ExampleServiceProvider extends ServiceProvider {

	/**
	 * Register services.
	 *
	 * @return void
	 */
	public function register() {
		$this->app->singleton(\App\Folder\Sample::class, \App\Folder\Sample::class);
		$this->app->singleton(\App\Folder\Dependency::class, \App\Folder\Dependency::class);
	}

	/**
	 * Bootstrap any application services.
	 *
	 * @return void
	 */
	public function boot() {
		// This is a handy place to put filters if you wish.
		add_filter( 'some_filter', '__return_true' );

		// Resolve all of the registered singletons
        $this->app->make(\App\Folder\Sample::class);

		/*
		 * We could explicitly make \App\Folder\Dependency::class here, but service container auto-injection should handle
		 * it for us.
		 */
	}

}
// app/Folder/Sample.php
<?php

namespace App\Folder;

class Sample {

	public function __construct( protected \App\Folder\Dependency $dependency ) {
		// Add filters, actions, etc, here.
		add_action( 'some_hook', [ $this, 'someActionOrFilterCallback' ] );
	}

	public function someActionOrFilterCallback() {
		// Do something here.
		$this->dependency->someMethod();
	}

}
// app/Folder/Dependency.php
<?php

namespace App\Folder;

class Dependency {

	public function __construct() {
		// Add filters, actions, etc, here.
	}

	public function someMethod() {
		// Do something here.
	}

}
4 Likes