Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3276,16 +3276,10 @@ parameters:
count: 1
path: tests/Reflection/ReflectionClassTest.php

-
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with false and true will always evaluate to false\.$#'
identifier: method.impossibleType
count: 1
path: tests/Reflection/ReflectionClassTest.php

-
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#'
identifier: method.alreadyNarrowedType
count: 1
count: 2
path: tests/Reflection/ReflectionClassTest.php

-
Expand Down
12 changes: 12 additions & 0 deletions src/ClassExtension/Hook/CastObjectHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ public function getCastType(): int
return $this->type;
}

/**
* Returns the cast type as a named case, or null for an id unknown to this PHP line
*
* Prefer this over comparing getCastType() against numeric constants: the cast-only type ids
* have shifted between PHP minors before, and the enum is guarded against the generated
* engine ground truth.
*/
public function getCastTypeEnum(): ?CastType
{
return CastType::tryFrom($this->type);
}

/**
* Returns an object instance
*/
Expand Down
47 changes: 47 additions & 0 deletions src/ClassExtension/Hook/CastType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/**
* Z-Engine framework
*
* @copyright Copyright 2026, Lisachenko Alexander <lisachenko.it@gmail.com>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*
*/
declare(strict_types=1);

namespace ZEngine\ClassExtension\Hook;

/**
* Named view of the type ids the engine passes to a cast_object handler
*
* The backing values are the zval type ids from Zend/zend_types.h for the PHP minor this branch
* targets, and they are guarded against the generated ground truth by EngineConstantsTest. Prefer
* dispatching on these cases over raw ReflectionValue constants: the cast-only ids have moved
* between PHP minors before (PHP 8.1 inserted IS_NEVER = 17, shifting _IS_BOOL and _IS_NUMBER up
* by one), and a namesake constant with a stale value misroutes casts silently.
*/
enum CastType: int
{
/** IS_LONG: explicit (int) casts and integer coercion */
case Long = 4;

/** IS_DOUBLE: explicit (float) casts and float coercion */
case Double = 5;

/** IS_STRING: explicit (string) casts, string interpolation and echo */
case String = 6;

/** IS_ARRAY: passed by extensions that invoke cast_object directly with an array target */
case Array = 7;

/** IS_OBJECT: passed by extensions that invoke cast_object directly with an object target */
case Object = 8;

/** _IS_BOOL: explicit (bool) casts and every boolean context (if, &&, !, ...) */
case Bool = 18;

/** _IS_NUMBER: numeric coercion where either int or float is acceptable */
case Number = 19;
}
8 changes: 8 additions & 0 deletions src/ClassExtension/Hook/GetPropertiesForHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ public function getPurpose(): int
return $this->purpose;
}

/**
* Returns the purpose as a named case, or null for a value unknown to this PHP line
*/
public function getPurposeEnum(): ?PropertyPurpose
{
return PropertyPurpose::tryFrom($this->purpose);
}

/**
* Proceeds with default handler
*/
Expand Down
44 changes: 44 additions & 0 deletions src/ClassExtension/Hook/PropertyPurpose.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* Z-Engine framework
*
* @copyright Copyright 2026, Lisachenko Alexander <lisachenko.it@gmail.com>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*
*/
declare(strict_types=1);

namespace ZEngine\ClassExtension\Hook;

/**
* Named view of the zend_prop_purpose values the engine passes to a get_properties_for handler
*
* The backing values mirror the zend_prop_purpose enumeration in Zend/zend_object_handlers.h for
* the PHP minor this branch targets. They are not yet exported by the header generator manifest,
* so unlike CastType they carry no generated-ground-truth guard — the enumeration has been stable
* since PHP 7.4 introduced it, but verify against zend_object_handlers.h when bumping the branch
* to a new minor.
*/
enum PropertyPurpose: int
{
/** ZEND_PROP_PURPOSE_DEBUG: var_dump() and friends; supersedes the get_debug_info handler */
case Debug = 0;

/** ZEND_PROP_PURPOSE_ARRAY_CAST: explicit (array) casts */
case ArrayCast = 1;

/** ZEND_PROP_PURPOSE_SERIALIZE: serialize() using the "O" scheme */
case Serialize = 2;

/** ZEND_PROP_PURPOSE_VAR_EXPORT: var_export(); the data is passed to __set_state() */
case VarExport = 3;

/** ZEND_PROP_PURPOSE_JSON: json_encode() */
case Json = 4;

/** ZEND_PROP_PURPOSE_GET_OBJECT_VARS: get_object_vars() */
case GetObjectVars = 5;
}
5 changes: 3 additions & 2 deletions src/Reflection/ReflectionValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class ReflectionValue implements ReferenceCountedInterface
public const IS_VOID = 14;
public const IS_STATIC = 15;
public const IS_MIXED = 16;
public const IS_NEVER = 17;

