diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 33232c7d..63dd112e 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -1303,7 +1303,26 @@ trait AssignOpTrait $this->assertReadonlyPropertyReferenceForbidden($expr->var, $expr, true); $this->assertReadonlyPropertyReferenceForbidden($expr->expr, $expr, false); - $left = $this->parseWritableIdentifier($expr->var); + $propertyReferenceTarget = null; + $nativeObjectProperty = $expr->var instanceof Expr\PropertyFetch + && $this->getNativePropertyClassDef($expr->var)?->nativeObject === true; + if ($expr->var instanceof Expr\PropertyFetch && !$nativeObjectProperty) { + // A property reference assignment must go through Zend's property + // metadata path. A plain Variant indirect slot cannot maintain + // typed-property reference sources safely. Parse the complete LHS + // before the RHS so PHP's source evaluation order is preserved. + $object = $this->parseOrderedOperand($expr->var->var, false); + $member = $this->isIdExpr($expr->var->name) + ? $this->propertyNameToStr($expr->var->name, literal: true) + : $this->parseOrderedOperand($expr->var->name, false, true); + $scope = $this->usesTraitPropertyScope($object) + ? 'php::FakeScopeGuard::current()' + : ($this->class ? $this->getClassEntryPtr($this->getFullClassName()) : 'nullptr'); + $propertyReferenceTarget = [$object, $member, $scope]; + $left = ''; + } else { + $left = $this->parseWritableIdentifier($expr->var); + } // Keep this write-context form for every RHS kind. Re-parsing it as a // read later breaks append and missing-key targets such as // `$array[] =& $source`. @@ -1397,8 +1416,10 @@ trait AssignOpTrait } $this->context->beforeStmtLines[] = $rightExpr . ';'; - if ($expr->var instanceof Expr\PropertyFetch && $this->isNativePropertyAccess($expr->var)) { - return $left . '.rebindReference(' . $tmpVar . ')'; + if ($propertyReferenceTarget !== null) { + [$object, $member, $scope] = $propertyReferenceTarget; + return 'typephp_rebind_property_reference(' + . $object . ', ' . $member . ', ' . $tmpVar . ', ' . $scope . ')'; } return $left . ' = &' . $tmpVar; } diff --git a/tests/compiler/ref/property-reference-rebind.phpt b/tests/compiler/ref/property-reference-rebind.phpt new file mode 100644 index 00000000..4308f434 --- /dev/null +++ b/tests/compiler/ref/property-reference-rebind.phpt @@ -0,0 +1,170 @@ +--TEST-- +Object property reference assignment preserves aliases and typed-property sources +--FILE-- +value = &$source; + } + + public function value(): ?array + { + return $this->value; + } +} + +final class PropertyReferenceDep +{ + public ?array $map = null; +} + +function bindObjectProperty(object $holder, mixed &$source): void +{ + $holder->value = &$source; +} + +function replaceReference(mixed &$target, mixed $value): void +{ + $target = $value; +} + +function propertyReferenceTarget(array &$events, object $holder): object +{ + $events[] = 'object'; + return $holder; +} + +function propertyReferenceName(array &$events): string +{ + $events[] = 'property'; + return 'value'; +} + +function &propertyReferenceSource(array &$events, mixed &$source): mixed +{ + $events[] = 'source'; + return $source; +} + +function trackThroughSplObjectStorage(object $target): PropertyReferenceDep +{ + $storage = new SplObjectStorage(); + $storage->offsetSet($target, []); + $map = $storage[$target]; + + $dep = new PropertyReferenceDep(); + $dep->map = &$map; + $map['dep'] = $dep; + $storage->offsetSet($target, $map); + return $dep; +} + +function main(): void +{ + $holder = new PropertyReferenceHolder(); + $source = ['initial' => 1]; + $holder->value = &$source; + $source['source'] = 2; + $holder->value['property'] = 3; + var_dump($source); + + $dynamicHolder = new PropertyReferenceHolder(); + $dynamicSource = []; + bindObjectProperty($dynamicHolder, $dynamicSource); + $dynamicSource['dynamic'] = true; + var_dump($dynamicHolder->value); + + $wrong = 'invalid'; + try { + bindObjectProperty($dynamicHolder, $wrong); + echo "missing initial TypeError\n"; + } catch (TypeError $error) { + echo "initial TypeError\n"; + } + $dynamicSource['preserved'] = true; + var_dump($dynamicHolder->value); + + $replacement = ['replacement' => true]; + $holder->value = &$replacement; + replaceReference($source, 'detached'); + var_dump($source); + try { + replaceReference($replacement, 'invalid'); + echo "missing write TypeError\n"; + } catch (TypeError $error) { + echo "write TypeError\n"; + } + var_dump($holder->value); + + $privateSource = []; + $privateHolder = new PrivatePropertyReferenceHolder(); + $privateHolder->bind($privateSource); + $privateSource['private'] = true; + var_dump($privateHolder->value()); + + $events = []; + $orderedSource = []; + $orderedHolder = new PropertyReferenceHolder(); + propertyReferenceTarget($events, $orderedHolder)->{propertyReferenceName($events)} + = &propertyReferenceSource($events, $orderedSource); + $orderedSource['ordered'] = true; + var_dump($events, $orderedHolder->value); + + $dep = trackThroughSplObjectStorage(new stdClass()); + var_dump(isset($dep->map['dep'])); +} +?> +--EXPECT-- +array(3) { + ["initial"]=> + int(1) + ["source"]=> + int(2) + ["property"]=> + int(3) +} +array(1) { + ["dynamic"]=> + bool(true) +} +initial TypeError +array(2) { + ["dynamic"]=> + bool(true) + ["preserved"]=> + bool(true) +} +string(8) "detached" +write TypeError +array(1) { + ["replacement"]=> + bool(true) +} +array(1) { + ["private"]=> + bool(true) +} +array(3) { + [0]=> + string(6) "object" + [1]=> + string(8) "property" + [2]=> + string(6) "source" +} +array(1) { + ["ordered"]=> + bool(true) +} +bool(true)