Native Class 支持 ArrayAccess

master
韩天峰 1 week ago
parent 2ffd4ded19
commit 464de23051
  1. 4
      docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md
  2. 4
      docs/NATIVE_CLASS_OBJECT.md
  3. 16
      phpunit/code/native-class-array-access-coalesce-assign.php
  4. 16
      phpunit/code/native-class-array-access-compound.php
  5. 16
      phpunit/code/native-class-array-access-increment.php
  6. 16
      phpunit/code/native-class-array-access-nested-write.php
  7. 16
      phpunit/code/native-class-array-access-property-write.php
  8. 16
      phpunit/code/native-class-array-access-reference.php
  9. 58
      phpunit/src/NativeClass/NativeClassValidationTest.php
  10. 79
      src/CompilerBase.php
  11. 100
      src/NativeClass/NativeClassSupportTrait.php
  12. 19
      src/Parser/ArrayExpressionTrait.php
  13. 46
      src/Parser/AssignOpTrait.php
  14. 9
      src/Parser/PropertyAccessTrait.php
  15. 6
      src/Parser/SelectionExpressionTrait.php
  16. 107
      tests/compiler/native-class/array-access.phpt

@ -78,6 +78,8 @@
| 动态魔术方法、变量属性/方法名不支持 | Native magic/dynamic access deny-list | dynamic magic、variable method/property 负向测试 | 已验证 |
| `toArray/toString/toInt/toFloat/toBool` 要求实体方法和精确返回类型 | Native keyword method resolution | `keyword-conversions.phpt` 及 missing/wrong return 负向测试 | 已验证 |
| `count($obj)` 仅在实现 Countable 时特化 | Native count optimizer | `keyword-conversions.phpt`、count-without-countable 负向测试 | 已验证 |
| `ArrayAccess` 直接语法映射到 Native `offset*()` 方法 | Native array access lowering | `array-access.phpt` | 已验证 |
| Native `ArrayAccess` 禁止间接修改和引用 | writable-chain/reference validators | ArrayAccess compound/increment/nested/property/reference/coalesce 负向测试 | 已验证 |
## 6. GC 与生命周期
@ -127,6 +129,6 @@ vendor/bin/phpunit phpunit/src/NativeClass/NativeClassValidationTest.php
--gtest_filter='wren_gc.*:native_gc.*'
```
本次结果分别为:69/69 PHPT、131/131 PHPUnit、17/17 PHPX C++ tests。
本次结果分别为:70/70 PHPT、137/137 PHPUnit、17/17 PHPX C++ tests。
完整回归结果为:编译器 PHPUnit 1431/1431、编译器 PHPT 1037/1037(另有 2 项按
环境跳过)、PHPX C++ tests 1016/1016。Native 分支的公共 hook 未影响普通对象模型。

@ -1284,7 +1284,9 @@ $json = json_encode($nativeObject->toArray());
| 动态 callback | 不支持 |
| 动态 PHP/eval 使用 | 不支持 |
| 普通 PHP array 保存 Native Object | 不支持 |
| Native Object 作为 PHP array key 或 `[]` receiver | 不支持;编译期 FatalError |
| Native Object 作为 PHP array key | 不支持;编译期 FatalError |
| Native Object 作为 `[]` receiver | 实现 `ArrayAccess` 时支持直接读写、追加、`isset`、`empty`、`??` 和 `unset`;直接生成 Native `offset*()` 调用 |
| Native `ArrayAccess` 元素间接修改 | 不支持 `++/--`、复合赋值、`??=`、嵌套写入、属性写入和取引用;编译期 FatalError |
| Box/Std Container 属性 | 不支持 |
| Box 保存 Native Object | 不支持 |
| 局部 Std Container 保存 Native Object | 仅支持函数顶层局部变量和具体 Native class value type;容器 Root Frame 参与 GC tracing |

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeArrayAccessCoalesceAssign implements ArrayAccess
{
public function offsetExists(mixed $offset): bool { return false; }
public function offsetGet(mixed $offset): mixed { return null; }
public function offsetSet(mixed $offset, mixed $value): void {}
public function offsetUnset(mixed $offset): void {}
}
function modifyNativeArrayAccessCoalesce(): void
{
$value = new NativeArrayAccessCoalesceAssign();
$value['key'] ??= 1;
}

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeArrayAccessCompound implements ArrayAccess
{
public function offsetExists(mixed $offset): bool { return false; }
public function offsetGet(mixed $offset): mixed { return null; }
public function offsetSet(mixed $offset, mixed $value): void {}
public function offsetUnset(mixed $offset): void {}
}
function modifyNativeArrayAccessCompound(): void
{
$value = new NativeArrayAccessCompound();
$value['key'] += 1;
}

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeArrayAccessIncrement implements ArrayAccess
{
public function offsetExists(mixed $offset): bool { return false; }
public function offsetGet(mixed $offset): mixed { return null; }
public function offsetSet(mixed $offset, mixed $value): void {}
public function offsetUnset(mixed $offset): void {}
}
function modifyNativeArrayAccessIncrement(): void
{
$value = new NativeArrayAccessIncrement();
$value['key']++;
}

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeArrayAccessNestedWrite implements ArrayAccess
{
public function offsetExists(mixed $offset): bool { return false; }
public function offsetGet(mixed $offset): mixed { return []; }
public function offsetSet(mixed $offset, mixed $value): void {}
public function offsetUnset(mixed $offset): void {}
}
function modifyNativeArrayAccessNestedValue(): void
{
$value = new NativeArrayAccessNestedWrite();
$value['key'][] = 1;
}

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeArrayAccessPropertyWrite implements ArrayAccess
{
public function offsetExists(mixed $offset): bool { return false; }
public function offsetGet(mixed $offset): mixed { return new stdClass(); }
public function offsetSet(mixed $offset, mixed $value): void {}
public function offsetUnset(mixed $offset): void {}
}
function modifyNativeArrayAccessProperty(): void
{
$value = new NativeArrayAccessPropertyWrite();
$value['key']->property = 1;
}

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeArrayAccessReference implements ArrayAccess
{
public function offsetExists(mixed $offset): bool { return false; }
public function offsetGet(mixed $offset): mixed { return null; }
public function offsetSet(mixed $offset, mixed $value): void {}
public function offsetUnset(mixed $offset): void {}
}
function referenceNativeArrayAccessValue(): void
{
$value = new NativeArrayAccessReference();
$reference = &$value['key'];
}

@ -743,34 +743,76 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-array-dim-key.php');
}
public function testRejectsArrayAccessOnNativeObjects(): void
public function testRejectsArrayAccessWithoutArrayAccessInterface(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support array dimension access');
$this->expectExceptionMessage('must implement `ArrayAccess` to use array access syntax');
$this->compile('native-class-array-access.php');
}
public function testRejectsArrayWritesOnNativeObjects(): void
public function testRejectsArrayWritesWithoutArrayAccessInterface(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support array dimension access');
$this->expectExceptionMessage('must implement `ArrayAccess` to use array access syntax');
$this->compile('native-class-array-access-write.php');
}
public function testRejectsArrayIssetOnNativeObjects(): void
public function testRejectsArrayIssetWithoutArrayAccessInterface(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support array dimension access');
$this->expectExceptionMessage('must implement `ArrayAccess` to use array access syntax');
$this->compile('native-class-array-access-isset.php');
}
public function testRejectsArrayUnsetOnNativeObjects(): void
public function testRejectsArrayUnsetWithoutArrayAccessInterface(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support array dimension access');
$this->expectExceptionMessage('must implement `ArrayAccess` to use array access syntax');
$this->compile('native-class-array-access-unset.php');
}
public function testRejectsNativeArrayAccessCompoundModification(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported');
$this->compile('native-class-array-access-compound.php');
}
public function testRejectsNativeArrayAccessIncrement(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported');
$this->compile('native-class-array-access-increment.php');
}
public function testRejectsNativeArrayAccessNestedWrite(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported');
$this->compile('native-class-array-access-nested-write.php');
}
public function testRejectsNativeArrayAccessPropertyWrite(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported');
$this->compile('native-class-array-access-property-write.php');
}
public function testRejectsNativeArrayAccessReferences(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('References to Native ArrayAccess elements are not supported');
$this->compile('native-class-array-access-reference.php');
}
public function testRejectsNativeArrayAccessCoalesceAssignment(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported');
$this->compile('native-class-array-access-coalesce-assign.php');
}
public function testRejectsInaccessibleNativeCloneMethod(): void
{
$this->expectException(TestError::class);

@ -3202,6 +3202,7 @@ class CompilerBase implements PropertyAccessContext
protected function parsePreInc(Expr\PreInc $expr): string
{
$this->assertNativeArrayAccessDirectWrite($expr->var, false);
$this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '++');
$this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
@ -3590,6 +3591,7 @@ class CompilerBase implements PropertyAccessContext
protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string
{
$this->assertNativeArrayAccessDirectWrite($expr->var, false);
$this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, str_repeat($op, 2));
$this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
@ -3640,6 +3642,7 @@ class CompilerBase implements PropertyAccessContext
protected function parsePreDec(Expr\PreDec $expr): string
{
$this->assertNativeArrayAccessDirectWrite($expr->var, false);
$this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '--');
$this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
@ -4209,8 +4212,14 @@ class CompilerBase implements PropertyAccessContext
protected function parseChainedExpr(NodeAbstract $node, string $op, bool $getValue = false): string
{
if ($op === self::OP_REFVAL) {
$this->assertNativeArrayAccessReferenceForbidden($node);
$this->assertNativeObjectReferenceForbidden($node, $node);
}
if ($node instanceof Expr\ArrayDimFetch
&& $this->isNativeObjectClass($this->detectClassOfExpr($node->var))
) {
return $this->parseNativeArrayAccessPresence($node, $op, $getValue);
}
if (in_array($op, [self::OP_ISSET, self::OP_EMPTY, self::OP_NOT_EMPTY], true)) {
$nativePresence = $this->parseNativeObjectPresenceChain($node, $op);
if ($nativePresence !== null) {
@ -4266,6 +4275,10 @@ class CompilerBase implements PropertyAccessContext
$list = [];
while (true) {
if ($this->isArrayDimFetch($expr)) {
if ($this->isNativeObjectClass($this->detectClassOfExpr($expr->var))) {
$var = $this->parseArrayDimFetchRead($expr);
break;
}
if ($expr->dim === null) {
$this->fatalError($expr, 'Cannot use [] for reading');
}
@ -4310,6 +4323,72 @@ class CompilerBase implements PropertyAccessContext
}
}
/**
* Lower isset/empty/coalesce without exposing the Native pointer to the
* generic Variant chain walker. Repeated offsetExists/offsetGet operations
* share one receiver and key evaluation, matching PHP ArrayAccess order.
*/
protected function parseNativeArrayAccessPresence(
Expr\ArrayDimFetch $access,
string $op,
bool $getValue,
): string {
if ($access->dim === null) {
$this->fatalError($access, 'Cannot use [] for reading');
}
if ($op === self::OP_REFVAL) {
$this->assertNativeArrayAccessReferenceForbidden($access);
}
if (!in_array($op, [self::OP_ISSET, self::OP_EMPTY, self::OP_NOT_EMPTY], true)) {
$this->fatalError($access, 'Unsupported Native ArrayAccess operation');
}
if ($op === self::OP_ISSET && !$getValue) {
return $this->parseNativeArrayAccessCall(
$access,
'offsetExists',
[new Node\Arg($access->dim)],
);
}
$receiver = $access->var;
$class = $this->getNativeArrayAccessClass($receiver, $access);
if (!$this->isVarExpr($receiver)) {
$receiverName = $this->materializeNativeObjectReceiver($receiver, $class);
$receiver = new Expr\Variable($receiverName, $receiver->getAttributes());
}
$key = $this->addTmpVar(Type::VAR);
$keyExpr = $this->parseOrderedOperand($access->dim, false);
$this->context->beforeStmtLines[] = $key . ' = ' . $keyExpr . ';';
$stableAccess = new Expr\ArrayDimFetch(
$receiver,
new Expr\Variable($key, $access->dim->getAttributes()),
$access->getAttributes(),
);
$exists = $this->parseNativeArrayAccessCall(
$stableAccess,
'offsetExists',
[new Node\Arg($stableAccess->dim)],
);
$value = $this->parseNativeArrayAccessCall(
$stableAccess,
'offsetGet',
[new Node\Arg($stableAccess->dim)],
);
if ($getValue) {
$result = $this->addTmpVar(Type::VAR);
$access->setAttribute('chainOpResult', $result);
return '(' . $exists . ' && ((' . $result . ' = ' . $value . '), true))';
}
if ($op === self::OP_EMPTY) {
return '(!(' . $exists . ') || !php::notEmpty(' . $value . '))';
}
return '(' . $exists . ' && php::notEmpty(' . $value . '))';
}
/**
* Lower a named Native property chain without converting its raw pointers
* to Variant. The short-circuit lambda preserves PHP's isset()/empty()

@ -357,13 +357,107 @@ trait NativeClassSupportTrait
}
}
protected function assertNotNativeObjectArrayDimensionReceiver(
/**
* Return the statically known Native receiver class for array syntax.
* PHP only dispatches [] through ArrayAccess; having similarly named
* methods without the interface is not sufficient.
*/
protected function getNativeArrayAccessClass(
NodeAbstract $receiver,
NodeAbstract $errorNode,
): ?string {
$class = $this->detectClassOfExpr($receiver);
if (!$this->isNativeObjectClass($class)) {
return null;
}
$implementsArrayAccess = false;
foreach ($this->getClassImplementedInterfaces($this->getClass($class)) as $interface) {
if (strcasecmp(ltrim($interface, '\\'), 'ArrayAccess') === 0) {
$implementsArrayAccess = true;
break;
}
}
if (!$implementsArrayAccess) {
$this->fatalError(
$errorNode,
"Native class `{$class}` must implement `ArrayAccess` to use array access syntax",
);
}
return $class;
}
/** Locate the first Native ArrayAccess dimension inside a writable chain. */
protected function findNativeArrayAccessDimension(NodeAbstract $expression): ?Node\Expr\ArrayDimFetch
{
if ($expression instanceof Node\Expr\ArrayDimFetch) {
if ($this->isNativeObjectClass($this->detectClassOfExpr($expression->var))) {
return $expression;
}
return $this->findNativeArrayAccessDimension($expression->var);
}
if ($expression instanceof Node\Expr\PropertyFetch
|| $expression instanceof Node\Expr\NullsafePropertyFetch
) {
return $this->findNativeArrayAccessDimension($expression->var);
}
return null;
}
protected function assertNativeArrayAccessDirectWrite(
NodeAbstract $target,
bool $allowDirectDimension,
): void {
if ($this->isNativeObjectClass($this->detectClassOfExpr($receiver))) {
$this->fatalError($errorNode, 'Native objects do not support array dimension access');
$dimension = $this->findNativeArrayAccessDimension($target);
if ($dimension === null) {
return;
}
$this->getNativeArrayAccessClass($dimension->var, $dimension);
if ($allowDirectDimension && $dimension === $target) {
return;
}
$this->fatalError(
$target,
'Indirect modification of Native ArrayAccess elements is not supported',
);
}
protected function assertNativeArrayAccessReferenceForbidden(NodeAbstract $expression): void
{
$dimension = $this->findNativeArrayAccessDimension($expression);
if ($dimension === null) {
return;
}
$this->getNativeArrayAccessClass($dimension->var, $dimension);
$this->fatalError(
$expression,
'References to Native ArrayAccess elements are not supported',
);
}
/**
* Build a normal MethodCall so Native dispatch, visibility, signature
* checks, virtual thunks and PHP evaluation ordering remain centralized.
*
* @param list<Node\Arg> $arguments
*/
protected function parseNativeArrayAccessCall(
Node\Expr\ArrayDimFetch $access,
string $method,
array $arguments,
): ?string {
if ($this->getNativeArrayAccessClass($access->var, $access) === null) {
return null;
}
if ($access->dim !== null) {
$this->assertNotNativeObjectArrayKey($access->dim);
}
return $this->parseMethodCall(new Node\Expr\MethodCall(
$access->var,
new Node\Identifier($method),
$arguments,
$access->getAttributes(),
));
}
protected function assertNotNativeObjectDynamicClassTarget(

@ -110,6 +110,14 @@ trait ArrayExpressionTrait
protected function parseWritableIdentifier(NodeAbstract $expr): string
{
if ($expr instanceof Expr\ArrayDimFetch) {
$dimension = $this->findNativeArrayAccessDimension($expr);
if ($dimension !== null) {
$this->getNativeArrayAccessClass($dimension->var, $dimension);
$this->fatalError(
$expr,
'Indirect modification of Native ArrayAccess elements is not supported',
);
}
return $this->parseArrayDimFetchUpdate($expr);
}
@ -174,7 +182,16 @@ trait ArrayExpressionTrait
protected function parseArrayDimFetch(Expr\ArrayDimFetch $node): string
{
$this->assertNotNativeObjectArrayDimensionReceiver($node->var, $node);
if ($this->isNativeObjectClass($this->detectClassOfExpr($node->var))) {
if ($node->dim === null) {
$this->fatalError($node, 'Cannot use [] for reading');
}
return $this->parseNativeArrayAccessCall(
$node,
'offsetGet',
[new Node\Arg($node->dim)],
);
}
if ($node->dim !== null) {
$this->assertNotNativeObjectArrayKey($node->dim);
}

@ -24,9 +24,6 @@ trait AssignOpTrait
if ($left instanceof Expr\ArrayDimFetch && $left->dim !== null) {
$this->assertNotNativeObjectArrayKey($left->dim);
}
if ($left instanceof Expr\ArrayDimFetch) {
$this->assertNotNativeObjectArrayDimensionReceiver($left->var, $left);
}
if ($this->isPropertyFetch($left)) {
return $this->parseAssignPropertyArrayDim($left, $right);
}
@ -236,6 +233,45 @@ trait AssignOpTrait
protected function parseAssignFinally(Expr $left, Expr $right): string
{
$this->assertNotNullsafeWriteContext($left);
$this->assertNativeArrayAccessDirectWrite($left, true);
if ($left instanceof Expr\ArrayDimFetch
&& $this->isNativeObjectClass($this->detectClassOfExpr($left->var))
) {
if ($this->isNativeObjectClass($this->detectClassOfExpr($right))) {
$this->fatalError(
$right,
'Native objects cannot cross a PHP/ZendVM argument boundary',
);
}
$result = $this->addTmpVar(Type::VAR);
$key = $this->addTmpVar(Type::VAR);
$store = $this->parseNativeArrayAccessCall(
$left,
'offsetSet',
[
new Node\Arg(new Variable($key)),
new Node\Arg(new Variable($result)),
],
);
if ($left->dim === null) {
$keyExpr = self::VALUE_NULL;
$keyBefore = $keyAfter = [];
} else {
[$keyExpr, $keyBefore, $keyAfter] = $this->parseExprWithCapturedStmts($left->dim);
}
[$rightExpr, $rightBefore, $rightAfter] = $this->parseExprWithCapturedStmts($right);
$code = '[&]() -> php::Var {' . PHP_EOL;
$code .= $this->formatCapturedStmtLines($keyBefore);
$code .= $this->getIndent() . $key . ' = ' . $keyExpr . ';' . PHP_EOL;
$code .= $this->formatCapturedStmtLines($keyAfter);
$code .= $this->formatCapturedStmtLines($rightBefore);
$code .= $this->getIndent() . $result . ' = ' . $rightExpr . ';' . PHP_EOL;
$code .= $this->formatCapturedStmtLines($rightAfter);
$code .= $this->getIndent() . $store . ';' . PHP_EOL;
$code .= $this->getIndent() . 'return ' . $result . ';' . PHP_EOL;
return $code . $this->getIndent() . '}()';
}
$rightClass = $this->detectClassOfExpr($right);
// A Native-element std container owns a PHPX Box but its raw pointer
@ -703,6 +739,7 @@ trait AssignOpTrait
protected function parseAssignOp(Expr\AssignOp $node, string $op): string
{
$this->assertNativeArrayAccessDirectWrite($node->var, false);
$this->assertNativeObjectOperatorOperandSupported($node->var, $node, $op);
$this->assertNotNullsafeWriteContext($node->var);
$this->assertNativePropertyHookDirectWriteTarget($node->var);
@ -1035,6 +1072,8 @@ trait AssignOpTrait
protected function parseAssignRef(Expr\AssignRef $expr): string
{
$this->assertNativeArrayAccessReferenceForbidden($expr->var);
$this->assertNativeArrayAccessReferenceForbidden($expr->expr);
$this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->expr);
@ -1179,6 +1218,7 @@ trait AssignOpTrait
protected function parseAssignOpCoalesce(Expr\AssignOp\Coalesce $expr): string
{
$this->assertNativeArrayAccessDirectWrite($expr->var, false);
$this->checkLeftValue($expr->var);
$rightClass = $this->detectClassOfExpr($expr->expr);

@ -788,11 +788,16 @@ trait PropertyAccessTrait
$this->assertNotNullsafeWriteContext($var);
$this->assertNativePropertyHookDirectWriteTarget($var);
if ($this->isArrayDimFetch($var)) {
$this->assertNotNativeObjectArrayDimensionReceiver($var->var, $var);
if ($var->dim === null) {
$this->fatalError($var, 'Cannot use [] for array unset');
}
if ($this->isStdContainerExpr($var)) {
if ($this->isNativeObjectClass($this->detectClassOfExpr($var->var))) {
$lines[] = $this->parseNativeArrayAccessCall(
$var,
'offsetUnset',
[new Node\Arg($var->dim)],
) . ';';
} elseif ($this->isStdContainerExpr($var)) {
$lines[] = $this->parseStdContainerOffsetUnset($var) . ';';
} else {
$array = $this->parseIdentifier($var->var);

@ -256,7 +256,11 @@ trait SelectionExpressionTrait
if ($this->isNativeObjectClass($nativeClass)) {
return $this->parseNativeValueSelection($left, $right, $nativeClass);
}
$leftExpr = $this->parseIdentifier($left);
$nativeArrayAccessLeft = $left instanceof Expr\ArrayDimFetch
&& $this->isNativeObjectClass($this->detectClassOfExpr($left->var));
// Native ArrayAccess coalesce must let the presence helper evaluate
// offsetExists() before offsetGet(), with receiver/key evaluated once.
$leftExpr = $nativeArrayAccessLeft ? '' : $this->parseIdentifier($left);
if ($this->isVarExpr($left)) {
$this->checkVarMustExist($left, $leftExpr);
}

@ -0,0 +1,107 @@
--TEST--
Native class: ArrayAccess syntax lowers to direct native method calls
--FILE--
<?php
#[Native]
class NativeArrayBag implements ArrayAccess
{
public array $values = [];
public function offsetExists(mixed $offset): bool
{
return isset($this->values[$offset]);
}
public function offsetGet(mixed $offset): mixed
{
return $this->values[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
if ($offset === null) {
$this->values[] = $value;
} else {
$this->values[$offset] = $value;
}
}
public function offsetUnset(mixed $offset): void
{
unset($this->values[$offset]);
}
}
function receiver(NativeArrayBag $bag): NativeArrayBag
{
echo 'R';
return $bag;
}
function keyValue(): string
{
echo 'K';
return 'ordered';
}
function assignedValue(): int
{
echo 'V';
return 42;
}
function countedKey(string $key): string
{
echo 'Q';
return $key;
}
function main(): void
{
$bag = new NativeArrayBag();
$bag['first'] = 1;
$assigned = ($bag['second'] = 2);
$bag[] = 3;
$bag['nested'] = ['child' => 7];
var_dump($bag['first']);
var_dump($assigned);
var_dump($bag[0]);
var_dump($bag['nested']['child']);
var_dump(isset($bag['nested']['child']));
var_dump($bag['nested']['child'] ?? 'fallback');
var_dump(isset($bag['second']));
var_dump(empty($bag['missing']));
var_dump($bag['missing'] ?? 'fallback');
var_dump(isset($bag[countedKey('second')]));
var_dump(empty($bag[countedKey('missing')]));
var_dump($bag[countedKey('missing')] ?? 'fallback');
unset($bag['second']);
var_dump(isset($bag['second']));
receiver($bag)[keyValue()] = assignedValue();
echo PHP_EOL;
$ordered = ($bag['result'] = 42);
var_dump($ordered, $bag['ordered']);
}
?>
--EXPECT--
int(1)
int(2)
int(3)
int(7)
bool(true)
int(7)
bool(true)
bool(true)
string(8) "fallback"
Qbool(true)
Qbool(true)
Qstring(8) "fallback"
bool(false)
RKV
int(42)
int(42)
Loading…
Cancel
Save