Support for "modules", similar to zend expressive?

Zend expressive has a great feature that allows applications to separate routes into “modules”: https://docs.zendframework.com/zend-expressive/v3/features/modular-applications/

This is helpful if you want to group your routes into a domain or component of a larger application.

There have been a few suggestions about how to do this in Slim:

In my opinion these feel somewhat disorganized and error prone. This also doesn’t address problems with dependency injection (i.e. all your DI configurations need to still be declared together).

Does the team plan to add any interface support for this functionality? Ideally users would be able to specify a route “prefix” all declared routes would include and the dependencies the given route grouping needs.

Hello @gdanis

First, I am not part of the core, so I cannot speak for the Slim Framework core members, but only from my personal perspective.

The organization of projects is a very specific task, because every project (and company) is different. Slim does not force a fixed structure.

How to organize a large Slim Framework application - Slim Framework

IMHO this link quite outdated. I would recommend the pds/skeleton for the project root directories.

In my opinion they feel a bit disorganized and error-prone.

Multiple routing files

In my experience, this is not a good approach when it comes to testability, single responsibility (SOLID), static analysis, refactoring, etc… Instead, I would organize the “controller” in a directory and the “action” in a single action class.

Example:

src/
   Action/
       User/
           CreateUserAction.php
           UpdateUserAction.php
           DeleteUserAction.php
       Invoice/
           CreateInvoiceAction.php
           PrintInvoiceAction.php
           ...

Organizing the “Domain” layer depends on your needs.

This also does not fix any dependency injection problems (i.e. all your DI configurations still need to be declared together).

Use autowiring to keep your container entries minimal. I am maintaining a project with over 1000+ classes and my container contains only 10 entries because I use the autowrite feature and (constructor) dependency injection.

The central declaration of dependencies is a big plus in terms of maintenance and debugging, it is also faster. But again, if you use Dependency Injection and Autowiring, you only need tiny set of container entries, even in large projects.

Does the team plan to add interface support for this functionality?

Slim itself is not responsible for the container implementation. But you can already manually bind each interface to a class. No container can do this for you because the container cannot know which class you want to inject.

1 Like

First of all, I’m new to php and web development.

I’m also looking for a separate by “modules” structure.
While I think that slim should not enforce that by reason that @odan said, I feel there is a lack of resources on this theme.

I was experimenting with a repeated basic structure inside each module in a “modules” directory, while keeping generic and shared functionality in a “core” directory.

src/
|__core/
   |___Action/
   |___Domain/
   |___Routes/
       |_____api.php
       |_____web.php
   |___templates/
   |___...
|__modules/
|_________ModuleA/
          |______Action/
          |______Domain/
          |______Routes/
                 |_____api.php
                 |_____web.php
          |______templates/
          |______...
|_________ModuleB/

To load routes (and templates) for each module a assumed a given pattern, that is, the main “route.php” load a “group route” (named by the module nome) for each module based on the directory structure: modules//Routes/<api.php|web.php>

I also separated “api” and “web” routes inside Routes dir.
For each module then I could share functionality to either api or web routes.

Of course any deviations from the imposed structure would break routes loading. Instead, its possible to write a configuration for that, which would also makes easy to enable/disabled the module.

@odan what are yout thoughts on this approach? Do you have any concerns why that would not be good for long-term, possible growing bigger project?

PS:
Here are some thoughts about project structure:

I tried something with a similar approach and file structure, it works well enough and i would prefer to handle my own modules anyway, its not something i would use even if the framework offered it

so far ive only had a chance to make a basic test so its kinda crude but might give you some ideas
at the end of the main index file i used this ugly business to kick it off for now

foreach (glob(‘…/app/module/ * / * .Config.php’) as $file){ include($file); }
(ignore extra spaces not sure why it wont display slash star slash star)

ie a path like this

“/app/module/test/Test.Config.php”

which is a class that instantiates itself, which can dial back to the main application to register and request further loading

i just use the module folder / key as a url group to keep the urls separate

then i just relative my way to the template dir in the controller for now

$this->renderStandard($response, ‘/…/…/module/test/view/page/getTest.php’, $args);

which at least got me into a template and on screen

so rather than trying to do bootstrap/container/route for core and all modules at the same time, i just do bootstrap/container/route for core first, then load each module completely one at a time, where more checking / communication with core can be done and then hopefully a module shouldnt be able to sink the main app and it should be possible to install/uninstall/enable/disable/rollback whatever

only unknown i have is the public resources

im still rendering with the main application layouts so i have the core js/css loaded
i could do the same again and expand on something like this in the template with the head tag

