Skip to content

Loaders & Object Registries

Alex Standiford edited this page Nov 29, 2021 · 1 revision

A frustrating thing about WordPress is the myriad number of ways things get "added". Everything works just a little differently, and this means a lot of time is spent looking up "how do I do that, again?"

Loaders make it so that everything uses an identical pattern to add items to WordPress. With this system, all of these things use nearly exact same set of steps to register.

Currently, there are several different loaders that can be installed alongside Underpin, and used to extend its core functionality.

  1. Admin Bar Menu Loader Create custom menus on the WP Admin Bar.
  2. Admin Notice Loader Loader That assists with adding admin notices to a WordPress website.
  3. Admin Pages Quickly spin up admin settings pages.
  4. Background Process Loader Run slow processes in a separate, asynchornous thread.
  5. Batch Task Loader Create, register, and implement batch tasks.
  6. Block Loader Create, register, and manage WordPress blocks.
  7. Cron Job Loader Create, manage, and execute cron jobs.
  8. Custom Post Type Loader Loader That assists with adding custom Post Types to a WordPress website.
  9. CLI Loader Create WP CLI commands.
  10. Eraser Loader Loader That assists with adding GDPR-compliant erasers to a WordPress website.
  11. Exporter Loader Loader That assists with adding GDPR-compliant exporters to a WordPress website.
  12. Menu Loader Register, and manage custom theme nav menus
  13. Meta Loader Manage custom meta to store in various meta tables
  14. Option Loader Register , and manage values to store in wp_options
  15. Rest Endpoint Loader Create, register, and manage REST endpoints
  16. Role Loader Create, and register custom roles
  17. Script Loader Create, and enqueue scripts
  18. Shortcode Loader Create, and render custom shortcodes
  19. Sidebar Loader Create, and manage WordPress sidebars
  20. Style Loader Create, and enqueue styles
  21. Taxonomy Loader Create, and manage custom taxonomies
  22. Underpin BerlinDB Register, and manage custom database tables with BerlinDB
  23. Widget Loader Create widgets, complete with admin settings.

Creating Custom Loaders

To put it simply, a Loader is just an array containing a list of instances of a specific PHP class. This could be anything from a list of post types, to a list of database tables, or something completely new. If your plugin has something that can be described as a list of a specific type of thing, you'll probably benefit from putting it in a loader registry.

For example, let's say you're working on a restrict content plugin, and you want to make it possible to add new logic that determines if a person can view the content. This could be solved using a WordPress filter, or even an Underpin decision, but we can also make this a list of roles.

This could look something like this:

// Register the loader
plugin_name()->loaders()->add( 'roles', [
    'default_factory' => 'Role_Instance',
    'abstraction_class' => 'Role'
] );

// Create the abstraction class
abstract class Role{
// Set up the class
}

// Create the Role Instance

class Role_Instance extends Role{
  // Optional. Use Instance_Setter trait to help set up this instance
  use Instance_Setter;

  // Set up the factory to build the abstraction.
}

You can then add any roles you want using plugin_name()->roles()->add(). See "Registering things" below.

plugin_name()->roles()->add('role_name', [/** Arguments to set up the role here **/]);

Registering Things

Everything is registered with Underpin::make_class, and can be registered in one of three ways:

  1. A string reference to a class name
  2. An anonymous class
  3. An array containing the class name and the constructor arguments
  4. An array containing constructor arguments.

The class name you register must be an instance of the loader's abstraction_class value, so if you wanted to register a shortcode, you must make a class that extends Underpin\Abstracts\Shortcode.

The examples below work with any loader class, and work in basically the same way. The extended class houses all of the logic necessary to tie everything together.

EXAMPLE: Register A Shortcode

Expanding on this example, let's say you wanted to register a new shortcode. It might look something like this:

class Hello_World extends \Underpin_Shortcodes\Abstracts\Shortcode {
	
	protected $shortcode = 'hello_world';
	
	public function shortcode_actions() {
		// TODO: Implement shortcode_actions() method.
	}
}

Extended Class

First you would create your Shortcode class. This class happens to have an abstract method, shortcode_actions.

Looking at the Shortcode abstract, we can see that our shortcode atts are stored in $this->atts, so we could access that directly if we needed. Since this is a simple example, however, we're simply going to return 'Hello world!"

Namespace Underpin\Shortcodes;

class Hello_World extends \Underpin_Shortcodes\Abstracts\Shortcode {
	
	protected $shortcode = 'hello_world';
	
	public function shortcode_actions() {
		return 'Hello world!';
	}
}

Now that our class has been created, we need to register this shortcode. This is done like this:

plugin_name()->shortcodes()->add( 'hello_world','Underpin\Shortcodes\Hello_World' );

Register Inline

Alternatively, you can register the class inline. This will automatically use a default instance of the Shortcode with no customizations.

plugin_name()->shortcodes()->add( 'hello_world', [
	'shortcode'                  => 'hello_world',                   // Required. Shortcode name.
	'shortcode_actions_callback' => function ( $parsed_atts ) {      // Required. Shortcode action.
		return 'Hello world!'; // 'value'
	},
] );

Register Inline With Factory

Finally, you can register the class inline, using a different class for the factory. This makes it possible to customize the factory that is used.

This is particualrly useful in cases where multiple registered items need similar treatment. It provides a way to extend classes without creating unique classes in the process.

First, extend the instance in whatever way you want.

Namespace Underpin\Factories;

class Hello_World_Instance extends \Underpin_Shortcodes\Factories\Shortcode_Instance {

  /* Cusotmize the class */

}

Finally, instruct Underpin to use a different class.

