Skip to content

Instantly share code, notes, and snippets.

@johnalarcon
Created December 18, 2018 21:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johnalarcon/0f2081333657fe56c4d2e2c92b597b9b to your computer and use it in GitHub Desktop.
Save johnalarcon/0f2081333657fe56c4d2e2c92b597b9b to your computer and use it in GitHub Desktop.
Autoloader for ClassicPress/WordPress Plugin Classes
<?php
/**
* Autoload classes
*
* Autoloaded classes? Yes. Support for namespaces, too? Of course! It helps
* to have a good understanding of how this autoloader works, so you can use
* it to your benefit, and not as a reason to pull out your hair. ;) Classes
* do not have to be namespaced, but it is recommended they are, for maximum
* plays-well-with-others...ness. Let's start with the super-basics...
*
* Say you had a class named ImageProcessor, under the same namespace as the
* plugin, the filename would be:
*
* ImageProcessor.class.php
*
* ...and, at the very least, it would need to contain:
*
* namespace ThePluginsNamespace; // same namespace as your plugin
*
* class ImageProcessor{}
*
* ...and it would go into:
*
* ../your-plugin-name/classes/
*
* The above example handles autoloading classes under the same namespace as
* the plugin and they are stored in a ../classes/ directory in your plugin.
* Note that, even if your class isn't namespaced, the above still works.
*
* To use deeper namespacing, just let your class names reflect their server
* path, relative to the ../classes/ directory. For example, if the variable
* $class contains underscores (or backslashes) those are first converted to
* forward slashes (/) and then a path is built. In other words, this object
* instantiation:
*
* new Snazzy_Thing;
*
* ...would make the autoloader search for a class called "Thing", which was
* namespaced under "Snazzy", and located in the directory:
*
* ../classes/Snazzy/Thing.class.php
*
* The contents of Thing.class.php would be something like:
*
* namespace Snazzy;
*
* class Thing{}
*
* As another example, just to help this stick, let's say you wanted to have
* a class of "Test" and you wanted to place it under the "Admin" namespace,
* which would be a sub-namespace of the plugin's primary namespace. Phew!
*
* 1. Create a directory called "Admin" inside the ../classes/ directory.
* 2. In the new directory, create a new file called "Test.class.php"
* 3. In the new file, add the namespace at the top, like this:
* namespace = Admin;
* 4. In the new file, add a class definition for the Test class, like this:
* class Test{}
* 5. Create a new Test object elsewhere in the plugin, with, say:
* $myclass = new Admin\Test;
* 6. Marvel in your awesomeness.
*
* Ok, great. But, what if you wanted to house your new namespace within the
* plugin's primary namespace, but you also wanted your own classes to be in
* directories that make sense to your project? Easy. Just change your code,
* in steps 3 and 5 (above) to:
*
* 3. note: primary namespace IS NOT preceded by backslash
* namespace = ThePluginsNamespace\Admin;
*
* 5. note: primary namespace IS preceded by backslash
* $myclass = new \ThePluginsNamespace\Admin\Test;
*
* See the big comment block below for specific examples of where the plugin
* is going to look for classes, and in what order. Note that extensions can
* filter in their own custom directories, if they choose. This is supported
* by the plugin.
*
* @author John Alarcon
*
* @since 0.1.0
*
* @param string $class The class name, fully qualified or not.
*/
public function autoload_classes($class) {
// Initialization.
$path = '';
$parts = [];
// Replace underscores with backslashes.
$class = str_replace('_', '\\', $class);
// Separate qualified class name on backslashes.
$parts = explode('\\', $class);
// If dealing with the same (ie, this) namespace, no need to include it.
if (isset($parts[0]) && $parts[0] === __NAMESPACE__) {
unset($parts[0]);
}
if (isset($parts[1]) && $parts[1] === __NAMESPACE__) {
unset($parts[1]);
}
// If anything is left in the array, build a path out of it.
if (!empty($parts)) {
foreach($parts as $part) {
$path .= '/'.$part;
}
}
// First: load internal utility classes; ie, InputHandler, Navigation.
if (file_exists($file = PATH_CLASSES.$path.'.class.php')) {
require_once $file;
return;
}
// Next, deal with internal extensions; ie, Settings, Help, etc.
/**
* INTERNAL extensions' classes should be checked for in 3 ways. This is
* done to accommodate common approaches in structuring an extension. If
* an extension has only a single file, it can be dropped right into the
* ../extensions/ directory. When an extension has scripts or styles, it
* can be stored in its own directory and dropped into the ../extensions
* directory, as well. Finally, if an extension holds its classes inside
* a dedicated ../classes/ directory, that's fine, too. Note that all of
* these checks are ONLY for internal extensions.
*
* 1) single-file classes stored in ../extensions/ directory
* /plugins/my-plugin-name/extensions/MyClass.class.php
*
* 2) directory-based classes in ../extensions/ directory
* /plugins/my-plugin-name/extensions/MyClass/MyClass.class.php
*
* 3) directory-based extensions with a ../classes/ directory inside
* /plugins/my-plugin-name/extensions/MyClass/classes/MyClass.class.php
*
* EXTERNAL extensions are those that get installed later – sometimes as
* a premium add-on. If your plugin gets popular, other developers might
* even write extensions to enhance it. Awesome!
*
* 4) directory-based; by other authors, premium add-ons, etc.
* /plugins/your-extension/YourExtension.class.php
* /plugins/your-extension/classes/YourExtension.class.php
*
*/
// Load internal extension classes; for single-file extensions.
if (file_exists($file = PATH_EXTENSIONS.'/'.$part.'.class.php')) {
require_once $file;
return;
}
// Load internal extension classes; for directory-stored extensions.
if (file_exists($file = PATH_EXTENSIONS.'/'.$part.'/'.$part.'.class.php')) {
require_once $file;
return;
}
// Load internal extension classes; directory-stored, /class directory.
if (file_exists($file = PATH_EXTENSIONS.'/'.$part.'/classes/'.$part.'.class.php')) {
require_once $file;
return;
}
// Note to self: Self, does this prevent breakage during core updates?
// Self: Why, yes, it does. Otherwise, this autoloader hijacks the show.
if (!isset($parts[1]) || empty($parts[1])) {
return;
}
// Class not found? Check external extensions' ../classes/ directory.
$extensions = get_option(PLUGIN_PREFIX.'_extensions_active', []);
foreach ($extensions as $file_slug) {
if (file_exists($file = PATH_PLUGINS.'/'.substr($file_slug, 0, strlen($file_slug) - (strlen($file_slug) - strrpos($file_slug, '/'))).'/classes/'.$parts[1].'.class.php')) {
require_once $file;
$class_found = true;
break;
}
}
if (isset($class_found)) {
return;
}
// Still not found? Last chance! Did an extension add an include path?
$autoload_paths = apply_filters(PLUGIN_PREFIX.'_autoload_paths', []);
foreach ($autoload_paths as $path) {
$file = $path.'/'.$parts[1].'.class.php';
if (file_exists($file)) {
require_once $file;
break;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment