Phalcon is not trully modular framework - and how to solve it.

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:

  1. Loads all modules classes and register: autoloaders, services, events etc.
  2. Is well aware of modules components such as models and controllers by default (with the ability to change it)
  3. 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.phalconphp.com/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.

Well, perhaps you could submit a PR with Zephir code which will resolve those concerns and make Phalcon "truly" modular, at least one leap in that direction.

For me this is bad idea. I like how all modules are indepndent and making applications faster, it's stupid to initialize all modules if they don't used.

edited 21d ago

Well, perhaps you could submit a PR with Zephir code which will resolve those concerns and make Phalcon "truly" modular, at least one leap in that direction.

That might be some idea however i don't know zephyr that much, i can cooperate and provide my php code to someone willing to translate it into zephyr.

I like how all modules are indepndent and making applications faster,

They are not making application faster. Your statement is wrong on so many levels. If my application would be like default phalcon i would still need to register all the services and all the events in some place. I just split that initialization into meaninigful modules like auth, crud, admin, helpers etc.

it's stupid to initialize all modules if they don't used.

You don't get it at all. They are used, and they are doing something meaningful (adding services, using events), For all the other modules it would just creating one empty class.

Right now phalcon lets you basically split application into 2 modules: frontend and backend.

edited 20d ago

I think this is a poor naming decision and a matter of semantics. The functionality you are describing falls into the roles of Plugins in the Phalcon world.

Modules are decoupled blocks of code, I think of them as application sharding.

Since when module is decoupled blocks of code? In just about every modern framework a "module" does have access to events. Laravel? Yep there is a service, and you have option to "boot" your module: http://kamranahmed.info/blog/2015/12/03/creating-a-modular-application-in-laravel/

Symfony calls modules a bundles (which is fine): https://symfony.com/doc/current/bundles.html and yes, you can hook events there, without writing a custom code to initialize your modules.

Plugins? You mean like this? https://github.com/phalcon/invo/blob/master/app/plugins/SecurityPlugin.php

Then it falls short compared to true module.

Lets take a look at authentication/user example: Module name: UserModule (or AuthModule)

What it should be able to do?

  1. Encapsulate user model and all user-related models like remember token, confirmation token, roles
  2. Encapsulate auth and user related controllers like auth and profile controller
  3. Encapsulate services like Auth (which does the actual job of logging in customer) and Acl (which checks if user has access to specific content)
  4. Encapsulate routes specific to those controllers (doesn't matter if they are automatic or not)
  5. Hook events that will let module to restrict access to content for example beforeDispatch

Why is that important to have everything in one place? Because if I ever want to create application that will not need users or authentication at all I can simply remove module folder and with some minor template tweaks (removal of acl checks) application will be running in full open mode.

And again you cannot call this a "plugin", it is a fully fledged module. Which almost works in bare phalcon Application except for point 5. And that is because modules are not initialized, they do not have a possibility to hook any events, they don't have any init/boot/whatever function that is called regardless of which is the "current" module.

Another example: AdminActionLogModule - a module responsible of logging each admin user actions. Such module needs to hook every action (either before or afterDispatch). If phalcon would let that by default it would be pretty easy to write it in one place.

There are tons of examples of modules that simply needs to initialize on every request, or hook some global event.

Now there is another thing, check this repo: https://github.com/phalcon/mvc and search for setDefaultNamespace in file named Module.php

Why would you need to define default namespace in each module? That is a unnecessary redundancy. Why is module namespace not defined in module definition?

By knowing:

  1. directory of where modules are located
  2. Namespace of each module

We have access to every namespace and path that is required. Instead we have to provide className and path to "Module.php" (which is redundancy by itself). It is cool if we have option to customize paths, namespaces but that should not be required but optional.

What am i saying is that modules in phalcon are incomplete, just as if modularity was a new idea and never implemented to the end. And by modularity I mean a possibility to encapsulate whole code related to specific field.

What about CLI tools for module? There is no way to keep your tasks inside a module. For example I want to have a cli command inside Auth/User module that lets me create admin users in console. I fixed that problem in my application but that required me to rewrite ConsoleApplication and convert zephyr code from Phalcon/Cli/Console back to PHP, namely "handle" function. Why was that a problem? Because code had no idea where CLI task resides in, code needed to know what is the namespace of Cli task of each module.

So to sumamrize what IN MY OPINION is wrong in phalcon about modules:

  1. Modules has no way to initialize themselves.
  2. Dispatcher is not aware of module namespace, and you cannot set namespace in module definition
  3. Router has the same problem as dispatcher

The questions you raise are valid, Modules in Phalcon do seem a bit awkward to use. I've ended up heavily customizing them to suit my needs.

I think this topic is worth a github discussion. Changing the responsibility, or renaming Module is not possible (unless in a major?), so I guess a new class should be introduced to handle the functionality you are describing. Bundle is not used anywhere yet :]

That might be idea however I am not the one to decide. There would be 2 ways to handle this. Either make bundles completely separate of modules (which may be a bad idea) or make bundles like extended modules that work exactly like modules but has addtional features:

$bundlesDirectory = APP_PATH . 'modules/';
$bundles = [
    'bundleName' => [
        'namespace' => 'Vendor\BundleName'
    ]
]
$app->registerBundles($bundles);

Which would roughly translate to something like:

$modules = [
    'bundleName' => [
        'namespace' => 'Vendor\BundleName',
        'className' => '...',
        'path' => '...',
        'type' => 'bundle'
    ]
]