/* internal types */
public const IS_INDIRECT = 12;
Expand All @@ -132,8 +133,8 @@ class ReflectionValue implements ReferenceCountedInterface
public const _IS_ERROR = 15;

/* used for casts */
public const _IS_BOOL = 17;
public const _IS_NUMBER = 18;
public const _IS_BOOL = 18;
public const _IS_NUMBER = 19;

private const Z_TYPE_FLAGS_MASK = 0xFF00;

Expand Down
36 changes: 32 additions & 4 deletions tests/EngineConstantsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use ZEngine\AbstractSyntaxTree\NodeKind;
use ZEngine\ClassExtension\Hook\CastType;
use ZEngine\Reflection\ReflectionValue;
use ZEngine\System\OpCode;

/**
Expand All @@ -31,10 +33,12 @@ final class EngineConstantsTest extends TestCase
public static function constantOwnerProvider(): array
{
return [
'ZEND_ACC_* on Core' => [Core::class, 'ZEND_ACC_'],
'ZEND_PROPERTY_HOOK_* on Core' => [Core::class, 'ZEND_PROPERTY_HOOK_'],
'opcodes on OpCode' => [OpCode::class, ''],
'AST kinds on NodeKind' => [NodeKind::class, 'AST_'],
'ZEND_ACC_* on Core' => [Core::class, 'ZEND_ACC_'],
'ZEND_PROPERTY_HOOK_* on Core' => [Core::class, 'ZEND_PROPERTY_HOOK_'],
'opcodes on OpCode' => [OpCode::class, ''],
'AST kinds on NodeKind' => [NodeKind::class, 'AST_'],
'zval type ids on ReflectionValue' => [ReflectionValue::class, 'IS_'],
'cast/internal ids on ReflectionValue' => [ReflectionValue::class, '_IS_'],
];
}

Expand Down Expand Up @@ -77,6 +81,30 @@ public function testModuleApiVersionMatchesRunningEngine(): void
);
}

public function testCastTypeCasesMatchGeneratedGroundTruth(): void
{
$generated = self::loadGeneratedConstants();
$symbolByCase = [
'Long' => 'IS_LONG',
'Double' => 'IS_DOUBLE',
'String' => 'IS_STRING',
'Array' => 'IS_ARRAY',
'Object' => 'IS_OBJECT',
'Bool' => '_IS_BOOL',
'Number' => '_IS_NUMBER',
];

foreach (CastType::cases() as $case) {
$this->assertArrayHasKey($case->name, $symbolByCase, "CastType::{$case->name} has no engine symbol mapping in this test");
$engineName = $symbolByCase[$case->name];
$this->assertSame(
$generated[$engineName],
$case->value,
"CastType::{$case->name} ({$case->value}) does not match engine {$engineName} ({$generated[$engineName]})",
);
}
}

/**
* @return array<string, int>
*/
Expand Down
28 changes: 14 additions & 14 deletions tests/Reflection/ReflectionClassTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\TestCase;
use ZEngine\ClassExtension\Hook\CastObjectHook;
use ZEngine\ClassExtension\Hook\CastType;
use ZEngine\ClassExtension\Hook\CloneObjectHook;
use ZEngine\ClassExtension\Hook\CompareValuesHook;
use ZEngine\ClassExtension\Hook\CreateObjectHook;
Expand Down Expand Up @@ -293,19 +294,18 @@ public function testInstallCastObjectHandler(): void
$handler = Closure::fromCallable([ObjectCreateTrait::class, '__init']);
$this->refClass->setCreateObjectHandler($handler);
$this->refClass->setCastObjectHandler(function (CastObjectHook $hook) {
$castType = $hook->getCastType();
switch ($castType) {
case ReflectionValue::IS_LONG:
case ReflectionValue::_IS_NUMBER:
return 1;
case ReflectionValue::IS_DOUBLE:
return 2.0;
case ReflectionValue::IS_STRING:
return 'test';
case ReflectionValue::_IS_BOOL:
return false;
}
throw new \UnexpectedValueException('Unknown type ' . ReflectionValue::name($castType));
return match ($hook->getCastTypeEnum()) {
CastType::Long, CastType::Number => 1,
CastType::Double => 2.0,
CastType::String => 'test',
// The engine accepts a boolean cast result only as IS_TRUE/IS_FALSE: a handler
// misrouted into the numeric branch would produce long(1), which the engine
// reports as false - so asserting true below proves the _IS_BOOL id is correct
CastType::Bool => true,
default => throw new \UnexpectedValueException(
'Unknown type ' . ReflectionValue::name($hook->getCastType()),
),
};
});

$testClass = new TestClass();
Expand All @@ -316,7 +316,7 @@ public function testInstallCastObjectHandler(): void
$string = (string) $testClass;
$this->assertSame('test', $string);
$bool = (bool) $testClass;
$this->assertSame(false, $bool);
$this->assertTrue($bool);
$this->markTestIncomplete('Initialization object handler brings segfaults thus run it separately');
}

Expand Down
Loading