I'm working on a Cake PHP 2 project and have got a custom module system set up that exists within app/modules/MyModule, within this directory I have folders such as:
Controller
View
Model
I've added a new one called Console, and within this have created a Command directory where I'll put commands.
However, the Console/cake command exists within app/Console rather than app/modules/MyModule/Console directory.
How can I have commands that exist within what is effectively a plugin, but executed from the main Console/cake directory.
My custom module set up works like this:
<?php
App::uses('BaseModule', 'Modules');
App::uses('CakeEventManager', 'Event');
/**
* Helper class to load modules of a specific format from /app/modules directory,
* and create instances that can connect to system events, modify system behaviours etc.
*
* Usage:
*
* $_modules = new Modules();
$mods_arr = $_modules->initModules(ROOT.'/app/modules');
*
*
*/
class Modules
{
public function initModules($modules_base_dir)
{
$modules = array();
//loop over all directories in /app/modules/
foreach (new DirectoryIterator($modules_base_dir) as $dir)
{
if($dir->isDot()) continue;
if($dir->getFilename()=='.svn') continue;
if($dir->isFile()) {
continue;
}
//compile a list of all modules, and load each Module class
$classname = $dir->getFilename();
App::import('modules/'.$classname, $classname);
$module = new $classname();
array_push($modules, $module);
//enumerate all events from BaseModule so we know what we need to handle
$base_events_array = array();
$methods = get_class_methods('BaseModule');
foreach($methods as $method)
{
//strip out any method that starts with "handle_"
if(substr($method, 0, 7)=='handle_')
{
$base_events_array[] = substr($method, 7);
}
}
//IF this module is enabled
if($module->_enabled)
{
//register any EXISTING event handlers for this module
foreach($base_events_array as $event_name)
{
if(method_exists($module, 'handle_'.$event_name))
{
CakeEventManager::instance()->attach(array($module, 'handle_'.$event_name), $event_name);
}
}
//connect up any additional controllers,views, models, bootstraps from this module
App::build(array(
'Console' => array($modules_base_dir.'/'.$classname.'/Console/'),
'Controller' => array($modules_base_dir.'/'.$classname.'/Controller/'),
'View' => array($modules_base_dir.'/'.$classname.'/View/'),
'Model' => array($modules_base_dir.'/'.$classname.'/Model/'),
'Vendor' => array($modules_base_dir.'/'.$classname.'/Vendor/')
));
if(file_exists($modules_base_dir.'/'.$classname.'/bootstrap.php'))
{
include_once $modules_base_dir.'/'.$classname.'/bootstrap.php';
}
}
}
//die(var_dump(App::path('Controller')));
return $modules;
}
}
You'd have to connect the sub-namespace/package where CakePHP will look for shells, ie Console/Command, and if you have tasks, that would need to be connected too:
// ...
'Console/Command' => array("$modules_base_dir/$classname/Console/Command/"),
'Console/Command/Task' => array("$modules_base_dir/$classname/Console/Command/Task/"),
// ...
Related
I'm working on a Cake PHP 2 project that has a custom module system allowing for a folder within app called modules, and then each module can have Views, Controllers, Models etc... I've extended it to include a webroot directory with a css directory inside and have added these directories to my App::build() array.
I've got some css in my module, and a layout which is working, but trying to load my module's css doesn't load it and instead is trying to pull the css from my main app's webroot, where of course it doesn't exist.
What am I missing?
My paths are:
app/modules/QueueManagerModule/webroot/css/queue.css
app/modules/QueueManagerModule/View/Layouts/QueueManagerLayout.ctp
I'm doing the following inside of my layout to echo CSS:
echo $this->Html->css('queue');
This though generates the path of: mydomain.com/css/queue.css.
My module set up for loading everything looks like:
<?php
App::uses('BaseModule', 'Modules');
App::uses('CakeEventManager', 'Event');
/**
* Helper class to load modules of a specific format from /app/modules directory,
* and create instances that can connect to system events, modify system behaviours etc.
*
* Usage:
*
* $_modules = new Modules();
$mods_arr = $_modules->initModules(ROOT.'/app/modules');
*
*
*/
class Modules
{
public function initModules($modules_base_dir)
{
$modules = array();
//loop over all directories in /app/modules/
foreach (new DirectoryIterator($modules_base_dir) as $dir)
{
if($dir->isDot()) continue;
if($dir->getFilename()=='.svn') continue;
if($dir->isFile()) {
continue;
}
//compile a list of all modules, and load each Module class
$classname = $dir->getFilename();
App::import('modules/'.$classname, $classname);
$module = new $classname();
array_push($modules, $module);
//enumerate all events from BaseModule so we know what we need to handle
$base_events_array = array();
$methods = get_class_methods('BaseModule');
foreach($methods as $method)
{
//strip out any method that starts with "handle_"
if(substr($method, 0, 7)=='handle_')
{
$base_events_array[] = substr($method, 7);
}
}
//IF this module is enabled
if($module->_enabled)
{
//register any EXISTING event handlers for this module
foreach($base_events_array as $event_name)
{
if(method_exists($module, 'handle_'.$event_name))
{
CakeEventManager::instance()->attach(array($module, 'handle_'.$event_name), $event_name);
}
}
//connect up any additional controllers,views, models, bootstraps from this module
App::build(array(
'Config' => array($modules_base_dir.'/'.$classname.'/Config/'),
'Console/Command' => array($modules_base_dir.'/'.$classname.'/Console/Command/'),
'Console/Command/Task' => array($modules_base_dir.'/'.$classname.'/Console/Command/Task/'),
'Lib/Event' => array($modules_base_dir.'/'.$classname.'/Lib/Event/'),
'Controller' => array($modules_base_dir.'/'.$classname.'/Controller/'),
'View' => array($modules_base_dir.'/'.$classname.'/View/'),
'View/Elements' => array($modules_base_dir.'/'.$classname.'/View/Elements'),
'Model' => array($modules_base_dir.'/'.$classname.'/Model/'),
'Vendor' => array($modules_base_dir.'/'.$classname.'/Vendor/'),
'webroot' => array($modules_base_dir.'/'.$classname.'/webroot/'),
'webroot/css' => array($modules_base_dir.'/'.$classname.'/webroot/css/'),
'webroot/js' => array($modules_base_dir.'/'.$classname.'/webroot/js/')
));
if(file_exists($modules_base_dir.'/'.$classname.'/Config/events.php'))
{
require_once $modules_base_dir.'/'.$classname.'/Config/events.php';
}
if(file_exists($modules_base_dir.'/'.$classname.'/bootstrap.php'))
{
include_once $modules_base_dir.'/'.$classname.'/bootstrap.php';
}
}
}
//die(var_dump(App::path('Controller')));
return $modules;
}
}
I have a module hierarchy as below
yii/web/Application ->
frontend\modules\mangomod\MangomodModule -> frontend\modules\mangomod\modules\lemonmod\Lemonmod -> frontend\modules\mangomod\modules\lemonmod\modules\grapemod\Grapemod
Bootstrapped modules are:
yii\debug\Module and yii\gii\Module
Now in DefaultController of Grapemod module I need to know all its parent modules. How?
and
Now in DefaultController of Lemonmod module I need to know all its child modules. How?
I have tried yii\base\Application::$loadedModules but it also provides bootstrapped modules such as gii and debug which I don't want in the list.
Here is the result of yii\base\Application::$loadedModules:
[0] => yii\web\Application
[1] => frontend\modules\mangomod\MangomodModule
[2] => yii\debug\Module
[3] => yii\gii\Module
[4] => frontend\modules\mangomod\modules\lemonmod\Lemonmod
[5] => frontend\modules\mangomod\modules\lemonmod\modules\grapemod\Grapemod
UPDATE
lemonmod got its sibling module
frontend\modules\mangomod\modules\lemonmod2\Lemonmod2Module
grapemod got its sibling module
frontend\modules\mangomod\modules\lemonmod\modules\grapemod2\Grapemod2Module
To get all ancestor modules of controller you may use Controller::getModules() method:
$modules = $controller->getModules();
Returns all ancestor modules of this controller.
The first module in the array is the outermost one (i.e., the
application instance), while the last is the innermost one.
https://www.yiiframework.com/doc/api/2.0/yii-base-controller#getModules()-detail
To get list of children modules you can use Module::getModules(). However it will return module config instead of module instance for not loaded modules - it is documented in $loadedOnly parameter of this method:
Whether to return the loaded sub-modules only. If this is set false, then all sub-modules registered in this module will be returned, whether they are loaded or not. Loaded modules will be returned as objects, while unloaded modules as configuration arrays.
If you want to get instances for all modules, you should instantiate non-loaded modules manually:
$modules = [];
foreach ($module->getModules() as $id => $child) {
if (!$child instanceof Module) {
$child = $module->getModule($id);
}
$modules[] = $child;
}
Also this will return only direct children modules - to get all descendant you need to traverse modules tree:
/**
* Return all descendants of given module.
*
* #param Module $module
* #return Module[]
*/
$traverse = function (Module $module) use (&$traverse) {
$modules = [];
foreach ($module->getModules() as $id => $child) {
if (!$child instanceof Module) {
$child = $module->getModule($id);
}
$modules[] = $child;
$modules = array_merge($modules, $traverse($child));
}
return $modules;
};
$modules = $traverse($module);
Codeigniter does not find trivial classes:
Unable to load the requested class: Bcrypt
But the same goes for custom made classes defined in files in application/libraries/. I am used that django lists the folders where it searched for a file, but did not find one. Obviously CI must also iterate over some list of folders or files, but is not as polite to display them along with the error.
It seems as if CI has a naming convention to deduce the (set of) filename(s) where it would expect a class to be. How can I programmatically error_log the list of folders or filenames that Codeigniter or PHP tried to track down this class?
EDIT: The lines of code that produce such a loading-error are:
$autoload['libraries'] = array('database','session','mi_file_fetcher');
in application/config/autoload.php and
$this->load->library("bcrypt");
in application/models/User.php
As stated in the comments, I was not asking for a fix, I was asking for a list.
I managed to do so by updating system/core/Loader.php
protected function _ci_load_library_files_tried($class, $subdir, $params, $object_name)
{
$files_tried = array(BASEPATH . 'libraries/' . $subdir . $class . '.php');
foreach ($this->_ci_library_paths as $path) {
if ($path === BASEPATH) {
continue;
}
array_push($files_tried, $path . 'libraries/' . $subdir . $class . '.php');
}
return $files_tried;
}
protected function _ci_load_library($class, $params = NULL, $object_name = NULL)
{
// ...
// If we got this far we were unable to find the requested class.
$files_tried = $this->_ci_load_library_files_tried($class, $subdir, $params, $object_name);
log_message('error', 'Unable to load the requested class: '.$class .
", tried these files:\n" . join("\n", $files_tried));
show_error('Unable to load the requested class: '.$class .
', tried these files:<ul><li>' . join('</li><li>', $files_tried) . '</li></ul>');
}
Would be great if CI actually provided decent debugging information.
The CodeIgniter (CI) documentation does tell you the default locations of libraries, models, helpers, views and many other framework objects. There isn't a section that explicitly lists the folders though. The Loader Class documentation does tell you, but you have to dig for it a bit.
The subtopics on the General Topics section of the docs clearly state the default locations for the various classes the frameworks uses and where to put custom classes.
In most cases following the prescribed file structure and using the loader class, e.g. (Bcrypt.php is in /application/libraries/)
$this->load->library('bcrypt');
works perfectly.
There are cases (none of which seem to be involved in your problem) where CI needs help. Rather than hack or extend CI_Loader an autoloader is useful in these cases.
There are lots of ways to add an autoloader but my preference is to use CI's Hooks feature. Here's how.
In config.php set 'enable_hooks' to TRUE
$config['enable_hooks'] = TRUE;
These lines go in /application/config/hooks.php
$hook['pre_system'][] = array(
'class' => '',
'function' => 'register_autoloader',
'filename' => 'Auto_load.php',
'filepath' => 'hooks'
);
The following is the contents of /application/hooks/Auto_load.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
function register_autoloader()
{
spl_autoload_register('my_autoloader');
}
/**
* Allows classes that do not start with CI_ and that are
* stored in these subdirectories of `APPPATH`
* (default APPPATH = "application/" and is defined in "index.php")
* libraries,
* models,
* core
* controllers
* to be instantiated when needed.
* #param string $class Class name to check for
* #return void
*/
function my_autoloader($class)
{
if(strpos($class, 'CI_') !== 0)
{
if(file_exists($file = APPPATH.'libraries/'.$class.'.php'))
{
require_once $file;
}
elseif(file_exists($file = APPPATH.'models/'.$class.'.php'))
{
require_once $file;
}
elseif(file_exists($file = APPPATH.'core/'.$class.'.php'))
{
require_once $file;
}
elseif(file_exists($file = APPPATH.'controllers/'.$class.'.php'))
{
require_once $file;
}
}
}
The function log_message($level, $message) could be used in the above if you wanted.
If you are using some other creative folder structure you will have to modify the above to accommodate that layout.
Hi I am having an issue
Say I have a folder structure in CodeIgniter
application/
controllers/
models/
views/
gmail_library/
Now I have written a controller
class invite_friends extends CI_Controller {
function __construct() {
parent::__construct();
$this->load->gmail_library('Config'); // this line is giving me error
session_start();
}
}
How can I set this thing like this?
First of all, note that CodeIgniter doesn't use overloading by __call() to implement dynamic methods. Thus there is no way to get such a gmail_library() methods to work.
The conventional method
From the User Guide:
Your library classes should be placed within your
application/libraries folder, as this is where CodeIgniter will look
for them when they are initialized.
If you're using CI Loader class to load a library or helper, you should follow CI's conventions.
application/libraries/Myclass.php
$this->load->library('myclass');
$this->myclass->my_method();
Using relative paths
1) You put your library files in sub-directories within the main libraries folder:
application/libraries/gmail/Gmail_config.php
I renamed your Config.php file name to prevent the occurrence of conflict with CI config core class.
$this->load->library('gmail/gmail_config');
2) Also you can use relative path within the Loader::library() method to load the library file from the outside of the library folder, as follows:
The path to the file is relative. So you can use ../ to go one UP level in path.
Again: I renamed your Config.php file name to prevent the occurrence of conflict with CI config core class.
$this->load->library('../gmail_library/Gmail_config');
An old question, I know, but I came across this looking for a way to use classes (libraries) from outside the application folder and I liked to keep it in 'the CI way of doing this'. I ended up extending the CI_Loaderclass:
I basically copied the _ci_load_class function and added an absolute path
<? if (!defined('BASEPATH')) exit('No direct script access allowed');
class MY_Loader extends CI_Loader {
protected $absPath = '/home/xxxxx/[any-path-you-like]/common/';
/**
* Load class
*
* This function loads the requested class.
*
* #param string the item that is being loaded
* #param mixed any additional parameters
* #param string an optional object name
* #return void
*/
public function commonLibrary($class, $params = NULL, $object_name = NULL)
{
// Get the class name, and while we're at it trim any slashes.
// The directory path can be included as part of the class name,
// but we don't want a leading slash
$class = str_replace('.php', '', trim($class, '/'));
// Was the path included with the class name?
// We look for a slash to determine this
$subdir = '';
if (($last_slash = strrpos($class, '/')) !== FALSE)
{
// Extract the path
$subdir = substr($class, 0, $last_slash + 1);
// Get the filename from the path
$class = substr($class, $last_slash + 1);
}
// We'll test for both lowercase and capitalized versions of the file name
foreach (array(ucfirst($class), strtolower($class)) as $class)
{
$subclass = $this->absPath.'libraries/'.$subdir.config_item('subclass_prefix').$class.'.php';
// Is this a class extension request?
if (file_exists($subclass))
{
$baseclass = BASEPATH.'libraries/'.ucfirst($class).'.php';
if ( ! file_exists($baseclass))
{
log_message('error', "Unable to load the requested class: ".$class);
show_error("Unable to load the requested class: ".$class);
}
// Safety: Was the class already loaded by a previous call?
if (in_array($subclass, $this->_ci_loaded_files))
{
// Before we deem this to be a duplicate request, let's see
// if a custom object name is being supplied. If so, we'll
// return a new instance of the object
if ( ! is_null($object_name))
{
$CI =& get_instance();
if ( ! isset($CI->$object_name))
{
return $this->_ci_init_class($class, config_item('subclass_prefix'), $params, $object_name);
}
}
$is_duplicate = TRUE;
log_message('debug', $class." class already loaded. Second attempt ignored.");
return;
}
include_once($baseclass);
include_once($subclass);
$this->_ci_loaded_files[] = $subclass;
return $this->_ci_init_class($class, config_item('subclass_prefix'), $params, $object_name);
}
// Lets search for the requested library file and load it.
$is_duplicate = FALSE;
foreach ($this->_ci_library_paths as $path)
{
$filepath = $this->absPath.'libraries/'.$subdir.$class.'.php';
// Does the file exist? No? Bummer...
if ( ! file_exists($filepath))
{
continue;
}
// Safety: Was the class already loaded by a previous call?
if (in_array($filepath, $this->_ci_loaded_files))
{
// Before we deem this to be a duplicate request, let's see
// if a custom object name is being supplied. If so, we'll
// return a new instance of the object
if ( ! is_null($object_name))
{
$CI =& get_instance();
if ( ! isset($CI->$object_name))
{
return $this->_ci_init_class($class, '', $params, $object_name);
}
}
$is_duplicate = TRUE;
log_message('debug', $class." class already loaded. Second attempt ignored.");
return;
}
include_once($filepath);
$this->_ci_loaded_files[] = $filepath;
return $this->_ci_init_class($class, '', $params, $object_name);
}
} // END FOREACH
// One last attempt. Maybe the library is in a subdirectory, but it wasn't specified?
if ($subdir == '')
{
$path = strtolower($class).'/'.$class;
return $this->_ci_load_class($path, $params);
}
// If we got this far we were unable to find the requested class.
// We do not issue errors if the load call failed due to a duplicate request
if ($is_duplicate == FALSE)
{
log_message('error', "Unable to load the requested class: ".$class);
show_error("Unable to load the requested class: ".$class);
}
}
}
Put the file MY_Loader.php in the application/core folder and load your libs with:
$this->load->commonLibrary('optional_subfolders/classname', 'classname');
$this->classname->awesome_method();
I am trying to use phpThumb in my application by making a custom autoloader.
class My_Loader_Autoloader_PhpThumb implements Zend_Loader_Autoloader_Interface {
static protected $php_thumb_classes = array(
'PhpThumb' => 'PhpThumb.inc.php',
'ThumbBase' => 'ThumbBase.inc.php',
'PhpThumbFactory' => 'ThumbLib.inc.php',
'GdThumb' => 'GdThumb.inc.php',
'GdReflectionLib' => 'thumb_plugins/gd_reflection.inc.php',
);
/**
* Autoload a class
*
* #param string $class
* #return mixed
* False [if unable to load $class]
* get_class($class) [if $class is successfully loaded]
*/
public function autoload($class) {
$file = APPLICATION_PATH . '/../library/PhpThumb/' . self::$php_thumb_classes[$class];
if (is_file($file)) {
require_once($file);
return $class;
}
return false;
}
}
i saved this file as PhpThumb.php in the loaders/Autoloader folder. Then added this line to the bootstrap file:
Zend_Loader_Autoloader::getInstance()->pushAutoloader(new My_Loader_Autoloader_PhpThumb());
But it produces an error saying the class was not found. I am guessing that the 'CustomLoader_PhpThumb.php' needs to be saved somewhere else. Any idea guys ?
Update1:
Bootstrap.php file contents
<?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
protected function _initAutoload()
{
$autoLoader=Zend_Loader_Autoloader::getInstance();
$resourceLoader=new Zend_Loader_Autoloader_Resource(array(
'basePath'=>APPLICATION_PATH,
'namespace'=>'',
'resourceTypes'=>array(
'form'=>array(
'path'=>'forms/',
'namespace'=>'Form_'
),
'models'=>array(
'path'=>'models/',
'namespace'=>'Model_'
),
)
));
//return $autoLoader;
$resourceLoader->addResourceType('loader', 'loaders/', 'My_Loader_');
$autoLoader->pushAutoloader($resourceLoader);
$autoLoader->pushAutoloader(new My_Loader_Autoloader_PhpThumb());
}
}
?>
I'm also using PhpThumb and the same autoloader. In my case however it is called My_Loader_Autoloader_PhpThumb and is located in APPLICATION_PATH . '/loaders/Autoloader/PhpThumb.php.
In my Bootstrap.php, first I load the loaders path to the Zend_Loader_Autoloader and then I push the My_Loader_Autoloader_PhpThumb autoloader as follows:
$autoLoader = Zend_Loader_Autoloader::getInstance();
$resourceLoader = new Zend_Loader_Autoloader_Resource(array(
'basePath' => APPLICATION_PATH,
'namespace' => '',
));
$resourceLoader->addResourceType('loader', 'loaders/', 'My_Loader_');
$autoLoader->pushAutoloader($resourceLoader);
$autoLoader->pushAutoloader(new My_Loader_Autoloader_PhpThumb());
Hope this will help.
PhpThumb.php should be saved to library/CustomLoader folder, and you should setup autoloader for this class also, for example in application.ini:
autoloaderNamespaces[] = "CustomLoader_"
or in Bootstrap.php:
$autoloader->registerNamespace('CustomLoader_');
I couldn't try, I hope it works!
I did it in another way, long more easy, without any autoloader, just using "My".
1.- Create a phpThumbFolder with all the files of the library into project/library/My/PhpThumb
2.- Create a file named PhpThumbFactory.php into that folder with the following content:
require_once('ThumbLib.inc.php');
class My_PhpThumb_PhpThumbFactory
{
public static function create ($filename = null, $options = array(), $isDataStream = false)
{
return PhpThumbFactory::create($filename, $options, $isDataStream);
}
}
3.- Enjoy:
public function editimageAction()
{
//rutaImg is the physical path to my images defined in application.ini
$rutaImg = Zend_Registry::get('config')->rutaImg. "test.jpg";
//Call to our "gateway" class
$thumb = My_PhpThumb_PhpThumbFactory::create($rutaImg);
//Resize
$thumb->adaptiveResize(20, 20);
//Show
$thumb->show();
$this->_helper->viewRenderer->setNoRender();
$this->_helper->layout->disableLayout();
}
I hope it can help anyone ;)
P.D. The "gateway" class is to avoid modifying any code of the library, so any possible update is easy.