I’m hoping to design a simple plugin based on pico CMS and call it in a protected function in my main class, called App
.
Plugin is called AppHelper
.
Attempt
I added a required_once
on the top of App
class:
require_once "/Library/WebServer/usco/master/plugins/AppHelper.php";
and tested using this (which first AppHelper::isMobile
is not right):
// ********************MOBILE OR DESKTOP***********************// if (AppHelper::isMobile($_SERVER['HTTP_USER_AGENT'])) { $viewSetting = $_SESSION["view-setting"] = array("device" => "mobile"); // IF MOBILE } else { $viewSetting = $_SESSION["view-setting"] = array("device" => "desktop"); // IF DESKTOP } // ********************MOBILE OR DESKTOP***********************//
in a protected method:
protected function loadConfig() { $config = null; if (file_exists($this->getConfigDir() . 'config.php')) { require $this->getConfigDir() . 'config.php'; } // ********************MOBILE OR DESKTOP***********************// if (AppHelper::isMobile($_SERVER['HTTP_USER_AGENT'])) { $viewSetting = $_SESSION["view-setting"] = array("device" => "mobile"); // IF MOBILE } else { $viewSetting = $_SESSION["view-setting"] = array("device" => "desktop"); // IF DESKTOP } // ********************MOBILE OR DESKTOP***********************// $defaultConfig = array( 'company_name' => 'USCO', 'page_title' => 'USCO', 'base_url' => '', 'rewrite_url' => null, 'date_format' => '%D %T', 'twig_config' => null, 'blog_content_dir' => null, 'blog_content_path' => null, 'blog_articles_dir' => null, 'blog_articles_path' => null, 'blog_content_ext' => '.md', 'device' => $viewSetting["device"], 'timezone' => '', ); $this->config = is_array($this->config) ? $this->config : array(); $this->config += is_array($config) ? $config + $defaultConfig : $defaultConfig; if (empty($this->config['base_url'])) { $this->config['base_url'] = $this->getBaseUrl(); } else { $this->config['base_url'] = rtrim($this->config['base_url'], '/') . '/'; } if ($this->config['rewrite_url'] === null) { $this->config['rewrite_url'] = $this->isUrlRewritingEnabled(); } $local_domain = "usco.loc"; if ($_SERVER['SERVER_NAME'] === $local_domain) { $defaultTwigConfig = array('cache' => false, 'autoescape' => false, 'debug' => true); // $defaultTwigConfig = array('cache' => __DIR__ . "/../cache", 'autoescape' => false, 'debug' => false); } else { $defaultTwigConfig = array('cache' => __DIR__ . "/../cache", 'autoescape' => false, 'debug' => false); } if (!is_array($this->config['twig_config'])) { $this->config['twig_config'] = $defaultTwigConfig; } else { $this->config['twig_config'] += $defaultTwigConfig; } $this->config['blog_content_path'] = $this->getAbsolutePath($this->config['blog_content_dir'], true); $this->config['blog_articles_path'] = $this->config['blog_content_path'] . $this->config['blog_articles_dir']; if (empty($this->config['timezone'])) { // explicitly set a default timezone to prevent a E_NOTICE // when no timezone is set; the `date_default_timezone_get()` // function always returns a timezone, at least UTC $this->config['timezone'] = @date_default_timezone_get(); } date_default_timezone_set($this->config['timezone']); }
and works okay with just a warning. However, this is not right.
How do I correctly design/add this plugin without
required_once
and autoload it usingcomposer install
, like all my other plugins that function so?
AppHelper
class AppHelper extends AbstractAppPlugin { public function __construct(App $app) { parent::__construct($app); } public function isMobile($useragent) { if ( preg_match('/(android|bbd+|meego).+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap|windows ce|xda|xiino/i', $useragent) || preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i', substr($useragent, 0, 4)) ) { return true; // is mobile } else { return false; // is not mobile }; } }
AbstractAppPlugin
abstract class AbstractAppPlugin implements AppPluginInterface { /** * Current instance of App * * @see AppPluginInterface::getApp() * @var App */ private $app; /** * Boolean indicating if this plugin is enabled (true) or disabled (false) * * @see AppPluginInterface::isEnabled() * @see AppPluginInterface::setEnabled() * @var boolean */ protected $enabled = true; /** * Boolean indicating if this plugin was ever enabled/disabled manually * * @see AppPluginInterface::isStatusChanged() * @var boolean */ protected $statusChanged = false; /** * List of plugins which this plugin depends on * * @see AbstractAppPlugin::checkDependencies() * @see AppPluginInterface::getDependencies() * @var string[] */ protected $dependsOn = array(); /** * List of plugin which depend on this plugin * * @see AbstractAppPlugin::checkDependants() * @see AppPluginInterface::getDependants() * @var object[] */ private $dependants; /** * @see AppPluginInterface::__construct() */ public function __construct(App $app) { $this->app = $app; } /** * @see AppPluginInterface::handleEvent() */ public function handleEvent($eventName, array $params) { // plugins can be enabled/disabled using the config if ($eventName === 'onConfigLoaded') { $pluginEnabled = $this->getConfig(get_called_class() . '.enabled'); if ($pluginEnabled !== null) { $this->setEnabled($pluginEnabled); } else { $pluginConfig = $this->getConfig(get_called_class()); if (is_array($pluginConfig) && isset($pluginConfig['enabled'])) { $this->setEnabled($pluginConfig['enabled']); } elseif ($this->enabled) { // make sure dependencies are already fulfilled, // otherwise the plugin needs to be enabled manually try { $this->checkDependencies(false); } catch (RuntimeException $e) { $this->enabled = false; } } } } if ($this->isEnabled() || ($eventName === 'onPluginsLoaded')) { if (method_exists($this, $eventName)) { call_user_func_array(array($this, $eventName), $params); } } } /** * @see AppPluginInterface::setEnabled() */ public function setEnabled($enabled, $recursive = true, $auto = false) { $this->statusChanged = (!$this->statusChanged) ? !$auto : true; $this->enabled = (bool) $enabled; if ($enabled) { $this->checkDependencies($recursive); } else { $this->checkDependants($recursive); } } /** * @see AppPluginInterface::isEnabled() */ public function isEnabled() { return $this->enabled; } /** * @see AppPluginInterface::isStatusChanged() */ public function isStatusChanged() { return $this->statusChanged; } /** * @see AppPluginInterface::getApp() */ public function getApp() { return $this->app; } /** * Passes all not satisfiable method calls to App * * @see App * @param string $methodName name of the method to call * @param array $params parameters to pass * @return mixed return value of the called method */ public function __call($methodName, array $params) { if (method_exists($this->getApp(), $methodName)) { return call_user_func_array(array($this->getApp(), $methodName), $params); } throw new BadMethodCallException( 'Call to undefined method ' . get_class($this->getApp()) . '::' . $methodName . '() ' . 'through ' . get_called_class() . '::__call()' ); } /** * Enables all plugins which this plugin depends on * * @see AppPluginInterface::getDependencies() * @param boolean $recursive enable required plugins automatically * @return void * @throws RuntimeException thrown when a dependency fails */ protected function checkDependencies($recursive) { foreach ($this->getDependencies() as $pluginName) { try { $plugin = $this->getPlugin($pluginName); } catch (RuntimeException $e) { throw new RuntimeException( "Unable to enable plugin '" . get_called_class() . "': " . "Required plugin '" . $pluginName . "' not found" ); } // plugins which don't implement AppPluginInterface are always enabled if (is_a($plugin, 'AppPluginInterface') && !$plugin->isEnabled()) { if ($recursive) { if (!$plugin->isStatusChanged()) { $plugin->setEnabled(true, true, true); } else { throw new RuntimeException( "Unable to enable plugin '" . get_called_class() . "': " . "Required plugin '" . $pluginName . "' was disabled manually" ); } } else { throw new RuntimeException( "Unable to enable plugin '" . get_called_class() . "': " . "Required plugin '" . $pluginName . "' is disabled" ); } } } } /** * @see AppPluginInterface::getDependencies() */ public function getDependencies() { return (array) $this->dependsOn; } /** * Disables all plugins which depend on this plugin * * @see AppPluginInterface::getDependants() * @param boolean $recursive disabled dependant plugins automatically * @return void * @throws RuntimeException thrown when a dependency fails */ protected function checkDependants($recursive) { $dependants = $this->getDependants(); if (!empty($dependants)) { if ($recursive) { foreach ($this->getDependants() as $pluginName => $plugin) { if ($plugin->isEnabled()) { if (!$plugin->isStatusChanged()) { $plugin->setEnabled(false, true, true); } else { throw new RuntimeException( "Unable to disable plugin '" . get_called_class() . "': " . "Required by manually enabled plugin '" . $pluginName . "'" ); } } } } else { $dependantsList = 'plugin' . ((count($dependants) > 1) ? 's' : '') . ' '; $dependantsList .= "'" . implode("', '", array_keys($dependants)) . "'"; throw new RuntimeException( "Unable to disable plugin '" . get_called_class() . "': " . "Required by " . $dependantsList ); } } } /** * @see AppPluginInterface::getDependants() */ public function getDependants() { if ($this->dependants === null) { $this->dependants = array(); foreach ($this->getPlugins() as $pluginName => $plugin) { // only plugins which implement AppPluginInterface support dependencies if (is_a($plugin, 'AppPluginInterface')) { $dependencies = $plugin->getDependencies(); if (in_array(get_called_class(), $dependencies)) { $this->dependants[$pluginName] = $plugin; } } } } return $this->dependants; } }
Advertisement
Answer
I don’t see any namespace in your classes? Can you add how the classes map to the filesystem? There is a PHP-standard called PSR-4 which is commonly used. If this does not meet your requirements, composer offers a few alternative autoloading options that you can specify in your plugin’s composer.json.
See: https://getcomposer.org/doc/04-schema.md#autoload
For example if your plugin only consists of this one AppHelper class that is kept in a file src/AppHelper.php
and the abstract base class you could just add a classmap with those 2 files to your composer.json:
... "autoload" { "classmap": ["src/AppHelper.php", "src/AbstractAppHelper.php"] }, ...
Since both classes map to the classes this would work just as well with PSR-0:
... "autoload" { "psr-0": { "\": "src/" } }, ...
This will add all the php files in the src directory to a corresponding class, so:
src/AppHelper.php -> class AppHelper src/AbstractAppHelper.php -> class AbstractAppHelper
and so on. You might run into trouble though as you are mapping into the global namespace. Using a custom namespace for those classes instead is probably a better choice and usually can be done quickly with the help of some tools like the refactoring helpers in PhpStorm.
edit: Sorry, I missed the link to the actual repository. In your case it should probably look like:
"autoload": { "psr-0": { "\": "master/plugins", } }