plugin_name()->shortcodes()->add( 'hello_world', [
    'class' => 'Underpin\Factories\Hello_World_Instance',
    'args'  => [
	  'shortcode'                  => 'hello_world',                   // Required. Shortcode name.
	  'shortcode_actions_callback' => function ( $parsed_atts ) {      // Required. Shortcode action.
	  	  return 'Hello world!';                                       // 'value'
	  },
	]
] );

Either way, this shortcode can be accessed using do_shortcode('hello_world');, or you can access the class, and its methods directly with underpin()->shortcodes()->get( 'hello_world' );

Example With Constructor

Sometimes, it makes more sense dynamically register things using a constructor. This pattern works in the same manner as above, the only difference is how you pass your information to the add() method.

Let's say you want to register a shortcode for every post type on the site. You could do with the help of a constructor. something like:

class Post_Type_Shortcode extends \Underpin\Abstracts\Shortcode {

	public function __construct( $post_type ) {
		$this->shortcode = $post_type . '_is_the_best';

		$this->post_type = $post_type;
	}

	public function shortcode_actions() {
		echo $this->post_type . ' is the best post type';
	}
}

And then register each one like so:

add_action( 'init', function() {
	$post_types    = get_post_types( [], 'objects' );

	foreach ( $post_types as $post_type ) {
         $this->shortcodes()->add( $post_type->name . '_shortcode', [
             'class' => 'Flare_WP\Shortcodes\Post_Type_Shortcode',
             'args'  => [ $post_type ],
         ] );
     }
} );

The key part here is how differently we handled the add method. Instead of simply providing a instance name, we instead provide an array containing the class, and an array of ordered args to pass directly into the contstructor. As a result, we register this class to be constructed if it is ever needed.

Accessing Loaded Items

Any item inside a loader registry can be accessed by the key. This makes it possible to retrieve the object that was registered to the loader directly.

$item = plugin_name()->loader_name()->get( 'key' );

It's a common practice to check and see if the item exists by confirming the item is not a WP_Error, like so:

$item = plugin_name()->loader_name()->get( 'key' );

if( is_wp_error( $item ) ){
  // Uh oh, the item is an error, we should do something.
} else {
  // Looks like we got the object okay, let's use some of the methods within.
}

You can also loop directly through the entire registry by typecasting the loader to an array:

foreach( (array) plugin_name()->loader_name() as $key => $item ){
  if( is_wp_error( $item ) ){
    // Uh oh, the item is an error, we should do something.
  } else {
    // Looks like we got the object okay, let's use some of the methods within.
  }
}

Filtering Loader Items

All underpin loaders include a method, filter, that makes it possible to filter a list of items by a value inside of each item's class. Let's say you have a Loader registry called fruits, and each Fruit class inside of this registry has color, shape, and has_edible_seeds:

// Set up the loader
plugin_name()->loaders()->add( 'fruits', [
  'abstraction_class' => 'Fruit',
  'default_factory' => 'Fruit_Instance'
] );

abstract class Fruit{
  public $color;
  public $shape;
  public $has_edible_seeds
}

class Fruit_Instance extends Fruit{
  use Instance_Setter;

  public function __construct( $args ){
    $this->set_values( $args );
  }
}

// Register some fruits
plugin_name()->fruits()->add( 'orange', ['name' => 'Orange', 'color' => 'orange', 'shape' => 'round', 'has_edible_seeds' => true ] );
plugin_name()->fruits()->add( 'apple', ['name' => 'Apple', 'color' => 'red', 'shape' => 'round', 'has_edible_seeds' => false ] );
plugin_name()->fruits()->add( 'banana', ['name' => 'Banana', 'color' => 'yellow', 'shape' => 'crescent', 'has_edible_seeds' => true ] );

Once it's all set, you can filter these items using filter.

$round_fruits = plugin_name()->fruits()->filter( ['shape' => 'round'] );
$fruits_with_inedible_seeds = plugin_name()->fruits()->filter( ['has_edible_seeds' => false] );

Both $round_fruits and $fruits_with_inedible_seeds would contain a list of Fruit objects that match the filter settings. You can then loop through these items and do something on each one, if you wanted:

foreach( $round_fruits as $round_fruit ){
  echo 'I found a round fruit called '. $round_fruit->name;
}


foreach( $fruits_with_inedible_seeds as $fruit_with_inedible_seeds ){
  echo 'I found a fruit with inedible seeds, it is called '. $fruit_with_inedible_seeds->name;
}

This would output

I found a round fruit called Orange
I found a round fruit called Apple
I found a fruit with inedible seeds, it is called Apple

Finding Loader Items

Much like filter, there's another method inside all loader objects called find. this works much like the filter example above, only instead of returning a list of everything that matches the query, find will either return the first item it finds that matches the criteria, or it will return a WP_Error object.

$orange_round_fruit = plugin_name()->fruits()->find( [ 'color' => 'orange', 'shape' => 'round'] ); // Fruits['orange']
$purple_fruit = plugin_name()->fruits()->find( [ 'color' => 'purple' ] ); // WP_Error, since we don't have any purple fruits.

if( ! is_wp_error( $orange_round_fruit ) ){
  echo 'I found an orange, round fruit. It is called an ' . $orange_round_fruit->name;
} else {
  echo 'I do not know of any orange, round fruits';
}


if( ! is_wp_error( $purple_fruit ) ){
  echo 'I found a purple fruit. It is called a ' . $orange_round_fruit->name;
} else {
  echo 'I do not know of any purple fruits';
}

This would output:

I found an orange, round fruit. It is called an Orange
I do not know of any purple fruits