zetacomponents/mvc-tools
Zeta Components MVC Tools offers a lightweight MVC layer for PHP, with dispatchers, controllers, request/response objects, routing, and filters to structure web apps cleanly and flexibly.
MvcTools is the routing/controller/view layer of Apache Zeta Components: a small pipeline you assemble yourself, not a framework that discovers your code. The mental model is fixed and easy to hold in your head:
ezcMvcRequestezcMvcRoutingInformation (controller class + action + captures)ezcMvcResult (status, variables, headers, cookies, cache)ezcMvcResponseEvery one of those is an interface, so day-to-day work is mostly writing five small classes and wiring them in one configuration object.
Install:
composer require zetacomponents/mvc-tools
It pulls zetacomponents/base. Before investing, run it on your target PHP version — the code predates PHP 7 and Zeta Components is archived, so expect deprecation fixes on PHP 8.
Where to look first, in this order:
src/interfaces/ — ezcMvcDispatcherConfiguration, ezcMvcRouter, ezcMvcController, ezcMvcView, ezcMvcRequestParser, ezcMvcResponseWriter, ezcMvcFilter. These are the contracts you implement.src/ — the HTTP defaults: ezcMvcHttpRequestParser, ezcMvcHttpResponseWriter, ezcMvcConfigurableDispatcher, ezcMvcRegexpRoute.ezcMvc*), so grep is your navigation tool; there is no generated API index you can trust across versions.First use case — GET /hello/World returning HTML. Front controller only:
// public/index.php
require __DIR__ . '/../vendor/autoload.php';
$dispatcher = new ezcMvcConfigurableDispatcher( new App\DispatcherConfiguration() );
$dispatcher->run();
The composition root is a single class implementing ezcMvcDispatcherConfiguration:
class DispatcherConfiguration implements ezcMvcDispatcherConfiguration
{
public function createRequestParser()
{
return new ezcMvcHttpRequestParser();
}
public function createRouter( ezcMvcRequest $request )
{
return new App\Router( $request );
}
public function createController( ezcMvcRequest $request, ezcMvcRoutingInformation $info )
{
$class = $info->controllerClass;
return new $class( $info->action, $request );
}
public function createView( ezcMvcRoutingInformation $info, ezcMvcRequest $request, ezcMvcResult $result )
{
return new App\HtmlView( $request, $result );
}
public function createResponseWriter( ezcMvcRequest $request )
{
return new ezcMvcHttpResponseWriter();
}
public function createFatalResponse( ezcMvcRequest $request )
{
// return an error response/view — and log here
}
}
Copy the interface out of vendor/zetacomponents/mvc-tools/src/interfaces/ rather than any tutorial: method signatures drift between releases, and a missing method is a fatal error at boot.
The configuration class is the composition root. Keep it boring. If controllers need a repository or logger, instantiate them in createController() (or inject a small factory). There is no container, so the config doubles as your service locator — resist letting it grow into business logic.
Routing. Subclass ezcMvcRouter and return route objects from createRoutes(), or implement createRoutingInformation() by hand when you need HTTP-method, subdomain, or header-based routing. MvcTools does not care about verbs by default; check $request->method yourself. Regex captures typically land in $request->variables.
// inside a controller action
$id = (int) $this->request->variables['id'];
Controllers. Public methods named after the action, one controller per resource, constructor gets ($action, ezcMvcRequest $request). Return an ezcMvcResult and set status, variables, headers, cache, cookies on it — never echo.
Redirects. ezcMvcInternalRedirect re-enters the dispatcher without a browser round trip (post/redirect/get, or fallback handling); ezcMvcExternalRedirect sends the browser elsewhere.
public function create()
{
$id = $this->repository->insert( $this->request->variables );
return new ezcMvcInternalRedirect( '/user/' . $id );
}
Views. One view class per output format is the cleanest split. A shared base class that copies result fields into the response saves you from silent dropped headers.
class JsonView extends ezcMvcView
{
public function createResponse( ezcMvcRoutingInformation $info, ezcMvcRequest $request )
{
$response = new ezcMvcResponse();
$response->headers['Content-Type'] = 'application/json';
$response->body = json_encode( $this->result->variables );
return $response;
}
}
For HTML, render PHP templates with output buffering, or use the built-in zone mechanism (createZone) if you want layout/partial composition without a template engine.
Filters for cross-cutting concerns. Auth, locale/translation, request logging, CORS, ETag/cache headers — register them in the configuration and keep them stateless. They run on every request, including redirects, so order matters and they should be cheap.
Testing is the real win. Skip the HTTP layer entirely: construct ezcMvcRequest and ezcMvcResult by hand, call the controller action, then call the view. Only ezcMvcHttpRequestParser and ezcMvcHttpResponseWriter need integration tests.
Running outside Apache/FPM. Swap the parser for one fed by a PSR-7 request (or a long-running worker's request object) and swap the writer for one that returns a PSR-7 response. Nothing else changes — this is the seam that makes the package usable in a modern stack.
Sizing. A handful of routes and hand-rolled templates: good fit. Anything larger: keep business logic out of controllers from day one, because there is no auto-wiring to bail you out later.
ezcMvc*. Composer autoloading works through the package's own config, but mixing in the bundled ezcBase autoloader manually causes "class not found" surprises. Never require_once individual classes.ezcMvcHttpRequestParser reads $_SERVER, $_GET, $_POST, $_FILES, and headers directly. It is awkward to test and unsafe under long-running workers where globals go stale — write your own parser for those environments.createFatalResponse / redirect handling is easy to misread. Always copy from the installed src/interfaces/.$info->action against a per-controller whitelist in createController() so public helper methods can't be reached.ezcMvcResult; the view must copy them onto ezcMvcResponse. Forget one and it silently vanishes. Centralize the mapping in a base view.filter() mutates and returns, or returns a new object, for your version. An exception inside a filter will typically fall through to the fatal-response path — log inside filters rather than relying on the error handler.ezcMvcRequest, the ezcMvcRoutingInformation, and the final response status/body length from inside the configuration's factory methods. Log $e in createFatalResponse or you get a blank 500 with no trace.ezcMvcRequestParser (CLI/tests/PSR-7), custom ezcMvcResponseWriter (streaming, JSON, PSR-7), custom router (verbs, subdomains), and filters (auth, i18n). Those are the seams worth building on; everything else you replace wholesale.How can I help you explore Laravel packages today?