🌐 AI搜索 & 代理 主页
Skip to content

Conversation

@nicolas-grekas
Copy link
Contributor

Fixes #394

@nicolas-grekas
Copy link
Contributor Author

Green btw :)

@Ocramius Ocramius self-assigned this Nov 15, 2017
@Ocramius Ocramius added the bug label Nov 15, 2017
@Ocramius Ocramius added this to the 2.1.2 milestone Nov 15, 2017
@Ocramius
Copy link
Owner

Ocramius commented Nov 15, 2017

There are two issues with this patch, sadly. Here's the pseudo-code representing the issue:

$proxy = $theFactory->createProxy(
    SomeClass::class,
    function (& $wrappedObject, LazyLoadingInterface $proxy, $method, array $parameters, & $initializer) {
        $initializer   = null;
        $wrappedObject = new HeavyComplexObject();

        // return false; purposely omitting the `return` value will break the initialization
    }
); 
$proxy = $theFactory->createProxy(
    SomeClass::class,
    function (& $wrappedObject, LazyLoadingInterface $proxy, $method, array $parameters, & $initializer) {
        $initializer   = null;
        $wrappedObject = new HeavyComplexObject();

        $proxy->setSomething('someValue'); // infinite initialization loop

        return true;
    }
); 

@Ocramius
Copy link
Owner

I'll work on alternate generated code in a bit. Will likely need to move away from a single boolean expression.

});

$proxy->compress('Lazy ');
$dummy->reference = 123;
Copy link
Owner

Choose a reason for hiding this comment

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

This is misuse of a reference... Not much I can do against it: that's a bug in the consumer code.

Copy link
Owner

Choose a reason for hiding this comment

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

$proxy->compress('Lazy ');
$foo = 123;
$dummy->reference = & $foo;
$proxy->compress('Loaded!')

This is how it SHOULD be done.

@Ocramius
Copy link
Owner

According to https://github.com/Ocramius/ProxyManager/pull/396/files#r151069163, the test case is invalid.

Something else is going on - the current patch seems to mitigate a symptom of some other root cause.

@Ocramius
Copy link
Owner

In the current status, this issue is invalid, but I and @nicolas-grekas had a chat about it at SymfonyCon and this may be an OpCache problem.

Until a proper test scenario is found (may be tricky), this is gonna stay in a limbo.

@nicolas-grekas
Copy link
Contributor Author

Test case updated, you might find it more practical now?

purposely omitting the return value will break the initialization

I'm not sure: the generated code is independent of the return value of the closure

$proxy->setSomething('someValue'); // infinite initialization loop

Not an infinite loop: the initializer being reseted, this cannot loop.
But it throw an Error: Call to a member function doFoo() on null.

I think this case is hit because of a bug in OPcache, that forgets to clean the "is_ref" state on the local variable. I cannot reproduce it on a simple case (but the linked issue contains a reproducer using Docker, on 7.1-stable).

FYI, I already fixed that in Symfony, by patching the generated code (we're free from any issue with this "Call to a member function doFoo() on null" since the initializer is also generated).

?>
--EXPECT--
123
234
Copy link
Contributor Author

Choose a reason for hiding this comment

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

echoes "123" twice without the patch

@Ocramius
Copy link
Owner

@nicolas-grekas thanks, new test looks much better!

@Ocramius Ocramius removed the invalid label Nov 23, 2017
@Ocramius Ocramius self-assigned this Nov 23, 2017
@Ocramius
Copy link
Owner

Ocramius commented Dec 4, 2017

Note: this will need to be reproduced locally with/without opcache. I'll try reducing it to a minimal test case for php-src.

@Ocramius
Copy link
Owner

Ocramius commented Dec 5, 2017

Dug further - this is not an opcache bug, as the code behaves the same way with or without opcache enabled:

--TEST--
Verifies that lazy loading value holder cannot be turned to a reference
?>
--FILE--
<?php

class Proxied
{
    public $publicProperty;
}

class Proxy extends Proxied
{
    /** @var Proxied|null */
    private $wrapped;

    /** @var callable|null */
    private $initializer;

    public static function staticProxyConstructor(callable  $initializer) : self
    {
        $instance = new self();

        unset($instance->publicProperty);

        $instance->initializer = $initializer;

        return $instance;
    }

    public function & __get($name)
    {
        $this->initializer && ($this->initializer)($this->wrapped, $this->initializer);

        return $this->wrapped->$name;
    }

    public function __set($name, $value)
    {
        $this->initializer && ($this->initializer)($this->wrapped, $this->initializer);

        return ($this->wrapped->$name = $value);
    }

    public function __isset($name)
    {
        $this->initializer && ($this->initializer)($this->wrapped, $this->initializer);

        return isset($this->wrapped->$name);
    }

    public function __clone()
    {
        $this->initializer && ($this->initializer)($this->wrapped, $this->initializer);

        $this->wrapped = clone $this->wrapped;
    }
}

$dummy = new stdClass();

$proxy = Proxy::staticProxyConstructor(function (& $wrapped, & $initializer) use ($dummy) {
    $initializer      = null;
    $wrapped          = new Proxied();
    $dummy->reference = & $wrapped;
});

$proxy->publicProperty = 123;

$clone                 = clone $proxy;
$clone->publicProperty = 234;

echo $proxy->publicProperty, "\n";
echo $clone->publicProperty, "\n";

?>
--EXPECT--
123
234

@Ocramius
Copy link
Owner

Ocramius commented Dec 5, 2017

Reduced the examples even further and posted at https://bugs.php.net/bug.php?id=75629

@Ocramius
Copy link
Owner

Ocramius commented Dec 5, 2017

@nicolas-grekas is this reproducible only with clone or are there other scenarios too?

@Ocramius
Copy link
Owner

Ocramius commented Dec 5, 2017

According to @nikic (https://chat.stackoverflow.com/transcript/message/40315447#40315447):

"the Container#$referencedProperty is never a reference itself, but it is passed by reference to the $loadFieldByReference() closure and stored in $references"
I think you are suffering from (a very common) misunderstanding about how references work
References are not directional
If you pass $foo->bar by ref to &$ref, and then store $ref in $refs[] &= $ref, then all of $foo->bar, $ref, and $ref[] point to the same reference
$ref gets dropped after the function all, but $refs[] is still a reference to $foo->bar and $foo->bar is still a reference to $refs[]
When you clone an object, then (clone $foo)->bar, $foo->bar, $ref[] will all reference each other, because cloning preserves references
If you do $this->bar = clone $this->bar in the __clone method, you're not changing much, because you are still assigning to $this->bar, which is still a reference, so it's going to affect everything
To avoid that you'd have to break the reference first, e.g. using $bar = $this->bar; unset($this->bar); $this->bar = clone $bar;. The unset breaks the reference
Or alternatively $bar = clone $this->bar; $this->bar =& $bar;

@nicolas-grekas this seems to be expected behavior. In this specific case, the reference needs to be broken in the clone method, but what is probably going on in your scenario is that the reference being kept in the container is not being de-referenced before being replaced.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lazy initializers turn proxy value-holder to a reference

2 participants