Phalcon v5.18.0 is out, and it brings a second way to build an application. Alongside it, the ecosystem grew two new repositories and the v6 PHP implementation crossed from alpha into beta.
A different shape for an application
The headline of 5.18 is Phalcon\ADR, an Action-Domain-Responder HTTP stack that sits next to MVC rather than replacing it. Where MVC hands a controller several actions and a view, ADR splits a request into three focused roles:
- an Action - one invokable class per route, found by convention instead of a route table
- a Domain - your business logic, which returns a
Phalcon\ADR\Payload\Payloadand never touches HTTP - a Responder - which turns that payload into a response
The namespace ships complete: a Container wired for Phalcon\Container\Container, a SapiEmitter, an onion middleware stack, an Input bag, chainable responders (JSON, text, redirect, error, and the new ViewResponder that renders a .phtml through the new Phalcon\Contracts\View\Renderer contract - which Phalcon\Mvc\View\Simple now implements), granular exceptions, events wired to the Events Manager, and a one-command Front class that boots and runs the whole thing.
Phalcon\ADR\Application is a self-contained composition root: bind(), define(), factory(), set(), extend() and getContainer() are the whole registration surface, with setBaseNamespace() and secureWith() for the convention router and its namespace guard. AbstractHttpFront::boot() returns the built container without running the application, so a bootstrap file can be a single return (new AppFront(dirname(__DIR__)))->boot();.
The ADR (convention) router descends namespaces - a path segment becomes a namespace segment only when the matching directory exists - and derives exactly one Action class per path. The convention is also introspectable in both directions: classFor(), pathFor(), methodFor() and candidatesFor() are on the contract, so any tooling that asks the framework what a route resolves to is available. setWordSeparator() handles separators, and the new Phalcon\ADR\Router\AttributeFilter lets an Action declare a static params() method so route segments arrive validated against a regex, cast to int/float/string and optionally converted - before the Action runs. A regex miss is a 404, not a bad argument.
Fewer queries in the ORM
Relations picked up eager loading. Phalcon\Mvc\Model::find() now accepts an eager parameter - an array of dot-delimited relation paths, optionally with per-path options - and pre-loads them with one query per relation instead of one per record. Phalcon\Mvc\Model\Criteria::eager() exposes the same on the criteria surface. In the same area, the relation cache is now probed with array_key_exists(), so a to-one relation that legitimately resolves to no record stops being re-queried on every access.
A bug with a long service record
Phalcon\Db\Result\PdoResult::numRows() deserves its own paragraph. SQLite hands PDO a streaming cursor that does not know how many rows it holds until it has been stepped to the end, so the count costs a second statement. To build it, the old code matched the original SQL with preg_match("/^SELECT\s+(.*)/i", ...) and re-assembled it as a SELECT COUNT(*). In PCRE, . does not cross a newline unless you ask it to - and nobody asked. So any statement carrying a line break matched only up to the first one and produced a truncated, invalid subquery; and a statement that already began with SELECT COUNT(*) skipped the count entirely and returned a hard-coded 1, which quietly reported the wrong number for a GROUP BY.
That code has been sitting there since 2013. It now wraps the original statement verbatim - any SELECT is a valid subquery - so multi-line SQL, common table expressions and grouped counts all report correctly.
Model persistence fixes worth calling out
Two long-standing bugs in the insert path are fixed, and both surfaced the same way - a MySQL generated column:
- Attributes registered with
skipAttributes(),skipAttributesOnCreate()andskipAttributesOnUpdate()were being ignored. - A column the database can fill itself was receiving a literal
nullinstead of theDEFAULTkeyword (or being omitted entirely on an adapter withoutDEFAULTsupport, such as SQLite).
The framework now starts describing itself
A small set of additions exists so tooling can ask the framework questions instead of guessing: Phalcon\Container\Container::getServiceNames() with the Phalcon\Contracts\Container\Service\Enumerable contract, and Phalcon\Events\Manager::getListenerMap() returns each event type mapped to its listeners in a single call - with Phalcon\Contracts\Events\Enumerable. Crest (below) is the first consumer, and it types against those contracts rather than the concrete classes.
HTTP and the database layer
Phalcon\Http\Request::getAttributes() returns a Phalcon\Http\Request\Bag\AttributeBag, a mutable string-keyed bag of application-defined values that routers, dispatchers and security components can attach to a request as it travels. Appending with $bag[] = ... throws NullKeyException, since bag elements are always string-keyed.
Cookie deletion was fixed twice over: Phalcon\Http\Response\Cookies::delete() now falls back to the $_COOKIE superglobal so it can delete a cookie that was not set in the same request, and Phalcon\Http\Cookie::delete() expires the cookie with its own stored path and domain instead of the defaults.
On MariaDB, describeColumns() was returning string, date and time defaults still wrapped in quotes ('fi' rather than fi), because MariaDB reports COLUMN_DEFAULT as DDL source rather than a resolved literal. If you were stripping those quotes yourself, remove the workaround. MySQL is unaffected.
Housekeeping
5.18 builds on Zephir 1.2.0. The distribution tarball that Composer and PIE download now ships only what is needed to build the extension - tests, Zephir sources, tooling and the Docker development setup are excluded through .gitattributes. Internally, fully-qualified class-name strings were replaced with ::class references across the factories, service providers and DI containers.
Everything above is mirrored in the v6 PHP implementation, released as v6.0.0beta5 - the package moved out of alpha during this cycle.
The ecosystem
- Crest - brand new this cycle (WIP). A command line application for Phalcon projects aimed to replace the phalcon-devtools which has not seen any traffic since v4. Crest offer for now: generators (
make:action,make:command,make:middleware,make:provider,make:responder,stub:publish) and introspection (route:list,container:list,event:list,config:show,about). It reads the ADR routing convention from the framework rather than reimplementing it, andcontainer:list/event:listare built on the newEnumerablecontracts in 5.18. Stubs are overridable per project, and crest itself needs neither the C extension nor the PHP package to run - install it withcomposer require --dev phalcon/crest. It is still work in progress, but the plan is to extend its features to MVC, CLI and Micro component generation. - Vökuró ADR - also new. The full Vökuró sample application rebuilt as ports and adapters, with no
DiInterfaceanywhere in the delivery ring: authentication, ACL permissions, user and profile management, forms, CSRF and mailing, all behind contracts with the technology-bound adapters kept inApplicationandInfrastructure. It carries a unit suite driven by in-memory fakes plus a MySQL integration suite with merged coverage, and adocs/folder covering architecture, payloads and testing. It was built against v6; with the ADR stack shipping in 5.18, the v5 build is now unblocked. - Talon - reached
v0.9.0. MariaDB joins the recognized drivers with its own suite, service container and CI job, configured independently of MySQL. A newDatabase\Dialectenum (andDatabaseTrait::getDialect()) resolves a connection to its SQL dialect and quotes identifiers accordingly, so reserved words such asorderandkeywork. Two quiet correctness bugs are gone:select()now matches anullcriterion withIS NULL(soassertNotInDatabase()was previously passing regardless of the data), andDATA_POSTGRES_SCHEMAis actually applied asSET search_path. Coverage is now merged across the unit, MariaDB, MySQL and PostgreSQL suites. Earlier in the cyclev0.8.0added the REST/JSON testing surface -RestTrait,RestAssertionsTrait,AbstractRestTestCase,HttpCode,JsonTypeandJsonSubset. - Vökuró - shipped
v5.0.4at the start of the cycle: the debug bar’s logger adapter now feeds the bar directly, aControllerBase::flashForward()helper replaces the repeated flash-and-forward blocks, and the controllers, forms, models, providers and plugins are fully typed with PHPStan sitting at level 7. - INVO - now installable with
composer create-project, with its suite driven by the Talon runner and shared forward/flash/not-found helpers pulled up intoControllerBase(plus anIdAndNameFieldsTraitshared by the Companies, Products and ProductTypes forms).
Thanks
As always, thank you to everyone who reported an issue, opened a pull request, or tested a build before it shipped.
Changelog
5.18.0 (2026-07-31)
Tools
- Zephir 1.2.0 (83d8f68)
Changed
- Changed
Phalcon\ADR\Applicationinto a self-contained composition root: it owns (or accepts) aPhalcon\Container\Containerand exposes a small registration surface -bind(),define(),factory(),set(),extend()andgetContainer()- plussetBaseNamespace()andsecureWith()for convention-router and namespace-prefix guard configuration.Phalcon\ADR\Front\AbstractHttpFrontgained a protectedgetApplication()hook returningPhalcon\Contracts\ADR\Application, so a front controller can configure the application or wire a different implementation. #17389 [doc] - Replaced fully-qualified class-name strings with
::classreferences (anduseimports) across factories, service providers and DI containers #17380 - Changed
Phalcon\Mvc\Model::getRelated()andPhalcon\Mvc\Model::isRelationshipLoaded()to test the relation cache witharray_key_exists()instead ofisset(), so a to-one relation that resolves to no record is no longer re-queried on every access #17331 [doc] - Changed
Phalcon\Mvc\Model\Manager::mergeFindParameters()fromfinal protectedtofinal public static#17331 - Changed
Phalcon\ADR\Router\Routerfrom a candidate chain to a namespace descent: a path segment becomes a namespace segment only if the matching directory exists, after which at most two Action classes are probed instead of five. An Action can no longer be silently shadowed by an earlier candidate. RequiressetActionDirectory(). #17405 [doc] - Changed
Phalcon\ADR\Router\Routerto derive exactly one Action class per path: the class name is the verb followed by every static segment concatenated. #17410 [doc] - Changed
Phalcon\Mvc\Model\Resultset::refresh()to reset the cursor - position, current row, buffered rows and active row - after replaying the statement. #17399 [doc] - Changed
Phalcon\Events\Manager::getEventTypes()togetListenerMap(), which now returns each event type mapped to its listeners. #17416 [doc] - Changed the distribution tarball that Composer and PIE download to ship only the files needed to build the extension; tests, Zephir sources, tooling and the Docker development setup are now excluded through
.gitattributes. #17419
Added
- Added the Action-Domain-Responder (ADR) HTTP stack under
Phalcon\ADR, an alternative to MVC that splits request handling into three focused roles: an Action (one invokable class per route) drives a Domain (your business logic, which returns aPhalcon\ADR\Payload\Payloadand never touches HTTP) and hands the result to a Responder that turns it into a response.Phalcon\ADR\Container: registrations for thePhalcon\Container\ContainerPhalcon\ADR\Emitter:SapiEmitterto emit response dataPhalcon\ADR\Events: events wired to the Events ManagerPhalcon\ADR\Exceptions: granular exceptions the namespace throwsPhalcon\ADR\Front: One-command class wiring the application and executing itPhalcon\ADR\Input: Bag of request data to be passed to the domainPhalcon\ADR\Middleware: Onion based middleware stackPhalcon\ADR\Payload: VO for transferring data from the domain and the responderPhalcon\ADR\Responder: Chainable responders, handling redirect, json, text, error etc.Phalcon\ADR\Router: Router for the application. #17341 [doc]
- Added
Phalcon\ADR\Responder\ViewResponder, which renders a.phtmltemplate and returns it as an HTML response. The action picks the template withwithTemplate(), and the view receivesresult,messagesandstatus. Any renderer implementing the newPhalcon\Contracts\View\Renderercan be used -Phalcon\Mvc\View\Simplenow does. #17379 [doc] - Added request attributes support to
Phalcon\Http\Request.Phalcon\Http\Request::getAttributes()returns aPhalcon\Http\Request\Bag\AttributeBag, a mutable, string-keyed bag of arbitrary application-defined values attached to the request during its lifecycle (router, dispatcher, security components etc.). Writing with anullkey (the$bag[] = ...append form) throws the newPhalcon\Http\Request\Exceptions\NullKeyException, since bag elements are always string-keyed. #17367 [doc] - Added opt-in route-parameter pre-filtering to the ADR convention router via the new
Phalcon\ADR\Router\AttributeFilter. An Action that declares a staticparams()method has its positional route segments validated against a regex, cast to a scalar type (int,float,string) and optionally passed through a converter closure, then written to the request as named attributes - all before the Action runs. A regex miss is treated as a route miss (404). #17393 [doc] - Added eager loading of model relations:
Phalcon\Mvc\Model::find()accepts aneagerparameter - an array of dot-delimited relation paths, optionallypath => options- which pre-loads the named relations with one query per relation instead of one per record.Phalcon\Mvc\Model\Criteria::eager()exposes the same on the criteria surface. #17331 [doc] - Added
candidatesFor()to thePhalcon\Contracts\ADR\Router\Routercontract and toPhalcon\ADR\Router\Router. It returns every Action class the convention router would try for a given HTTP method and path, in the order it tries them. #17403 [doc] - Added
pathFor(),setActionDirectory()andsetWordSeparator()to thePhalcon\Contracts\ADR\Router\Routercontract and toPhalcon\ADR\Router\Router; the two setters are also onPhalcon\ADR\Application.pathFor()is the inverse of the routing convention, returning the canonical path an Action answers ornull. AddedPhalcon\ADR\Exceptions\ActionDirectoryNotSet. #17405 [doc] - Added
Phalcon\Container\Container::getServiceNames(), returning the names of every registered service definition. #17406 [doc] AddedPhalcon\Events\Manager::getEventTypes(), returning the event types that currently have at least one listener attached, including those contributed by subscribers. #17406 [doc]- Added
classFor()to thePhalcon\Contracts\ADR\Router\Routercontract and toPhalcon\ADR\Router\Router. It names the Action class the convention would use for a static path. #17410 [doc] - Added
Phalcon\ADR\Front\AbstractHttpFront::boot(), which builds the container, loads the environment and registers the providers, then returns the container - for consumers that need it before, or instead of,run(). The container is built once and cached, soboot()andrun()share the same instance. A bootstrap file can now bereturn (new AppFront(dirname(__DIR__)))->boot();. #17413 [doc] - Added
Phalcon\Contracts\Container\Service\Enumerableimplemented inPhalcon\Container\Container. #17416 [doc] - Added
Phalcon\Contracts\Events\Enumerable, implemented inPhalcon\Events\Manager. #17416 [doc] - Added
methodFor()to thePhalcon\Contracts\ADR\Router\Routercontract and toPhalcon\ADR\Router\Router. It names the HTTP method an Action class answers, the counterpart topathFor(). #17416 [doc]
Fixed
- Fixed
Phalcon\Container\Definition\ServiceDefinition::resolveArgs()treating any constructor-argument object that merely exposes aresolve()method as a lazy value, usingmethod_exists(). BecausePhalcon\Container\Containerdefines a privateresolve(), autowiring a service whose constructor receives the container - i.e.__construct(?Container $container)- mistook the injected container for a lazy resolvable. The lazy check is now guarded for non-object arguments. #17391 - Fixed
Phalcon\Http\Response\Cookies::delete()not deleting a cookie that was not set in the same request; it now falls back to the$_COOKIEsuperglobal.Phalcon\Http\Cookie::delete()expires the cookie with its storedpathanddomaininstead of the defaults. #17395 [doc] - Fixed
Phalcon\Mvc\Modelignoring attributes registered withskipAttributes(),skipAttributesOnCreate()andskipAttributesOnUpdate(), so a skipped column was emitted in the generatedINSERT/UPDATE(breaking, for instance, inserts into a table with a MySQL generated column). The skip list is keyed withnullvalues, andisset()on an array offset now follows PHP semantics (key present and value notnull), which made every skipped attribute read as not registered; the checks indoLowInsert(),doLowUpdate()and the not-null validation now usearray_key_exists(). #17382 [doc] - Fixed
Phalcon\Mvc\Modelinserting a literalnullfor a column the database can supply a value for, instead of theDEFAULTkeyword (or omitting the column on an adapter withoutDEFAULTsupport, such as SQLite). This restores inserts against a MySQLGENERATED ALWAYS AS (...) STOREDcolumn, which rejects an explicitnullwithSQLSTATE[HY000]: General error: 3105but acceptsDEFAULT. #17382 [doc] - Fixed
Phalcon\ADR\Router\Routertreating-and_as the same delimiter, so/forgot-passwordand/forgot_passwordresolved to the same Action and the path-to-class map had no inverse. A single separator now applies in both directions, default-and settable withsetWordSeparator();_is literal. #17405 [doc] - Fixed
Phalcon\Mvc\Model::find()andPhalcon\Mvc\Model::findFirst()raising an errorCall to undefined method static::getpreparedquery(). Usingselfvs. ‘static` for the internal calls. #17409 [doc] - Fixed
Phalcon\Mvc\Model\Resultsetcosting two statements for every resultset on SQLite. #17399 [doc] - Fixed
Phalcon\Db\Result\PdoResult::numRows()failing on multi-line statements. #17399 - Fixed
Phalcon\Mvc\Model\Resultset::seek()leaving the previous row in place as the current one when seeking past the end of a resultset held in memory. #17399 [doc] - Fixed
Phalcon\Db\Adapter\Pdo\Mysql::describeColumns()returning string, date and time defaults still wrapped in quotes on MariaDB, e.g.'fi'instead offi. MariaDB reportsCOLUMN_DEFAULTas the DDL source rather than the resolved literal. Expression defaults are unaffected. Note that on MariaDB this changes whatPhalcon\Db\Column::getDefault()returns and what the model layer writes into an unsetNOT NULLcolumn on save; if you were stripping the quotes yourself, remove that workaround. MySQL is unaffected. #17417 [doc]
Removed
- Removed
.php_cs.dist, superseded byresources/php-cs-fixer.php. #17419
Upgrade
Developers can upgrade using PIE
pie install phalcon/cphalcon-5.18.0
To compile from source, follow our installation document
Chat - Q&A
Support
Social Media
Videos
<3 Phalcon Team