Share this question

Welcome to Teachnovice Q&A, where you can ask questions and receive answers from other members of the community.

This is a collaboratively edited question and answer site for computer enthusiasts and power users. It's 100% free, no registration required.

Writing a Pluggable PHP Application – Part 3?

0 like 0 dislike
184 views

I take no credit for this. This was posted on failover by Jethro Solomon.

In the first and second part of this series, I explained and demonstrated the usage of action and filter hooks. We will now look at a simple example implementing the code I posted in part one and two. We will then dig into the WordPress code and look at their methods, as well as touch on the Observer Pattern in Object Orientated Programming.

The Application Example
We are going to write a simple email form, then make it extensible (and extend it).

The Folder/File Structure

/MySimpleApp

  • includes/
    • pluggable.php
    • config.php
  • plugins/
  • index.php

Pluggable.php
This file contains the system we looked at previously to implement the plugin system:

<?php
//Arrays to store user-registered events and functions.
$action_events = array();
$filter_events = array();

//Functions for Action Hooks
function hook_action($event)
{
    global $action_events;
    if(isset($action_events[$event]))
    {
        foreach($action_events[$event] as $func)
        {
            if(!function_exists($func)) {
                die('Unknown function: '.$func);
            }
                call_user_func($func, $args);
         }
    }
}

function register_action($event, $func)
{
    global $action_events;
    $action_events[$event][] = $func;
}

//Functions for Filter Hooks
function hook_filter($event,$content) {
 
    global $filter_events;
    if(isset($filter_events[$event]))
    {
        foreach($filter_events[$event] as $func) {
            if(!function_exists($func)) {
                die('Unknown function: '.$func);
            }
          $content = call_user_func($func,$content);
        }
    }
    return $content;
}

function register_filter($event, $func)
{
    global $filter_events;
    $filter_events[$event][] = $func;
}
?>

Config.php
This file contains some simple configuration variables:

<?php
//Heading before the form
$heading = "My Simple Mailer";
//Adress email will be sent to
$send_email = 'example@example.com';
//If message is sent
$success = 'Message sent.';
//If message is not sent.
$fail = 'Message Not sent.';
//Whether to display the form or not
$display_form = 'true';
?>

Index.php – First Step
Below is the code we are going to make extensible. As you can see, it is very stripped down:

<?php
//include files for adding plugin functionality
require_once "includes/pluggable.php";
require_once "includes/config.php";
//Load Plugins
foreach( glob("plugins/*.php")  as $plugin) {
  require_once($plugin);
}  

//Display page heading
echo $heading;

//Try send email
if (isset($_POST['message'])) {
    $subject = $_POST['subject'];
    $message = $_POST['message'];
    if(mail($send_email,$subject,$message)) {
        echo $success;
        $display_form = false;
    }else {
        echo $fail;
        $display_form = false;
    }
}
//Decide whether to display the form
if ($display_form == true) {
echo <<<END
    <form method="post"> 
    Your Email
    <input name="email" type="text" value="" /><br />
    Subject
    <input name="subject" type="text" value="" /><br />
    Your Message
    <textarea name="message"></textarea><br />
    <input name="send" type="submit" value="Send Message" />
    </form>
END;
}
?>

Index.php – Making the Application Extensible
Now, we are going to modify index.php to allow things to be modified and played with using hooks in the plugin files. We will add one action hook and multiple filter hooks for the sake of this tutorial:

//include files for adding plugin functionality
require_once "includes/pluggable.php";
require_once "includes/config.php";
//Load Plugins
foreach( glob("plugins/*.php")  as $plugin) {
  require_once($plugin);
}  
 
hook_action('initialize');
 
//Display page heading
$heading = hook_filter('the_heading',$heading);
echo $heading;
 
//Try send email
if (isset($_POST['message'])) {
    $subject = $_POST['subject'];
    $subject = hook_filter('the_subject',$subject);
    $message = $_POST['message'];
    $message = hook_filter('the_message',$message);
    $send_email = hook_filter('the_email',$message);
    if(mail($send_email,$subject,$message)) {
        $success = hook_filter('message_sent',$success);
        echo $success;
        $display_form = false;
        $display_form = hook_filter('display_form_success',$display_form);
    }else {
        $fail = hook_filter('message_fail',$success);
        echo $fail;
        $display_form = false;
        $display_form = hook_filter('display_form_fail',$display_form);
    }
}
//Decide whether to display the form
if ($display_form == true) {
echo <<<END
    <form method="post"> 
 
    Your Email
    <input name="email" type="text" value="" /><br />
 
    Subject
    <input name="subject" type="text" value="" /><br />
 
    Your Message
    <textarea name="message"></textarea><br />
 
    <input name="send" type="submit" value="Send Message" />
    </form>
END;
}
?>

The script is now ready to be extended. We can modify some output and add some functionality.

Plugins
We are going to write two plugins. Each plugin file must be saved in the plugins folder with a unique name.

Plugin 1 – IP Ban
Someone has been abusing the mail form to send SPAM! Oh Noes! Luckily we have their IP address from the server logs. Lets create a file called ip_ban.php in the plugins folder and use the following code:

<?PHP
//Rgister the action using the 'initialize' action hook we defined earlier.
register_action('initialize','ban');
 
function ban() {
    //Array of banned ip adresses
    $deny = array("111.111.111", "222.222.222", "333.333.333");
    //Check if user is banned, if so, give them a nice message.
    if (in_array ($_SERVER['REMOTE_ADDR'], $deny)) {
        die('You are BANNED from using this epic email form!');
    }
}
 
?>


Plugin 2 – Customize Multiple Things
We want to change the error messages, display the form if the sending failed, and change the heading. Lets call this plugin customize.php:

<?PHP
//This wraps <h1> tags around the header
register_filter('the_heading','new_heading');
 
function new_heading($heading) {
    return '<h1>'.$heading.'</h1>';
}
 
//This enures the form is displayed if the message is not sent
register_filter('display_form_fail','display_form');
 
function display_form($bool) {
    return true;
}
 
//This customizes our success and fail messages
register_filter('message_sent','sent');
 
function sent($message) {
    return 'WOOP WOOP! Message sent :-D';
}
 
register_filter('message_fail','fail');
 
function fail($message) {
    return 'FUUUUUUUU! Message not sent >:(';
}
?>


Finished
Further Reading & Exploration
If you are interested in looking into the WordPress code for implementing their plugin API, look at the files plugin.php and pluggable.php in the wp-content folder of your WordPress installation.

Often the Observer Pattern is used for implementing event handling systems. I would suggest reading a bit about it here (Zend Developer Zone).

I was planning on actually giving some of my personal input on the two above mentioned items, but there is enough info out there already. You Know where to go now ;-)

asked Oct 21, 2016 by pak786 (2,100 points)  
reopened Oct 24, 2016 by pak786

That’s what I need right now. Thanks for sharing.

1 Answer

0 like 0 dislike

Just though I’d share a contribution for a slightly more advanced hook system. This combines filter and action hooks by allowing you to pass through references in arrays.

it also makes full use of exceptions and wraps it all up in a class with static methods. It’s not fully tested yet but can give people an idea of where to go from your tutorials

** **

class Hooks {
 
    private static $hooks = array();
    private static $exceptions = array(
        'unknown' => 'Unknown error has occured',
        'incorrect_callback_array_size' => 'Array callback must have 2 values (object/class name and method name)',
        'undefined_zero_index_in_callback_array' => 'index 0 of callback array does not exist. No object/class name detected',
        'incorrect_zero_index_callback_array_format' => 'index 0 must be a string or object',
        'callback_does_not_exists' => 'The class specified for the callback does not exist',
        'callback_not_object' => 'A class has been set as the callback but not valid class or object found',
        'undefined_index_one_in_callback_array' => 'index 1 of callback array does not exist. No method name detected',
        'cannot_access_method' => 'Cannot access the callback method',
    );
 
    private static function _throw_exception($error_code, $extra = '') {
        $default = 'unknown';
 
        if (!isset(self::$exceptions[$error_code])) {
            $error_code = $default;
        }
        throw new Hooks_Exception(self::$exceptions[$error_code], $error_code, $extra);
    }
 
