Skip to content

Commit 0cde937

Browse files
authored
Merge pull request #16801 from raicabogdan/5.0.x-validation-entity-binding
Entity binding of validation data when using the standalone Validation class.
2 parents 988cc57 + de0316d commit 0cde937

4 files changed

Lines changed: 211 additions & 26 deletions

File tree

CHANGELOG-5.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
### Changed
55

6+
- Changed `bind()` and `validate()` method in `Phalcon\Filter\Validation` and `Phalcon\Filter\Validation\ValidationInterface` to accept `$whitelist` array of only allowed fields to be mutated when using entity [#16800](https://github.com/phalcon/cphalcon/issues/16800)
7+
68
### Added
79

810
- Added `fails()` method helper to `Phalcon\Filter\Validation` useful for standalone validation [#16798](https://github.com/phalcon/cphalcon/issues/16798)
@@ -12,6 +14,7 @@
1214
- Fixed `Phalcon\Config\Adapter\Yaml` constructor to handle `null` return values from `yaml_parse_file()`, ensuring empty configuration files are treated as empty arrays instead of throwing errors.
1315
- Fixed `Phalcon\Http\Request` method `getClientAddress(true)` to return correct IP address from trusted forwarded proxy. [#16777](https://github.com/phalcon/cphalcon/issues/16777)
1416
- Fixed `Phalcon\Http\Request` method `getPost()` to correctly return json data as well and unified both `getPut()` and `getPatch()` to go through the same parsing method. [#16792](https://github.com/phalcon/cphalcon/issues/16792)
17+
- Fixed `Phalcon\Filter\Validation` method `bind()` and `validate()` to correctly bind data when using entity as well as skip binding of fields not included in `$whitelist` [#16800](https://github.com/phalcon/cphalcon/issues/16800)
1518

1619
### Removed
1720

phalcon/Filter/Validation.zep

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class Validation extends Injectable implements ValidationInterface
4646
*/
4747
protected filters = [];
4848

49+
/**
50+
* @var array
51+
*/
52+
protected whitelist = [];
53+
4954
/**
5055
* @var array
5156
*/
@@ -155,18 +160,74 @@ class Validation extends Injectable implements ValidationInterface
155160
* Assigns the data to an entity
156161
* The entity is used to obtain the validation values
157162
*
158-
* @param object entity
159-
* @param array|object data
163+
* ```php
164+
* $entity = new Author();
165+
* $fields = ['name', 'email', 'imageUrl'];
166+
* $validation = new AuthorValidation();
167+
* $validation->bind($entity, $_POST, $fields);
168+
* $validation->validate();
169+
* ```
170+
*
171+
* @param object $entity the entity object to assign data to
172+
* @param array|object $data the data that needs to be validated
173+
* @param array $whitelist only allow these fields to be mutated when entity is used
160174
*/
161-
public function bind(entity, data) -> <ValidationInterface>
175+
public function bind(var entity, var data, array whitelist = []) -> <ValidationInterface>
162176
{
177+
var container, field, value, fieldFilters, filterService, filters, method;
178+
179+
let this->data = data;
163180
this->setEntity(entity);
164181

165182
if unlikely (typeof data != "array" && typeof data != "object") {
166-
throw new Exception("Data to validate must be an array or object");
183+
return this;
167184
}
168185

169-
let this->data = data;
186+
let container = this->getDI();
187+
if container === null {
188+
let container = Di::getDefault();
189+
190+
if container === null {
191+
throw new Exception(
192+
"A dependency injection container is required to access the 'filter' service"
193+
);
194+
}
195+
}
196+
let filterService = <FilterInterface> container->getShared("filter");
197+
if unlikely typeof filterService != "object" {
198+
throw new Exception("Returned 'filter' service is invalid");
199+
}
200+
201+
if empty whitelist {
202+
let whitelist = this->whitelist;
203+
}
204+
205+
let filters = this->filters;
206+
207+
for field, value in data {
208+
/**
209+
* Check if the field is in the whitelist
210+
*/
211+
if !empty whitelist && !in_array(field, whitelist) {
212+
continue;
213+
}
214+
215+
if fetch fieldFilters, filters[field] {
216+
let value = filterService->sanitize(value, fieldFilters);
217+
}
218+
/**
219+
* Set value in entity
220+
*/
221+
let method = "set" . camelize(field);
222+
223+
if method_exists(this->entity, method) {
224+
entity->{method}(value);
225+
} elseif method_exists(this->entity, "writeAttribute") {
226+
entity->writeAttribute(field, value);
227+
} elseif property_exists(this->entity, field) {
228+
let entity->{field} = value;
229+
}
230+
}
170231

171232
return this;
172233
}
@@ -476,15 +537,32 @@ class Validation extends Injectable implements ValidationInterface
476537
/**
477538
* Validate a set of data according to a set of rules
478539
*
479-
* @param array|object data
480-
* @param object entity
540+
* You can use $validation->bind(entity, data, whitelist)->validate()
541+
* When you use bind(), the this->data is already set, so you can reuse it here
542+
*
543+
* ```php
544+
* // using bind() with $whitelist fields
545+
* $entity = new Author();
546+
* $fields = ['name', 'email', 'imageUrl'];
547+
* $validation = new AuthorValidation();
548+
* $validation->bind($entity, $_POST, $fields);
549+
* $validation->validate();
550+
*
551+
* // directly using validate
552+
* $validation = new AuthorValidation();
553+
* $validation->validate($_POST, $entity, $fields);
554+
* ```
555+
*
556+
* @param array|object $data the data that needs to be validated
557+
* @param object $entity the entity object to assign data to
558+
* @param array $whitelist only allow these fields to be mutated when entity is used
481559
*
482560
* @return Messages|false
483561
*/
484-
public function validate(var data = null, var entity = null) -> <Messages> | bool
562+
public function validate(var data = null, var entity = null, array whitelist = []) -> <Messages> | bool
485563
{
486564
var combinedFieldsValidators, field, scope, status, validator,
487-
validatorData, validators;
565+
validatorData, validators, inputData = null;
488566

489567
let validatorData = this->validators,
490568
combinedFieldsValidators = this->combinedFieldsValidators;
@@ -502,30 +580,34 @@ class Validation extends Injectable implements ValidationInterface
502580
* Implicitly creates a Phalcon\Messages\Messages object
503581
*/
504582
let this->messages = new Messages();
583+
if (data !== null) {
584+
// if data is provided
585+
if unlikely typeof data != "array" && typeof data != "object" {
586+
throw new Exception("Invalid data to validate");
587+
}
588+
let this->data = data;
589+
let inputData = data;
590+
} elseif !empty this->data {
591+
// else, if data === null, but we have this->data from bind(), reuse this->data
592+
let inputData = this->data;
593+
}
505594

506595
if entity !== null {
507-
this->setEntity(entity);
596+
// if user provided entity, bind and assign the data to the entity
597+
this->bind(entity, inputData, whitelist);
508598
}
509599

510600
/**
511601
* Validation classes can implement the 'beforeValidation' callback
512602
*/
513603
if method_exists(this, "beforeValidation") {
514-
let status = this->{"beforeValidation"}(data, entity, this->messages);
604+
let status = this->{"beforeValidation"}(inputData, this->entity, this->messages);
515605

516606
if status === false {
517607
return status;
518608
}
519609
}
520610

521-
if data !== null {
522-
if unlikely (typeof data != "array" && typeof data != "object") {
523-
throw new Exception("Invalid data to validate");
524-
}
525-
526-
let this->data = data;
527-
}
528-
529611
for field, validators in validatorData {
530612
for validator in validators {
531613
if unlikely typeof validator != "object" {
@@ -585,7 +667,7 @@ class Validation extends Injectable implements ValidationInterface
585667
* Get the messages generated by the validators
586668
*/
587669
if method_exists(this, "afterValidation") {
588-
this->{"afterValidation"}(data, entity, this->messages);
670+
this->{"afterValidation"}(inputData, this->entity, this->messages);
589671
}
590672

591673
return this->messages;

phalcon/Filter/Validation/ValidationInterface.zep

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ interface ValidationInterface
3636
* Assigns the data to an entity
3737
* The entity is used to obtain the validation values
3838
*
39-
* @param object entity
40-
* @param array|object data
39+
* @param object $entity
40+
* @param array|object $data
41+
* @param array $whitelist
4142
*/
42-
public function bind(entity, data) -> <ValidationInterface>;
43+
public function bind(var entity, var data, array whitelist = []) -> <ValidationInterface>;
4344

4445
/**
4546
* Returns the bound entity
@@ -101,10 +102,11 @@ interface ValidationInterface
101102
/**
102103
* Validate a set of data according to a set of rules
103104
*
104-
* @param array|object data
105-
* @param object entity
105+
* @param array|object $data
106+
* @param object $entity
107+
* @param array $whitelist
106108
*
107109
* @return Messages|false
108110
*/
109-
public function validate(var data = null, var entity = null) -> <Messages> | bool;
111+
public function validate(var data = null, var entity = null, array whitelist = []) -> <Messages> | bool;
110112
}

tests/integration/Filter/Validation/GetEntityCest.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,102 @@ public function filterValidationGetEntity(IntegrationTester $I): void
4040
$validation->getEntity()
4141
);
4242
}
43+
44+
/**
45+
* Tests Phalcon\Filter\Validation :: getEntity() - with filters
46+
*
47+
* @author Phalcon Team <team@phalcon.io>
48+
* @since 2025-08-12
49+
*/
50+
public function filterValidationGetEntityWithFilters(IntegrationTester $I): void
51+
{
52+
$I->wantToTest('Validation - getEntity() - with filters');
53+
54+
$user = new stdClass();
55+
$user->name = '';
56+
57+
$validation = new Validation();
58+
$validation->setFilters('name', ['trim', 'striptags']);
59+
$validation->validate(['name' => ' John <script>Chris</script>'], $user);
60+
61+
$I->assertSame(
62+
'John Chris',
63+
$validation->getEntity()->name
64+
);
65+
66+
$I->assertSame(
67+
'John Chris',
68+
$validation->getValue('name')
69+
);
70+
}
71+
72+
/**
73+
* Tests Phalcon\Filter\Validation :: getEntity() - using bind() with whitelist fields
74+
*
75+
* @author Phalcon Team <team@phalcon.io>
76+
* @since 2025-08-12
77+
*/
78+
public function filterValidationGetEntityUsingBindWithWhitelistFields(IntegrationTester $I): void
79+
{
80+
$I->wantToTest('Validation - getEntity() - using bind() with whitelist fields');
81+
82+
$user = new stdClass();
83+
$user->name = '';
84+
$user->email = '';
85+
$user->password = '';
86+
87+
$postData = [
88+
'name' => 'John Doe',
89+
'email' => 'name@example.com',
90+
'password' => 'new_password'
91+
];
92+
93+
$validation = new Validation();
94+
$validation
95+
->bind($user, $postData, ['name', 'password'])
96+
->validate();
97+
98+
$I->assertSame('John Doe', $validation->getEntity()->name);
99+
$I->assertSame('John Doe', $validation->getValue('name'));
100+
101+
$I->assertSame('', $validation->getEntity()->email);
102+
$I->assertSame('', $validation->getValue('email'));
103+
104+
$I->assertSame('new_password', $validation->getEntity()->password);
105+
$I->assertSame('new_password', $validation->getValue('password'));
106+
}
107+
108+
/**
109+
* Tests Phalcon\Filter\Validation :: getEntity() - using validate() with whitelist fields
110+
*
111+
* @author Phalcon Team <team@phalcon.io>
112+
* @since 2025-08-12
113+
*/
114+
public function filterValidationGetEntityUsingValidateWithWhitelistFields(IntegrationTester $I): void
115+
{
116+
$I->wantToTest('Validation - getEntity() - using validate() with whitelist fields');
117+
118+
$user = new stdClass();
119+
$user->name = '';
120+
$user->email = '';
121+
$user->password = '';
122+
123+
$postData = [
124+
'name' => 'John Doe',
125+
'email' => 'name@example.com',
126+
'password' => 'new_password'
127+
];
128+
129+
$validation = new Validation();
130+
$validation->validate($postData, $user, ['name', 'password']);
131+
132+
$I->assertSame('John Doe', $validation->getEntity()->name);
133+
$I->assertSame('John Doe', $validation->getValue('name'));
134+
135+
$I->assertSame('', $validation->getEntity()->email);
136+
$I->assertSame('', $validation->getValue('email'));
137+
138+
$I->assertSame('new_password', $validation->getEntity()->password);
139+
$I->assertSame('new_password', $validation->getValue('password'));
140+
}
43141
}

0 commit comments

Comments
 (0)