configData[$moduleID] = $setData; return array(); } else if(isset($this->configData[$moduleID])) { return $this->configData[$moduleID]; } else { return null; } } /** * Return the URL where the module can be edited, configured or uninstalled * * If module is not installed, it returns URL to install the module. * * #pw-group-configuration * * @param string|Module $className * @param bool $collapseInfo * @return string * */ public function getModuleEditUrl($className, $collapseInfo = true) { if(!is_string($className)) $className = $this->modules->getModuleClass($className); $url = $this->wire()->config->urls->admin . 'module/'; if(empty($className)) return $url; if(!$this->modules->isInstalled($className)) return $this->modules->getModuleInstallUrl($className); $url .= "edit/?name=$className"; if($collapseInfo) $url .= "&collapse_info=1"; return $url; } /** * Given a module name, return an associative array of configuration data for it * * - Applicable only for modules that support configuration. * - Configuration data is stored encoded in the database "modules" table "data" field. * * ~~~~~~ * // Getting, modifying and saving module config data * $data = $modules->getConfig('HelloWorld'); * $data['greeting'] = 'Hello World! How are you today?'; * $modules->saveConfig('HelloWorld', $data); * * // Getting just one property 'apiKey' from module config data * @apiKey = $modules->getConfig('HelloWorld', 'apiKey'); * ~~~~~~ * * #pw-group-configuration * #pw-changelog 3.0.16 Changed from more verbose name `getModuleConfigData()`, which can still be used. * * @param string|Module $class * @param string $property Optionally just get value for a specific property (omit to get all config) * @return array|string|int|float Module configuration data, returns array unless a specific $property was requested * @see Modules::saveConfig() * @since 3.0.16 Use method getModuleConfigData() with same arguments for prior versions (can also be used on any version). * */ public function getConfig($class, $property = '') { $emptyReturn = $property ? null : array(); $className = $class; if(is_object($className)) $className = wireClassName($className->className(), false); $id = $this->moduleID($className); if(!$id) return $emptyReturn; $data = isset($this->configData[$id]) ? $this->configData[$id] : null; if($data === null) return $emptyReturn; // module has no config data if(is_array($data)) { // great } else { // configData===1 indicates data must be loaded from DB $configable = $this->isConfigable($className); if(!$configable) return $emptyReturn; $database = $this->wire()->database; $query = $database->prepare("SELECT data FROM modules WHERE id=:id", "modules.getConfig($className)"); // QA $query->bindValue(":id", (int) $id, \PDO::PARAM_INT); $query->execute(); $data = $query->fetchColumn(); $query->closeCursor(); if(strlen($data)) $data = wireDecodeJSON($data); if(empty($data)) $data = array(); $this->configData[(int) $id] = $data; } if($property) return isset($data[$property]) ? $data[$property] : null; return $data; } /** * Is the given module interactively configurable? * * This method can be used to simply determine if a module is configurable (yes or no), or more specifically * how it is configurable. * * ~~~~~ * // Determine IF a module is configurable * if($modules->isConfigurable('HelloWorld')) { * // Module is configurable * } else { * // Module is NOT configurable * } * ~~~~~ * ~~~~~ * // Determine HOW a module is configurable * $configurable = $module->isConfigurable('HelloWorld'); * if($configurable === true) { * // configurable in a way compatible with all past versions of ProcessWire * } else if(is_string($configurable)) { * // configurable via an external configuration file * // file is identifed in $configurable variable * } else if(is_int($configurable)) { * // configurable via a method in the class * // the $configurable variable contains a number with specifics * } else { * // module is NOT configurable * } * ~~~~~ * * ### Return value details * * #### If module is configurable via external configuration file: * * - Returns string of full path/filename to `ModuleName.config.php` file * * #### If module is configurable because it implements a configurable module interface: * * - Returns boolean `true` if module is configurable via the static `getModuleConfigInputfields()` method. * This particular method is compatible with all past versions of ProcessWire. * - Returns integer `2` if module is configurable via the non-static `getModuleConfigInputfields()` and requires no arguments. * - Returns integer `3` if module is configurable via the non-static `getModuleConfigInputfields()` and requires `$data` array. * - Returns integer `4` if module is configurable via the non-static `getModuleConfigInputfields()` and requires `InputfieldWrapper` argument. * - Returns integer `19` if module is configurable via non-static `getModuleConfigArray()` method. * - Returns integer `20` if module is configurable via static `getModuleConfigArray()` method. * * #### If module is not configurable: * * - Returns boolean `false` if not configurable * * *This method is named isConfigurableModule() in ProcessWire versions prior to to 3.0.16.* * * #pw-group-configuration * * @param Module|string $class Module name * @param bool $useCache Use caching? This accepts a few options: * - Specify boolean `true` to allow use of cache when available (default behavior). * - Specify boolean `false` to disable retrieval of this property from getModuleInfo (forces a new check). * - Specify string `interface` to check only if module implements ConfigurableModule interface. * - Specify string `file` to check only if module has a separate configuration class/file. * @return bool|string|int See details about return values in method description. * @since 3.0.16 * * @todo this method has two distinct parts (file and interface) that need to be split in two methods. * */ public function isConfigurable($class, $useCache = true) { $className = $class; $moduleInstance = null; $namespace = $this->modules->info->getModuleNamespace($className); if(is_object($className)) { $moduleInstance = $className; $className = $this->modules->getModuleClass($moduleInstance); } $nsClassName = $namespace . $className; if($useCache === true || $useCache === 1 || $useCache === "1") { $info = $this->modules->getModuleInfo($className); // if regular module info doesn't have configurable info, attempt it from verbose module info // should only be necessary for transition period between the 'configurable' property being // moved from verbose to non-verbose module info (i.e. this line can be deleted after PW 2.7) if($info['configurable'] === null) { $info = $this->modules->getModuleInfoVerbose($className); } if(!$info['configurable']) { if($moduleInstance instanceof ConfigurableModule) { // re-try because moduleInfo may be temporarily incorrect for this request because of change in moduleInfo format // this is due to reports of ProcessChangelogHooks not getting config data temporarily between 2.6.11 => 2.6.12 $this->error( "Configurable module check failed for $className. " . "If this error persists, please do a Modules > Refresh.", Notice::debug ); $useCache = false; } else { return false; } } else { if($info['configurable'] === true) return $info['configurable']; if($info['configurable'] === 1 || $info['configurable'] === "1") return true; if(is_int($info['configurable']) || ctype_digit("$info[configurable]")) return (int) $info['configurable']; if(strpos($info['configurable'], $className) === 0) { if(empty($info['file'])) { $info['file'] = $this->modules->files->getModuleFile($className); } if($info['file']) { return dirname($info['file']) . "/$info[configurable]"; } } } } if($useCache !== "interface") { // check for separate module configuration file $dir = dirname($this->modules->files->getModuleFile($className)); if($dir) { $files = array( "$dir/{$className}Config.php", "$dir/$className.config.php" ); $found = false; foreach($files as $file) { if(!is_file($file)) continue; $config = null; // include file may override $this->modules->files->includeModuleFile($file, $className); $classConfig = $nsClassName . 'Config'; if(class_exists($classConfig, false)) { $parents = wireClassParents($classConfig, false); if(is_array($parents) && in_array('ModuleConfig', $parents)) { $found = $file; break; } } else { // bypass include_once, because we need to read $config every time if(is_null($config)) { $classInfo = $this->modules->files->getFileClassInfo($file); if($classInfo['class']) { // not safe to include because this is not just a file with a $config array } else { $ns = $this->modules->files->getFileNamespace($file); $file = $this->modules->files->compile($className, $file, $ns); if($file) { /** @noinspection PhpIncludeInspection */ include($file); } } } if(!is_null($config)) { // included file specified a $config array $found = $file; break; } } } if($found) return $found; } } // if file-only check was requested and we reach this point, exit with false now if($useCache === "file") return false; // ConfigurableModule interface checks $result = false; foreach(array('getModuleConfigArray', 'getModuleConfigInputfields') as $method) { $configurable = false; // if we have a module instance, use that for our check if($moduleInstance instanceof ConfigurableModule) { if(method_exists($moduleInstance, $method)) { $configurable = $method; } else if(method_exists($moduleInstance, "___$method")) { $configurable = "___$method"; } } // if we didn't have a module instance, load the file to find what we need to know if(!$configurable) { if(!wireClassExists($nsClassName, false)) { $this->modules->includeModule($className); } $interfaces = wireClassImplements($nsClassName, false); if(is_array($interfaces) && in_array('ConfigurableModule', $interfaces)) { if(wireMethodExists($nsClassName, $method)) { $configurable = $method; } else if(wireMethodExists($nsClassName, "___$method")) { $configurable = "___$method"; } } } // if still not determined to be configurable, move on to next method if(!$configurable) continue; // now determine if static or non-static $ref = new \ReflectionMethod(wireClassName($nsClassName, true), $configurable); if($ref->isStatic()) { // config method is implemented as a static method if($method == 'getModuleConfigInputfields') { // static getModuleConfigInputfields $result = true; } else { // static getModuleConfigArray $result = 20; } } else if($method == 'getModuleConfigInputfields') { // non-static getModuleConfigInputfields // we allow for different arguments, so determine what it needs $parameters = $ref->getParameters(); if(count($parameters)) { $param0 = reset($parameters); if(strpos($param0, 'array') !== false || strpos($param0, '$data') !== false) { // method requires a $data array (for compatibility with non-static version) $result = 3; } else if(strpos($param0, 'InputfieldWrapper') !== false || strpos($param0, 'inputfields') !== false) { // method requires an empty InputfieldWrapper (as a convenience) $result = 4; } } // method requires no arguments if(!$result) $result = 2; } else { // non-static getModuleConfigArray $result = 19; } // if we make it here, we know we already have a result so can stop now break; } return $result; } /** * Indicates whether module accepts config settings, whether interactively or API only * * - Returns false if module does not accept config settings. * - Returns integer `30` if module accepts config settings but is not interactively configurable. * - Returns true, int or string if module is interactively configurable, see `Modules::isConfigurable()` return values. * * @param string|Module $class * @param bool $useCache * @return bool|int|string * @since 3.0.179 * */ public function isConfigable($class, $useCache = true) { if(is_object($class)) { if($class instanceof ConfigModule) { $result = 30; } else { $result = $this->isConfigurable($class, $useCache); } } else { $result = $this->isConfigurable($class, $useCache); if(!$result && wireInstanceOf($class, 'ConfigModule')) $result = 30; } return $result; } /** * Populate configuration data to a ConfigurableModule * * If the Module has a 'setConfigData' method, it will send the array of data to that. * Otherwise it will populate the properties individually. * * @param Module $module * @param array|null $data Configuration data [key=value], or omit/null if you want it to retrieve the config data for you. * @param array|null $extraData Additional runtime configuration data to merge (default=null) 3.0.169+ * @return bool True if configured, false if not configurable * */ public function setModuleConfigData(Module $module, $data = null, $extraData = null) { $configurable = $this->isConfigable($module); if(!$configurable) return false; if(!is_array($data)) $data = $this->getConfig($module); if(is_array($extraData)) $data = array_merge($data, $extraData); $nsClassName = $module->className(true); $moduleName = $module->className(false); if(is_string($configurable) && is_file($configurable) && strpos(basename($configurable), $moduleName) === 0) { // get defaults from ModuleConfig class if available $className = $nsClassName . 'Config'; $config = null; // may be overridden by included file // $compile = strrpos($className, '\\') < 1 && $this->wire('config')->moduleCompile; $configFile = ''; if(!class_exists($className, false)) { $configFile = $this->modules->files->compile($className, $configurable); // $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable; if($configFile) { /** @noinspection PhpIncludeInspection */ include_once($configFile); } } if(wireClassExists($className)) { $parents = wireClassParents($className, false); if(is_array($parents) && in_array('ModuleConfig', $parents)) { $moduleConfig = $this->wire(new $className()); if($moduleConfig instanceof ModuleConfig) { $defaults = $moduleConfig->getDefaults(); $data = array_merge($defaults, $data); } } } else { // the file may have already been include_once before, so $config would not be set // so we try a regular include() next. if(is_null($config)) { if(!$configFile) { $configFile = $this->modules->files->compile($className, $configurable); // $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable; } if($configFile) { /** @noinspection PhpIncludeInspection */ include($configFile); } } if(is_array($config)) { // alternatively, file may just specify a $config array /** @var ModuleConfig $moduleConfig */ $moduleConfig = $this->wire(new ModuleConfig()); $moduleConfig->add($config); $defaults = $moduleConfig->getDefaults(); $data = array_merge($defaults, $data); } } } if(method_exists($module, 'setConfigData') || method_exists($module, '___setConfigData')) { /** @var _Module $module */ $module->setConfigData($data); return true; } foreach($data as $key => $value) { $module->$key = $value; } return true; } /** * Save provided configuration data for the given module * * - Applicable only for modules that support configuration. * - Configuration data is stored encoded in the database "modules" table "data" field. * * ~~~~~~ * // Getting, modifying and saving module config data * $data = $modules->getConfig('HelloWorld'); * $data['greeting'] = 'Hello World! How are you today?'; * $modules->saveConfig('HelloWorld', $data); * ~~~~~~ * * #pw-group-configuration * #pw-group-manipulation * #pw-changelog 3.0.16 Changed name from the more verbose saveModuleConfigData(), which will still work. * * @param string|Module $class Module or module name * @param array|string $data Associative array of configuration data, or name of property you want to save. * @param mixed|null $value If you specified a property in previous arg, the value for the property. * @return bool True on success, false on failure * @throws WireException * @see Modules::getConfig() * @since 3.0.16 Use method saveModuleConfigData() with same arguments for prior versions (can also be used on any version). * */ public function saveConfig($class, $data, $value = null) { $className = $class; if(is_object($className)) $className = $className->className(); $moduleName = wireClassName($className, false); $id = $this->moduleID($moduleName); if(!$id) throw new WireException("Unable to find ID for Module '$moduleName'"); if(is_string($data)) { // a property and value have been provided $property = $data; $data = $this->getConfig($class); if(is_null($value)) { // remove the property unset($data[$property]); } else { // populate the value for the property $data[$property] = $value; } } else { // data must be an associative array of configuration data if(!is_array($data)) return false; } // ensure original duplicates info is retained and validate that it is still current $data = $this->modules->duplicates()->getDuplicatesConfigData($moduleName, $data); $this->configData[$id] = $data; $json = count($data) ? wireEncodeJSON($data, true) : ''; $database = $this->wire()->database; $query = $database->prepare("UPDATE modules SET data=:data WHERE id=:id", "modules.saveConfig($moduleName)"); // QA $query->bindValue(":data", $json, \PDO::PARAM_STR); $query->bindValue(":id", (int) $id, \PDO::PARAM_INT); $result = $query->execute(); // $this->log("Saved module '$moduleName' config data"); return $result; } /** * Get the Inputfields that configure the given module or return null if not configurable * * #pw-internal * * @param string|Module|int $moduleName * @param InputfieldWrapper|null $form Optionally specify the form you want Inputfields appended to. * @return InputfieldWrapper|null * */ public function getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) { $moduleName = $this->modules->getModuleClass($moduleName); $configurable = $this->isConfigurable($moduleName); if(!$configurable) return null; /** @var InputfieldWrapper $form */ if(is_null($form)) $form = $this->wire(new InputfieldWrapper()); $data = $this->getConfig($moduleName); $fields = null; // check for configurable module interface $configurableInterface = $this->isConfigurable($moduleName, "interface"); if($configurableInterface) { if(is_int($configurableInterface) && $configurableInterface > 1 && $configurableInterface < 20) { // non-static /** @var ConfigurableModule|Module|_Module $module */ if($configurableInterface === 2) { // requires no arguments $module = $this->modules->getModule($moduleName); $fields = $module->getModuleConfigInputfields(); } else if($configurableInterface === 3) { // requires $data array $module = $this->modules->getModule($moduleName, array('noInit' => true, 'noCache' => true)); $this->setModuleConfigData($module); $fields = $module->getModuleConfigInputfields($data); } else if($configurableInterface === 4) { // requires InputfieldWrapper // we allow for option of no return statement in the method $module = $this->modules->getModule($moduleName); $fields = $this->wire(new InputfieldWrapper()); /** @var InputfieldWrapper $fields */ $fields->setParent($form); $_fields = $module->getModuleConfigInputfields($fields); if($_fields instanceof InputfieldWrapper) $fields = $_fields; unset($_fields); } else if($configurableInterface === 19) { // non-static getModuleConfigArray method $module = $this->modules->getModule($moduleName); $fields = $this->wire(new InputfieldWrapper()); /** @var InputfieldWrapper $fields */ $fields->importArray($module->getModuleConfigArray()); $fields->populateValues($module); } } else if($configurableInterface === 20) { // static getModuleConfigArray method $fields = $this->wire(new InputfieldWrapper()); /** @var InputfieldWrapper $fields */ $fields->importArray(call_user_func(array(wireClassName($moduleName, true), 'getModuleConfigArray'))); $fields->populateValues($data); } else { // static getModuleConfigInputfields method $nsClassName = $this->modules->info->getModuleNamespace($moduleName) . $moduleName; $fields = call_user_func(array($nsClassName, 'getModuleConfigInputfields'), $data); } if($fields instanceof InputfieldWrapper) { foreach($fields as $field) { $form->append($field); } } else if($fields instanceof Inputfield) { $form->append($fields); } else { $this->error("$moduleName.getModuleConfigInputfields() did not return InputfieldWrapper"); } } // check for file-based config $file = $this->isConfigurable($moduleName, "file"); if(!$file || !is_string($file) || !is_file($file)) { // config is not file-based } else { // file-based config $config = null; $ns = $this->modules->info->getModuleNamespace($moduleName); $configClass = $ns . $moduleName . "Config"; if(!class_exists($configClass)) { $configFile = $this->modules->files->compile($moduleName, $file, $ns); if($configFile) { /** @noinspection PhpIncludeInspection */ include_once($configFile); } } $configModule = null; if(wireClassExists($configClass)) { // file contains a ModuleNameConfig class $configModule = $this->wire(new $configClass()); } else { if(is_null($config)) { $configFile = $this->modules->files->compile($moduleName, $file, $ns); if($configFile) { /** @noinspection PhpIncludeInspection */ include($configFile); // in case of previous include_once } } if(is_array($config)) { // file contains a $config array $configModule = $this->wire(new ModuleConfig()); $configModule->add($config); } } if($configModule instanceof ModuleConfig) { $defaults = $configModule->getDefaults(); $data = array_merge($defaults, $data); $configModule->setArray($data); $fields = $configModule->getInputfields(); if($fields instanceof InputfieldWrapper) { foreach($fields as $field) { $form->append($field); } foreach($data as $key => $value) { $f = $form->getChildByName($key); if(!$f) continue; if($f instanceof InputfieldCheckbox && $value) { $f->attr('checked', 'checked'); } else { $f->attr('value', $value); } } } else { $this->error("$configModule.getInputfields() did not return InputfieldWrapper"); } } } // file-based config if($form) { // determine how many visible Inputfields there are in the module configuration // for assignment or removal of flagsNoUserConfig flag when applicable $numVisible = 0; foreach($form->getAll() as $inputfield) { if($inputfield instanceof InputfieldHidden || $inputfield instanceof InputfieldWrapper) continue; $numVisible++; } $flags = $this->modules->flags->getFlags($moduleName); if($numVisible) { if($flags & Modules::flagsNoUserConfig) { $info = $this->modules->info->getModuleInfoVerbose($moduleName); if(empty($info['addFlag']) || !($info['addFlag'] & Modules::flagsNoUserConfig)) { $this->modules->flags->setFlag($moduleName, Modules::flagsNoUserConfig, false); // remove flag } } } else { if(!($flags & Modules::flagsNoUserConfig)) { if(empty($info['removeFlag']) || !($info['removeFlag'] & Modules::flagsNoUserConfig)) { $this->modules->flags->setFlag($moduleName, Modules::flagsNoUserConfig, true); // add flag } } } } return $form; } public function getDebugData() { return array( 'configData' => $this->configData ); } }