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

Support readonly properties for read operations #9316

Merged
merged 2 commits into from
Jan 9, 2022

Conversation

derrabus
Copy link
Member

@derrabus derrabus commented Jan 1, 2022

This PR provides some testcases to check if the ORM handles readonly properties well for read operations. So far, it looks good except for lazy-loaded ManyToOne relations. Here, the proxy tries to unset readonly properties and fails.

@derrabus
Copy link
Member Author

derrabus commented Jan 1, 2022

CodeSniffer failure should be fixed by #9317

@beberlei
Copy link
Member

beberlei commented Jan 2, 2022

The RFC states about Reflection and readonly:

ReflectionProperty::setValue() can bypass the requirement that initialization occurs from the scope where the property has been declared. However, reflection cannot modify a readonly property that has already been initialized.

The problem here I believe is that "simple IDs" are already populated in proxies, and then when loading the entity the ID is set again, triggering this error from UnitOfWork::createEntity that refreshes the proxy.

The workaorund could be for now to avoid using readonly on ID, which makes no sense anyways, because at least for post insert ID generators, it is set to null first, then to an integer when first persisted.

@derrabus
Copy link
Member Author

derrabus commented Jan 2, 2022

The problem here I believe is that "simple IDs" are already populated in proxies, and then when loading the entity the ID is set again, triggering this error from UnitOfWork::createEntity that refreshes the proxy.

Why's that? Could we just not set the ID again when resolving the proxy? It should not change anyway, should it?

The workaorund could be for now to avoid using readonly on ID, which makes no sense anyways, because at least for post insert ID generators, it is set to null first, then to an integer when first persisted.

The test assumes that the entity model is used for read operations only. And in that case, readonly IDs are reasonable, I think.

@derrabus derrabus force-pushed the improvement/readonly-properties branch from 13244f6 to f031be5 Compare January 2, 2022 12:56
@derrabus derrabus marked this pull request as ready for review January 2, 2022 12:56
@derrabus derrabus changed the title Failing test for readonly properties Support readonly properties for read operations Jan 2, 2022
@derrabus derrabus added this to the 2.11.0 milestone Jan 2, 2022
@derrabus
Copy link
Member Author

derrabus commented Jan 2, 2022

I've pushed a commit that fixes the issue. Can you have a look?

@derrabus derrabus requested a review from beberlei January 2, 2022 12:59
phpstan-baseline.neon Outdated Show resolved Hide resolved
@derrabus derrabus force-pushed the improvement/readonly-properties branch from f031be5 to 2ef01eb Compare January 2, 2022 13:38
@beberlei
Copy link
Member

beberlei commented Jan 2, 2022

This is the hot code path, this change is probably too expensive since it affects every property. I would imagine again we need a special reflection instance here

@derrabus derrabus force-pushed the improvement/readonly-properties branch from 2ef01eb to e800bf5 Compare January 3, 2022 00:48
@derrabus
Copy link
Member Author

derrabus commented Jan 9, 2022

Do you already have an idea how to implement that "special reflection instance"? I'm a bit lost here, I'm afraid.

@greg0ire
Copy link
Member

greg0ire commented Jan 9, 2022

@derrabus from what I understood it would look like so:

<?php

declare(strict_types=1);

namespace Doctrine\ORM\Mapping;

use LogicException;
use ReflectionProperty;

use function sprintf;

class ReflectionReadOnlyProperty extends ReflectionProperty
{
    /** @var ReflectionProperty */
    private $originalReflectionProperty;

    public function __construct(ReflectionProperty $originalReflectionProperty)
    {
        $this->originalReflectionProperty = $originalReflectionProperty;
    }

    /**
     * @param object $object
     * @param mixed  $value
     */
    public function setValue($object, $value = null): void
    {
        if ($this->originalReflectionProperty->getValue($object) === $value) {
            return;
        }

        throw new LogicException(sprintf(
            'Attempting to change readonly field %s.',
            $this->originalReflectionProperty->getName()
        ));
    }
}

Then, in ClassMetadataInfo, you would do

                if ($this->reflFields[$field]->isReadOnly()) {
                    $this->reflField[$field] = new ReflectionReadOnlyProperty($this->reflFields[$field])];
                }

where appropriate

@derrabus derrabus force-pushed the improvement/readonly-properties branch 3 times, most recently from 7d5f432 to 5923e57 Compare January 9, 2022 18:19
@derrabus
Copy link
Member Author

derrabus commented Jan 9, 2022

I think I got it right now. 😓

@derrabus derrabus force-pushed the improvement/readonly-properties branch from 5923e57 to 9c4c171 Compare January 9, 2022 18:22
final class ReflectionReadonlyProperty extends ReflectionProperty
{
public function __construct(
private ReflectionProperty $wrappedProperty
Copy link
Member

Choose a reason for hiding this comment

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

Oh yeah 😎

psalm.xml Outdated
@@ -75,6 +75,12 @@
<referencedClass name="Doctrine\DBAL\Platforms\PostgreSQLPlatform" />
</errorLevel>
</InvalidClass>
<MethodSignatureMismatch>
<errorLevel type="suppress">
<!-- Psalm is wrong about ReflectionProperty signatures. -->
Copy link
Member

Choose a reason for hiding this comment

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

Is there a github issue to track this?

Copy link
Member Author

Choose a reason for hiding this comment

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

I haven't researched that yet.

Copy link
Member Author

Choose a reason for hiding this comment

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

I just filed vimeo/psalm#7357

Copy link
Member

Choose a reason for hiding this comment

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

Great! I also observed this locally: https://psalm.dev/r/5c7d43694a , but maybe that's because I'm using 8.0 🤔

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 error goes away if you run Psalm with PHP 8.1.

@derrabus derrabus force-pushed the improvement/readonly-properties branch from 9c4c171 to 5816424 Compare January 9, 2022 18:32
@derrabus derrabus force-pushed the improvement/readonly-properties branch from 5816424 to 9696bd3 Compare January 9, 2022 18:59
@derrabus derrabus merged commit 580b919 into doctrine:2.11.x Jan 9, 2022
@derrabus derrabus deleted the improvement/readonly-properties branch January 9, 2022 19:16
derrabus added a commit to derrabus/orm that referenced this pull request Jan 9, 2022
* 2.11.x:
  Leverage generic ObjectManagerDecorator (doctrine#9312)
  Fix WhereInWalker description to better describe the behaviour of this class (doctrine#9268)
  Regenerate Psalm baseline
  Update Psalm baseline for Persistence 2.3 (doctrine#9349)
  Support readonly properties for read operations (doctrine#9316)
derrabus added a commit that referenced this pull request Jan 9, 2022
* 2.11.x:
  Leverage generic ObjectManagerDecorator (#9312)
  Fix WhereInWalker description to better describe the behaviour of this class (#9268)
  Regenerate Psalm baseline
  Update Psalm baseline for Persistence 2.3 (#9349)
  Support readonly properties for read operations (#9316)
@solcik
Copy link

solcik commented Jan 27, 2022

@greg0ire @derrabus

Hello, Thank you for your work!

I tried readonly nullable properties in Doctrine entities and I got an error. In Proxy constructor there is unset.

I tried to do failing test case here #9431

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.

4 participants