Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PropertyAccess] Fix checking for missing properties #54194

Merged
merged 1 commit into from Mar 13, 2024

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Mar 7, 2024

Q A
Branch? 6.4
Bug fix? yes
New feature? no
Deprecations? no
Issues Fix #48970, fix #53972, fix #51919
License MIT

PropertyAccess and PropertyInfo make a few assumptions when magic methods are involved.
Namely they assume that if a __get method is defined, then any property name is accessible.
This assumption is generally wrong, and this becomes more evident with the introduction of lazy objects.
The linked issue gives some more details.

In this PR, I tweak these assumptions in order to make less of them.
Note that there is no bullet-proof way to decide if a virtual property exists in PHP. We're missing an __exists() magic method for that. Because of this, I'm submitting this PR to 6.4 and not 5.4. Let 5.4 end its life quietly and ensure 6.4 works at its best with lazy ghosts, where they're mainstream.

@chalasr
Copy link
Member

chalasr commented Mar 12, 2024

Is there a way to test this?

@nicolas-grekas
Copy link
Member Author

I think I did add tests, do you have something more in mind?

@chalasr
Copy link
Member

chalasr commented Mar 12, 2024

Something must have blinded me 👀 Sorry!

Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the __exists() your next RFC?

@nicolas-grekas
Copy link
Member Author

I don't think so :)

@nicolas-grekas nicolas-grekas merged commit dc4f3fb into symfony:6.4 Mar 13, 2024
7 of 10 checks passed
@nicolas-grekas nicolas-grekas deleted the pa-magic branch March 13, 2024 15:33
@alexandre-le-borgne
Copy link

I tested this fix but it doesn't work for me

I use PhpDocExtractor to be able to denormalize objects with properties like this:

class Form // Not a doctrine entity
{
    /**
     * @var Step[]
     */
    private array $steps;
}

class Step // Not a doctrine entity
{
}

However I still get the same error when I serialize this kind objects :

class FormOutput // Not a doctrine entity
{
    /**
     * @var IndicatorType                                    <--- A Doctrine entity
     */
    private IndicatorType $indicatorType;
}

User Notice: Undefined property: Proxies_CG_\App\Entity\IndicatorType::$lazyObjectState in /home/.../src/Serializer/PropertyAccessor.php on line 419

@xabbuh
Copy link
Member

xabbuh commented Mar 25, 2024

@alexandre-le-borgne Can you please open a new issue and provide a small example application that allows to reproduce your issue?

@alexandre-le-borgne
Copy link

@xabbuh #54393

