Structuring Phalcon Micro APIs with Handler Classes and Event Middleware
Move Phalcon Micro route logic into handler classes and bind middleware through the application's events manager. Includes a complete DI and routing setup, version caveats, and verification steps.
18 Aug 2026, 20:12 UTC

Once a Phalcon Micro application grows past a few routes, the most practical engineering decision is to stop embedding business logic in anonymous functions. Map each route to a dedicated handler class method, and implement cross-cutting concerns such as authentication or logging by attaching to the Micro application's event manager rather than the full MVC dispatcher. This keeps the entry point thin and makes handlers testable in isolation.
What Phalcon Micro Actually Does
Phalcon Micro (Phalcon\Mvc\Micro) is a stripped-down application that functions as both a router and a handler executor. When a request arrives, it matches the URL against registered routes and invokes the associated callable directly. It does not instantiate controller classes or run the dispatcher lifecycle used by the full MVC stack, which reduces overhead for lightweight REST APIs.
The application relies on a Dependency Injection (DI) container to resolve shared services such as the HTTP request and response objects. Route parameters—such as the numeric id in the example below—are extracted by the router and passed as positional arguments to the callable, so the handler signature must accept them in the order they appear in the route pattern. Because there is no dispatcher, traditional controller plugins do not fire automatically; middleware must be wired through the Micro object's own event system.
Worked Setup: DI, Events, and a Handler
The following pattern shows a complete entry point. It creates a DI container, instantiates the Micro application, attaches a before-execution event for API-key validation, and maps a parameterized route to a handler class. The example assumes a recent Phalcon 4.x/5.x namespace layout; verify exact class names against your installed version.
<?php
// index.php
use Phalcon\Mvc\Micro;
use Phalcon\Di\Di;
use Phalcon\Http\Request;
use Phalcon\Http\Response;
use Phalcon\Events\Manager as EventsManager;
$di = new Di();
$di->setShared('request', new Request());
$di->setShared('response', new Response());
$app = new Micro($di);
// Middleware via the Micro event manager
$events = new EventsManager();
$events->attach(
'micro:beforeExecuteRoute',
function ($event, $app) {
if ($app->request->getHeader('X-API-Key') !== 'expected-value') {
$app->response->setStatusCode(401, 'Unauthorized');
$app->response->setJsonContent(['error' => 'Invalid API key']);
$app->response->send();
return false; // Halt route execution
}
}
);
$app->setEventsManager($events);
// Thin route definition delegating to a class
$app->get(
'/api/products/{id:[0-9]+}',
[new ProductHandler($di), 'show']
);
$app->handle($_SERVER['REQUEST_URI']);
The handler class receives the DI container in its constructor and returns a fully formed Phalcon\Http\Response. Passing the container explicitly keeps the class decoupled from global state, so tests can substitute a mock container or a stubbed response without booting the full application.
<?php
// ProductHandler.php
use Phalcon\Http\Response;
class ProductHandler
{
protected $di;
public function __construct($di)
{
$this->di = $di;
}
public function show($id)
{
// Replace with a model or service lookup
$data = ['id' => (int) $id, 'name' => 'Example Product'];
$response = new Response();
$response->setStatusCode(200, 'OK');
$response->setJsonContent($data);
return $response;
}
}
Ensure ProductHandler is autoloaded—typically through Composer's PSR-4 configuration—or explicitly required before the route is defined.
Micro vs. Full MVC
| Concern | Phalcon Micro | Full Phalcon MVC |
|---|---|---|
| Routing | Direct mapping to callables | Dispatcher maps to Controller/Action |
| Middleware | Event manager attached to Micro | Dispatcher plugins and events |
| View layer | Not initialized by default | Automatic view rendering |
| Overhead | Minimal; no dispatcher cycle | Full application lifecycle |
Limits and Common Mistakes
- Version drift: Phalcon 3, 4, and 5 differ in namespaces, DI internals, and event names. The example above targets a modern namespace layout. Confirm method signatures and event strings against the documentation for the exact version installed.
- Event names are version-sensitive:
micro:beforeExecuteRouteis the documented event for pre-route middleware in recent versions, but verify the exact string and firing behavior in your environment. - Dispatcher plugins do not apply: Because Micro bypasses the MVC dispatcher, plugins registered for
Phalcon\Mvc\Dispatcherwill not execute. Any behavior they provided must be reimplemented as Micro events. - Response handling: If a handler returns a
Phalcon\Http\Response, Micro sends it. If it returnsnull, nothing is sent unless you call$app->response->send()or set content explicitly. - Route order matters: Phalcon evaluates routes in the order they are registered. Define specific patterns before generic catch-alls so they are not shadowed.
- Installation is not Composer-only: Phalcon is a C extension loaded by the PHP runtime. Depending on the platform, it is installed via PECL, system packages, or pre-built binaries. Composer packages may provide helpers, but they do not replace the extension itself.
- Bloated entry files: Keeping all logic in closures inside
index.phpmakes the API hard to maintain. Moving handlers to classes is the primary structural fix.
Verifying the Setup
Because Phalcon must be loaded as an extension, first confirm it is active in the terminal:
php -m | grep -i phalcon
If nothing is printed, the extension is not enabled in the active php.ini. Next, start the built-in server from the project root:
php -S localhost:8000
Then send a request that matches the route pattern:
curl -i http://localhost:8000/api/products/123
Check that the response carries a 200 status and a JSON body. If the before-event blocks the request, you should instead receive a 401 with the error payload. A 404 usually means the route pattern did not match or the URI was not passed correctly to handle().
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.