6.x - Allow using concrete properties for entity fields.#19313
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to let Cake\ORM\Entity use concrete (declared/typed) PHP properties as entity fields, while continuing to support dynamically-set fields.
Changes:
- Refactors
EntityTraitto split storage between declared properties and a newdynamicFieldsarray. - Updates
Entityconstruction to always go throughpatch()(removing the old “direct fields assignment” fast-path). - Adds a new entity fixture (
UserWithProps) and a large dedicated test suite for entities using concrete properties.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
src/Datasource/EntityTrait.php |
Introduces dynamicFields + propertyFields, adds property existence checks, and rewires patch/get/unset/errors/debug behavior to support concrete properties. |
src/ORM/Entity.php |
Removes the markClean && !useSetters early return so construction uses patch() consistently. |
tests/test_app/TestApp/Model/Entity/UserWithProps.php |
Adds a test entity class with declared typed properties. |
tests/TestCase/ORM/EntityWithConcretePropertiesTest.php |
Adds extensive test coverage targeting concrete-property entities (largely mirroring EntityTest). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }; | ||
| $entity->things = ['a', 'b']; | ||
| $entity->things[] = 'c'; | ||
| $this->assertEquals(['a', 'b'], $entity->things); |
There was a problem hiding this comment.
This is a caveat of using properties through magic __get. Indirect modification of arrays won't work.
There was a problem hiding this comment.
Returning direct reference to the property can work but it would fail if the field has our method based getter or PHP's get property hook.
There was a problem hiding this comment.
Isn't this long standing behavior as well? I remember it always working this way because of the limitations you listed.
There was a problem hiding this comment.
Oh, guess I never tried such indirect modification for arrays hence didn't remember. So it's not a regression and now we have test case for it :)
| 'dynamicFields', | ||
| 'propertyFields', | ||
| 'original', | ||
| 'originalFields', | ||
| 'hidden', | ||
| 'virtual', | ||
| 'dirty', | ||
| 'new', | ||
| 'errors', | ||
| 'invalid', | ||
| 'patchable', | ||
| 'registryAlias', | ||
| 'hasBeenVisited', | ||
| 'requireFieldPresence', | ||
| 'restrictedProperties', |
There was a problem hiding this comment.
Is there a way we could automate this list matching all of the properties defined on the trait? I could see us making a mistake here in the future.
There was a problem hiding this comment.
That would need some type of reflection which I have avoided in general when implementing this feature. I can instead add a test case which would report a failure if this list goes out of sync with the trait properties.
There was a problem hiding this comment.
Added EntityWithConcretePropertiesTest::testRestrictedPropertiesMatchesTraitProperties().
I had actually missed adding accessors to the list which was caught by the test :)
There was a problem hiding this comment.
I had actually missed adding accessors to the list which was caught by the test :)
Huzzah! Even the 🤖 s missed this.
| }; | ||
| $entity->things = ['a', 'b']; | ||
| $entity->things[] = 'c'; | ||
| $this->assertEquals(['a', 'b'], $entity->things); |
There was a problem hiding this comment.
Isn't this long standing behavior as well? I remember it always working this way because of the limitations you listed.
|
The stan failure is because rector wants nullable properties to be initialized as |
Update tests
It ensures EntityTrait::$restrictProperties stays in sync with the trait's properties
EntityInterface::has() treats properties set to null different than uninitialized properties
This allows using isset() instead of in_array()
- Convert $originalFields to keyed map for O(1) isset() lookups - Simplify setOriginalField() using array_combine and += merge - Swap branches in setHidden/setVirtual/setDirty for common path first - Use truthy checks instead of === true/false comparisons - Hoist $asOriginal in patch() before loop - Pass $this->propertyFields directly in clean() - Improve patch() phpdoc param type
Refs #18201