LCW Plugin Architecture 2.0

Don’t remember¬† LCW Plugin Architecture 1.0? Do you remember Web 1.0?

LCW Plugin Architecture 1.0

The primary goal of this architecture was to create plugins that were easily customized. This was done by writing the plugin utilizing object oriented programming. This allows a class to be created that is a child of another class (inherits the parent class) so that methods (functions) can be modified. This is desirable because WordPress deletes the plugin before re-installing an update. Implementing this correctly places the modifications outside of the plugin directory preventing them from being overwritten.

Consider the following directory structure:

arch_dir_v1jpg

The trick is to include either the <classname>.php and instantiate it or to include both classes and instantiate the custom class. Examine the following code:

if (!function_exists('lcw_common_add_class')) {
  function lcw_common_add_class($classname, $plugin_dir = '', $plugin_url = '') {
    GLOBAL $core;
    // Set default plugin directory and url if not defined
    if ($plugin_dir == '') $plugin_dir = str_replace('\\', '/', plugin_dir_path( __FILE__ ));
    if ($plugin_url == '') $plugin_url = plugin_dir_url( __FILE__ );
    // include the class
    include_once ($plugin_dir.'objects/'.$classname.'.php');
    // locate if it exists the custom class
    $custom = $plugin_dir.'_custom/'.$classname.'_custom.php';
    if (file_exists($custom)) {
      // include the custom class and instantiate it
      include_once ($plugin_dir.'custom/'.$classname.'_custom.php');
      $custom_class= $classname.'_custom';
      $class = new $custom_class();
    } else {
      // instantiate the 
      $class = new $classname();
    }
    // save the core class for class initialization later
    if (strpos($classname, '_core')) $core = $class;
    // Initialize the class
    $class->initialize($plugin_url, $plugin_dir, 'lcw_pm');
    // point the class to the core class
    if (method_exists ( $class , 'set_core' )) {
      $class->set_core(&$core);
    }
    return $class;
  }
}

This is slightly different than my v1 plugins in that I can pass in (and make a practice to do so) the url and directory of the plugin that is initializing the object. Note that the function declaration is in an if block, all my v2 plugins do this and this brings me to the reason why I pass in the url and dir, I have no idea which plugin declares the function and I don’t care but I don’t have to keep inventing new names for the function.

This is basically the creation of a child plugin but only works if the plugin author implements it. Also checking for custom css and javascript files that are linked/included after the ones I wrote for the plugin allow for more customization.

The plugins consist of several classes, an optional core class, an admin class that controls the dashboard UI, a client class for the public UI and optional widget classes for widgets. If I am using custom post types I create a class for their declaration. Common functions and classes are declared in the core class.

LCW Plugin Architecture 2.0

I found as I was writing more plugins, that there were a few classes that I used repeatedly. I decided to find a place to store these and to include them. What I do is to include a common directory in all my plugins if they use any of these classes. When the plugin is activated, they are conditionally copied to the mu-plugins directory. This is to insure that the plugin code is included before any of the plugins that use the classes.

arch_dir_v2

The file lcw_common.php does two things, the first is to include the files in the lcw_common directory so they can be initialized in plugins and the second is to return the version of the common files library.

    function activate() {
      // get version of included common library
	$file = $this->plugin_dir.'common/lcw_common.php';
	$fileContents = file_get_contents($file);
	$version = substr($fileContents, 
			  strpos($fileContents, 'Version: ') + 9, 
			  strpos($fileContents, 'Description: ') - strpos($fileContents, 'Version: ') - 11);
	// get file locations, source and destination
	$dest = $this->get_source();
	$source = $this->plugin_dir.'common';
	// get verdion of installed common library
	if (function_exists('lcw_common_version')) {
	  $current = lcw_common_version();
	  if ($version > $current) {
	    // update library
	    if (!$this->do_tree($source, $dest)) { // recursive function that copies the contents of the source to the destination
	      exit ('Common Library copy failed');
	    }
	    $current = $version;
	  }
	} else {
	  // copy library
	  if (!$this->do_tree($source, $dest)) { // recursive function that copies the contents of the source to the destination
	    exit ('Common Library copy failed');
	  }
	  $current = $version;
	}
      }

The above code is is the activation routine for my plugins. If the common library doesn’t exist or irs version is less than the version of the common library included with the plugin, it copies the common library to the mu-plugins directory. It then sets an option flag indicating the use of the common library.

    function deactivate() {
      // update access count
      $options = get_option('lcw_common_library');
      unset ($options['DMC_Media']);
      if (!$options || count($options) == 0) {
	// delete the library
        $dir = $this->get_source();
	@unlink ($dir.'lcw_common.php');
	$status = $this->del_tree($dir.'lcw_common'); // recursive function to delete the common library
        @rmdir ($dir.'lcw_common');
	delete_option('lcw_common_library');
      } else {
	update_option('lcw_common_library', $options, false);
      }
    }

The above function is the deactivate function. It clears the use flag and if there are no other plugins using the common library, deletes the library.

Additional features added to the Architecture

The first feature is a usability feature and should be implemented for most plugins. My epiphany came when I created a site for a non-profit that had members performing functions for the organization, such as membership, calendar maintenance and others. These members need access to specific options in the Admin Menu but the default capability for these menu items is activate-plugin, which by default is given to the Administrator. There are still items in the menu I want shielded from these users, for both simplicity sake as well as site protection. My solution is to create capabilities that can be assigned to users using a good role editor and defining the access capability for the menu items to these capabilities. I add something like the following to the activate routine:

	
    // set up plugin capabilities
    $role = get_role( 'administrator' );
    $role->add_cap( 'dmc_media_admin' );
    $role->add_cap( 'dmc_media' );

I can now set up the menus as follows:

	
    function admin_menu () {
      add_menu_page( 'DMC Media','DMC Media','dmc_media','dmc-media', array( &$this, 'DMC_page' ), $this->plugin_url.'images/notes.png', 13 );
      add_submenu_page('dmc-media','DMC Media Add' ,'Add New', 'dmc_media', 'dmc-media&edit=0', array(&$this, 'DMC_add'));
      add_submenu_page('dmc-media','DMC Media Categories' ,'Categories', 'dmc_media', 'dmc-categories', array(&$this, 'DMC_categories'));
      add_submenu_page('dmc-media','DMC Media Options' ,'Options', 'dmc_media_admin', 'dmc-options', array(&$this, 'DMC_options'));
    }
    add_action( 'admin_menu', array( $this, 'admin_menu' ) );

 

Comments are closed.