@@ -411,8 +411,18 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
throw $e;
}
} elseif (PropertyReadInfo::TYPE_PROPERTY === $type) {
if ($access->canBeReference() && !isset($object->$name) && !\array_key_exists($name, (array) $object) && !(new \ReflectionProperty($class, $name))->hasType()) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why the $access->canBeReference() check has been dropped here ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes: that check was just wrong - __get can return by reference, so this was an unrelated check

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the information.

reviewtypo3org pushed a commit to TYPO3/typo3 that referenced this pull request Apr 4, 2024
To harden the Extbase LazyLoadingProxy implementation
a new test has been added with #101400, which creates
a proxy directly and using ObjectAccess::getProperty()
to access a property of the not loadable child proxy
class instance.

Due to the use of magic methods`__get()` and `__isset()`
methods in the `LazyLoadingProxy` class and some wrong
assumptions of the Symfony PropertyAccessor simply null
has been retrieved even if the concrete class could not
been retrieved (property on a null value instead of an
object). The property accessor is used under the hood
within the Extbase internal ObjectAccess implementation.

The Symfony Team recently declared that behaviour a bug
and fixed it with releases `7.0.6` and `6.4.6` and now
correctly throwing the `PropertyNotAccessibleException`.

Failing nightlies with Symfony releases containing the
bugfix revealed a incorrect test implementation, using
a constructed and unrealistic construct.

This change modifies the test to use a correct access
on the LazyLoadingProxy without changing the scenario
of the test.

[1] https://github.com/symfony/symfony/releases/tag/v7.0.6
[2] https://github.com/symfony/symfony/releases/tag/v6.4.6
[3] symfony/symfony#54194

Resolves: #103531
Related: #101400
Releases: main, 12.4
Change-Id: I5bdd52955af138c1e99b4492c9b5a43839c743cf
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83645
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Oliver Klee <typo3-coding@oliverklee.de>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Torben Hansen <derhansen@gmail.com>
Reviewed-by: Oliver Klee <typo3-coding@oliverklee.de>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Torben Hansen <derhansen@gmail.com>
Tested-by: Stefan Bürk <stefan@buerk.tech>
reviewtypo3org pushed a commit to TYPO3/typo3 that referenced this pull request Apr 4, 2024
To harden the Extbase LazyLoadingProxy implementation
a new test has been added with #101400, which creates
a proxy directly and using ObjectAccess::getProperty()
to access a property of the not loadable child proxy
class instance.

Due to the use of magic methods`__get()` and `__isset()`
methods in the `LazyLoadingProxy` class and some wrong
assumptions of the Symfony PropertyAccessor simply null
has been retrieved even if the concrete class could not
been retrieved (property on a null value instead of an
object). The property accessor is used under the hood
within the Extbase internal ObjectAccess implementation.

The Symfony Team recently declared that behaviour a bug
and fixed it with releases `7.0.6` and `6.4.6` and now
correctly throwing the `PropertyNotAccessibleException`.

Failing nightlies with Symfony releases containing the
bugfix revealed a incorrect test implementation, using
a constructed and unrealistic construct.

This change modifies the test to use a correct access
on the LazyLoadingProxy without changing the scenario
of the test.

[1] https://github.com/symfony/symfony/releases/tag/v7.0.6
[2] https://github.com/symfony/symfony/releases/tag/v6.4.6
[3] symfony/symfony#54194

Resolves: #103531
Related: #101400
Releases: main, 12.4
Change-Id: I5bdd52955af138c1e99b4492c9b5a43839c743cf
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83658
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Stefan Bürk <stefan@buerk.tech>
TYPO3IncTeam pushed a commit to TYPO3-CMS/extbase that referenced this pull request Apr 4, 2024
To harden the Extbase LazyLoadingProxy implementation
a new test has been added with #101400, which creates
a proxy directly and using ObjectAccess::getProperty()
to access a property of the not loadable child proxy
class instance.

Due to the use of magic methods`__get()` and `__isset()`
methods in the `LazyLoadingProxy` class and some wrong
assumptions of the Symfony PropertyAccessor simply null
has been retrieved even if the concrete class could not
been retrieved (property on a null value instead of an
object). The property accessor is used under the hood
within the Extbase internal ObjectAccess implementation.

The Symfony Team recently declared that behaviour a bug
and fixed it with releases `7.0.6` and `6.4.6` and now
correctly throwing the `PropertyNotAccessibleException`.

Failing nightlies with Symfony releases containing the
bugfix revealed a incorrect test implementation, using
a constructed and unrealistic construct.

This change modifies the test to use a correct access
on the LazyLoadingProxy without changing the scenario
of the test.

[1] https://github.com/symfony/symfony/releases/tag/v7.0.6
[2] https://github.com/symfony/symfony/releases/tag/v6.4.6
[3] symfony/symfony#54194

Resolves: #103531
Related: #101400
Releases: main, 12.4
Change-Id: I5bdd52955af138c1e99b4492c9b5a43839c743cf
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83645
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Oliver Klee <typo3-coding@oliverklee.de>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Torben Hansen <derhansen@gmail.com>
Reviewed-by: Oliver Klee <typo3-coding@oliverklee.de>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Torben Hansen <derhansen@gmail.com>
Tested-by: Stefan Bürk <stefan@buerk.tech>
TYPO3IncTeam pushed a commit to TYPO3-CMS/extbase that referenced this pull request Apr 4, 2024
To harden the Extbase LazyLoadingProxy implementation
a new test has been added with #101400, which creates
a proxy directly and using ObjectAccess::getProperty()
to access a property of the not loadable child proxy
class instance.

Due to the use of magic methods`__get()` and `__isset()`
methods in the `LazyLoadingProxy` class and some wrong
assumptions of the Symfony PropertyAccessor simply null
has been retrieved even if the concrete class could not
been retrieved (property on a null value instead of an
object). The property accessor is used under the hood
within the Extbase internal ObjectAccess implementation.

The Symfony Team recently declared that behaviour a bug
and fixed it with releases `7.0.6` and `6.4.6` and now
correctly throwing the `PropertyNotAccessibleException`.

Failing nightlies with Symfony releases containing the
bugfix revealed a incorrect test implementation, using
a constructed and unrealistic construct.

This change modifies the test to use a correct access
on the LazyLoadingProxy without changing the scenario
of the test.

[1] https://github.com/symfony/symfony/releases/tag/v7.0.6
[2] https://github.com/symfony/symfony/releases/tag/v6.4.6
[3] symfony/symfony#54194

Resolves: #103531
Related: #101400
Releases: main, 12.4
Change-Id: I5bdd52955af138c1e99b4492c9b5a43839c743cf
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83658
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Stefan Bürk <stefan@buerk.tech>
@vsoroka
Copy link

vsoroka commented Apr 4, 2024

I guess that a better sollution will be to create an interface for classes that will ckeck existance of magic properties, e.g. MagicPropertyExistanceExtractorInterface, and if Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor implements this interface delegate the existance check to it. It will allow to implement application depended logic related to magic properties without workarounds.
Unfortutattely since version 6.4.6 I have to configure the property accessor to ignore invalid properties in my application and try to find a way to do this check outside the property accessor.

@craigh
Copy link

craigh commented Apr 4, 2024

This change broke my tests. They work with 6.4.4 and not with 6.4.6.
I get the error Can\'t get a way to read the property "%s" in class "%s".'

I use magic getters in one of my entities like so:

    public function __get(string $arg)
    {
        if (preg_match('/custom_checkbox_(\d+)_entry(_other)?/', $arg, $matches)) {
            if (isset($matches[2])) {
                return $this->getCustomCheckboxEntryOther((int) $matches[1]);
            }

            return $this->getCustomCheckboxEntryForCheckbox((int) $matches[1]);
        }
        if (preg_match('/customCheckboxEntryValueByCheckboxId(\d+)/', $arg, $matches)) {
            return $this->getCustomCheckboxEntryValueByCheckboxId((int) $matches[1]);
        }
    }

my tests use liip/test-fixtures-bundle (if this matters).

@nicolas-grekas
Copy link
Member Author

@craigh you might have a broken implementation of __isset, please have a look and try to fix it.

@craigh
Copy link

craigh commented Apr 4, 2024

@nicolas-grekas of course, you are correct 🎉 thank you for your keen insight and quick response!

jenkins-oroinc-app bot pushed a commit to oroinc/platform that referenced this pull request Apr 5, 2024
…ess v6.4.6 (#38013)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/crm that referenced this pull request Apr 5, 2024
…ess v6.4.6 (#38013)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/OroEntitySerializedFieldsBundle that referenced this pull request Apr 5, 2024
…ess v6.4.6 (#38013)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/crm-application that referenced this pull request Apr 5, 2024
…ess v6.4.6 (#38013)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/platform-application that referenced this pull request Apr 5, 2024
…ess v6.4.6 (#38013)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/orocommerce-application that referenced this pull request Apr 5, 2024
…ess v6.4.6 (#38013)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/orocommerce-application-de that referenced this pull request Apr 5, 2024
…ess v6.4.6 (#38013)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
RocKordier pushed a commit to RocKordier/platform that referenced this pull request Apr 9, 2024
…ess v6.4.6 (#38013)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
@fmonts
Copy link

fmonts commented Apr 12, 2024

This is a BC break, I'm now getting:

[NoSuchPropertyException]
Can't get a way to read the property "offerFallback" in class "App\Entity\Offer".

Even if I have this, to access dynamic properties stored in a json column:

public function __get(string $name): mixed
{
    if (property_exists(Offer::class, $name)) {
        return $this->$name;
    }
    return $this->getSetting($name) ?? null;
}

It's look like the magic getter is never called

@nicolas-grekas
Copy link
Member Author

@fmonts same as above: you likely have a broken implementation of __isset, please have a look and try to fix it.

@fmonts
Copy link

fmonts commented Apr 12, 2024

@fmonts same as above: you likely have a broken implementation of __isset, please have a look and try to fix it.

I don't have any __isset(), I accept virtually any name, this would require me to add

public function __isset(string $name): bool
{
    return true;
}

to many entities...

@nicolas-grekas
Copy link
Member Author

That's why then. You relied on a specific behavior of PropertyAccess, which is broken in the generic case (eg the ?? operator should be broken with you objects, etc).

@barryvdh
Copy link
Contributor

This seems to break access to properties in Laravel Eloquent models when the value is null. This is because __isset() returns false if the value is null and no actual property exists on the model. And if a model is not loaded from the database yet, it will have no properties. So it's impossible to check if it will be allowed.

I'm not sure if there is a 'standard' way to check for access there, because of the way ActiveRecord works (at least in Laravel). In Laravel it is always possible to set or get a property on the model..
Eg. when setting $model->name = 'Foo'; it will set $model->attributes['name'] (and perform additional actions when setting/getting) using the magic __get() and __set() methods. If an attribute is requested that does not exist, it will return null.

jenkins-oroinc-app bot pushed a commit to oroinc/platform that referenced this pull request May 10, 2024
…ess v6.4.6 (#38014)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/crm that referenced this pull request May 10, 2024
…ess v6.4.6 (#38014)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/OroEntitySerializedFieldsBundle that referenced this pull request May 10, 2024
…ess v6.4.6 (#38014)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/crm-application that referenced this pull request May 10, 2024
…ess v6.4.6 (#38014)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/platform-application that referenced this pull request May 10, 2024
…ess v6.4.6 (#38014)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/orocommerce-application that referenced this pull request May 10, 2024
…ess v6.4.6 (#38014)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
jenkins-oroinc-app bot pushed a commit to oroinc/orocommerce-application-de that referenced this pull request May 10, 2024
…ess v6.4.6 (#38014)

- since symfony/property-access 6.4.6 the application crashed with a lot of exceptions like "Can't get a way to read the property ... in class ...", see symfony/symfony#54194
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet