underpin/decision-list-loader

Decision List loader for Underpin

1.0.0 2021-05-06 00:44 UTC

This package is auto-updated.

Last update: 2024-10-11 23:24:57 UTC


README

Loader That assists with adding decision lists to a WordPress website.

Installation

Using Composer

composer require underpin/decision-list-loader

Manually

This plugin uses a built-in autoloader, so as long as it is required before Underpin, it should work as-expected.

require_once(__DIR__ . '/underpin-decision-lists/decision-lists.php');

Setup

  1. Install Underpin. See Underpin Docs
  2. Register new decision lists as-needed.

Decision Lists

Typically, WordPress plugins rely solely on WordPress hooks to determine extended logic. This works for simple solutions, but it becomes cumbersome very fast as soon as several plugins are attempting to override one-another. The biggest issue is that the actual logic that determines the decision is decentralized. Since there isn't a single source-of-truth to dictate the order of logic, let alone what the actual choices are, you have no easy way of understanding why a plugin decided to-do what it did.

Decision lists aim to make this easier to work with by making the extensions all centralized in a single registry. This registry is exported in the Underpin console when WP_DEBUG is enabled, so it is abundantly clear what the actual hierarchy is for this site.

If you're debugging a live site, you can output the decision list using a PHP console tool, such as debug bar console.

var_dump(plugin_name_replace_me()->decision_lists()->get('email'));

Set Up

Fundamentally a Decision List is nothing more than a loader class, and can be treated in the same way.

Let's say we wanted to create a decision list that allows people to override an email address in a plugin. This would need to check an options value for an email address, and fallback to a hard-coded address. It also needs to be possible to override this value with other plugins.

In traditional WordPress, you would probably see this done using apply_filters at the end of the function, something like this:

function get_email_address(){
 
 return apply_filters('plugin_name_replace_me_email_address',get_option('email_address', 'admin@webmaster.com'));
}

With a decision list, however, this is put inside of a class, and that class can be extended. Like so:

/**
 * Class Email To
 * Class Email to list
 *
 * @since   1.1.0
 * @package DFS_Monitor\Factories
 */
class Email_To extends Decision_List {

	public $dedecision listion = 'Determines which email address this plugin should use.';
	public $name = 'Email Address';

	/**
	 * @inheritDoc
	 */
	protected function set_default_items() {

		$this->add( 'option', new class extends Integration_Frequency_Decision {

			public $id = 'option';
			public $name = 'Option Value';
			public $dedecision listion = 'Uses the value of the db option, if it is set.';
			public $priority = 100;


			public function is_valid( $params = [] ) {
				if(!is_email(get_option('email_address'))){
				  return plugin_name_replace_me()->logger()->log(
				    'notice',
                    'email_address_option_invalid',
                    'A decision tree did not use the option value because it is not set.'
                  );
                } else{
                  return true;  
              }             
			}

			/**
			 * @inheritDoc
			 */
			public function valid_actions( $params = [] ) {
				return get_option('email_address');
			}
		} );


		$this->add( 'hard_coded', new class extends Integration_Frequency_Decision {

			public $id = 'hard_coded';
			public $name = 'Hard coded email';
			public $dedecision listion = 'Uses a hard-coded email address for this site.';
			public $priority = 1000;

			public function is_valid( $params = [] ) {
				return true;
			}

			public function valid_actions( $params = [] ) {
				return 'admin@webmaster.com';
			}
		} );
	}
}

Notice that I'm using anonymous classes here, just to keep everything in a single file. You absolutely do not have to use anonymous classes. In fact, in most cases you shouldn't. If you pass a reference to the class as a string, it will not instantiate the class unless it's explicitly called. This saves on resources and keeps things fast.

The $priority value inside each class tells the decision tree which option to try to use first. If it returns a WP_Error, it moves on to the next one. As soon as it finds an option that returns true, it grabs the value from the valid_actions method, and move on.

Like the custom logger class, this needs to be registered inside Service_Locator.

	/**
	 * Set up active loader classes.
	 *
	 * This is where you can add anything that needs "registered" to WordPress,
	 * such as shortcodes, rest endpoints, blocks, and cron jobs.
	 *
	 * All supported loaders come pre-packaged with this plugin, they just need un-commented here
	 * to begin using.
	 *
	 * @since 1.0.0
	 */
	protected function _setup() {
      plugin_name_replace_me()->decision_lists()->add('email', '\Plugin_Name_Replace_Me\Decision_Lists\Email_To');
	}

Finally, we can use this decision list directly in our get_email_address function:

function get_email_address(){
 
 // Decide which action we should take.
 $decide = plugin_name_replace_me()->decision_lists()->get('email')->decide();

 // Return the valid decision.
 if(!is_wp_error($decide) && $decide['decision'] instanceof Decision){
   return $decide['decision']->valid_actions();
 }

 // Bubble up the error, otherwise.
 return $decide;
}

Now that we have this set up, it can be extended by other plugins using the add method. The example below would force the decision list to run this before any other option.

plugin_name_replace_me()->decision_lists()->get('email')->add('custom_option',new class extends \Underpin\Abstracts\Decision{

  // Force this to run before all other options
  public $priority = 50;
  public $name = 'Custom Option Name';
  public $dedecision listion = 'This custom name is used in an extension, and overrides the default';

  public function is_valid($params = []){
    // TODO: Implement is_valid() method.
  }

  public function valid_actions($params = []){
  // TODO: Implement valid_actions() method.
  }


});

Example

A very basic example could look something like this.

\Underpin\underpin()->decision_lists()->add( 'example_decision_list', [
	// Decision one
	[
		'valid_callback'         => '__return_true',
		'valid_actions_callback' => '__return_empty_string',
		'name'                   => 'Test Decision',
		'description'            => 'A single decision',
		'priority'               => 500,
	],

	// Decision two
	[
		'valid_callback'         => '__return_true',
		'valid_actions_callback' => '__return_empty_array',
		'name'                   => 'Test Decision Two',
		'description'            => 'A single decision',
		'priority'               => 1000,
	],
] );

Alternatively, you can extend decision list and reference the extended class directly, like so:

underpin()->decision_lists()->add('key','Namespace\To\Class');

This is especially useful when using decision lists, since they have a tendency to get quite long, and nest deep.

Getting Decision Results

When a decision list determines an action, it does three things:

  1. Sorts all decisions by priority, smallest numbers first.
  2. Loops through each item, and stops on the first decision that passes their respective test.
  3. Returns the result of the decision's valid_actions callback.

The example above would return '', because the first item to pass would be the item with the smallest priority that will pass.

underpin()->decision_lists()->decide('example_decision_list', [] );

The second argument, $params is an array of arguments that are passed to both the valid_callback and valid_actions_callback.