    /**
     * Binds a new hook callback into the system. Will overwrite any previously assigned callback
     * under the same unique name
     *
     * @param string $hook_name
     * @param string $unique_name  register a unique identifier for a callback
     * @param string|array $callback
     */
    public static function bind($hook_name, $unique_name, $callback) {
        //we need to perform some checks to make sure we're receiving a valid callback
        if (is_array($callback)) {
            //must have 2 values for the array
            if (count($callback) != 2)
                self::_throw_exception('incorrect_callback_array_size');
 
            //0 index must be set
            if (!isset($callback[0]))
                self::_throw_exception('undefined_zero_index_in_callback_array');
 
 
            //$callback[0] can be either a string(static class) or an object
            if (is_string($callback[0])) {
                //check if this class exists
                if (!class_exists($callback[0], FALSE)) //FALSE means don't try and autoload
                    self::_throw_exception('callback_does_not_exists');
            }
            //if it's not a string, it must be an object
            elseif (!is_object($callback[0])) {
                self::_throw_exception('callback_not_object');
            }
 
            //we must now check the specified method
            if (!isset($callback[1]))
                self::_throw_exception('undefined_index_one_in_callback_array');
 
            //does the method exist and is it accessible?
            //This is quicker and simple than doing reflections. It also avoids private methods
            if (!in_array($callback[1], get_class_methods($callback[0])))
                self::_throw_exception('cannot_access_method');
        }
        //$callback[1] can only be a string. Implies a function
        elseif (is_string($callback)) {
            //if it's a string, it must be a function
            if (!function_exists($callback))
                self::_throw_exception('cannot_access_method');
        }
 
 
        //sanity check
        if (!isset(self::$events[$hook_name]))
            self::$events[$hook_name] = array();
 
        //it's valid, lets set it
        self::$hooks[$hook_name][$unique_name] = $callback;
    }
 
    /**
     * Unbinds a particular hook callback
     *
     * @param string $hook_name
     * @param string $unique_name
     * @return void
     */
    public static function unbind($hook_name, $unique_name) {
        if (!self::is_registered($hook_name, $unique_name))
            return;
 
        unset(self::$events[$hook_name][$unique_name]);
    }
 
    /**
     * Checks to see if a particular
     *
     * @param string $hook_name
     * @param string $unique_name
     * @return bool
     */
    public static function isRegistered($hook_name, $unique_name) {
        if (!isset(self::$events[$hook_name]))
            return FALSE;
 
        if (!isset(self::$events[$hook_name][$unique_name]))
            return FALSE;
 
        return TRUE;
    }
 
    /**
     * Returns the list of registered hooks
     *
     * @param string $hook_name
     * @return array
     */
    private static function hookList($hook_name) {
        if (isset(self::$hooks[$hook_name]))
            return self::$hooks[$hook_name];
        else
            return array();
    }
 
    /**
     * Runs the hooks. They should only return true or false.
     * Data should be passed through as a reference in the arguments if you want a filter system
     * (ie $args = array(&$some data, &$validation_errors); AAHooks::run('event', $args))
     * This combines both the action and filter style hooks
     *
     * @param string $hook_name
     * @param array $args
     * @param bool $enable_break enables a break point
     * @param bool $break_on Sets when the function should break. Only ever on TRUE or FALSE
     */
    public static function run($hook_name, array $args = array(), $enable_break = FALSE, $break_on = FALSE) {
 
        $result = array();
        foreach (self::hookList($hook_name) as $unique_name => $callback) {
            $result[$unique_name] = call_user_func_array($callback, $args);
 
            if ($enable_break) {
                if ($result[$unique_name] === $break_on)
                    break;
            }
        }
 
        return $result;
    }
 
}
 
/**
 * General exception edited to support string codes and extra data
 */
class Hooks_Exception extends Exception {
 
    private $extra = '';
 
    public function __construct($message, $code, $extra = '') {
        parent::__construct($message);
 
        if (is_object($code)) //forces the __toString method
            $this->code = (string) $code;
        else
            $this->code = $extra;
 
        if (is_string($extra)) {
            $this->extra = $extra;
        }
    }
 
    public function getExtra() {
        return $this->extra;
    }
 
}
answered Oct 21, 2016 by pak786 (2,100 points)  
...