5.13.0 (2026-05-18)
Changed
- Changed
Phalcon\Contracts\Support\Collectionto declare the expanded method surface (column,each,filter,first,getType,isEmpty,keys,last,map,reduce,replace,sort,values,where) so the contract matches the implementation #17000 [doc] - Changed
Phalcon\Events\Eventto be declaredfinal. The class is a value object holdingtype,source,data,cancelable, andstopped; no subclasses exist in the cphalcon tree and any future typed-event work would add new sibling classes implementingEventInterfacerather than extendingEvent. Marking itfinallets the C extension fold the per-fire getters (getType,getSource,getData,isCancelable,isStopped,isPropagationStopped) into direct dispatch. BC note: any userlandclass MyEvent extends Eventnow fails #17006 [doc] - Changed
Phalcon\Events\Manager::attach()to classify the handler kind once at attach time so the dispatch loop can route via a single branch instead of runninginstanceof Closure/is_callable/typeofper fire per listener. Four kinds are recognized:0= Closure (direct invocation via Zephir's{handler}(args)syntax, no arg-array allocation),1=[obj, method]array callable (direct dynamic dispatchhandler[0]->{handler[1]}(args), nocall_user_func_arrayoverhead),2= plain object with method named after the event - the classic Phalcon listener pattern (class name is captured at attach time and stored on the tuple to skipget_class()per fire),3= generic callable (string function name, invokable object,[class, staticMethod]) routed throughcall_user_func_array. The subscriber array-form attach paths ([methodName, priority]and[[methodA, priorityA], [methodB, priorityB]]) now route throughinsertHandlerEntrydirectly withkind=1, bypassing the classification cascade since the resulting handler shape is already known.methodExistsCacheaccess in the dispatch loop is tightened to a singleissetfast path #17006 [doc] - Changed
Phalcon\Events\Manager::detach()to drop theeventTypekey entirely when its queue empties (removing the last listener), sohasListeners()andfire()'s short-circuit tell the truth. Previously an emptied queue left the key in place with an empty array value, causingisset($this->events[$eventType])to returntruewith no actual listeners to dispatch to. The matchingDetachTestexpectations are updated to reflect the new contract #17006 [doc] - Changed
Phalcon\Events\Manager::detachAll(null)to reseteventsto[]instead ofnull. The previousnullreset brokeisset($this->events[$key])semantics insideattach()andfire()until the next assignment refilled the property; the empty-array form keeps all access paths consistent #17006 [doc] - Changed
Phalcon\Events\Manager::fire()andPhalcon\Events\Manager::fireAll()to wrap dispatch intry { ... } catch \Throwable, ex { cleanup; throw ex; }. A throwing listener restores stashed responses (if nested) and decrementsfireDepthback to its pre-call value before the exception propagates, so manager state stays clean for the next fire - important for long-lived managers (workers, daemons) where a single dirty teardown would poison every subsequent fire #17006 [doc] - Changed
Phalcon\Events\Manager::fire()to parse theeventTypeonly once per unique string and cache the result ([typePrefix, eventName]) in an internaleventNameCachekeyed by the original string. Repeated fires of the same event name (db:beforeQuery × Nper request, model lifecycle events on hot save paths) collapse to a single hash lookup after warm-up. The cache never needs invalidation because the parse is deterministic for a given input string.fire()also short-circuits before allocating theEventinstance when no listeners match either the type queue or the fully-qualified queue - in production most fires have zero matching listeners (model lifecycle events with no user-attached behavior, DB events without a tracer), so the avoided allocation is significant in hot paths #17006 [doc] - Changed
Phalcon\Events\Manager::fire(),Phalcon\Events\Manager::attach(), andPhalcon\Events\Manager::fireQueue()to be declaredfinal. The class itself stays open so genuine subclasses that only add new methods continue to work, but the dispatch hot path is locked to enable C-level direct dispatch on the three methods that run per-event. The remaining public surface (addSubscriber,removeSubscriber,detach,detachAll,getListeners,getResponses,getSubscribers,hasListeners,isCollecting,isValidHandler,collectResponses,enablePriorities,arePrioritiesEnabled,isStrict,setStrict,fireAll,clearSubscribers) is left non-finalso decorator-style subclasses that wrap these less-hot methods can still override them. BC note: subclasses that overridefire,attach, orfireQueuenow fail #17006 [doc] - Changed
Phalcon\Events\Manager::fireQueue()to be a thin BC-preserving wrapper around a new privatePhalcon\Events\Manager::dispatch()helper. The public signaturefireQueue(array $queue, EventInterface $event)is unchanged; the framework's ownfire()path bypassesfireQueueand callsdispatch()directly with hoisted arguments (eventName,source,data,cancelable,collect) so the second dispatch leg of a two-queuefire()(type queue + fully-qualified queue) does not re-extract identical values from the event. The dispatch path also applies a single-handler fast path: when the queue has exactly one listener (very common in unit tests and lightly-instrumented production), the foreach plus per-iteration cancelable/isStopped check is skipped and dispatch goes straight through the type-branch #17006 [doc] - Changed
Phalcon\Events\Managerdispatch return-value contract to last non-null wins. Previously every listener return overwrote the runningstatus, so a chain of ("value",null) ended withfire()returningnulland silently losing the earlier real value. The new contract only updatesstatuswhen the listener return is non-null - the last meaningful return survives. Thestop()determinism rule overrides last-non-null: when a listener calls$event->stop()(andcancelable=true), that listener's return value is whatfire()returns - even ifnull- because the caller asked for the stopping listener's verdict explicitly. Returningfalsefrom a listener does not short-circuit the queue; callers wanting to stop downstream listeners must call$event->stop(). Consumers likePhalcon\Mvc\Dispatcherthat interpret afalsereturn fromfire()as a cancel signal are unaffected because that check happens in their own dispatch logic, not in the events manager #17006 [doc] - Changed
Phalcon\Events\Managerlistener storage fromSplPriorityQueueto a sorted array of[handler, type, priority]tuples (with an additionalclassNameelement ontype=2tuples). TheSplPriorityQueue::EXTR_BOTHclone-per-fire and O(n)setExtractFlags()rebuild on detach are eliminated; the "empty heap" warnings produced bySplPriorityQueueon never-fired event types disappear as a side effect. Insert order under the same priority is preserved (FIFO). WhenenablePrioritiesis off (the default),insertHandlerEntryshort-circuits to a plain append - the sorted-insert loop only runs when priorities are explicitly enabled. When it does run, the insert usesarray_spliceinstead of a per-element rebuild #17006 [doc] - Changed
Phalcon\Html\Escaperinto a façade over five per-context escapers (Phalcon\Html\Escaper\HtmlEscaper,AttributeEscaper,CssEscaper,JsEscaper,UrlEscaper); each is reachable viagetXxxEscaper()/setXxxEscaper()so individual contexts can be swapped without subclassing the façade. The legacysetEncoding(),setFlags(), andsetDoubleEncode()setters fan out to all sub-escapers so existing code keeps working #16971 [doc] - Changed
Phalcon\Html\Helper\AbstractSeries::__toString()toksort()itsstorebefore rendering so entries are emitted in position order rather than registration order. #16971 [doc] - Changed
Phalcon\Html\Helper\Input\CheckboxandPhalcon\Html\Helper\Input\Radioto extend a new sharedPhalcon\Html\Helper\Input\AbstractChecked;Radiono longer extendsCheckbox. Two paths now renderchecked="checked": the unconditional opt-ins["checked" => "checked"](case-insensitive) and["checked" => true], and a value-match path comparing the suppliedcheckedattribute against the input'svalue(==by default for mixed int/string round-tripping,===understrict(true)) #16971 [doc] - Changed
Phalcon\Html\Helper\Style::add()andPhalcon\Html\Helper\Script::add()now accept an optionalint $position = -1argument that is routed through the new protectedpushOrPlace()helper (negative pushes onto the next auto-increment slot, non-negative places at that key, advancing past occupied slots) #16971 [doc] - Changed
Phalcon\Html\TagFactoryto no longer extendPhalcon\Factory\AbstractFactory; the registration pipeline now accepts class-strings, closures/callables, or[className, [depKey, ...]]/[className, [depKey, ...], [extraArg, ...]]tuples, and helpers are lazily cached per name in a separateinstancesmap soset()overrides correctly invalidate the previous instance.has()now reports against the recipe map instead of the resolved-instance map #16971 [doc] - Changed
Phalcon\Mvc\Model\Resultset\Complex::current()LEFT-JOIN hydration to be controlled by the neworm.resultset_empty_left_join_modelini setting (defaulttrue). When the flag istrue(default) a LEFT JOIN that matches no row hydrates an empty Model instance whose every column isnull- preserving the pre-5.13 behavior so applications upgrading from 5.9.x do not need code changes. Setting the flag tofalse(viaphp.ini,.htaccess, orPhalcon\Support\Settings::set('orm.resultset_empty_left_join_model', false)) restores the 5.13.0 contract introduced by #16239, where the unmatched slot is plainlynull. The new key is wired intoPhalcon\Support\Settings::get()/set()alongside the otherorm.*toggles #16960 [doc] - Changed
Phalcon\Mvc\Url\UrlInterface::get()signature to match the implementation: the previously undeclaredvar baseUri = nullfourth parameter (added toPhalcon\Mvc\Url::get()in 2015 but never propagated to the interface) and the newbool replaceArgs = falsefifth parameter are now part of the contract #16986 [doc] - Changed
Phalcon\Support\Collection::__construct()to accept two additional opt-in parameters -bool $strictNull = falseand?string $type = null. WhenstrictNullistrue,get()returns storednullvalues verbatim instead of substituting the default (previous behavior was unconditional default-substitution); whentypeis set, everyset()/init()runs through the newvalidateType()guard, which maps the scalar tokensint/string/bool/float/array/objectto theiris_*checks and treats anything else as a class/interface name compared withinstanceof, throwingInvalidArgumentExceptionon mismatch #17000 [doc] - Changed
Phalcon\Support\Collection::__serialize()to return a structured array["data", "insensitive", "strictNull", "type"]so a serialize/unserialize round-trip restores the full configuration (previously onlydatasurvived).__unserialize()accepts both the new structured format and the legacy flat-array format, so existing serialized blobs keep deserializing without intervention #17000 [doc] - Changed
Phalcon\Support\Collection::getKeys()andPhalcon\Support\Collection::getValues()to delegate to the newkeys()/values()methods and marked theget*-prefixed pair@deprecated; behavior is unchanged but new code should callkeys()/values()instead #17000 [doc]
Added
- Added CHECK-constraint support via new
Phalcon\Db\Checkvalue object andPhalcon\Db\CheckInterface.Checktakes a constraint name (string; empty string means an unnamed constraint, in which case the dialect omits theCONSTRAINT <name>prefix) and a definition array containing the requiredexpressionkey (the boolean SQL predicate).Phalcon\Db\Dialect\Mysql,Phalcon\Db\Dialect\Postgresql, andPhalcon\Db\Dialect\Sqliteall recognizedefinition["checks"](array ofCheckInterface) insidecreateTable()and emit an inline[CONSTRAINT "<name>"] CHECK (<expr>)line alongside the column/index/reference lines. New dialect methodsaddCheck()anddropCheck()emit the equivalentALTER TABLE ... ADD CONSTRAINT ... CHECK (...)andALTER TABLE ... DROP CHECK|CONSTRAINT ...SQL for MySQL 8.0.16+ and PostgreSQL; SQLite throws (CHECK constraints can only be declared at CREATE TABLE time in SQLite, the same limitation that already applies to FK constraints). New adapter methodsPhalcon\Db\Adapter\AbstractAdapter::addCheck()andPhalcon\Db\Adapter\AbstractAdapter::dropCheck()provide the symmetric one-call ergonomics already available foraddForeignKey()/dropForeignKey(). The new dialect and adapter methods are declared as commented-out@todo v7stubs onPhalcon\Db\DialectInterfaceandPhalcon\Db\Adapter\AdapterInterfaceto avoid breaking third-party implementors during the v5 line #14719 [doc] - Added MySQL 8.0+
INVISIBLEindex support toPhalcon\Db\Index. The constructor's second parameter is now backward-compatibly overloaded: passing a plain list of column names continues to work (legacy positional form), while passing an associative array containing acolumnskey activates the new definition-array form (["columns" => [...], "type" => "...", "invisible" => true]). The third positionaltypeargument is honored only when the second argument is the legacy list form; in definition-array modetypecomes from the array.Indexgains a matchingisInvisible(): boolaccessor and throws aPhalcon\Db\Exceptionif the definition-array path is taken butcolumnsis not itself an array.Phalcon\Db\Dialect\Mysql::addIndexandPhalcon\Db\Dialect\Mysql::createTableemit a trailingINVISIBLEkeyword for invisible indexes.Phalcon\Db\Adapter\Pdo\Mysql::describeIndexesreverse-engineers the flag from the MySQL 8.0+Visiblecolumn ofSHOW INDEXES(absent on 5.7, which defaults to visible). PostgreSQL and SQLite have no equivalent and their dialects ignore the flag. The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\Indexto avoid breaking third-party implementors during the v5 line. This definition-array hook is the path that will be reused by upcoming items #8 (descending indexes), #9 (partial indexes), and #10 (functional indexes) so the constructor stays tidy #14719 [doc] - Added MySQL 8.0.23+
INVISIBLEcolumn support toPhalcon\Db\Column. A new boolean definition-array keyinvisible(defaultfalse) is parsed by the constructor; a matchingisInvisible(): boolaccessor reports the state at runtime.Phalcon\Db\Dialect\Mysql::addColumn,Phalcon\Db\Dialect\Mysql::createTable, andPhalcon\Db\Dialect\Mysql::modifyColumnemitINVISIBLEafter the NOT NULL/NULL clause when the flag is set. PostgreSQL and SQLite have no equivalent and their dialects ignore the flag.Phalcon\Db\Adapter\Pdo\Mysql::describeColumnsreverse-engineers the flag from theEXTRAcolumn ofinformation_schema.COLUMNS(already in the result set since item #1's switch fromSHOW FULL COLUMNS) - substring-matched so the flag is still detected when MySQL concatenates it with other extras likeINVISIBLE STORED GENERATED. The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\Columnto avoid breaking third-party implementors during the v5 line #14719 [doc] - Added PostgreSQL
CREATE INDEX CONCURRENTLYsupport via a newconcurrentlydefinition-array key onPhalcon\Db\Index(defaultfalse).Phalcon\Db\Index::isConcurrent(): boolexposes the flag at runtime.Phalcon\Db\Dialect\Postgresql::addIndexnow emitsCONCURRENTLYbetween theINDEXkeyword and the index name when the flag is set (CREATE INDEX CONCURRENTLY "idx_email" ON "schema"."table" (...)), so the index can be built without taking the strong lock that normally blocks writers. MySQL and SQLite have no equivalent concept and their dialects ignore the flag. No reverse engineering is meaningful (the option is creation-time only - once built, the index is indistinguishable from one built non-concurrently). The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\Indexto avoid breaking third-party implementors during the v5 line #14719 [doc] - Added PostgreSQL materialized-view support to the Db dialect and adapter layers. Three new methods land on
Phalcon\Db\Dialect:createMaterializedView(string $viewName, array $definition, string $schemaName = null): string(definition takes a requiredsqlkey, same shape ascreateView),dropMaterializedView(string $viewName, string $schemaName = null, bool $ifExists = true): string, andrefreshMaterializedView(string $viewName, string $schemaName = null, bool $concurrent = false): string(passing$concurrent = trueemitsREFRESH MATERIALIZED VIEW CONCURRENTLY ...for non-blocking refresh - requires a unique index on the view).Phalcon\Db\Dialect\Postgresqloverrides all three to emit the correct SQL; the base implementations throwPhalcon\Db\Exception, which is inherited unchanged byPhalcon\Db\Dialect\MysqlandPhalcon\Db\Dialect\Sqlite(neither engine has a materialized-view concept).Phalcon\Db\Adapter\AbstractAdaptergains three matchingcreateMaterializedView()/dropMaterializedView()/refreshMaterializedView()wrappers that execute the dialect-built SQL and return bool. #14719 [doc] - Added PostgreSQL-specific column types and array-column support to
Phalcon\Db\ColumnandPhalcon\Db\Dialect\Postgresql. Ten newColumn::TYPE_*constants are introduced:TYPE_BYTEA(30),TYPE_INET(31),TYPE_CIDR(32),TYPE_MACADDR(33),TYPE_INT4RANGE(34),TYPE_INT8RANGE(35),TYPE_NUMRANGE(36),TYPE_TSRANGE(37),TYPE_TSTZRANGE(38),TYPE_DATERANGE(39).Phalcon\Db\Dialect\Postgresql::getColumnDefinitionrecognizes the new types and emits the matching keywords (BYTEA,INET,CIDR,MACADDR,INT4RANGE,INT8RANGE,NUMRANGE,TSRANGE,TSTZRANGE,DATERANGE). MySQL and SQLite dialects fall through their existingdefaultbranches for these constants - users targeting those engines should pick a portable base type instead. Additionally, a new boolean definition-array keyarrayand aPhalcon\Db\Column::isArray(): boolaccessor expose array-column intent; whenisArray()istrue, the PostgreSQL dialect appends[]to the type (INTEGER[],TEXT[],INET[], etc.). MySQL and SQLite ignore the flag.Phalcon\Db\Adapter\Pdo\Postgresql::describeColumnsreverse-engineers the new types by matching thedata_typecolumn frominformation_schema.columnsand setsarraywhendata_typereportsARRAYor contains[]. The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\Columnto avoid breaking third-party implementors during the v5 line #14719 [doc] - Added
FOR SHAREshared-lock emission toPhalcon\Db\Dialect\Postgresql::sharedLock(); it previously returned the original query unchanged (silent no-op), so callers had no way to express PostgreSQL's row-level shared lock through the cphalcon dialect API. The method now appends" FOR SHARE"and also accepts the optionalstring $modifier = ""second argument introduced by item #3, so callers can requestFOR SHARE NOWAIT/FOR SHARE SKIP LOCKEDvia thePhalcon\Contracts\Db\Dialect::LOCK_NOWAIT/LOCK_SKIP_LOCKEDconstants. The signature change is propagated toPhalcon\Contracts\Db\Dialect::sharedLock,Phalcon\Contracts\Db\Adapter\Adapter::sharedLock, and the SQLite and MySQL impls - SQLite remains a no-op regardless of the modifier (no row-level locking), and MySQL still emits its legacyLOCK IN SHARE MODEand silently ignores any modifier (the legacy syntax does not supportNOWAIT/SKIP LOCKED; users on MySQL 8.0+ who need those modifiers can useforUpdate()instead).Phalcon\Db\Adapter\AbstractAdapter::sharedLockpasses the modifier through to the dialect #14719 [doc] - Added
NOWAIT/SKIP LOCKEDrow-lock modifiers toforUpdate(). The dialect and adapterforUpdate()methods now accept an optional secondstring $modifier = ""argument; pass one of the new contract constantsPhalcon\Contracts\Db\Dialect::LOCK_NONE(default),Phalcon\Contracts\Db\Dialect::LOCK_NOWAIT, orPhalcon\Contracts\Db\Dialect::LOCK_SKIP_LOCKEDto emitSELECT … FOR UPDATE,SELECT … FOR UPDATE NOWAIT, orSELECT … FOR UPDATE SKIP LOCKEDrespectively. Recognized by MySQL 8.0+ and PostgreSQL 9.5+; SQLite has no row-level locking and silently ignores the modifier. Signature change is propagated toPhalcon\Contracts\Db\Dialect::forUpdate,Phalcon\Contracts\Db\Adapter\Adapter::forUpdate,Phalcon\Db\Dialect::forUpdate(base),Phalcon\Db\Dialect\Sqlite::forUpdate(override remains a no-op), andPhalcon\Db\Adapter\AbstractAdapter::forUpdate(pass-through). Existing single-argument call sites are unaffected - the second parameter defaults to""#14719 [doc] - Added
ON CONFLICT (...) DO UPDATE SET ...upsert support to the Db dialect and adapter layers. New SQL-transformer methodPhalcon\Db\Dialect::onConflictUpdate(string $sqlQuery, array $conflictColumns, array $updateColumns): stringappends an upsert clause to the supplied INSERT statement using the SQL-standard form recognized by PostgreSQL 9.5+ and SQLite 3.24+:INSERT INTO ... ON CONFLICT ("col") DO UPDATE SET "other" = excluded."other". The base implementation inPhalcon\Db\Dialect::onConflictUpdateprovides the standard emission (inherited byPhalcon\Db\Dialect\PostgresqlandPhalcon\Db\Dialect\Sqlite);Phalcon\Db\Dialect\Mysql::onConflictUpdateoverrides to throw because MySQL'sINSERT ... ON DUPLICATE KEY UPDATEshape is incompatible (deferred to parser item #23). ThrowsPhalcon\Db\Exceptionwhen either theconflictColumnsorupdateColumnsarray is empty.Phalcon\Db\Adapter\AbstractAdapter::onConflictUpdateprovides the symmetric one-call passthrough. The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\DialectandPhalcon\Contracts\Db\Adapter\Adapterto avoid breaking third-party implementors during the v5 line #14719 [doc] - Added
Phalcon\Contracts\Dbnamespace housing the canonical contracts for the Db layer:Phalcon\Contracts\Db\Check,Phalcon\Contracts\Db\Column,Phalcon\Contracts\Db\Dialect,Phalcon\Contracts\Db\Index,Phalcon\Contracts\Db\Reference,Phalcon\Contracts\Db\Result, andPhalcon\Contracts\Db\Adapter\Adapter. The legacy interfaces (Phalcon\Db\CheckInterface,Phalcon\Db\ColumnInterface,Phalcon\Db\DialectInterface,Phalcon\Db\IndexInterface,Phalcon\Db\ReferenceInterface,Phalcon\Db\ResultInterface,Phalcon\Db\Adapter\AdapterInterface) are kept as thin extensions of their contract counterparts, marked@deprecated(matching thePhalcon\Support\Collection\CollectionInterfacemigration pattern), so existing implementors and typehints continue to work in the v5 line. The@todo v7commented-out stubs for the generated-column API (item #1) and CHECK-constraint API (item #2) live in the canonical contract files, which is where they will be uncommented when v7 ships #14719 [doc] - Added
Phalcon\Contracts\Events\Stoppable- a Phalcon-owned mirror of PSR-14'sStoppableEventInterfacewith the singleisPropagationStopped(): boolmethod.Phalcon\Events\Eventimplements it and routes the call through the same internalstoppedflag asisStopped(), so existing$event->stop()callers automatically expose the PSR-14-shaped accessor without changing their code. Keeps PSR types out of the extension while letting a futurephalcon/events-psr14-bridgepackage map Phalcon ↔ PSR-14 in PHP land #17006 [doc] - Added
Phalcon\Contracts\Events\Subscriber- a subscriber contract requiring a single staticgetSubscribedEvents(): arraymethod. Supported shapes per entry:['event:name' => 'methodName'](plain string),['event:name' => ['methodName', priority]](method with priority), or['event:name' => [['methodA', priorityA], ['methodB', priorityB]]](multiple listeners per event). Replaces the magic-method-only routing as the primary discoverability mechanism while keeping that path intact for BC #17006 [doc] - Added
Phalcon\Contracts\Forms\Schemainterface defining a singleload(): arraymethod for objects that supply a normalized list of form-element definitions #16996 [doc] - Added
Phalcon\Contracts\Mvc\Model\Relation\CacheKeyProviderwith a singlegetUniqueKey(): stringmethod; when a model implements this interface and a relation is markedreusable => true, the Model Manager uses the return value ofgetUniqueKey()as the cache key instead of the object-identity-basedunique_key()builtin, allowing different PHP object instances that represent the same database row to share the same reusable record cache entry #16993 [doc] - Added
Phalcon\Di\Di::hasShared(string $name): boolandPhalcon\Di\Di::removeShared(string $name): void(also declared onPhalcon\Di\DiInterface) to operate on the shared-instance cache independently of the service-definition registry.hasShared()reports whethergetShared()has already materialized an instance for the given name (in contrast tohas(), which reports on the definition registry).removeShared()drops the cached instance - both fromDi::$sharedInstancesand from theServiceobject's internalsharedInstancefield - without removing the service definition, so the nextgetShared()call rebuilds a fresh instance. Use case: fork-based multi-process workers that need to discard the parent's inherited resource handle (DB connection, etc.) without re-registering the service. Both methods are alias-aware #13440 [doc] - Added
Phalcon\Encryption\Security::setAutoRefresh(bool)andPhalcon\Encryption\Security::refreshToken()to make CSRF-token rotation opt-out. WhensetAutoRefresh(false)is called,getToken()andgetTokenKey()reuse the existing session values instead of generating a new pair on every call, eliminating the per-request session write that backend session stores billed per-write (DynamoDB, Redis-with-billing, etc.) would otherwise incur on read-only requests.refreshToken()provides an explicit, atomic rotation of both the token and the token key (used after login or any other state change where rotation is appropriate). The defaultautoRefresh = truepreserves the existing one-time-use behavior; opt-in only #14413 [doc] - Added
Phalcon\Events\Manager::addSubscriber(),Phalcon\Events\Manager::removeSubscriber(),Phalcon\Events\Manager::getSubscribers(), andPhalcon\Events\Manager::clearSubscribers()to register and unregisterSubscriberinstances. The subscriber'sgetSubscribedEvents()map is parsed once at attach time and each entry is routed through the regular listener pipeline; the result is cached per class name in a new internalsubscriberEventsCacheso repeated add/remove of the same subscriber class doesn't re-invoke the static method.removeSubscriber()is idempotent;clearSubscribers()iterates a snapshot of registered subscribers so the underlying property can be mutated safely during the walk. Subscribers are keyed byspl_object_id()so the same instance cannot be double-registered #17006 [doc] - Added
Phalcon\Events\Manager::fireAll(string $eventType, object $source, mixed $data = null, bool $cancelable = true): arrayreturning every listener's return value as an indexed array. Independent ofcollectResponses(); the caller's collected state on$this->responsesis stashed on entry and restored on exit so afireAll()call from inside acollect-modefire()does not pollute the outer accumulator #17006 [doc] - Added
Phalcon\Events\Manager::halt(),Phalcon\Events\Manager::resume(), andPhalcon\Events\Manager::isHalted(): bool- a manager-level kill switch separate from$event->stop().stop()only stops the current dispatch chain;halt()survives acrossfire()calls and makes every subsequentfire()/fireAll()/fireQueue()call return immediately (nullor[]) without dispatching, untilresume()clears the flag. Use case: a security check or initialization-failure path that needs to abort all downstream event activity for the rest of the request #17006 [doc] - Added
Phalcon\Events\Manager::setStopOnFalse(bool)/Phalcon\Events\Manager::isStopOnFalse(): bool- an opt-in per-event short-circuit. WhensetStopOnFalse(true)has been called and the fire'scancelableflag is on, a listener returning literalfalsestops the dispatch loop for that event and pins thefire()return asfalse; later listeners cannot overwrite the cancel. Default off, preserving the pre-5.13 last-wins return-value semantics so existing codebases are unaffected. Independent ofhalt()and$event->stop()- only governs how the dispatch loop reacts to a literalfalselistener return #17006 [doc] - Added
Phalcon\Events\Manager::setStrict(bool)/Phalcon\Events\Manager::isStrict(): boolto enable strict mode. When strict mode is on,fire()andfireAll()throwPhalcon\Events\Exceptionon dispatch of an event with zero matching listeners - useful in development for catching typos in event names. Default off; opt-in only #17006 [doc] - Added
Phalcon\Forms\Element\CheckGroupandPhalcon\Forms\Element\RadioGroupform elements that render multiple<input type="checkbox">/<input type="radio">inputs from a single registered form-element entry.CheckGroupauto-suffixes the name with[]when not already present so PHP collects checked values into an array on submission; both elements bind cleanly because they live under a single key #16996 [doc] - Added
Phalcon\Forms\Form::load(Schema $schema, FormsLocator $locator)that consumes element definitions from anySchemasource and resolves theirtypestrings through the supplied locator, allowing custom element types viaFormsLocator::setElement()#16996 [doc] - Added
Phalcon\Forms\FormsLocator, a closure-based registry for named forms (get/has/set) and element-type factories (getElement/hasElement/setElement); entity-less form lookups are lazily cached, while entity-aware lookups always rebuild. The default element registry seeds factories forcheck,checkgroup,date,email,file,hidden,numeric,password,radio,radiogroup,select,submit,text, andtextarea#16996 [doc] - Added
Phalcon\Forms\Loader\ArrayLoader,Phalcon\Forms\Loader\JsonLoader, andPhalcon\Forms\Loader\YamlLoader- threeSchemaimplementations that feedForm::load()from a PHP array, a JSON string/file, or a YAML string/file (pecl/yaml) respectively #16996 [doc] - Added
Phalcon\Forms\Manager::loadForm(string $name, Schema $schema, ?object $entity = null)andPhalcon\Forms\Manager::getLocator();loadForm()builds a form from aSchema, registers it in the manager, and also registers a factory in the locator so subsequent entity-aware retrievals viaFormsLocator::get($name, $entity)rebuild fresh instances.Manager::__construct(FormsLocator $locator = null)accepts a nullable locator and instantiates a defaultFormsLocatorwhen omitted (BC: pre-existingnew Manager()calls continue to work) #16996 [doc] - Added
Phalcon\Html\Escaper\AbstractEscaper,Phalcon\Html\Escaper\AttributeEscaper,Phalcon\Html\Escaper\CssEscaper,Phalcon\Html\Escaper\HtmlEscaper,Phalcon\Html\Escaper\JsEscaper, andPhalcon\Html\Escaper\UrlEscaperas the per-context building blocks behindPhalcon\Html\Escaper#16971 [doc] - Added
Phalcon\Html\Helper\Input\AbstractGroup,Phalcon\Html\Helper\Input\CheckboxGroup, andPhalcon\Html\Helper\Input\RadioGroup; the base resolves option entries (scalar label or[label, ...attrs]map) into<input>+<label>pairs sharing a single HTMLname, with auto-generatedidper value ({name}_{value}) and per-item attribute pass-through.CheckboxGroupmatches against an array (or scalar coerced into one) forchecked;RadioGroupmatches against a single scalar. Registered inPhalcon\Html\TagFactoryasinputCheckboxGroupandinputRadioGroup#16996 [doc] - Added
Phalcon\Html\Helper\Input\GenericandPhalcon\Html\Helper\Input\AbstractChecked;Genericaccepts the HTML5typevia the constructor (withsetType()to change it after construction), lettingTagFactoryregister a single class for all type-string-only inputs and differentiate them through the recipe map #16971 [doc] - Added
Phalcon\Html\Helper\Input\Select::placeholder(string $text)to inject a<option value="" disabled selected>$text</option>first entry, andPhalcon\Html\Helper\Input\Select::strict(bool $flag = true)to opt the option/selectedcomparison from the new loose default into strict (===) matching #16971 [doc] - Added
Phalcon\Html\Helper\Script::beginInternal()andendInternal(array $attributes = [], int $pos = -1)to capture inline JavaScript via output buffering and append it to the asset stack as a<script>...</script>block #16971 [doc] - Added
Phalcon\Html\Helper\Tag(open tag) andPhalcon\Html\Helper\VoidTag(self-closing tag) as escape hatches for arbitrary tag names without a dedicated helper; available viaTagFactoryastagandvoidTag#16971 [doc] - Added
Phalcon\Mvc\Router::getMethodRoutes(): array- returns the internal HTTP-method index (routes bucketed by method string, unconstrained routes under"*").handle()now builds a candidate list from only the matching-method bucket plus the unconstrained bucket and iterates that subset in reverse, eliminating the O(n) per-route HTTP-method check that previously ran against every registered route on each request #17015 [doc] - Added
Phalcon\Mvc\Router::loadFromConfig(array|ConfigInterface $config): RouterInterface- initializes the router from a data structure, accepting the top-level keysdefaultRoutes,removeExtraSlashes,defaults,notFound,routes, andgroups. Eachroutesentry supportsmethod(one ofconnect,delete,get,head,options,patch,post,purge,put,trace; omitted = any method),pattern,paths,name, andhostname. Eachgroupsentry supportsprefix,hostname,paths, and a nestedroutesarray, then is mounted viamount()#15050 [doc] - Added
Phalcon\Mvc\Router\RouterFactorywithload(array|ConfigInterface $config): RouterInterfaceandnewInstance(bool $defaultRoutes = true): RouterInterface, mirroring the idiomaticConfigFactory/LoggerFactorypattern;load()honors the optional top-leveldefaultRouteskey (defaulting totrue) and delegates route assembly toRouter::loadFromConfig()#15050 [doc] - Added
Phalcon\Paginator\Adapter\QueryBuilderCursor- a keyset (cursor-based) pagination adapter that accepts aQueryBuilder, alimit, and acursorColumn(the column used as the cursor key, typically a primary key). Eachpaginate()call fetcheslimit + 1rows usingcursorColumn > :cursor:to skip already-seen rows; the extra row is used only to detect whether a next page exists and is never returned.getNext()returns the last visible row's cursor value, or0when no further page exists.setCursor(int|null $cursor)advances or resets the position.getTotalItems()andgetLast()return0by design - no COUNT query is issued. Registered inPaginatorFactoryas"queryBuilderCursor"#14754 [doc] - Added
Phalcon\Support\Collection:column(string $propertyOrMethod): array(lift a single property/method off every item, keyed by the original collection key) #17000 [doc] - Added
Phalcon\Support\Collection:each(callable $callback)(run the callback for side effects and return$thisfor chaining) #17000 [doc] - Added
Phalcon\Support\Collection:filter(callable $callback)(new collection of items where the callback returns truthy) #17000 [doc] - Added
Phalcon\Support\Collection:first()/last()(head/tail value ornullon empty) #17000 [doc] - Added
Phalcon\Support\Collection:isEmpty(): bool,getType(): string|null(the type guard configured at construction) #17000 [doc] - Added
Phalcon\Support\Collection:keys(bool $insensitive = true)andvalues()(the non-get-prefixed names, withgetKeys/getValuesretained as deprecated wrappers) #17000 [doc] - Added
Phalcon\Support\Collection:map(callable $callback)(new collection of transformed values, keys preserved) #17000 [doc] - Added
Phalcon\Support\Collection:reduce(callable $callback, mixed $initial = null): mixed(callback signature($accumulator, $value, $key)) #17000 [doc] - Added
Phalcon\Support\Collection:replace(array $data): void(clear theninitin one call) #17000 [doc] - Added
Phalcon\Support\Collection:sort(?callable $callback = null, int $order = SORT_ASC)(uasortwhen a callback is supplied, otherwiseasort/arsortkeyed by$order) #17000 [doc] - Added
Phalcon\Support\Collection:where(string $propertyOrMethod, mixed $value)(strict-equality filter viaextractValue) #17000 [doc] - Added
Phalcon\Time\Clock\Exceptionwith theinvalidModifier()named constructor;FrozenClock::adjust()throws this exception uniformly across PHP versions when the modifier string cannot be parsed (catchingDateMalformedStringExceptionon PHP 8.3+ and trapping theE_WARNINGplusfalsereturn on earlier versions, leaving the clock state untouched on failure) #16965 [doc] - Added
Phalcon\Time\Clocknamespace withClockInterface,SystemClock, andFrozenClockto wrap clock functionality for the application;SystemClockreturns the current time as aDateTimeImmutablein a configurable timezone (withfromUTC()andfromSystemTimezone()named constructors), whileFrozenClockreturns a fixed instant for deterministic testing and exposesset()andadjust()to move the clock in place (returning$thisfor fluent chaining) #16965 [doc] - Added
RETURNINGclause support to the Db dialect and adapter layers. New SQL-transformer methodPhalcon\Db\Dialect::returning(string $sqlQuery, array $columns): stringappends aRETURNINGclause to anINSERT/UPDATE/DELETEstatement; pass["*"]forRETURNING *or a list of column identifiers forRETURNING "col1", "col2".Phalcon\Db\Dialect\Postgresql::returningandPhalcon\Db\Dialect\Sqlite::returningprovide the emission (SQLite requires 3.35+). The base implementation inPhalcon\Db\Dialect::returningthrowsPhalcon\Db\Exception, which is inherited unchanged byPhalcon\Db\Dialect\Mysqlsince MySQL has no RETURNING construct. An emptycolumnsarray throws on PgSQL and SQLite.Phalcon\Db\Adapter\AbstractAdapter::returningprovides the symmetric one-call passthrough so users can do$connection->query($connection->returning($sql, ["id"])). The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\DialectandPhalcon\Contracts\Db\Adapter\Adapterto avoid breaking third-party implementors during the v5 line #14719 [doc] - Added
Rawfactory variantsaRaw,buttonRaw,elementRaw,labelRaw,olRaw, andulRawtoPhalcon\Html\TagFactory, each backed by a tuple recipe that pinsraw = trueon the constructor of the underlying helper #16971 [doc] - Added an opt-in
bool $replaceArgs = falsefifth parameter toPhalcon\Mvc\Url::get(); whentrueand the supplied$urialready contains a query string, the existing query is parsed viaparse_str()and merged underarray_merge($existing, (array) $args)so user-supplied keys override colliding ones (e.g.$url->get('http://example.com?page=1', ['page' => 5], null, null, true)now returnshttp://example.com?page=5instead ofhttp://example.com?page=1&page=5). The default (flag omitted) preserves the legacy append-with-&behavior to keep existing callers working #16986 [doc] - Added back the
generate_peclworkflow job andpackage.xml, restoring publication of aphalcon-pecl.tgzartifact (also attached to GitHub releases) for a few more versions to give downstream users time to migrate to PIE - Added functional/expression index support to
Phalcon\Db\Index. Entries inside thecolumnsarray can now bePhalcon\Db\RawValueinstances; string entries continue to be treated as column identifiers and escaped. The basePhalcon\Db\Dialect::getIndexColumnList()helper detectsRawValueentries and per-dialect renders them - MySQL and PostgreSQL wrap each expression in its own parentheses (KEY idx ((LOWER(col)))andCREATE INDEX ON t ((lower(col)))respectively), while SQLite emits the expression verbatim (its grammar acceptsexprdirectly as anindexed-column). The helper gains an optionalbool $wrapExpressions = trueflag - defaults are tuned per dialect at the call site so users do not need to think about it. Expressions compose with the descending direction (#8) and partial-index predicate (#9) without any additional API surface. Reverse-engineering of expressions is deferred (PostgreSQL viapg_get_expr(pg_index.indexprs, ...), SQLite viasqlite_master.sqlparsing - same conservative cutoff used in item #1). No new accessor method is needed -Index::getColumns()continues to return the entries (now of mixed string /RawValuetype) #14719 [doc] - Added generated/computed column support to
Phalcon\Db\Columnvia two new definition-array keys:generated(the SQL expression as a string;nullkeeps the column non-generated) andgenerationStored(bool,false→VIRTUAL,true→STORED; PostgreSQL ignores the flag and always emitsSTORED). Three new public methods report the state at runtime -getGenerationExpression(): string | null,isGenerated(): bool,isGenerationStored(): bool. The class enforces that a generated column cannot also declaredefaultorautoIncrement. All three dialects (Mysql,Postgresql,Sqlite) emitGENERATED ALWAYS AS (<expr>) VIRTUAL\|STOREDfromaddColumn(),createTable(), and (where supported)modifyColumn(), and skip theDEFAULT/AUTO_INCREMENT/AUTOINCREMENTclauses for generated columns. Reverse-engineering throughdescribeColumns()is also wired up: MySQL switches fromSHOW FULL COLUMNSto an equivalentinformation_schema.COLUMNSquery that additionally returnsGENERATION_EXPRESSION; PostgreSQL extends itsinformation_schema.columnsquery withis_generatedandgeneration_expression; SQLite switches fromPRAGMA table_infotoPRAGMA table_xinfoso thehiddenflag (2→ VIRTUAL,3→ STORED) can populateisGenerated()/isGenerationStored(). SQLite cannot expose the generation expression through any pragma, sogetGenerationExpression()round-trips as an empty string for SQLite-introspected generated columns (documented limitation). The new methods are declared as commented@todo v7stubs onPhalcon\Db\ColumnInterfaceto avoid breaking third-party implementors during the v5 line #14719 [doc] - Added hostname-aware URL generation to
Phalcon\Mvc\Url::get(): when a named route carries asetHostname()restriction the returned URL is now protocol-relative (//hostname/path) so it works transparently under both HTTP and HTTPS #17007 [doc] - Added new Contracts (aka Interfaces) for
Phalcon\Encryption\SecuritynamelyPhalcon\Contracts\Encryption\Security\Security,CryptoUtils,CsrfProtection, andPasswordSecurityand tied them to the component #16991 [doc] - Added partial-index support on
Phalcon\Db\Indexvia a newwheredefinition-array key (string).Phalcon\Db\Index::getWhere(): stringexposes the configured predicate (empty string when none).Phalcon\Db\Dialect\Postgresql::addIndexandPhalcon\Db\Dialect\Sqlite::addIndexappendWHERE <expr>to the emittedCREATE INDEXstatement. MySQL has no partial-index feature and its dialect ignores the value. Reverse-engineering of the predicate is deferred for both PostgreSQL (requirespg_get_expr(pg_index.indpred, pg_index.indrelid)) and SQLite (requiressqlite_master.sqlparsing) - same conservative cutoff used for SQLite generation expressions in item #1. ThrowsPhalcon\Db\Exceptionif the definition-arraywherekey is supplied with a non-string value. The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\Index#14719 [doc] - Added per-column sort direction (
ASC/DESC) support onPhalcon\Db\Indexvia a newdirectionsdefinition-array key. The array is parallel tocolumns; trailing positions absent fromdirectionsdefault toASCat emission time.Phalcon\Db\Index::getDirections(): arrayexposes the configured list (empty array means "no per-column direction declared" - preserves the legacy plain(col1, col2)rendering exactly). A new protectedPhalcon\Db\Dialect::getIndexColumnList(IndexInterface)helper centralizes the direction-aware emission and is now used byPhalcon\Db\Dialect\Mysql::addIndex/createTable,Phalcon\Db\Dialect\Postgresql::addIndex/createTable, andPhalcon\Db\Dialect\Sqlite::addIndex/createTable.Phalcon\Db\Adapter\Pdo\Mysql::describeIndexesreverse-engineers directions from theCollationcolumn ofSHOW INDEXES(A= ASC,D= DESC, NULL = ASC); the resultingIndexonly carries a non-emptydirectionsarray when at least one column is DESC, so existing introspection workflows that don't expect direction metadata see no diff. PostgreSQL and SQLite reverse-engineering of directions is deferred (pg_index.indoptionandsqlite_master.sqlparsing respectively - same conservative cutoff used for SQLite generation expressions in item #1). The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\Indexto avoid breaking third-party implementors during the v5 line #14719 [doc] - Added per-option HTML attribute support to the
Phalcon\Html\Helper\Input\Selectdata-provider path:Phalcon\Html\Helper\Input\Select\SelectDataInterfacenow also exposesgetAttributes()returning[optionValue => [attrName => stringValue, ...]];ArrayDataaccepts a second constructor argument with the resolved per-value attribute map, andResultsetDataaccepts a thirdattributesMapargument (htmlAttr => string|callable) whose closures receive the current row. Resolution happens once in the providers (withfalse/nullresults dropped); rendering continues to flow through the existingAbstractHelperattribute pipeline #16983 [doc] - Added precompiled ARM64 binaries for Linux (
ubuntu-22.04-armrunner) and macOS (macos-14, Apple Silicon) to the release artifacts. Thebuild_extensionCI matrix now producesphalcon-php<ver>-<ts>-ubuntu-gcc-arm64.zipandphalcon-php<ver>-<ts>-macos-clang-arm64.zipalongside the existing x64 builds, and the macOS composite action installspcre2/re2cvia Homebrew, pointsCPPFLAGS/LDFLAGSat the keg-onlypcre2headers, and enables the extension in PHP'sPHP_CONFIG_FILE_SCAN_DIRsophp --ri phalconsucceeds #16553 - Added spatial / geometry column-type support to
Phalcon\Db\Columnand the MySQL and PostgreSQL dialects. Eight newColumn::TYPE_*constants land:TYPE_GEOMETRY(40),TYPE_POINT(41),TYPE_LINESTRING(42),TYPE_POLYGON(43),TYPE_MULTIPOINT(44),TYPE_MULTILINESTRING(45),TYPE_MULTIPOLYGON(46),TYPE_GEOMETRYCOLLECTION(47).Phalcon\Db\Dialect\Mysql::getColumnDefinitionandPhalcon\Db\Dialect\Postgresql::getColumnDefinitionemit the matching keywords (MySQL recognizes them natively from 5.7; PostgreSQL needs PostGIS installed, which interprets the keywords). SQLite has no native spatial type and its dialect leaves these constants in thedefaultbranch.Phalcon\Db\Adapter\Pdo\Mysql::describeColumnsreverse-engineers the new types bystarts_with-matching the column type - order in the switch was chosen so the longer multi-* / geometrycollection variants are matched before their shorter prefixes (linestringbeforepolygon, etc.). PostgreSQL reverse-engineering for spatial types is deferred becauseinformation_schema.data_typedoes not consistently expose PostGIS type names without joiningpg_type- users introspecting PostGIS schemas should query metadata directly until then. Caveat - read-side WKB hydration is not part of this change. APOINTselected directly withSELECT location FROM itemsstill returns raw WKB bytes (cphalcon issues #14769 and #13670); the workaround is to projectST_AsText(location)/ST_AsBinary(location)/ST_AsGeoJSON(location)server-side. The DDL/describe support shipped here is the schema-level prerequisite for any future result-set hydration helper #14719 [doc] - Added support for arbitrary SQL expressions in DDL
DEFAULTclauses by recognizingPhalcon\Db\RawValueinstances passed asdefinition["default"]. Previously each dialect quoted any non-numeric, non-CURRENT_TIMESTAMP, non-NULLdefault as a string literal - preventing legitimate expression defaults like MySQL 8.0.13+DEFAULT (UUID()), PostgreSQLDEFAULT gen_random_uuid()/DEFAULT nextval('seq'), or SQLite 3.31+DEFAULT strftime('%s','now').Phalcon\Db\Dialect\Mysql::addColumn/createTable/modifyColumn,Phalcon\Db\Dialect\Postgresql::castDefault(used by all three of its DDL paths), andPhalcon\Db\Dialect\Sqlite::addColumn/createTablenow detectRawValuedefaults and emitDEFAULT <raw>verbatim. Plain-scalar andCURRENT_TIMESTAMP/NULLkeyword defaults continue to take the existing whitelist path unchanged.Column::hasDefault()already treats aRawValueas a non-null default, soisAutoIncrement()semantics and the generated-column "no default allowed" guard from item #1 remain correct #14719 [doc] - Added the canonical Events contracts under the
Phalcon\Contracts\Eventsnamespace:Phalcon\Contracts\Events\Event(replacesPhalcon\Events\EventInterface),Phalcon\Contracts\Events\EventsAware(replacesPhalcon\Events\EventsAwareInterface), andPhalcon\Contracts\Events\Manager(replacesPhalcon\Events\ManagerInterface). The legacyPhalcon\Events\*Interfacetypes are kept as thin extensions of their canonical counterparts, marked@deprecated, matching the migration pattern used forPhalcon\Support\Collection\CollectionInterface. Existing implementors and typehints continue to work in the v5 line #17006 [doc] - Enabled
dropColumn()on the SQLite dialect to emitALTER TABLE ... DROP COLUMN ...instead of throwing unconditionally - SQLite 3.35+ natively supportsALTER TABLE DROP COLUMN, and pre-empting it at the cphalcon dialect level prevented modern users from using the feature. On engines older than 3.35 the server itself rejects the statement at execution time, with a clearer "near 'DROP': syntax error" message than the previous cphalcon-side throw.Phalcon\Db\Adapter\Pdo\Sqlite::dropColumnalready passes through the dialect output viaAbstractAdapter, so users now get a one-call$connection->dropColumn(...)on SQLite #14719 [doc]
Fixed
- Fixed
.ci/release-notes.shfailing intermittently withgrep: write error: Broken pipeonceCHANGELOG-5.0.mdgrew past the pipe-buffer threshold; thegrep ... | head -n Npipelines (withset -o pipefailactive) gavegrepSIGPIPEwhenheadclosed the FD beforegrepfinished writing, aborting the step. Replaced both pipelines with single-passawkprograms that exit on the matching line, eliminating the broken-pipe race entirely while keeping the original "extract from the latest##heading down to one line above the next#heading" behavior - Fixed
Phalcon\Annotations\AnnotationsFactory::newInstance("memory")segfaulting the PHP process #16962 [doc] - Fixed
Phalcon\Cli\Console::handle()andPhalcon\Mvc\Application::handle()not propagating the configureddefaultModuleto the dispatcher when the router returned no module name; both methods resolved a localmoduleName(router value, falling back to$this->defaultModule) for the module-loading path but then called$dispatcher->setModuleName($router->getModuleName()), discarding the fallback. As a result, dispatcher events such asdispatch:beforeDispatchLoopsaw an empty module name even whensetDefaultModule()had been called. Both call sites now pass the resolvedmoduleNameto the dispatcher #17013 [doc] - Fixed
Phalcon\Db\Dialect::getSqlExpression()not propagating the outerbindCountsmap into the recursiveselect()call rendered for nested sub-SELECTs, so an array placeholder ({name:array}) inside a sub-SELECT kept the parse-timetimescount baked into the cached PHQL intermediate; a second execution of the same outer PHQL with the same bind name but a different bind-array size fell back to the stale count and PDO raised "Invalid parameter number: number of bound variables does not match number of tokens" (workaround wasPhalcon\Mvc\Model\Query::clean()between calls). Theselectbranch now seeds the nested definition'sbindCountsfrom the outerbindCountson a local copy of the nested intermediate so the runtime placeholder-count override fires at every nesting level without mutating the cached intermediate #17004 [doc] - Fixed
Phalcon\Events\Manager::fireQueue()segfaulting when invoking aClosurelistener whose declared parameter count is less than the three arguments the manager passes ($event, $source, $data). The previoushandler->__invoke(...)form routed through PHP's strict C-level method-call path which sets up a 3-slot frame that the closure's own 2-slot (or fewer) frame cannot accept, producing undefined behavior and a process crash. The dispatch path now uses Zephir's{handler}(args)callable-invocation syntax, which routes throughzend_call_functionand handles closure arity mismatch correctly #17006 [doc] - Fixed
Phalcon\Forms\Form::bind()leaving the entity property untouched when aCheckelement's key was absent from submitted data (the browser behavior for an unchecked checkbox), so a previously-truefield could never be reset by re-submitting the form.Phalcon\Forms\Element\Checkgains opt-insetUncheckedValue()/getUncheckedValue()/hasUncheckedValue(); when an unchecked value has been registered and the element's data key is missing from the bind payload,bind()injects the registered value into$databefore the main loop so it flows through whitelist, filters, and entity setters identically to a submitted value. Checks without an explicitsetUncheckedValue()preserve the previous behavior (entity untouched) #16982 [doc] - Fixed
Phalcon\Forms\Form::bind()silently dropping data forRadioelements registered under distinct form-element identifiers but sharing the same HTMLnameattribute (e.g.new Radio('r0', ['name' => 'banned']),new Radio('r1', ['name' => 'banned'])bound from['banned' => 'yes']); the bind loop only looked up$this->elements[$key]and skipped the value when no element matched, so the entity property was never set. The lookup now falls back to scanning registered elements for one whose HTMLnameattribute matches the data key #15957 [doc] - Fixed
Phalcon\Html\Helper\Input\AbstractInput::__invoke()always assigning anidattribute equal to the element name, producing invalid markup likeid="tags[]"when the name contained brackets (array names or indexed names likeroles[0]). The auto-idis now skipped when the name contains[, matching the behavior of the corresponding helper in the PHP-sidephalcon/phalconlibrary - Fixed
Phalcon\Mvc\Model::__set()not clearing the cached related record when abelongsTorelation alias is assignednull; calling a getter before setting the relation to null causedpreSaveRelatedRecords()to overwrite the FK back to its previous value on save #16611 [doc] - Fixed
Phalcon\Mvc\Model::assign()andPhalcon\Mvc\Model::writeAttribute()silently dropping values for columns whose name maps to a reserved internal setter (source,schema,dirtyState,connectionService,readConnectionService,writeConnectionService,eventsManager,transaction,snapshotData,oldSnapshotData);Phalcon\Mvc\Model::possibleSetter()returnedtruefor these reserved names while skipping both the setter call and the property assignment, so the value never landed on the model. It now returnsfalsefor reserved setters, letting the caller fall through to direct property assignment while still preventing the reserved internal setter from being hijacked by the column #16617 [doc] - Fixed
Phalcon\Mvc\Model::cloneResultMap()andPhalcon\Mvc\Model::possibleSetter()throwingTypeErrorduring hydration when a model setter has a strict type hint (e.g.?array) and the raw database value is incompatible; the ORM now catchesTypeErrorand falls back to direct property assignment #16956 [doc] - Fixed
Phalcon\Mvc\Model::doLowInsert()throwingUnable to insert into <table> without datawhen saving a model whose only column is an auto-increment primary key; on dialects whereuseExplicitIdValue()isfalse(MySQL, SQLite) the identity branch produced an emptyvaluesarray. The identity column is now added with the connection's default identity value when the resultingvaluesarray would otherwise be empty #156 [doc] - Fixed
Phalcon\Mvc\Model::find()return type regression introduced in 5.7.0 (PR #16578) that removed the: ResultsetInterfaceruntime declaration while adding@template Tgeneric docblocks for IDE autocompletion. Subclass overrides that narrowed the return type to: ResultsetInterfaceand call sites that assigned the result to a typed property (private ResultsetInterface $robots = Robots::find(...)) broke on upgrade from 5.6.x. The runtime return type is restored, and the docblock claim was tightened fromT[]|\Phalcon\Mvc\Model\Resultset<int, T>to\Phalcon\Mvc\Model\Resultset<int, T>so it is a strict subtype of the runtime declaration - Psalm previously raisedMismatchingDocblockReturnTypeon the generated ide-stubs becauseT[]resolves toarray<int, T>, which is not aResultsetInterface(theT[]half of the union was always false;find()never returns a plain PHP array, even underHYDRATE_ARRAYS). Modern IDEs and static analyzers continue to resolve element-typeTthrough the surviving generic annotation.findFirst()is unchanged - it has always returnedvar | nullbecause it can legitimately return aModelInterface, aRow, ornulldepending on the projection #16633 [doc] - Fixed
Phalcon\Mvc\Model::findFirst()docblock claim that diluted the generic return type. The annotation was@return T|\Phalcon\Mvc\ModelInterface|\Phalcon\Mvc\Model\Row|null(introduced by PR #16578), but since@template T of staticand everyModelsubclass implementsModelInterface, theModelInterfaceunion member is a supertype ofTand Psalm/PHPStan collapsed the union to its widest member - losingTand forcing user code that called child-class-specific methods onfindFirst()'s result to assertinstanceof selffirst. The redundantModelInterfacemember is removed; the annotation is now@return T|\Phalcon\Mvc\Model\Row|null, preservingTfor static analyzers while keepingRow(returned when the query projects scalar columns) andnull(no match) honest #16661 [doc] - Fixed
Phalcon\Mvc\Model::preSaveRelatedRecords()skippingdoSave()forbelongsToparents whosedirtyStatewasDIRTY_STATE_PERSISTENT; modifications made to an existing parent (loaded viafindFirst()) and then attached to a new child were silently dropped on save, while only the FK was written. The dirtyState short-circuit (originally added to break circular hasMany ⇄ belongsTo recursion) is removed and recursion is now prevented by thevisitedcollection already enforced insidedoSave()#16222 [doc] - Fixed
Phalcon\Mvc\Model\Query::executeUpdate()andPhalcon\Mvc\Model::doLowUpdate()for PHQLUPDATE ... SET <expr>expressions with placeholders (e.g.col = col + :inc:): named placeholders embedded in expression SQL are now resolved before creatingRawValueto avoid PDO "mixed named and positional parameters", and dynamic-update comparisons now always treatRawValueassignments as changed so updates are not skipped when the current numeric value is0#16976 [doc] - Fixed
Phalcon\Mvc\Model\Query::parse()returning a cached intermediate representation with the model's original schema/source baked into thetablesslot, so subsequentfind()/findFirst()/findBy*()calls for the same PHQL string kept emitting SQL pointing at the original schema after the model changed its schema at runtime (e.g. via a wrapper callingsetSchema()); the cache-hit branch now refreshes thetablesentries from the livemodel->getSchema()/getSource()before returning, preserving the parse-cache benefit while keeping the rendered FROM clause in sync with current model state #17020 [doc] - Fixed
Phalcon\Mvc\Router::handle()leaving unmatched optional named parameters (e.g.{id:(/[0-9]+)?}{slug:(/.+)?}) in the resulting params with their raw regex group positions (id => 1, slug => 2) instead of unsetting them; the unset branch was nested underelseoftypeof converters === "array", butRoute::$convertersdefaults to[]so the branch was unreachable. The check is now flattened so an unmatched positional parameter is removed when no per-part converter exists #16559 [doc] - Fixed
Phalcon\Mvc\View::getActiveRenderPath()returning only the first candidate path as a string when a singleviewsDirwas configured with multiple registered render engines and the view was not found; the method now collapses the internalactiveRenderPathsarray to a string only when it contains exactly one element, returning the full array of candidate paths in all other cases #16614 [doc] - Fixed
Phalcon\Mvc\View\Engine\Volt\Compilerrejecting single-quoted Volt string literals containing an escaped single quote (e.g.{{ 'Let\'s Encrypt' }}) with a downstream PHPT_STRINGparse error; the Volt scanner already preserves the user-written\'verbatim inexpr["value"], so the compiler'sstr_replace("'", "\\'", expr["value"])step double-escaped it intoLet\\'s Encrypt, producing PHP source'Let\\'s Encrypt'that PHP read asLet\followed by an unexpected identifier. Dropped the redundantstr_replace; the scanner's regex ((['] ([\\][']|[\\].|...) ['])) guaranteesexpr["value"]is already valid PHP single-quoted string content #17002 [doc] - Fixed
Phalcon\Support\Collection::get()returning the wrapping object's mangled property table when called with$cast = 'array'on a stored object (e.g. a nestedPhalcon\Config\Config, whichConfig::setData()wraps around every array value). The previous implementation calledsettype($value, 'array')unconditionally, which on an object exposes PHP's null-prefixed internal protected-property names ("\0*\0data","\0*\0lowerKeys", etc.) instead of the intended array form, so$config->get('outKey', [], 'array')['inKey']raisedNotice: Undefined index. The cast branch now special-cases'array'for objects exposing atoArray()method and delegates to that method; scalar casts (int,bool,float,string,null,object) and'array'on a plainstdClass(notoArray()) are unchanged #17005 [doc] - Fixed
Phalcon\Tag\Select::optionsFromArray()not escaping option label text, allowing XSS injection via malicious values; labels are now escaped withescapeHtml()and option values withescapeHtmlAttr()via the escaper service, consistent withoptionsFromResultset()#16660 [doc] - Fixed nested
Phalcon\Events\Manager::fire()calls clobbering the outer caller's collected$this->responsesstate. Previously the inner fire would reset$this->responses = []and append its own listener returns there, so after the nested fire returned the outer caller'sgetResponses()saw the inner fire's results instead of its own.fire()now tracks re-entrancy depth via a new internalfireDepthcounter and, on a nested call withcollect=true, stashes the outer's responses on entry and restores them on exit so the outer accumulator is preserved across the inner dispatch.fireAll()applies the same stash-and-restore pattern unconditionally so it never pollutes outer collected state regardless of the manager'scollectResponses()setting #17006 [doc]
Removed
- Removed calls to
version_comparethat led to pre PHP 8.0 code #16966 - Removed the per-type input helpers
Phalcon\Html\Helper\Input\Color,Date,DateTime,DateTimeLocal,Email,File,Hidden,Image,Input,Month,Numeric,Password,Range,Search,Submit,Tel,Text,Time,Url, andWeek; theirTagFactoryservice names (inputColor,inputDate, ...) now resolve toPhalcon\Html\Helper\Input\Genericwith the appropriatetypebaked into the recipe, so callers using the factory keep working unchanged #16971 [doc]