Skip to content

Container

Justin Tadlock edited this page Aug 5, 2018 · 6 revisions

Have you ever used the singleton pattern for a class? Or a global variable? Then, a while later that you find out that singletons and globals are "bad"? To make matters worse, it's rare that anyone actually shares a solution to this problem with you.

Once you first start dipping your toe into object-oriented programming (OOP), you quickly run into a major problem with how WordPress theme/plugin system loads things up. And, if you ever use core WP hooks, it introduces a new layer of issues. Singletons are usually the go-to answer for that.

Hybrid Core addresses these issues by offering a single, unified container for handling setting up your theme.

The container is a pretty powerful tool that allows you to add bindings that you can access later. This tutorial is going to be a rundown on how you use it.

Creating a new Application

When you first open up your /app/bootstrap-app.php file, you're introduced to this line of code:

$mythic = new \Hybrid\Core\Application();

The Application class is a sub-class of Hybrid\Container\Container. It's your initial access point to the container and how you will work with it.

All the above code does is create a new instance of the Application class and assigns it to the $mythic variable.

Bindings

Bindings allow you to bind objects or even arbitrary data to the container.

The container's bind() method is the primary method for adding bindings. instance() and singleton() are wrappers for this method.

The basic method looks like this:

public function bind( $abstract, $concrete = null, $shared = false )
  • $abstract is the abstract class/interface name (or an arbitrary key) to access the binding.
  • $concrete is the implementation of the abstract. This can be a class name, an instance of Closure, an object, or any arbitrary data.
  • $shared is a boolean to decide if there should only be one shared instance of this binding.

Basic binding

You can add a basic binding by passing a class/interface name with a Closure that implements the class:

$mythic->bind( 'Mythic\Example', function() {

	return new Mythic\Example();
} );

Or, make that even simpler (the container can automatically resolve class names):

$mythic->bind( 'Mythic\Example', 'Mythic\Example' );

Or, even simpler than that (since $abstract and $concrete are the same):

$mythic->bind( 'Mythic\Example' );

The point is to show you the different ways of using the bind() method.

Binding instances

Instances are shared objects. Whenever you add it to the container, you'll always get back the same instance when retrieving it later.

You can bind an existing object instance. In the following, I'm just using a custom key/name and instantiating a new instance of the Customize class.

$mythic->instance( 'mythic/customize', new \Mythic\Customize\Customize() );

You can also bind an instance of arbitrary data:

$mythic->instance( 'mythic/version', '1.0.0' );

Binding singletons

Like instances, singletons are also shared. The difference is that singletons are lazily loaded -- not created until you first use it.

The following is example code from Mythic's app/bootstrap-app.php file that binds a singleton.

$mythic->singleton( 'mythic/mix', function() {
	$file = get_theme_file_path( 'dist/mix-manifest.json' );

	return file_exists( $file ) ? json_decode( file_get_contents( $file ), true ) : null;
} );

Binding aliases

Let's say you want to bind a long class name like Mythic\Tools\Jump\Around to the container.

$mythic->bind( 'Mythic\Tools\Jump\Around' );

If you want to access that from the container, you'll need to know the Mythic\Tools\Jump\Around name and reference it every time. That's no good. You can create an alias for that.

$mythic->alias( 'Mythic\Tools\Jump\Around', 'jumparound' );

Then, you can access jumparound instead of the full name.

Accessing Data

What if we need to access something like the mythic/mix binding that we added? Hybrid Core has you covered with two different methods:

  • Hybrid\App - Static class.
  • Hybrid\app() - Function.

Here's how to access that binding with both methods:

$mix = Hybrid\App::get( 'mythic/mix' );

$mix = Hybrid\app( 'mythic/mix' );

Unhooking stuff

Now that you have the basics of adding bindings and retrieving them, how does this work with hooks?

Let's say you have a class that adds some actions or filters:

namespace Mythic;

class PostSupport implements \Hybrid\Contracts\Bootable {

	public function boot() {
		add_action( 'init', [ $this, 'postTypeSupport' ] );
	}
}

Then, you add that class to the container:

$mythic->instance( 'Mythic\PostSupport', new \Mythic\PostSupport() )->boot();

How do you remove that postTypeSupport() action from the init action hook? Many people use globals or singletons to solve this problem. We don't need to do that. We can simply tap into the container and get the instance that we need and remove the action.

remove_action( 'init', [ \Hybrid\App::get( 'Mythic\PostSupport' ), 'postTypeSupport' ] );

Automatic Dependency Injection

The container can automatically resolve dependencies.

Basic dependency injection

Let's create a few example classes:

namespace Mythic;

class ExampleA {

	__construct( ExampleB $b ) {}
}

class ExampleB {

	__construct( ExampleC $c ) {}
}

class ExampleC {}

Normally, you'd have to manually resolve these dependencies like so:

$c = new ExampleC();
$b = new ExampleB( $c );
$a = new ExampleA( $b );

With just a few classes, that's not so bad. However, with a large dependency tree, it gets messy quickly. The container will automatically take care of that for you.

$mythic->bind( 'Mythic\ExampleA' );

Dependency injection with contracts

When you get into using interfaces, it gets even better. Let's use an example with interfaces.

namespace Mythic;

class FruitCollection implements FruitCollectionInterface {

	__construct( FruitInterface $favorite_fruit ) {}
}

class Watermelon implements FruitInterface {}

Here's how that might look in your code:

$mythic->bind( FruitCollectionInterface::class, FruitCollection::class );

$mythic->bind( FruitInterface::class, Watermelon::class );

Then, in the future, you can just switch out the concrete implementation without breaking the contract/interface. And, you can do it in one place in your application's code.

Here's an example of replacing the concrete implementation of FruitInterface with a different class:

$mythic->bind( FruitInterface::class, Apple::class );

Any code in your application will use the new Apple class instead of the old Watermelon class.

Service Providers

Mythic intentionally doesn't have a service provider by default. This is because I didn't want to complicate the theme building process with another concept that might not be familiar to theme authors. And, it's not something you need for the average theme.

However, when you get into more complex theme development, you'll likely want to use service providers to handle working with the container.

In this example, we'll use the existing Customize class and code from within Mythic to show you how to do it.

In app/bootstrap-app.php, you have this line of code to bind an instance of the customize class:

$mythic->instance( 'mythic/customize', new \Mythic\Customize\Customize() )->boot();

What we want to do is replace that code with the following.

$mythic->provider( 'Mythic\Customize\CustomizeProvider' );

That's going to call a new class that we'll need to create in a file named app/customize/class-customize-provider.php.

namespace Mythic\Customize;

use Hybrid\Tools\ServiceProvider;

class CustomizeProvider extends ServiceProvider {

	public function register() {

		$this->app->singleton( 'mythic/customize', Customize::class );
	}

	public function boot() {

		$this->app->resolve( 'mythic/customize' )->boot();
	}
}

That seems like a lot of extra for replacing that single line of code, right? Well, it can be. However, the organization is much nicer when you have a lot of services that you need for your theme. If just adding a few bindings, it's unnecessary.

The only things you really need to know about service provider classes are:

  • $this->app refers back to the instance of the application.
  • register() allows you to add any bindings.
  • boot() lets you run any code you need when the app is booted (this is not needed if you don't need to boot anything).

Static proxies

On occasion, you might want to create an easy-to-use static class reference to an object you have stored in the container. This static proxy class would simply be a wrapper for the class/object you have registered and allow you to call its methods statically.

Let's create an example class:

namespace Mythic\Utility\Template;

class Post {

	public function title() {}
}

Then, you need to create a proxy class:

namespace Mythic\Utility\Template;

class PostProxy {

	protected static function accessor() {

		return 'Mythic\Utility\Template\Post';
	}
}

Then, in your bootstrapping code, you bind this to the container and add an alias for your static proxy class:

$mythic->bind( 'Mythic\Utility\Template\Post' );

$mythic->proxy( 'Mythic\Utility\Template\PostProxy', 'Mythic\Post' );

Then, you'd be able to access the class title() method using Mythic\Post::title().

The sharpest readers may have even picked up on the similarity with the Hybrid\App class. It uses the same method. It's an alias of Hybrid\Proxies\App and allows you to access the application instance statically.

You can’t perform that action at this time.