Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG-5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@

### Added

- Added `beforeBind` and `afterBind` hook methods to `Phalcon\Forms\Form`. When defined on a form subclass, `beforeBind(array $data, ?object $entity)` runs at the start of `bind()` (returning `false` cancels the bind) and `afterBind(?object $entity)` runs after the data has been assigned. Both also fire when `bind()` is reached through `isValid()`. [#14598](https://github.com/phalcon/cphalcon/issues/14598) [[doc]](https://docs.phalcon.io/5.14/forms/)
- Added a `sync` option to many-to-many (`hasManyToMany`) relations and a chainable `Phalcon\Mvc\Model::setSync()` method to synchronize related records on save. When enabled, saving deletes the intermediate rows for records no longer present in the assigned array (add/update/delete), instead of only appending. [#17071](https://github.com/phalcon/cphalcon/issues/17071) [[doc]](https://docs.phalcon.io/5.14/db-models-relationships/)
- Added a `trace()` method to `Phalcon\Logger\Logger` together with a new `TRACE` log level (value `9`, label `trace`). [#17047](https://github.com/phalcon/cphalcon/issues/17047) [[doc]](https://docs.phalcon.io/5.14/logger/)

### Fixed

- Fixed PHQL parser cache to use string-keyed lookups (`zend_hash_str_find`/`zend_hash_str_update`) instead of integer keys derived from `zend_inline_hash_func`, eliminating hash collisions that caused different PHQL queries to return identical cached ASTs [#14791](https://github.com/phalcon/cphalcon/issues/14791)
- Fixed `Phalcon\Annotations\Reader` failing to parse a docblock when an annotation argument is a string literal containing a parenthesis (e.g. `@SomeAnnotation(key='value(')`). The docblock pre-scan that locates each `@Annotation(...)` span counted every `(`/`)`, including those inside quoted values, so an unbalanced parenthesis in a string consumed the rest of the comment and produced a "Scanning error". [#16084](https://github.com/phalcon/cphalcon/issues/16084)
- Fixed `Phalcon\Di\Injectable::__get()` to no longer cache resolved services as dynamic object properties. Services accessed via magic properties (e.g. `$this->request`) are now re-resolved through the container on each access, so replacing or updating a service in the container is reflected in controllers, views, and other injectable classes. Properties already declared on the class continue to be populated. [#17052](https://github.com/phalcon/cphalcon/issues/17052)
- Fixed `Phalcon\Mvc\Model\Query\Builder::orderBy()` when the array syntax is used with complex PHQL expressions. Previously any array item containing a space was split as a simple `column direction` pair, corrupting expressions such as `CASE WHEN inv_status_flag = 1 THEN 0 ELSE 1 END ASC`. The builder now only treats a trailing `ASC`/`DESC` as the direction (autoescaping a simple column) and preserves complex expressions verbatim. [#17077](https://github.com/phalcon/cphalcon/issues/17077)
- Fixed `Phalcon\Mvc\Model\Query` (PHQL) parsing of identifiers whose name begins with the `NOT` keyword. Columns, tables, and aliases such as `notice_id` were truncated to `ice_id` (the leading `not` was dropped), causing the database to report the column as unknown - most visibly in `Phalcon\Mvc\Model\Query\Builder` join conditions built via `createBuilder()`. The scanner's re2c backtracking marker shared the token-start pointer, so the `NOT BETWEEN` rule advanced it past `not`; escaped identifiers containing internal escapes (e.g. `[col\[0\]]`) were corrupted by the same root cause. [#16831](https://github.com/phalcon/cphalcon/issues/16831)
- Fixed PHQL parser cache to use string-keyed lookups (`zend_hash_str_find`/`zend_hash_str_update`) instead of integer keys derived from `zend_inline_hash_func`, eliminating hash collisions that caused different PHQL queries to return identical cached ASTs [#14791](https://github.com/phalcon/cphalcon/issues/14791)
- Fixed `Phalcon\Annotations\Reader` failing to parse a docblock when an annotation argument is a string literal containing a parenthesis (e.g. `@SomeAnnotation(key='value(')`). The docblock pre-scan that locates each `@Annotation(...)` span counted every `(`/`)`, including those inside quoted values, so an unbalanced parenthesis in a string consumed the rest of the comment and produced a "Scanning error". [#16084](https://github.com/phalcon/cphalcon/issues/16084)
- Fixed the compilation failure (`'name_zv' undeclared`) in `Phalcon\Container\Container::callableGet()` and `callableNew()`. Both closures captured the typed `string name` parameter directly via `use (name)`. [#17078](https://github.com/phalcon/cphalcon/issues/17078)

### Removed
Expand Down
16 changes: 16 additions & 0 deletions phalcon/Forms/Form.zep
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ class Form extends Injectable implements Countable, Iterator, AttributesInterfac
throw new NoFormElements();
}

/**
* Check if there is a method 'beforeBind'
*/
if method_exists(this, "beforeBind") {
if this->{"beforeBind"}(data, entity) === false {
return this;
}
}

if empty whitelist {
let whitelist = this->whitelist;
}
Expand Down Expand Up @@ -306,6 +315,13 @@ class Form extends Injectable implements Countable, Iterator, AttributesInterfac
let this->data = assignData;
let this->filteredData = filteredData;

/**
* Check if there is a method 'afterBind'
*/
if method_exists(this, "afterBind") {
this->{"afterBind"}(entity);
}

return this;
}

Expand Down
26 changes: 26 additions & 0 deletions tests/unit/Forms/Fake/FakeFormAfterBind.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/**
* This file is part of the Phalcon Framework.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Tests\Unit\Forms\Fake;

use Phalcon\Forms\Form;

class FakeFormAfterBind extends Form
{
public bool $afterBindCalled = false;

public function afterBind(?object $entity): void
{
$this->afterBindCalled = true;
}
}
24 changes: 24 additions & 0 deletions tests/unit/Forms/Fake/FakeFormBeforeBind.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/**
* This file is part of the Phalcon Framework.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Tests\Unit\Forms\Fake;

use Phalcon\Forms\Form;

class FakeFormBeforeBind extends Form
{
public function beforeBind(array $data, ?object $entity): bool
{
return false;
}
}
39 changes: 39 additions & 0 deletions tests/unit/Forms/Form/BindTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use Phalcon\Forms\Exception;
use Phalcon\Forms\Form;
use Phalcon\Tests\AbstractUnitTestCase;
use Phalcon\Tests\Unit\Forms\Fake\FakeFormAfterBind;
use Phalcon\Tests\Unit\Forms\Fake\FakeFormBeforeBind;
use stdClass;

/**
Expand Down Expand Up @@ -65,6 +67,43 @@ public function testFormsFormBind(): void
$this->assertEquals("test1", $form->getValue("test1"));
}

/**
* Tests that the afterBind() hook is invoked after bind() assigns data.
*
* @issue https://github.com/phalcon/cphalcon/issues/14598
* @author Phalcon Team <team@phalcon.io>
* @since 2026-06-05
*/
public function testFormsFormBindAfterBindCalled(): void
{
$form = new FakeFormAfterBind();
$form->add(new Text('name'));

$form->bind(['name' => 'test'], new stdClass());

$this->assertTrue($form->afterBindCalled);
}

/**
* Tests that returning false from beforeBind() cancels the bind so the
* entity is left untouched.
*
* @issue https://github.com/phalcon/cphalcon/issues/14598
* @author Phalcon Team <team@phalcon.io>
* @since 2026-06-05
*/
public function testFormsFormBindBeforeBindReturnsFalseAbortsBind(): void
{
$form = new FakeFormBeforeBind();
$form->add(new Text('name'));

$entity = new stdClass();
$entity->name = 'original';
$form->bind(['name' => 'changed'], $entity);

$this->assertSame('original', $entity->name);
}

/**
* Issue #15957 - Radio elements registered under distinct form-element
* identifiers but sharing the same HTML "name" attribute must bind to
Expand Down
Loading