Skip to content

Support readonly properties for read operations - #9316

Merged
derrabus merged 2 commits into
doctrine:2.11.xfrom
derrabus:improvement/readonly-properties
Jan 9, 2022
Merged

derrabus merged 2 commits into
doctrine:2.11.xfrom
derrabus:improvement/readonly-properties

Conversation

@derrabus

@derrabus derrabus commented Jan 1, 2022

Copy link
Copy Markdown
Member

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

derrabus commented Jan 1, 2022

Copy link
Copy Markdown
Member Author

CodeSniffer failure should be fixed by #9317

@beberlei

beberlei commented Jan 2, 2022 •

Copy link
Copy Markdown
Member

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

derrabus commented Jan 2, 2022

Copy link
Copy Markdown
Member Author

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

derrabus commented Jan 2, 2022

Copy link
Copy Markdown
Member Author

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
Comment thread phpstan-baseline.neon Outdated
@derrabus
derrabus force-pushed the improvement/readonly-properties branch from f031be5 to 2ef01eb Compare January 2, 2022 13:38
@beberlei

beberlei commented Jan 2, 2022

Copy link
Copy Markdown
Member

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

derrabus commented Jan 9, 2022

Copy link
Copy Markdown
Member Author

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

@greg0ire

greg0ire commented Jan 9, 2022 •

Copy link
Copy Markdown
Member

@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

derrabus commented Jan 9, 2022

Copy link
Copy Markdown
Member Author

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
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh yeah 😎

Comment thread psalm.xml Outdated
</InvalidClass>
<MethodSignatureMismatch>
<errorLevel type="suppress">
<!-- Psalm is wrong about ReflectionProperty signatures. -->

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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

solcik commented Jan 27, 2022

Copy link
Copy Markdown

@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