Ocramius/ProxyManager

Error with static return type

mrsuh opened this issue · 3 comments

mrsuh commented

Hi!
I didn't find issues with my problem and decide to ask you about error with : static return type.

How to reproduce

php -v
PHP 8.0.0 (cli) (built: Dec 18 2020 21:07:48) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies
composer info ocramius/proxy-manager
name     : ocramius/proxy-manager
descrip. : A library providing utilities to generate, instantiate and generally operate with Object Proxies
keywords : aop, lazy loading, proxy, proxy pattern, service proxies
versions : * 2.13.1
<?php

require_once __DIR__ . '/../vendor/autoload.php';

$factory = new \ProxyManager\Factory\LazyLoadingValueHolderFactory();

class HeavyComplexObject
{
    public function doFoo(): static
    {
        return $this;
    }
}

$proxy = $factory->createProxy(
    HeavyComplexObject::class,
    function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) {
        $wrappedObject = new HeavyComplexObject();        // instantiation logic here
        $initializer   = null;                            // turning off further lazy initialization

        return true; // report success
    }
);

$proxy->doFoo();

Output

Fatal error: Uncaught TypeError: ProxyManagerGeneratedProxy\__PM__\HeavyComplexObject\Generatedfda300b1d931030ebbdfaf2b78627d82::doFoo(): Return value must be of type ProxyManagerGeneratedProxy\__PM__\HeavyComplexObject\Generatedfda300b1d931030ebbdfaf2b78627d82, HeavyComplexObject returned in /app/vendor/ocramius/proxy-manager/src/ProxyManager/GeneratorStrategy/EvaluatingGeneratorStrategy.php(54) : eval()'d code:28
Stack trace:
#0 /app/bin/test.php(25): ProxyManagerGeneratedProxy\__PM__\HeavyComplexObject\Generatedfda300b1d931030ebbdfaf2b78627d82->doFoo()
#1 {main}
  thrown in /app/vendor/ocramius/proxy-manager/src/ProxyManager/GeneratorStrategy/EvaluatingGeneratorStrategy.php(54) : eval()'d code on line 28

Any ideas how to avoid this error?

A couple things:

  1. your HeavyComplexObject (I know it's taken from the examples) should return only itself, when using the static signature: decoration of methods returning : static has always been problematic because of that, and is not really fixable here:
    • declaring HeavyComplexObject in the proxy return type of doFoo() would break the type signature (compile error)
    • returning the proxy itself would break the behavior of doFoo(), swapping out the object at runtime, although we don't know if doFoo() should return the same instance ($this) or a newly instantiated HeavyComplexObject
    • I've described these problems in https://ocramius.github.io/blog/fluent-interfaces-are-evil/ in more detail
  2. for working around most of these problems on fluent interfaces, see https://github.com/Ocramius/ProxyManager/blob/8846623bea8749ded206db3ce701e4e508d516db/docs/lazy-loading-ghost-object.md
    • it is more complex to work with
    • it will preserve the identity of your object, as there is no value holder being wrapped by a proxy, but just the state of the proxy itself
    • it will still break in a declaration like public function doFoo(): static { return new static(); }, since the constructor would need to be final (or interfaced) to work reliably

This cannot be fixed in the library, as there's no declaration (in PHP) that tells us if the same instance, or a new instance, will be returned by doFoo(), so doing this in an automated way is not feasible, and even if we wanted to go into analyzing the AST, we could still have scenarios where this happens conditionally.

My recommendation is to try with ghost objects, linked above.

mrsuh commented

Thank you for detailed answer!
Example works with LazyLoadingGhostFactory.
Maybe It will help for symfony/symfony#46350

@mrsuh switching proxy type is feasible within the symfony/dependency-injection system, but is going to be a blood-bath to implement :-)

That would certainly be my recommendation though: I designed ghost objects much after I've designed the initial value-holder design, which got adopted by laminas/laminas-servicemanager, php-di/php-di and symfony/dependency-injection.

Now the path would be to migrate to the newer/better proxy type, but at the cost of added complexity and potential BC issues (the interface on the proxies changes)