‘…/app/module/public/*.js’) as $file) etc

but i dont really want to go rifling through modules from the core aside from the main loading
i want all the installing/loading code in one spot so it either works or doesnt and cant get stuck half way

i could use rendered sub-templates with script tags in templates to reuse css/js but that doesnt help me with images icons downloads etc

which i think leaves me at putting a module directory in the main public folder or some kind of mod_rewrite hackery

Hi @raffster

I use a very similar aproach like Nikola Poša has written down in his blog post.

  • The project root directories complies with the php-pds/skeleton.
  • I put my “modules”, the core of my application, into the src/Domain directory.
  • Then I structure the src/Domain sub-directories by “module”. e.g. src/Domain/Customer
  • Per module I create sub-directories per “type”. For example I put all repositories of the “Customer” module into a specific repository directory. For example: src/Domain/Customer/Repository/CustomerListRepository.php.
  • For the business logic (validation, calculation, file creation, etc…) I create specific “Service” classes for each use case. A service class should have only one public method.

More details about how to structure the src/Domain sub-directories: https://blog.juliobiason.net/thoughts/things-i-learnt-the-hard-way/#organize-projects-by-data-type-not-functionality

Example: https://github.com/odan/slim4-skeleton/tree/master/src/Domain/User

Reviving an old post… go me!

im busy trying to write my own skeleton in slim at the moment. “modules” is a core concept of how i work. every “section” of my project lives in its own “module”. i have a “shared” module for the defaults.

whilest i appreciate that some people can have a single folder and then branch out under that (directory under for instance controllers for each “module”) i prefer having all the files “for the module” together. so that i dont have to ever care about for instance auth files ever again instead of every time i look for a file they come up (phpstorm scopes are <3)

im busy looking at ways to try get it into my skeleton at the moment.

some points of interest:

  • each module must be able to define its own layout for twig / templater (default to shared module if none)
  • if the module uses its own layout it needs to be able to use that layout for any custom error pages.
  • each module must be able to define its own route prefix (like auth module would have a “” prefix but profile would have a /profile/… prefix)
  • each module must be able to define its own routes
  • each module must be able to define its own “permissions” (i use “permission meta files” to define my “user has permission” or “roles” checkboxes)

most of this gets solved with a simple $modules = array(modules1\Module::class, …) and setup a route(), permissions() etc in them for the main “thread” to be aware of and set it as an application space variable for each. (this post is more of an observation instead of a question)

still a work in progress. but for anyone else thats interested (i think this is a somewhat interesting approach)

keep a list of modules that are active

Application.php

$container->set(Modules::class, function (ContainerInterface $c) {
    return new Modules(array(
        \App\Shared\Module::class,
        new \App\Auth\Module($c->get(System::class))
    ),array(
        $c->get(System::class)
    ));
});

Modules.php (collection of modules)


class Modules  implements \IteratorAggregate {
    private $modules = array();
    function __construct($modules=array(),$args=array()) {
        foreach ($modules as $item){
            if ($item instanceof ModuleInterface){
                $this->modules[get_class($item)] = $item;
            } else {
                $this->modules[$item] = new $item(...$args);
            }
        }
    }
    function getIterator() {
        return new \ArrayIterator($this->modules);
    }

    function get($module) : ModuleInterface{
        return $this->modules[$module];
    }

}

the individual module


class Module extends AbstractModule {
    protected System $system;

    function __construct(System $System) {
        $this->system = $System;
    }
    function setRoutes(RouteCollectorProxy $module){
        $module->group("",function(RouteCollectorProxy $group){
            $group->get("/",Controllers\HomeController::class)->setName("shared_home");
            $group->get("/t",[Controllers\HomeController::class,"testing"])->setName("shared_test");
        });
    }
  
}

and back to Application.php for where it pulls it all together

foreach ($container->get(Modules::class) as $key=>$module){
    if (method_exists($module,"setRoutes")){
        $app->group("",function(RouteCollectorProxy $group) use ($module) {
            $module->setRoutes($group);
        })->add(function (Request $request, RequestHandler $handler) use ($module) {
// i set the module to my System object.
            $this->get(System::class)->set("MODULE",$module); 
            $response = $handler->handle($request);
            return $response;
        });
    }
}

so basically a middleware on a “module” group that sets the current “module” for any route under it. we pass the “group” back to the module for it to set a sub group / routes etc as per a normal slim application. the path here is left blank so that you can set it in the module instead of locking yourself up to being forced to have a path prefix for each module