Hierarchies

One thing that shows up often in Computer Science are graphs. It should be no surprise then that we make use of them too! The relevant namespace is called Hierarchy and the files are found here.

This document is not meant to replace a basic understanding of trees or linked lists. If those concepts are unfamiliar to you, you should first brush up on those topics before reading the rest of this page.

The two key concepts of a hierarchy are the so-called parents and the so-called children. The foundations module offers abstractions for children that have 0-or-1 parents and for parents that have 0-or-more children. Those are the ParentInterface interface (obligatory ParentTrait trait) and the ChildInterface interface (obligatory ChildTrait trait).

Because of the one parent constraint, the resulting graph will always be a tree where the only node without a parent is the so-called root.

More interesting, however, are objects that can be both a parent and a child. Those are called nodes and are modelled by the NodeInterface interface. A basic implementation is provided by the NodeTrait trait. The only noteworthy addition is the ability to query an instance's depth within the tree. The root has depth 0, its children have depth 1, its grandchildren have depth 2, and so on...

So far, these abstractions have universal applications. You can use them worry-free for anything that can be modelled as a tree. For example, you could build an abstract model of a table where the root is an instance of an imaginary Table class and the children are instances of an imaginary Column class.

However, the main reason why these abstractions were created was to offer the ability to create a plugin tree with a PluginInterface-implementing object as the root and PluginComponentInterface-implementing objects as the rest of the nodes. If you've skipped the Plugin section, that's where those interfaces are explained.

Therefore, two new abstract classes arise: the AbstractPluginRoot class and the AbstractPluginNode class which extend the AbstractPlugin and AbstractPluginComponent classes, respectively.

All the concepts presented in the Plugin section still apply. The only difference is that we now have a logical connection between all the individual plugin components. The root is at the top and everything else belongs somewhere in the abstract plugin tree structure. Getters like get_plugin now default to a simple tree traversal algorithm for retrieving the root.

The introduction of hierarchical plugin components also brings new extension traits and integration traits for both actions and states. You can find them all in their respective folders inside the Hierarchy namespace on GitHub, but here is a selection:

Here is a very basic example of a plugin tree:

<?php

namespace DeepWebSolutions\Plugins\MyTestPlugin;

use DeepWebSolutions\Framework\Foundations\Actions\InitializableInterface;
use DeepWebSolutions\Framework\Foundations\Actions\SetupableInterface;
use DeepWebSolutions\Framework\Foundations\Hierarchy\Plugin\AbstractPluginRoot;
use DeepWebSolutions\Framework\Foundations\Hierarchy\Plugin\AbstractPluginNode;
use DeepWebSolutions\Framework\Foundations\Hierarchy\Actions\InitializeChildrenTrait;
use DeepWebSolutions\Framework\Foundations\Hierarchy\Actions\MaybeSetupChildrenTrait;
use DeepWebSolutions\Framework\Foundations\Hierarchy\States\ActiveParentTrait;
use DeepWebSolutions\Framework\Foundations\Hierarchy\States\DisabledParentTrait;
use DeepWebSolutions\Framework\Foundations\States\ActiveableInterface;
use DeepWebSolutions\Framework\Foundations\States\DisableableInterface;
// Exceptions import and Local traits import ommitted for brevity

class MyPlugin extends AbstractPluginRoot implements InitializableInterface, SetupableInterface {
    use InitializeChildrenTrait;
    use MaybeSetupChildrenTrait;
    use SetupLocalTrait;
    use SetupOnInitializationTrait;
    
    public function get_plugin_file_path(): string {
        return $path_to_file_with_plugin_header_comment;
    }
    
    protected function setup_local(): ?SetupFailureException {
        // do some local setup ... like registering some actions and filters
        return null;
    }
}

class GenericComponents extends AbstractPluginNode implements InitializableInterface, SetupableInterface, ActiveableInterface, DisableableInterface {
    use InitializeChildrenTrait;
    use InitializeLocalTrait;
    use MaybeSetupChildrenTrait;
    use SetupLocalTrait;
    use ActiveLocalTrait;
    use ActiveParentTrait;
    use DisabledLocalTrait;
    use DisabledParentTrait;
    
    protected function is_active_local(): bool {
        // add your own logic for this node (and all of its descendants) to be active
        // for example, a setting in the WP admin area
        return true;
    }
    
    protected function is_disabled_local(): bool {
        // add your own logic for this node (and all of its descendants) to be disabled
        // for example, this plugin branch could be for compatibility with another plugin
        // and thus, if that plugin is not active, nothing here should run
        return false;
    }
    
    protected function initialize_local(): ?InitializationFailureException {
        // do some local initialization, basically just making sure that
        // everything is in order for the 'setup_local' call
        return null;
    }
    
    protected function setup_local(): ?SetupFailureException {
        // do some local setup, like registering actions and filters,
        // registering shortcodes, setting up REST routes,
        // adding admin notices, whatever
        return null;
    }
}

$plugin     = new MyPlugin();
$component1 = new GenericComponent();
$component2 = new GenericComponent();
$component3 = new GenericComponent();

$plugin->add_child( $component1 );
$component1->add_child( $component2 );
$component1->add_child( $component3 );

// automagically run everything!
$plugin->initialize();

In case you're still confused about what happens in the example above, here is the plugin tree structure:

... and here is the full call stack trace, spelled out, after calling initialize (noop method calls ommitted for brevity):

$plugin->initialize();
$plugin->initialize_local(); // see AbstractPlugin class in the Plugin namespace
$plugin->integrate_initialize_children();

$component1->initialize();
$component1->initialize_local();
$component1->integrate_initialize_children();
$component2->initialize();
$component2->initialize_local();
$component3->initialize();
$component3->initialize_local();

// full plugin tree has been initialized successfully

$plugin->setup(); // setup method called automagically because of the action integration trait
$plugin->setup_local();
$plugin->integrate_maybe_setup_children();

$component1->is_disabled();
$component1->is_disabled_local();
$component1->is_active();
$component1->is_active_local();
$component1->setup();
$component1->setup_local();
$component1->integrate_maybe_setup_children();

$component2->is_disabled();
$component2->is_disabled_local();
$component1->is_disabled(); // because of the parent checking trait
$component2->is_active();
$component2->is_active_local();
$component1->is_active(); // because of the parent checking trait
$component2->setup();
$component2->setup_local();

$component3->is_disabled();
$component3->is_disabled_local();
$component1->is_disabled();
$component3->is_active();
$component3->is_active_local();
$component1->is_active();
$component3->setup();
$component3->setup_local();

You read that right. All of the above happens automagically in the background upon calling $plugin->initialize() thanks to the traits we included. The call stack gets short-circuited if a local action method returns a corresponding exception or if a local state method returns the opposite boolean value of what we pre-configured in the example above.

Congratulations! If you understood the example above, then you understand exactly how to use the Foundations Module to build your own plugins -- just mix-and-match action and state traits in your plugin components to achieve the desired results. Or just skip it all and use the pre-built logic provided by the Core Module.

Last updated