Repository navigation
Conversation
`Database\Model::__isset()` resolved attributes and relations only, while
`__get()` goes through `extendableGet()` (dynamic properties, then behavior
properties, then the parent getter). `Halcyon\Model::__isset()` had the same
asymmetry.
Because both classes define `__isset()`, PHP routes `??`, `empty()` and Twig's
`is defined` / `default()` through it, so a property that reads back fine
reported as not set - eg `$model->extensionProperty ?? $default` always
returned the default, and `{{ model.extensionProperty }}` rendered empty
(Twig resolves object attributes through `isset()` as well).
Add `ExtendableTrait::extendableIsset()`, which inspects the extension data
directly instead of routing through `extendableGet()`, so attributes are not
resolved twice and relations are not lazy loaded during what looks like a
cheap existence check. It returns null when the property is not provided by
the extension system at all.
Null handling mirrors each reader, so `isset()` keeps agreeing with `__get()`:
- `extendableGet()` skips null dynamic properties, so `extendableIsset()` does
too and the caller falls back to attributes/relations.
- A behavior property is resolved by `propertyExists()`, so a null value there
reports "not set" (Eloquent's `isset()` semantics).
- `Halcyon\Model::__get()` uses `extendableGet($key) ?? getAttribute($key)`,
so `Halcyon\Model::__isset()` only short-circuits on a positive result.
Behavior awareness is a deliberate semantic change: `isset()` on a model now
returns true for a non-null public property declared on a behavior, where it
previously returned false (eg `Winter.Search`'s `searchable`). See the
compatibility notes in wintercms/winter#1536; this targets the next minor.
Out of scope: `__unset()` and `offsetExists()` remain attribute/relation only,
so `isset($model['extensionProperty'])` is unchanged.
Refs wintercms/winter#1536
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes
isset(),empty(),??and Twig property lookups on models see the same properties__get()sees.Database\Model::__get()resolves throughextendableGet()— dynamic properties, then behavior properties, then the parent getter — butDatabase\Model::__isset()only calledgetAttribute().Halcyon\Model::__isset()had the same asymmetry. Because both classes define__isset(), PHP routes??,empty()and Twig'sis defined/default()/ property resolution through it, so a property that reads back fine was reported as not set.Reproduction (before)
Plain
Extendableobjects are unaffected in the "wrong" direction because they define no__isset(), so??falls through to__get(); only models added the__isset()that shadows it.Changes
ExtendableTrait::extendableIsset($name)— new helper that inspectsextensionDatadirectly (dynamic properties first, then behavior properties) and returnsnullwhen the property is not extension-provided. It deliberately does not route throughextendableGet(), so attributes are not resolved twice and relations are not lazy loaded during what looks like a cheap existence check.Database\Model::__isset()— consults the helper, falls back to the existinggetAttribute()logic.Halcyon\Model::__isset()— same, but only short-circuits on a positive result.Null handling mirrors each reader, so
isset()keeps agreeing with__get():truenullextendableGet()skips it (same-named attribute/relation still resolves)truenullfalse(Eloquent'sisset()semantics)nullextendableGet($key) ?? getAttribute($key)falseCompatibility notes
This is a semantic change and should land in a minor, per the notes in wintercms/winter#1536:
isset()on a model now returnstruefor a non-null public property declared on a behavior where it previously returnedfalse(e.g.Winter.Search'ssearchable). Code branching onisset($model->prop)can take a different path.$model->extensionProperty ?? $defaultnow yields the property value instead of always the default. Plugins that worked around this (e.g. LukeTowers.EasyAudit) keep working, but anything depending on the old fallback will change.{{ model.extensionProperty }}and{% if model.extensionProperty is defined %}now resolve extension properties instead of rendering empty / false.isset($model->extensionProperty)no longer runshasRelation()(and its reflection scan) on the way togetAttribute().Out of scope, unchanged on purpose:
__unset()andoffsetExists(), soisset($model['extensionProperty'])still reportsfalse.Core has no extension-provided properties today (
addDynamicProperty()isn't called outside tests, and the core behaviors that declare public properties are allControllerBehaviors attached to controllers, which useExtendablerather thanModel), so nothing in Winter itself flips.Tests
New coverage:
tests/Database/ModelTest.php— dynamic property (isset/empty/??), null dynamic property, behavior property, null behavior property, and the fall-through when a null dynamic property shares a name with a real attribute; plus attribute/unknown-property regressions.tests/Extension/ExtendableTest.php—extendableIsset()returns true / false / null as documented.tests/Halcyon/HalcyonModelTest.php— dynamic property and the null behavior property fall-through to a same-named attribute.Verification done locally:
ModelTest25 tests,ExtendableTest34 tests,HalcyonModelTest21 tests — all green.src/makes the new tests fail (ModelTest::testIssetIsExtensionAware,testIssetIsAwareOfBehaviorProperties,HalcyonModelTest::testIssetOnDynamicProperty); reverting only the null handling makes the two fall-through tests fail.ResizerTestbecause this machine's GD build has no JPEG support (imagecreatefromjpegundefined) — unrelated to the change.develop) against this branch: 1107 tests, 3688 assertions, 29 skipped, all green.phpcson all changed files: clean.Refs wintercms/winter#1536 (issues are disabled on this repository).