proxy-manager-lts icon indicating copy to clipboard operation
proxy-manager-lts copied to clipboard

Public readonly properties

Open lexalium opened this issue 2 years ago • 2 comments

I have an issue when trying to read public readonly properties from the Proxy. I got an error:

Cannot initialize readonly property App\Entity\Customer::$firstName from scope App\Command\Command

Library version: 1.0.12

Preconditions: We have a class only with public properties.

<?php

declare(strict_types=1);

namespace App\Entity;

class Customer
{
    public function __construct(
        public readonly string $firstName,
        public readonly string $lastName,
    ) {
    }
}

Steps to Reproduce:

  1. Create a Proxy object
$instance = (new LazyLoadingGhostFactory())->createProxy(
    Customer::class,
    static function (
        GhostObjectInterface $ghostObject,
        string $method,
        array $parameters,
        ?Closure &$initializer,
        array $properties,
    ) {
        $initializer = null;

        $properties['firstName'] = 'User';
        $properties['lastName'] = 'Test';

        return true;
    },
);
  1. Getting value from any public property results in an error. Cannot initialize readonly property App\Entity\Customer::$firstName from scope App\Command\Command
$output->writeln('First Name: ' . $customer->firstName);
$output->writeln('Last Name: ' . $customer->lastName);

After investigation, I see the $class and $scopeObject variables equal to the App\Command\Command.

} elseif (isset(self::$privateProperties98259[$name])) {
    $callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
    $caller  = isset($callers[1]) ? $callers[1] : [];
    $class   = isset($caller['class']) ? $caller['class'] : '';
    ...
}

...
$targetObject = $realInstanceReflection->newInstanceWithoutConstructor();
$accessor = function & () use ($targetObject, $name) {
    return $targetObject->$name;
};
$backtrace = debug_backtrace(true, 2);
$scopeObject = isset($backtrace[1]['object']) ? $backtrace[1]['object'] : new \ProxyManager\Stub\EmptyClassStub();

Note But if we will add a getter for the property all works without errors.

class Customer
{
    public function __construct(
        public readonly string $firstName,
        public readonly string $lastName,
    ) {
    }

    public function getFirstName(): string
    {
        return $this->firstName;
    }
}
$output->writeln('First Name: ' . $customer->getFirstName());
$output->writeln('Last Name: ' . $customer->lastName);

lexalium avatar Sep 01 '22 13:09 lexalium

Can you provide a reproducing test case please?

nicolas-grekas avatar Oct 17 '22 19:10 nicolas-grekas

This test case fails:

--TEST--
Verifies that public readonly properties can be used
--FILE--
<?php

require_once __DIR__ . '/init.php';

use ProxyManager\Proxy\GhostObjectInterface;

class Kitchen
{
    public readonly string $sweets;
}

$factory = new \ProxyManager\Factory\LazyLoadingGhostFactory($configuration);

$proxy = $factory->createProxy(Kitchen::class, function (
    GhostObjectInterface $ghostObject,
    string $method,
    array $parameters,
    ?Closure &$initializer,
    array $properties,
) {
    $properties['sweets'] = 'cookies';
});

echo $proxy->sweets;
?>
--EXPECTF--
cookies

The reason is that PublicScopeSimulator generates an accessor that returns by reference for __get(). Feel free to give a fix a try. Alternatively, you might want to wait for Symfony 6.2, which will provide another implementation of ghost objects that's free from this issue already. See https://symfony.com/blog/revisiting-lazy-loading-proxies-in-php

nicolas-grekas avatar Oct 18 '22 08:10 nicolas-grekas