Last active
August 29, 2015 14:18
-
-
Save rheinardkorf/a43389b02a0586632522 to your computer and use it in GitHub Desktop.
One approach to implement a class autoloader for WordPress plugin development. This file will for the most part be untouched once the structure is in place. The real development starts in the library folder.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @package MyPlugin | |
*/ | |
//Before playing with this file, see the other file in the gist. Read the comments. | |
if( ! class_exists( 'MyPlugin' ) ) { | |
class MyPlugin { | |
public static function setup_plugin( $file, $header ) { | |
error_log( print_r( $header, true ) ); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @package MyPlugin | |
*/ | |
/* | |
Plugin Name: MyPlugin | |
Plugin URI: http://rheinardkorf.com | |
Description: MyPlugin Description | |
Version: 1.0.0 | |
Author: Rheinard Korf | |
Author URI: http://rheinardkorf.com | |
License: GPLv2 or later | |
Text Domain: myplugin | |
*/ | |
/* | |
This program is free software; you can redistribute it and/or | |
modify it under the terms of the GNU General Public License | |
as published by the Free Software Foundation; either version 2 | |
of the License, or (at your option) any later version. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
You should have received a copy of the GNU General Public License | |
along with this program; if not, write to the Free Software | |
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
*/ | |
/** | |
* Class MyPluginLauncher | |
* | |
* This class is responsible for setting up the autoloader of the plugin. | |
* Once the autoloader is in place it will call $plugin_prefix::setup_plugin() that is found in the $lib_path directory | |
* within the $plugin_prefix directory. | |
* | |
* None of the functionality of this plugin should be added to THIS file. $plugin_prefix::setup_plugin() becomes the | |
* primary entry point of the plugin. | |
*/ | |
class MyPluginLauncher { | |
/** | |
* @var string The path of containing the plugin. AKA, the library. | |
*/ | |
private static $lib_path = 'lib'; | |
/** | |
* @var string This prefix will be used in THIS file to add filters. | |
*/ | |
private static $filter_prefix = 'myplugin'; | |
/** | |
* @var string The prefix denotes the folder in the library for this plugin. As well as the $plugin_prefix.php file. | |
*/ | |
private static $plugin_prefix = 'MyPlugin'; // Also the class prefix | |
/** | |
* @var array Contains the plugin header information at the top of THIS file as well as other data. | |
*/ | |
private static $plugin_data; | |
/** | |
* Construct the launcher and launch the $pluging_prefix.php file. | |
*/ | |
public function __construct() { | |
/** | |
* Work out the path to use for the plugin within the $lib_path directory. Becomes the primary path for the | |
* class autoloader. | |
*/ | |
$library_path = dirname( __FILE__ ) . DIRECTORY_SEPARATOR . self::$lib_path . DIRECTORY_SEPARATOR; | |
/** | |
* Get data from the plugin header. Its unlikely that the required core file is loaded, so we'll load it now. | |
*/ | |
if ( ! function_exists( 'get_plugin_data' ) ) { | |
require_once ABSPATH . 'wp-admin/includes/plugin.php'; | |
} | |
self::$plugin_data = get_plugin_data( __FILE__ ); | |
self::$plugin_data['LibraryPath'] = $library_path; | |
/** | |
* Determine plugin paths. Handy if custom paths have been identified or the plugin is installed in mu-plugins. | |
*/ | |
if ( defined( 'WP_PLUGIN_URL' ) && defined( 'WP_PLUGIN_DIR' ) && file_exists( plugin_dir_path( __FILE__ ) . basename( __FILE__ ) ) ) { | |
self::$plugin_data['Location'] = 'plugins'; | |
self::$plugin_data['PluginDirectory'] = plugin_dir_path( __FILE__ ) . self::$lib_path . DIRECTORY_SEPARATOR; | |
self::$plugin_data['PluginURL'] = plugins_url( '/' . self::$lib_path . '/', __FILE__ ); | |
self::$plugin_data['BaseDirectory'] = plugin_dir_path( __FILE__ ); | |
self::$plugin_data['BaseURL'] = plugins_url( '/', __FILE__ ); | |
} else if ( defined( 'WPMU_PLUGIN_URL' ) && defined( 'WPMU_PLUGIN_DIR' ) && file_exists( WPMU_PLUGIN_DIR . DIRECTORY_SEPARATOR . basename( __FILE__ ) ) ) { | |
self::$plugin_data['Location'] = 'mu-plugins'; | |
self::$plugin_data['PluginDirectory'] = WPMU_PLUGIN_DIR . DIRECTORY_SEPARATOR . self::$lib_path . DIRECTORY_SEPARATOR; | |
self::$plugin_data['PluginURL'] = WPMU_PLUGIN_URL . '/' . self::$lib_path . '/'; | |
self::$plugin_data['BaseDirectory'] = WPMU_PLUGIN_DIR . DIRECTORY_SEPARATOR; | |
self::$plugin_data['BaseURL'] = WPMU_PLUGIN_URL . '/'; | |
} else { | |
wp_die( sprintf( __( 'An issue occurred when determining where %s was installed. Please install again.', self::$plugin_data['TextDomain'] ), self::$plugin_data['Name'] ) ); | |
} | |
/** | |
* Add a filter to specify which classes to autoload. | |
*/ | |
add_filter( self::$filter_prefix . '_class_loader_classes', array( get_class(), 'class_loader_classes' ) ); | |
/** | |
* Some classes might need to be overridden. So add a filter to specify which classes to map to what paths. | |
*/ | |
add_filter( self::$filter_prefix . '_class_loader_overrides', array( get_class(), 'class_loader_overrides' ) ); | |
/** | |
* The heart of the plugin really starts here. No more require or require_once calls. Huzzah! | |
* But it does follow a strict structure in the file naming -- based on PSR0. | |
*/ | |
spl_autoload_register( array( get_class(), 'class_loader' ) ); | |
/** | |
* Setup the plugin (using call_user_func for PHP 5.2 compatibility) | |
* This is calls the entry point of the plugin. This called function will be where we really hook into WordPress. | |
*/ | |
call_user_func( self::$plugin_prefix . '::setup_plugin', __FILE__, self::$plugin_data ); | |
} | |
/** | |
* Add the classes that the autoloader need to recognize. | |
* | |
* We do this so that there are no conflicts with other autoloaders. BUT, more importantly, we only want to handle | |
* classes that belong to this plugin as the structure is specific to this library path and class structure. | |
* | |
* NOTE: Each entry in the array is a simple regular expression or very specific class. | |
* | |
* @param $classes Array passed in by the filter. | |
* | |
* @return array | |
*/ | |
public static function class_loader_classes( $classes ) { | |
$added_classes = array( | |
// The primary plugin class | |
self::$plugin_prefix, | |
// Setup MVC structure | |
// All classes beginning with $plugin_prefix_Model. E.g. MyPlugin_Model_Example | |
'^' . self::$plugin_prefix . '_Model', | |
// All classes beginning with $plugin_prefix_View. | |
'^' . self::$plugin_prefix . '_View', | |
// All classes beginning with $plugin_prefix_Controller. | |
'^' . self::$plugin_prefix . '_Controller', | |
// All classes beginning with $plugin_prefix_Helper. | |
'^' . self::$plugin_prefix . '_Helper', | |
// Other patterns below | |
); | |
return array_unique( array_merge( $classes, $added_classes ) ); | |
} | |
/** | |
* Add the classes that the autoloader need to override. | |
* | |
* Sometimes we might want to add a specific file path for a given glass. Add those entries in here. | |
* | |
* Note: These classes will also need to fit the pattern in class_loader_classes or it will be ignored. | |
* | |
* @param $overrides Array that contains the pattern 'class_name' => 'class_file_path' | |
* | |
* @return array | |
*/ | |
public static function class_loader_overrides( $overrides ) { | |
$added_overrides = array( | |
// Example | |
//'My_Class' => str_replace( '/', DIRECTORY_SEPARATOR, '/custom/path/file.php'), | |
); | |
return array_unique( array_merge( $overrides, $added_overrides ) ); | |
} | |
/** | |
* This is where the magic happens to avoid seas of require and require_once statements. | |
* | |
* How it works: PHP is pretty clever with determining which classes to load. If, however, a class does not | |
* exist we will get a fatal error. We want to jump in before that happens and give PHP a lifeline and say | |
* "If you cannot find the class by your normal means, you might be able to find it here." | |
* | |
* Now we will look at the called class and if it fits any of the patterns in our $includes_classes array, then | |
* we will attempt to include the file if it exists. If it is not one of our classes, we hope that somewhere else | |
* responsibility will be taken to prevent the fatal error, but we've done our job. | |
* | |
* @param $class The class called by PHP. | |
* | |
* @return bool | |
*/ | |
public static function class_loader( $class ) { | |
$basedir = self::$plugin_data['LibraryPath']; | |
$class = trim( $class ); | |
// Add included classes using a filter. | |
$included_classes = apply_filters( self::$filter_prefix . '_class_loader_classes', array() ); | |
// Add class overrides using a filter. | |
$class_overrides = apply_filters( self::$filter_prefix . '_class_loader_overrides', array() ); | |
$override_keys = array_keys( $class_overrides ); | |
// Get the regular expression to determine if we need to handle the called class. | |
$pattern = '/' . implode( '|', $included_classes ) . '/'; | |
// Yes we do... | |
if ( preg_match( $pattern, $class ) ) { | |
// If its not overridden, try this first... | |
if ( ! in_array( $class, $override_keys ) ) { | |
if ( self::$plugin_prefix == $class ) { | |
// Is it our main class? | |
$filename = $basedir . $class . DIRECTORY_SEPARATOR . $class . '.php'; | |
} else { | |
$filename = $basedir . str_replace( '_', DIRECTORY_SEPARATOR, $class ) . '.php'; | |
} | |
} else { | |
// This class has a different filename, so try this one. | |
$filename = $basedir . $class_overrides[ $class ]; | |
} | |
// One last chance to override the filename. | |
$filename = apply_filters( self::$filter_prefix . '_class_file_override', $filename, $class ); | |
// Include it if it exists and we have access. | |
if ( is_readable( $filename ) ) { | |
include_once $filename; | |
return true; | |
} | |
} | |
return false; | |
} | |
} | |
/** | |
* LAUNCH! | |
*/ | |
new MyPluginLauncher(); | |
/** | |
* For this example to work, create the following structure within this plugin's root folder. | |
* /myplugin.php | |
* /lib/MyPlugin | |
* /lib/MyPlugin/MyPlugin.php | |
* /lib/MyPlugin/Controller/ | |
* /lib/MyPlugin/Helper/ | |
* /lib/MyPlugin/Model/ | |
* /lib/MyPlugin/View/ | |
* | |
* See next file in the gist for contents of /lib/MyPlugin/MyPlugin.php | |
*/ | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment