Disclaimer: All the above is my opinion. I have checked few apps out there that implemented MVC in phalcon but none of them suit my needs.
First and the biggest problem in phalcon about modularity is the lack of standard module structure. Lack of any automated way to load your modules. When checking docs, you find how to register module:
public function registerAutoloaders(DiInterface $di = null)
{
$loader = new Loader();
$loader->registerNamespaces(
[
'Multiple\Backend\Controllers' => '../apps/backend/controllers/',
'Multiple\Backend\Models' => '../apps/backend/models/',
]
);
$loader->register();
}
Each module would need to have another loader class. This is highly redundant. Not to mention you need to provide paths.
Second thing how you actually register modules. There are multiple problems with how it is done:
$application->registerModules(
[
'frontend' => [
'className' => 'Multiple\Frontend\Module',
'path' => '../apps/frontend/Module.php',
],
'backend' => [
'className' => 'Multiple\Backend\Module',
'path' => '../apps/backend/Module.php',
]
]
);
And what does the registerModules does? Well, nothing (Phalcon\Mcv\Application does not override this): https://github.com/phalcon/cphalcon/blob/master/phalcon/application.zep https://github.com/phalcon/cphalcon/blob/master/phalcon/mvc/application.zep
public function registerModules(array modules, boolean merge = false) -> <Application>
{
if merge {
let this->_modules = array_merge(this->_modules, modules);
} else {
let this->_modules = modules;
}
return this;
}
So what are those problems here? That initialization class of each modules is not a singleton, it is not called UNLESS router tells it to.
If you check handle
method of Phalcon\Mvc\Application, you will see that only the currently used module is being called. Other modules intialization classes are ignored. This is a serious problem. Imagine a simple scenario:
You have auth
module which handles user authentication and needs to bind beforeDispatch event to redirect user to login page if he tries to access some specific content.
It is simply impossible with current phalcon implementation to do that. Modules should be autonomous, like in practically any other modern PHP framework.
My solution to this problem was to extend Application (and ConsoleApplication because of the same thing) and override registerModules method like this:
public function registerModules(array $modules, $merge = null)
{
parent::registerModules($modules, $merge);
/** @var View $view */
$view = $this->di->get('view');
foreach ($this->getModules() as $moduleName => $module) {
$initClass = $this->getModuleClass($moduleName);
$initClass->registerAutoloaders($this->di);
$initClass->registerServices($this->di);
if (method_exists($initClass, 'init')) {
$initClass->init($this->di);
}
if (method_exists($initClass, 'registerEvents')) {
$initClass->registerEvents($this->di);
}
$this->registerRoutes($moduleName, $module);
$view->registerViewPaths($moduleName, $module);
}
}
As you can see I have added 2 methods that can be called as well. Currently initialization classes aka. Module.php (Init.php in my case) have only registerServices
and registerAutoloaders
. Frankly, they might have been merged into one moduleInit
method.
If you ever need to get the module initialization class, which should be possible, you create function like this:
/**
* @param $module
* @return \Phalcon\Mvc\ModuleDefinitionInterface|null
*/
public function getModuleClass($module)
{
if (isset($this->moduleInitClass[$module])) {
return $this->moduleInitClass[$module];
}
$mod = $this->getModule($module);
$class = $mod['className'];
return $this->moduleInitClass[$module] = new $class;
}
Which forces them to become singletons.
This was done because I needed such functionality. To have fully autonomous modules, inside one directory.
What am i saying right now. Phalcon needs to have better module initialization process. The one that:
- Loads all modules classes and register: autoloaders, services, events etc.
- Is well aware of modules components such as models and controllers by default (with the ability to change it)
- Does know where modules resides in so that you don't need to provide paths (but you should have ability to change it)
I skipped views on purpose because it can be decoupled from your application, also here is the topic about views (so lets not talk about problems with View itself) https://forum.phalcon.io/discussion/17305/phalcon-with-completely-custom-view-system
So how a minimal module definition should look like?
$modulesDirectory = APP_PATH . 'modules/';
$modules = [
'moduleName' => [
'namespace' => 'Vendor\ModuleName'
]
]
And thats it, application should be able to resolve class initialization, make instance of it, keep it as singleton. Then register Autoloades (if needed), register services, and register events.
If you need more customization then
$modulesDirectory = APP_PATH . 'modules/';
$modules = [
'moduleName' => [
'namespace' => 'Vendor\ModuleName',
'path' => APP_PATH . 'custom/path/to/your/module', //this should not point to a Module.php, initialization class can be autoloaded if you register namespace, original path that pointed to one file is redundant
'className' => 'Init', //no point in providing full namespace, as it is inside module it can be appended to namespace ex. $module['namespace'] . '\\' . $module['className']
'priority' => 20, //as phalcon doesnt load all modules it still should be possible to have some sort of priority loading mechanism
'controllersDirectory' => 'MyAwesome/Controllers', // ability to customize controllers directory for example if you wish it to be somewhere deeper, by default it may be 'Controllers'
'modeslDirectory' => 'MyAwesome/Models', // same as above but for models
]
]
I cannot provide a full code but I can provide some parts of it if needed. In my application all you need to do is to provide module.json inside APP_PATH /modules/// directories and they are being autoloaded with everything that is needed.
{
"name": "auth",
"enabled": true,
"priority": 1
}
and thats it.
Here is how my app looks like: https://i.imgur.com/RR0cLz1.png
And it is working 100%. I am using Twig for views and not using default phalcon View so extending/including cross module is possible.