Skip to content

Modules

A module is the unit Quiote uses to group related actions, views, and templates. Every action in your application lives in exactly one module, and the module name is half of what the framework needs to resolve a request into code — the route says which module, which action, and the module’s directory layout says where the classes are.

Modules are pure convention: a directory with an expected shape and a namespace that matches. There is no module registry to edit and no build step to run — drop the files in the right place with the right names and the request resolves.

A module is not a pipeline stage of its own — it is where the code lives that the pipeline runs. The connection is made during dispatch:

  1. RoutingMiddleware resolves the request to a _module and _action (see Routing).
  2. When DispatchMiddleware runs the action, ActionExecutor calls Controller::createActionInstance($module, $action), which builds the class name deterministically ({namespace_prefix}\Modules\{Module}\Actions\{Action}Action) and lets the autoloader find it.
  3. ViewNameResolver maps the same module + action + returned view name to the view class and template file.

RoutingMiddleware resolves _module / _actionDispatchMiddleware asks the Controller to instantiate the action class in that module → the action runs → ViewNameResolver locates the view and template in the same module → the response is rendered.

Because the mapping is deterministic, there is nothing to register — matching names and directories are all that wire a module in. The rest of this page is those naming rules and the optional per-module config.

A module is a directory under your application’s module directory (core.module_dir, Modules/ by default). Its name is the module name, PascalCase:

Modules/
Blog/
Actions/
PostAction.php
Views/
PostSuccessView.php
Templates/
PostSuccess.php
Models/ (optional) — DTOs, see Services & models
Validate/ (optional) — validators.xml per action
Config/ (optional) — module.xml, config_handlers.xml

Only Actions/, Views/, and Templates/ are needed for a working module. The rest are opt-in and covered below.

The classes in a module share a namespace built from your app’s namespace prefix:

<?php
namespace App\Modules\Blog\Actions; // App = core.namespace_prefix
use Quiote\Action\Action;
use Quiote\Request\WebRequest;
class PostAction extends Action
{
public function executeRead(WebRequest $rd)
{
$this->setAttribute('post', /* ... */);
return 'Success';
}
}

There is no scan and no manifest. When a route resolves to a module and action, the Controller builds the class name deterministically and lets the autoloader find it. For an action in module Blog named Post:

{core.namespace_prefix}\Modules\Blog\Actions\PostAction

The same pattern produces the view and model class names:

KindClassFile
ActionApp\Modules\Blog\Actions\PostActionModules/Blog/Actions/PostAction.php
ViewApp\Modules\Blog\Views\PostSuccessViewModules/Blog/Views/PostSuccessView.php
ModelApp\Modules\Blog\Models\PostModelModules/Blog/Models/PostModel.php

core.namespace_prefix is set in your settings config (App by default). As long as your namespaces and directories follow the pattern, the class resolves — the autoloader does the finding, so no registration is involved.

The convention that makes modules work is the alignment of three names — action, view, template:

  • An action in module Blog named Post returning 'Success'
  • …resolves to the view class PostSuccessView (action name + view name + View)…
  • …and the template PostSuccess.php (action name + view name).

ViewNameResolver performs this mapping. Keep the names aligned and the wiring stays implicit; deviate and you wire it explicitly. This is the same contract described in Actions and views and Templates and rendering — modules are just where those files live.

The filesystem layout above is not hard-coded — it is a set of per-module directives the framework sets up with conventional defaults on first use of a module, then reads whenever it needs to locate a file. Each key is modules.<lowercase-module>.quiote.*:

DirectiveDefault
quiote.action.path%core.module_dir%/${moduleName}/Actions/${actionName}Action.php
quiote.view.path%core.module_dir%/${moduleName}/Views/${viewName}View.php
quiote.template.directory%core.module_dir%/${module}/Templates
quiote.validate.path%core.module_dir%/${moduleName}/Validate/${actionName}.xml
quiote.cache.path%core.module_dir%/${moduleName}/cache/${actionName}.xml
quiote.view.name${actionName}${viewName}

A directive supplied by the module’s own config is never overwritten by the default, so a module can lay itself out differently — point quiote.action.path at a src/ subdirectory, say — without affecting any other module. Most applications never touch these.

A module can carry its own config in a Config/ directory. Two files are recognised on the module’s first initialisation:

  • Config/module.xml — the module’s own settings. If present, it is loaded (and compiled/cached) once; if absent, the module is simply enabled with defaults.
  • Config/config_handlers.xml — registers additional config handlers scoped to this module.

A Config.php file directly under the module directory, if present, is required on initialisation — a place for imperative module setup that doesn’t fit declarative config.

Every module has an enabled flag under modules.<lowercase-module>.enabled. It defaults to true; a disabled module throws DisabledModuleException when an action in it is requested (its views stay usable, so an error view from a disabled module still renders). Set it in the module’s module.xml, or centrally in your app settings:

Config/settings.php
return [
'modules.blog.enabled' => false, // turn the Blog module off
];

Module names are matched case-insensitively for these keys — the directory Blog is configured under modules.blog.*.

The scaffolded app’s Default module is the smallest real example. Its Contact action, view, and template:

Modules/Default/Actions/ContactAction.php
<?php
namespace App\Modules\Default\Actions;
use Quiote\Action\Action;
use Quiote\Request\WebRequest;
use Quiote\Routing\Attribute\Route;
#[Route('/contact', name: 'contact', methods: ['GET'])]
class ContactAction extends Action
{
public function executeRead(WebRequest $rd)
{
return 'Success';
}
}
Modules/Default/Views/ContactSuccessView.php
<?php
namespace App\Modules\Default\Views;
use Quiote\Request\WebRequest;
use Quiote\View\View;
class ContactSuccessView extends View
{
public function executeHtml(WebRequest $rd)
{
$this->loadLayout();
$this->setAttribute('title', 'Contact');
}
}
Modules/Default/Templates/ContactSuccess.php
<h1><?php echo htmlspecialchars($template['title'], ENT_QUOTES, 'UTF-8'); ?></h1>

Three files, three aligned names, one route — and GET /contact renders. Adding a feature to an application is usually adding a module (or an action to an existing one) in exactly this shape. See Your first application for the walkthrough that generates it.