/ */ class P4Cms_Module extends P4Cms_PackageAbstract { const PACKAGE_FILENAME = 'module.ini'; const CONFIG_RECORD_PATH = 'config/module'; protected $_configRecord = null; protected $_configFolderName = 'module'; protected static $_idField = 'name'; protected static $_coreModulesPath = null; protected static $_packagesPaths = array(); protected static $_configRecords = null; protected static $_isInitialized = array(); protected static $_isLoaded = array(); protected static $_dependencyTypes = array('library', 'module'); protected static $_fields = array( 'name' => array( 'accessor' => 'getName', 'mutator' => 'setName' ), 'label' => array( 'accessor' => 'getLabel', 'mutator' => 'readOnly' ), 'core' => array( 'accessor' => 'isCoreModule', 'mutator' => 'readOnly' ), 'enabled' => array( 'accessor' => 'isEnabled', 'mutator' => 'readOnly' ), 'configurable' => array( 'accessor' => 'isConfigurable', 'mutator' => 'readOnly' ), 'configUri' => array( 'accessor' => 'getConfigUri', 'mutator' => 'readOnly' ), 'version' => array( 'accessor' => 'getVersion', 'mutator' => 'readOnly' ), 'description' => array( 'accessor' => 'getDescription', 'mutator' => 'readOnly' ), 'maintainerInfo' => array( 'accessor' => 'getMaintainerInfo', 'mutator' => 'readOnly' ), 'dependencies' => array( 'accessor' => 'getDependencies', 'mutator' => 'readOnly' ), 'tags' => array( 'accessor' => 'getTags', 'mutator' => 'readOnly' ) ); /** * Throws read-only exceptions on behalf of most mutators. * * @throws P4Cms_Module_Exception Every time; configured for specific mutators. */ public function readOnly() { throw new P4Cms_Module_Exception('P4Cms_Module is primarily a read-only class.'); } /** * Set the full path to the package folder. * Extends parent to ensure that basename is valid in a class name. * * @param string $path the full path to the package folder. * @return P4Cms_PackageAbstract provides fluent interface. */ public function setPath($path) { // validate module name - only allow characters valid in a class name. if (preg_match('/[^a-z0-9\\/\\\\_.-]/i', basename($path))) { throw new P4Cms_Module_Exception( "Invalid module ('" . $name . "'). " . "The module name contains illegal characters." ); } return parent::setPath($path); } /** * Get the name of this package. * Extends parent to upper-case the first letter. * * @return string the name of this package. */ public function getName() { return ucfirst(parent::getName()); } /** * Set the name of this package. * * @param string $name The new name to use. * @return P4Cms_Module To maintain a fluent interface. */ public function setName($name) { return $this->_setValue('name'); } /** * Get the url to this module's resources folder. * * @return string the base url for this module's resources. */ public function getBaseUrl() { return parent::getBaseUrl() . '/resources'; } /** * Get any routes configured for this module. * * @return array list of routes from module.ini. */ public function getRoutes() { $info = $this->getPackageInfo(); return isset($info['routes']) && is_array($info['routes']) ? $info['routes'] : array(); } /** * Get all core modules. * * @return P4Cms_Model_Iterator all core modules. */ public static function fetchAllCore() { $modules = new P4Cms_Model_Iterator; foreach (static::fetchAll() as $module) { if ($module->isCoreModule()) { $modules[] = $module; } } return $modules; } /** * Get all modules that are enabled - includes core modules * unless $excludeCoreModules is set to true. * * @param bool $excludeCoreModules optional - defaults to false - set to true to * exclude core modules from the set of returned modules. * @return P4Cms_Model_Iterator all enabled modules. */ public static function fetchAllEnabled($excludeCoreModules = false) { $modules = new P4Cms_Model_Iterator; foreach (static::fetchAll() as $module) { if ((!$module->isCoreModule() || !$excludeCoreModules) && $module->isEnabled()) { $modules[] = $module; } } return $modules; } /** * Get all modules that are disabled. * * @return P4Cms_Model_Iterator all disabled modules. */ public static function fetchAllDisabled() { $modules = new P4Cms_Model_Iterator; foreach (static::fetchAll() as $module) { if (!$module->isEnabled()) { $modules[] = $module; } } return $modules; } /** * Enable this module for the current site. * * @return P4Cms_Module provides a fluent interface. * @throws P4Cms_Module_Exception if the module cannot be enabled. */ public function enable() { // ensure module is disabled. $this->_ensureDisabled(); // ensure dependencies are satisfied. if (!$this->areDependenciesSatisfied()) { throw new P4Cms_Module_Exception("Can't enable the '" . $this->getName() . "' module. One or more dependencies are not satisfied."); } // enable module by saving its configuration. // this will restore the previous config if one exists. $this->saveConfig(null, "Enabled '" . $this->getName() . "' module."); // initialize the module in the environment. $this->init(); try { if ($this->hasIntegrationMethod('enable')) { $this->callIntegrationMethod('enable'); } } catch (Exception $e) { $message = "Failed to enable the '" . $this->getName() . "' module."; P4Cms_Log::logException($message, $e); $this->disable(); throw new P4Cms_Module_Exception($message); } return $this; } /** * Disable this module. * * @return P4Cms_Module provides fluent interface. * @throws P4Cms_Module_Exception if the module cannot be disabled. */ public function disable() { // ensure module is enabled to begin with. $this->_ensureEnabled(); // ensure module is not a core module. if ($this->isCoreModule()) { throw new P4Cms_Module_Exception("The '" . $this->getName() . "' module is a core module. Core modules cannot be disabled."); } // ensure dependencies are respected. if ($this->hasDependents()) { throw new P4Cms_Module_Exception("Can't disable the '" . $this->getName() . "' module. One or more enabled modules depend upon it."); } // call the 'disable' integration method if defined. if ($this->hasIntegrationMethod('disable')) { $this->callIntegrationMethod('disable'); } // disable module by deleting config record. if ($this->_hasStoredConfigRecord()) { $this->_getConfigRecord()->delete("Disabled '" . $this->getName() . "' module."); unset(static::$_configRecords[$this->getName()]); } return $this; } /** * Check if this module is enabled. * A module is considered enabled if it has a config record in storage. * * @return boolean true if the module is enabled. */ public function isEnabled() { // core modules are always enabled. if ($this->isCoreModule()) { return true; } // check for stored config record. return $this->_hasStoredConfigRecord(); } /** * Check if the module can be enabled. * * @return bool true if the module can be enabled. */ public function canEnable() { if ($this->isCoreModule() || $this->isEnabled() || !$this->areDependenciesSatisfied()) { return false; } else { return true; } } /** * Check if the module can be disabled. * * @return bool true if the module can be disabled. */ public function canDisable() { if ($this->isCoreModule() || !$this->isEnabled() || $this->hasDependents()) { return false; } else { return true; } } /** * Determine if all of the module dependencies are satisfied and * the module can be enabled. * * @return boolean true if all of the required modules are installed and enabled. */ public function areDependenciesSatisfied() { // if no dependencies, can be enabled. if (!$this->hasDependencies()) { return true; } // check if required modules are installed and enabled. foreach ($this->getDependencies() as $dependency) { extract($dependency); if (!static::isDependencySatisfied($name, $type, $versions)) { return false; } } // all dependencies are satisfied - can enable! return true; } /** * Determine if the module has any dependencies. * * @return boolean true if the module has dependencies. */ public function hasDependencies() { return (bool) count($this->getDependencies()); } /** * Determine if any other enabled modules depend upon this module. * * @return boolean true if other enabled modules depend on this module. */ public function hasDependents() { // check if any enabled modules are dependent on this module. foreach (P4Cms_Module::fetchAllEnabled() as $module) { if (!$module->hasDependencies()) { continue; } $dependencies = $module->getDependencies(); foreach ($dependencies as $dependency) { if ($dependency['type'] === 'module' && $dependency['name'] == $this->getName() ) { return true; } } } return false; } /** * Check if the given dependency is satisified. * * Versions may be specified using wildcards such as '*' and '?'. * Matching is performed using fnmatch() and therefore supports * all of the same shell patterns recognized by that function. * * @param string $name the name of the required package. * @param string $type the type of dependency (module or library) * @param string|array $versions suitable versions of the package. * @return bool true if the correct package is installed/enabled. */ public static function isDependencySatisfied($name, $type, $versions) { // dependency name and a valid type must be specified. if (!isset($name) || !in_array($type, static::$_dependencyTypes)) { return false; } // accept single version string or array of versions. $versions = (array) $versions; // differentiate module and library dependency checking. if ($type === 'library') { $className = $name . "_Version"; if (!class_exists($className)) { return false; } $version = $className::VERSION; } else if ($type === 'module') { // check if the named module exists. if (!P4Cms_Module::exists($name)) { return false; } // fetch the named module and check if it's enabled. $module = P4Cms_Module::fetch($name); if (!$module->isEnabled()) { return false; } $version = $module->getVersion(); } // check version. foreach ($versions as $suitableVersion) { if (fnmatch($suitableVersion, $version)) { return true; } } return false; } /** * Get the name of the corresponding integration class for this module. * * @return string the name of the integration class. */ public function getIntegrationClassName() { return $this->getName() . '_Module'; } /** * Determine if this module is a core module. * * @return bool true if this module is a core (required) module. */ public function isCoreModule() { return (dirname($this->getPath()) == $this->getCoreModulesPath()); } /** * Set the path to the core modules. * * @param string $path the path to the core modules. */ public static function setCoreModulesPath($path) { static::$_coreModulesPath = $path; } /** * Get the path to the core modules. * * @return string the path that contains the core modules. */ public static function getCoreModulesPath() { if (!static::$_coreModulesPath) { throw new P4Cms_Module_Exception("Can't get core modules. Path is unset."); } return static::$_coreModulesPath; } /** * Extends parent to add core modules path last. * * @return array the list of paths that can contain packages. */ public static function getPackagesPaths() { $paths = static::$_packagesPaths; $paths[] = static::getCoreModulesPath(); return $paths; } /** * Get any dependencies that this module has on other packages. * * @return array list of dependencies - each entry contains three elements: * - name (name of package) * - type (type of package) * - versions (list of suitable versions) */ public function getDependencies() { $info = $this->getPackageInfo(); if (!isset($info['dependencies']) || !is_array($info['dependencies'])) { return array(); } // normalize/flatten dependencies. $dependencies = array(); foreach (static::$_dependencyTypes as $type) { if (!isset($info['dependencies'][$type]) || !is_array($info['dependencies'][$type])) { continue; } // convert comma-separated version list to array form. foreach ($info['dependencies'][$type] as $name => $versions) { $dependencies[] = array( 'name' => $name, 'type' => $type, 'versions' => str_getcsv($versions, ',') ); } } return $dependencies; } /** * Initialize the module in the environment. * * This method only needs to be called once per-module, per-request. * to register the module with the application. * * @return P4Cms_Module provides fluent interface. */ public function init() { // don't initialize modules twice if ($this->_isInitialized()) { return $this; } // an instance of the view is required to properly initialize a module. if (!Zend_Layout::getMvcInstance()) { Zend_Layout::startMvc(); } $view = Zend_Layout::getMvcInstance()->getView(); if (!$view instanceof Zend_View) { throw new P4Cms_Module_Exception("Can't initialize module. Failed to get view instance."); } // add module to loader so that module classes will auto-load. P4Cms_Loader::addPackagePath($this->getName(), $this->getPath()); // add module's controllers directory to front controller so that requests will route. Zend_Controller_Front::getInstance()->addControllerDirectory( $this->getPath() . '/controllers', basename($this->getPath()) ); // each module can have layouts - add layout script path. if (is_dir($this->getPath() . '/layouts/scripts')) { $view->addScriptPath($this->getPath() . '/layouts/scripts'); } // each module can have helpers (under views or layouts) - add helper paths. if (is_dir($this->getPath() . '/views/helpers')) { $view->addHelperPath( $this->getPath() . '/views/helpers/', $this->getName() . '_View_Helper_' ); } if (is_dir($this->getPath() . '/layouts/helpers')) { $view->addHelperPath( $this->getPath() . '/layouts/helpers/', $this->getName() . '_View_Helper_' ); } // each module can have form plugins: // elements, decorators, validators and filters. $plugins = array( array(Zend_Form::ELEMENT, '/forms/elements/', 'Form_Element'), array(Zend_Form::DECORATOR, '/forms/decorators/', 'Form_Decorator'), array(Zend_Form_Element::VALIDATE, '/validators/', 'Validate'), array(Zend_Form_Element::FILTER, '/filters/', 'Filter') ); foreach ($plugins as $plugin) { if (is_dir($this->getPath() . $plugin[1])) { P4Cms_Form::registerPrefixPath( $this->getName() . "_" . $plugin[2], $this->getPath() . $plugin[1], $plugin[0] ); } } // each module can have controller action helpers (under controllers) if (is_dir($this->getPath() . '/controllers/helpers')) { Zend_Controller_Action_HelperBroker::addPath( $this->getPath() . '/controllers/helpers/', $this->getName() . '_Controller_Helper_' ); } // call the 'init' integration method if defined. try { if ($this->hasIntegrationMethod('init')) { $this->callIntegrationMethod('init'); } } catch (Exception $e) { P4Cms_Log::logException("Failed to init the '" . $this->getName() . "' module.", $e); } // flag the module as initialized $this->_setInitialized(); return $this; } /** * Attempt to load this module * * @return P4Cms_Module provides fluent interface. * @todo Consider moving guts of this to init() as it isn't clear why * stylesheets, scripts, dojo modules and routes are added here and * not during init - we should keep load() for third-party modules. */ public function load() { // don't load modules twice (without unloading first) if ($this->isLoaded()) { return $this; } // add module meta, stylesheets and scripts to view. $this->_loadHtmlMeta(); $this->_loadStylesheets(); $this->_loadScripts(); // load dojo components. $this->_loadDojo(); // if the module is a route provider add the routes to the router. $router = Zend_Controller_Front::getInstance()->getRouter(); $router->addConfig(new Zend_Config($this->getRoutes())); try { if ($this->hasIntegrationMethod('load')) { $this->callIntegrationMethod('load'); } $this->_setLoaded(); } catch (Exception $e) { P4Cms_Log::logException("Failed to load the '" . $this->getName() . "' module.", $e); } return $this; } /** * Determine if this module has a corresponding static module integration class. * * @return boolean true if the module has a corresponding module integration class. */ public function hasIntegrationClass() { if (is_file($this->getPath() . '/Module.php') && class_exists($this->getIntegrationClassName())) { return true; } else { return false; } } /** * Determine whether an integration class method exists. * * @param string $method the name of the method to find. */ public function hasIntegrationMethod($method) { if (!$this->hasIntegrationClass()) { return false; } if (!method_exists($this->getIntegrationClassName(), $method)) { return false; } return true; } /** * Call a static method on the integration class. * * @param string $method the name of the method to call. * @param array $params optional - the arguments to pass to the method. * @return mixed the return value of the integration method. * @throws P4Cms_Module_Exception if the integration class or method does not exist. */ public function callIntegrationMethod($method, $params = array()) { if (!$this->hasIntegrationClass()) { throw new P4Cms_Module_Exception("Can't call integration method: '" . $method . "'. " . $this->getName() . " module does not have an integration class."); } if (!method_exists($this->getIntegrationClassName(), $method)) { throw new P4Cms_Module_Exception("Can't call integration method: '" . $method . "'. " . $this->getName() . " module does not have this method."); } return call_user_func_array(array($this->getIntegrationClassName(), $method), $params); } /** * Determine if this module is configurable. * Considered configurable if the module.ini file specifies * a configure controller. * * @return boolean true if the module is configurable. */ public function isConfigurable() { $info = $this->getPackageInfo(); return isset($info['configure']['controller']) && is_array($info['configure']); } /** * Get the URI to configure this module. * * @return string the URI to configure this module. */ public function getConfigUri() { if (!$this->isConfigurable()) { throw new P4Cms_Module_Exception( "Cannot get URI to configure '" . $this->getName() . "'. The module is not configurable." ); } // return assembled uri. $router = Zend_Controller_Front::getInstance()->getRouter(); return $router->assemble($this->getConfigRouteParams(), null, true); } /** * Get the route params to configure this module. * * @return array the route params to configure this module. */ public function getConfigRouteParams() { if (!$this->isConfigurable()) { throw new P4Cms_Module_Exception( "Cannot get URI params to configure '" . $this->getName() . "'. The module is not configurable." ); } // if no module is specified, default to this module. $info = $this->getPackageInfo(); $routeParams = $info['configure']; if (!isset($routeParams['module'])) { $routeParams['module'] = $this->getRouteFormattedName(); } return $routeParams; } /** * Get the route-formatted name of this module (e.g. foo-bar * instead of FooBar). * * @return string the route formatted module name. */ public function getRouteFormattedName() { return P4Cms_Controller_Router_Route_Module::formatRouteParam( $this->getName() ); } /** * Get the configuration of this module. * * This returns a Zend_Config object which can contain any * configuration information the module developer chooses to * store. * * @return Zend_Config the module configuration. */ public function getConfig() { return $this->_getConfigRecord()->getConfig(); } /** * Set the configuration of this module. This does * not save the configuration. You must call saveConfig() * to store the configuration persistently. * * @param Zend_Config $config the configuration to set. * @return P4Cms_Module provides fluent interface. * @throws InvalidArgumentException if the given config is invalid. */ public function setConfig($config) { $this->_getConfigRecord()->setConfig($config); return $this; } /** * Save the configuration of this module. * * @param Zend_Config $config optional - the configuration to save. * @param string $description optional - a description of the change. * @return P4Cms_Module provides fluent interface. * @throws P4Cms_Record_Exception if an invalid config object is given. */ public function saveConfig($config = null, $description = null) { $record = $this->_getConfigRecord(); // if config is given, set it. if ($config) { $record->setConfig($config); } // if no description is given, generate one. if (!$description) { $description = "Saved configuration for '" . $this->getName() . "' module."; } // save config record. $record->save($description); static::$_configRecords[$this->getName()] = $record; return $this; } /** * Clear the config records cache. */ public static function clearConfigCache() { static::$_configRecords = null; } /** * Clear the config cache, modules paths and the list of loaded and initialized modules. * This is useful for testing purposes. */ public static function reset() { static::$_isLoaded = array(); static::$_isInitialized = array(); static::$_coreModulesPath = null; static::clearPackagesPaths(); static::clearConfigCache(); } /** * Get the flag indicating loaded status. */ public function isLoaded() { return isset(static::$_isLoaded[$this->getName()]); } /** * Determine if this module has a configuration record in storage. * This method will only return true if the record is in storage. * * @return bool true if this module has a config record in storage; false otherwise. */ protected function _hasStoredConfigRecord() { return array_key_exists($this->getName(), $this->_getConfigRecords()); } /** * Get the instance copy of the config record associated with this module. * If no record exists in storage, one will be created in memory. * * @return P4Cms_Record_Config instance copy of this module's config record. */ protected function _getConfigRecord() { // get/make instance copy of config record if necessary. if ($this->_configRecord === null) { if ($this->_hasStoredConfigRecord()) { $records = $this->_getConfigRecords(); $this->_configRecord = clone $records[$this->getName()]; } else { $this->_configRecord = new P4Cms_Record_Config; $this->_configRecord->setId(self::CONFIG_RECORD_PATH . '/' . $this->getName()); } } return $this->_configRecord; } /** * Get all module config records from storage. * * Provides localized cache of records to avoid repeatedly * fetching records from Perforce. Records are keyed on module * name for quick/easy lookups. * * @return P4Cms_Model_Iterator all module config records. */ protected function _getConfigRecords() { // only fetch config records once. if (static::$_configRecords === null) { $records = P4Cms_Record_Config::fetchAll( P4Cms_Record_Query::create()->addPath(self::CONFIG_RECORD_PATH .'/...') ); // store records keyed on module name. static::$_configRecords = array(); foreach ($records as $record) { static::$_configRecords[basename($record->getId())] = $record; } } return static::$_configRecords; } /** * Ensure that this module is disabled. * * @throws P4Cms_Module_Exception if the module is not disabled. */ protected function _ensureDisabled() { if ($this->isEnabled()) { throw new P4Cms_Module_Exception( "The '" . $this->getName() . "' module is not disabled." ); } } /** * Ensure that this module is enabled. * * @throws P4Cms_Module_Exception if the module is not enabled. */ protected function _ensureEnabled() { if (!$this->isEnabled()) { throw new P4Cms_Module_Exception( "The '" . $this->getName() . "' module is a not enabled." ); } } /** * Set the flag indicating loaded status. */ private function _setLoaded() { static::$_isLoaded[$this->getName()] = true; } /** * Set the flag indicating unloaded status. */ private function _setUnloaded() { static::$_isLoaded[$this->getName()] = false; } /** * Get the flag indicating initialization status. */ private function _isInitialized() { return isset(static::$_isInitialized[$this->getName()]); } /** * Set the flag indicating initialization status. */ private function _setInitialized() { static::$_isInitialized[$this->getName()] = true; } }