- Implement ArrayDef attribute for compile-time array type contracts - Add support for list and map type definitions with key/value constraints - Create ArrayDefinition and ArrayDefWritePlan data structures - Integrate ArrayDef validation into assignment operations - Add documentation for ArrayDef usage and limitations - Support class value types, subclasses, and dynamic type checking - Enable static property array dimension assignments - Optimize literal assignments into variable declarations - Add comprehensive test cases for various ArrayDef scenariosmaster
parent
68bdedaa31
commit
d33073c021
40 changed files with 1376 additions and 14 deletions
@ -0,0 +1,37 @@ |
|||||||
|
# `#[ArrayDef]` compile-time array contracts |
||||||
|
|
||||||
|
`#[ArrayDef]` attaches key/value type information to a property declared |
||||||
|
exactly as `array`. It supports both Zend classes and `#[Native]` classes and |
||||||
|
has no runtime metadata or per-read overhead. |
||||||
|
|
||||||
|
```php |
||||||
|
class Index |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::String)] |
||||||
|
public array $names = []; // list<string> |
||||||
|
|
||||||
|
#[ArrayDef(Type::Int, Type::String)] |
||||||
|
public array $labels = []; // map<int, string> |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
One argument defines a list value type. Two arguments define a map key type |
||||||
|
and value type. Map keys are restricted to `Type::Int` or `Type::String`. |
||||||
|
`ClassName::class` is therefore valid only as a list element type or as the |
||||||
|
second (value) argument of a map. |
||||||
|
|
||||||
|
For direct writes whose expression types are known, the compiler either emits |
||||||
|
the normal write unchanged or reports a fatal type error. An `any` key/value is |
||||||
|
checked with PHPX exact-type helpers at runtime. No coercive `intval()` or |
||||||
|
string conversion is performed. |
||||||
|
|
||||||
|
List writes support `[]`, an existing integer index, and the exact append form |
||||||
|
`$object->property[count($object->property)]`. Existing-index writes use |
||||||
|
`php::safeIndex()`; negative and out-of-range indexes fail at runtime. Maps do |
||||||
|
not support `[]` append writes. |
||||||
|
|
||||||
|
The contract intentionally applies only to direct element assignment lowered |
||||||
|
by TypePHP. Reads and in-place operators are unchanged. Values passed through |
||||||
|
dynamic functions, callbacks, Reflection, `eval()`, or other ZendVM escape |
||||||
|
paths are outside the contract and have undefined behavior from ArrayDef's |
||||||
|
perspective. |
||||||
@ -0,0 +1,302 @@ |
|||||||
|
# TypePHP 三套对象存储与传递模型 |
||||||
|
|
||||||
|
> 状态:当前架构约束。本文解释 TypePHP 为什么同时保留 Zend Object、PHPX Box 和 |
||||||
|
> Native Class Object 三套对象式值模型,以及它们各自的所有权、传递方式和边界。 |
||||||
|
|
||||||
|
## 1. 结论 |
||||||
|
|
||||||
|
TypePHP 当前存在三套对象存储与传递机制: |
||||||
|
|
||||||
|
1. 普通 PHP/Zend Object; |
||||||
|
2. PHPX Box,包括 Std Container 和高精度类型; |
||||||
|
3. `#[Native]` Native Class Object。 |
||||||
|
|
||||||
|
三者并非同一设计的历史残留,而是分别解决三类互相冲突的问题: |
||||||
|
|
||||||
|
- Zend Object 保留 PHP 的动态对象语义和 ZendVM 生态兼容性; |
||||||
|
- Box 为无法完整写进 PHP 类型声明的 C++ 类型提供不透明 Zend value 载体; |
||||||
|
- Native Class Object 为静态可知的业务对象提供接近 C/C++ 的固定布局、裸指针调用和 |
||||||
|
tracing GC。 |
||||||
|
|
||||||
|
任何一种机制都不能在不损失另一种机制核心能力的前提下替代其余两种。当前设计明确接受 |
||||||
|
三套模型长期共存,不以“统一对象表示”为目标。 |
||||||
|
|
||||||
|
## 2. 总览 |
||||||
|
|
||||||
|
| 维度 | Zend Object | PHPX Box | Native Class Object | |
||||||
|
| --- | --- | --- | --- | |
||||||
|
| 典型值 | 普通 PHP class 实例 | Std Container、BigInt、BigFloat、Decimal | `#[Native] class` 实例 | |
||||||
|
| 主要表示 | `zend_object` / zval | `zend_resource` + `php::Box *` | Native Heap 中的 C++ struct + 裸指针 | |
||||||
|
| 类型身份 | `zend_class_entry *` | Box C++ 动态类型、`type_info`/类型 ID | 编译期 Native class,descriptor 保存动态类型 | |
||||||
|
| 生命周期 | Zend 引用计数 + Zend 循环 GC | Zend resource 引用计数调用 Box destructor | Wren 风格精确、非移动 mark-sweep GC | |
||||||
|
| 参数传递 | `php::Object` / `php::Var`,复制句柄并调整 RC | `php::Var` 携带 resource;热路径提取具体 C++ 引用 | 具体 `NativeClass *` 按值传递,不调整 RC | |
||||||
|
| 属性/方法访问 | Zend handlers、动态查找或已缓存 Native Call | 编译器根据具体 Box 类型生成操作 | 固定偏移字段访问和确定的 `php_*` Native Call | |
||||||
|
| 动态 PHP 互操作 | 完整 | 作为不透明 resource 有限互操作 | 不可进入 ZendVM value 边界 | |
||||||
|
| 循环图处理 | Zend GC 可扫描 Zend object graph | Zend GC 不扫描 Box 内部 C++ 对象图 | Native descriptor 精确 trace Native pointer graph | |
||||||
|
| 核心目标 | PHP 兼容性 | 携带 C++ 泛型/扩展值 | 极致静态性能 | |
||||||
|
|
||||||
|
## 3. 普通 PHP/Zend Object |
||||||
|
|
||||||
|
### 3.1 存储 |
||||||
|
|
||||||
|
普通 class 注册到 ZendVM,实例由 `zend_object` 表示。TypePHP 通过 `php::Object`、 |
||||||
|
`php::Variant`/`php::Var` 等 PHPX RAII 类型持有对应 zval。 |
||||||
|
|
||||||
|
对象具有 Zend 的 class entry、属性表、对象 handlers 和方法元数据。根据编译期信息, |
||||||
|
TypePHP 可以把部分访问优化为确定的 Native Call,但对象身份和生命周期仍属于 ZendVM。 |
||||||
|
|
||||||
|
### 3.2 传递和生命周期 |
||||||
|
|
||||||
|
PHP 对象赋值和参数传递复制对象句柄,不复制对象实体,并遵循 Zend 引用计数。对象图中的 |
||||||
|
循环引用由 Zend GC 处理。对象可以自然进入: |
||||||
|
|
||||||
|
- PHP array 和普通对象属性; |
||||||
|
- `mixed`/`object` 变量; |
||||||
|
- Closure、Generator、Fiber 和动态调用; |
||||||
|
- Reflection、序列化和扩展函数; |
||||||
|
- ZendVM 执行的 PHP 代码。 |
||||||
|
|
||||||
|
### 3.3 必须保留的原因 |
||||||
|
|
||||||
|
只有 Zend Object 能完整承载 PHP 的运行时对象语义。用 Box 替代会丢失 class entry、对象 |
||||||
|
handlers、可见性、Reflection 和动态分派;用 Native Object 替代则会失去 ZendVM 可见性, |
||||||
|
并迫使所有动态行为退化为编译期限制。 |
||||||
|
|
||||||
|
普通 PHP class 因此始终使用 Zend Object。编译器可以优化调用,但不能改变其对象模型。 |
||||||
|
|
||||||
|
## 4. PHPX Box |
||||||
|
|
||||||
|
### 4.1 存储 |
||||||
|
|
||||||
|
`php::Box` 是由 PHPX 管理的 C++ 多态基类。Box 指针注册为 Zend resource,并由 |
||||||
|
`php::Var` 携带: |
||||||
|
|
||||||
|
```text |
||||||
|
zval(IS_RESOURCE) |
||||||
|
-> zend_resource |
||||||
|
-> php::Box* |
||||||
|
-> concrete C++ value |
||||||
|
``` |
||||||
|
|
||||||
|
Zend resource 的析构回调最终调用 `Box::destroy()`。Box 因而可以经过普通 zval/Variant |
||||||
|
调用边界,同时隐藏 Zend 无法表达的具体 C++ 类型。 |
||||||
|
|
||||||
|
当前主要使用者包括: |
||||||
|
|
||||||
|
- `StdContainerBox<std::vector<T>>`; |
||||||
|
- `StdContainerBox<std::array<T, N>>`; |
||||||
|
- `StdContainerBox<map-like type>`; |
||||||
|
- BigInt、BigFloat、Decimal 等高精度值。 |
||||||
|
|
||||||
|
### 4.2 Std Container 的热路径 |
||||||
|
|
||||||
|
Std Container 局部变量具有两层表示: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::Var values = php::Var(new php::StdContainerBox<Container>(type_id)); |
||||||
|
auto &values_ref = values.toBox<php::StdContainerBox<Container>>()->container; |
||||||
|
``` |
||||||
|
|
||||||
|
`php::Var` 负责生命周期和必要的边界传递,具体容器引用用于后续元素访问,避免每次操作都 |
||||||
|
重复提取 Box。容器的 key/value/长度等泛型信息由编译器和具体 C++ 模板类型共同保存。 |
||||||
|
|
||||||
|
Std Container 跨 TypePHP 函数传递时,PHP 函数签名无法表达以下 C++ 类型信息: |
||||||
|
|
||||||
|
```text |
||||||
|
std::vector<int> |
||||||
|
std::vector<string> |
||||||
|
std::map<string, App\User> |
||||||
|
``` |
||||||
|
|
||||||
|
PHP 参数最多只能声明一个非泛型类名或伪类型,不能同时携带容器种类、key 类型、value |
||||||
|
类型、数组维度和长度。当前使用 `UnsafePtr`/`std::unsafe_cast()` 加编译器类型 ID 校验, |
||||||
|
而不是把所有组合生成为 PHP class。 |
||||||
|
|
||||||
|
理论上可以增加参数和返回值注解描述泛型,但这要求每个声明、调用、返回、属性和传播点 |
||||||
|
都维护额外元数据,PHP Reflection 仍无法完整表达它。当前不引入这套独立泛型 ABI。 |
||||||
|
|
||||||
|
### 4.3 Box 的边界 |
||||||
|
|
||||||
|
Box 是不透明值载体,不是通用对象系统: |
||||||
|
|
||||||
|
- Zend GC 只看见 resource,不会扫描 Box 内部保存的 C++ 引用; |
||||||
|
- Box 不提供 PHP class 的方法表、属性表、继承和 Reflection; |
||||||
|
- 通过 `dynamic_cast`、类型 ID 或专用 helper 恢复具体类型; |
||||||
|
- 不应使用 Box 构建需要跨 Zend/Box 双向追踪的任意循环对象图; |
||||||
|
- Std Container 的可用位置和逃逸路径继续受编译器限制。 |
||||||
|
|
||||||
|
Box 适合数值、容器和其他边界明确的扩展值。它不适合代替具有任意字段引用关系的 Native |
||||||
|
业务对象。 |
||||||
|
|
||||||
|
### 4.4 必须保留的原因 |
||||||
|
|
||||||
|
Std Container 的泛型类型无法由 PHP 函数参数完整表达;高精度值又需要作为 `php::Var` |
||||||
|
参与现有运算和调用。Box 同时提供: |
||||||
|
|
||||||
|
- 可放进 zval 的稳定载体; |
||||||
|
- C++ 具体类型的运行时恢复; |
||||||
|
- Zend request 生命周期内的自动析构; |
||||||
|
- 不为每一种模板实例注册一套 PHP class 的轻量实现。 |
||||||
|
|
||||||
|
Zend Object 无法直接表达 C++ 模板实例;Native 裸指针则无法安全穿过 `php::Var` 和动态 |
||||||
|
ZendVM 边界。因此 Box 仍有独立存在的必要。 |
||||||
|
|
||||||
|
## 5. Native Class Object |
||||||
|
|
||||||
|
### 5.1 存储 |
||||||
|
|
||||||
|
`#[Native]` class 不注册 Zend class,不生成 Zend object handlers,也没有 zval 表示。每个 |
||||||
|
对象是 Native Heap 中的固定布局 C++ struct,TypePHP 局部变量、参数、返回值和字段保存 |
||||||
|
具体 Native 指针: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php_app__point *point; |
||||||
|
``` |
||||||
|
|
||||||
|
方法继续使用 TypePHP 的自由函数 ABI: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::Float php_app__point__length(php_app__point &this_); |
||||||
|
``` |
||||||
|
|
||||||
|
普通调用只传递一个指针值。不会创建 zval、注册 resource、执行引用计数或通过 |
||||||
|
`zend_call_function()`。 |
||||||
|
|
||||||
|
### 5.2 生命周期 |
||||||
|
|
||||||
|
Native Object 使用 PHPX 中独立的 Wren 风格精确、非移动、stop-the-world mark-sweep GC: |
||||||
|
|
||||||
|
- Native 局部变量、参数、返回临时值和 global/static slot 进入精确 root frame; |
||||||
|
- Native 对象 descriptor 负责 trace Native pointer 字段; |
||||||
|
- Std Container 保存 Native pointer 时注册专用 container root frame; |
||||||
|
- 循环引用由 tracing GC 回收,不依赖引用计数降为零; |
||||||
|
- 16-byte GC header 保存收集器所需的最小状态; |
||||||
|
- `__destruct()` 由 Native finalization 执行,而不是由 Zend object destructor 执行。 |
||||||
|
|
||||||
|
Native 指针赋值不增加引用计数,也不需要 write barrier。固定字段直接按 C++ 偏移访问。 |
||||||
|
|
||||||
|
### 5.3 传递边界 |
||||||
|
|
||||||
|
Native Object 参数和返回值必须显式声明具体 Native class,或受支持的 nullable 具体类型: |
||||||
|
|
||||||
|
```php |
||||||
|
function distance(Point $left, Point $right): float; |
||||||
|
function findPoint(): ?Point; |
||||||
|
``` |
||||||
|
|
||||||
|
这使编译器可以把签名直接生成为 `Point *`。Native Object 不支持: |
||||||
|
|
||||||
|
- 传给 PHP/ZendVM 函数、Closure 或动态 callable; |
||||||
|
- 保存到 PHP array、普通 Zend Object 属性或 `mixed`; |
||||||
|
- 自动转换为 `php::Object`、`php::Var` 或 Interface value; |
||||||
|
- 依靠运行时 class name 恢复类型; |
||||||
|
- 使用通用 PHPX `toObject()` helper 完成装箱或拆箱。Native Class 可以声明自己的 |
||||||
|
`toObject(): object` 方法;关键词调用会直接解析为该 Native Call,并不提供通用 bridge。 |
||||||
|
|
||||||
|
需要进入 PHP API 时,用户必须显式转换数据,例如先调用 Native `toArray(): array`,再把 |
||||||
|
结果传给 `json_encode()`。该转换产生的是数据副本,不保留 Native 对象身份。 |
||||||
|
|
||||||
|
### 5.4 必须保留的原因 |
||||||
|
|
||||||
|
Native Class 的目标是接近 C/C++ 的热路径性能: |
||||||
|
|
||||||
|
- 一个机器字的对象句柄; |
||||||
|
- 固定字段布局; |
||||||
|
- 不进行 Zend RC 增减; |
||||||
|
- 不分配 `zend_object` 或 `zend_resource` carrier; |
||||||
|
- 确定符号 Native Call; |
||||||
|
- 可由 C++ 编译器内联和去虚化。 |
||||||
|
|
||||||
|
若改用 Box,每个 Native Object 都需要 resource/zval 封装、RC 管理和具体类型恢复,而且 |
||||||
|
Zend GC 无法扫描 Box 内部 Native 指针图;这既降低性能,也不能正确替代 Native tracing |
||||||
|
GC。若改用自定义 `zend_object`,虽然能够接入 Zend GC 和动态边界,但对象 header、RC、 |
||||||
|
handlers 和访问路径都会改变 Native Class 的性能定位。 |
||||||
|
|
||||||
|
因此 Native Class 继续使用独立 Native Heap 和裸指针 ABI。 |
||||||
|
|
||||||
|
## 6. 为什么不能统一 |
||||||
|
|
||||||
|
### 6.1 不能全部改为 Zend Object |
||||||
|
|
||||||
|
这样可以统一动态语义,却会让 Std Container 泛型实例和 Native Class 都承担 Zend object |
||||||
|
header、RC、handlers、class registration 与动态访问成本。Native Class 将不再接近 C/C++, |
||||||
|
Std Container 也需要为大量模板组合设计运行时 class 体系。 |
||||||
|
|
||||||
|
### 6.2 不能全部改为 Box |
||||||
|
|
||||||
|
Box 能通过 zval 携带 C++ 值,但 Zend GC 不理解 Box 内部对象图。它不能替代普通 PHP |
||||||
|
Object 的动态元数据,也不能在保持 Native 循环回收能力的同时提供裸指针热路径。 |
||||||
|
|
||||||
|
### 6.3 不能全部改为 Native pointer |
||||||
|
|
||||||
|
Native pointer 要求完整静态类型。普通 PHP 对象需要 Reflection、动态属性、动态 callable |
||||||
|
和 Zend 扩展互操作;Std Container 的完整泛型类型又无法写入 PHP 参数签名。把这些值都 |
||||||
|
改为裸指针会产生无法静态证明安全的类型擦除,并可能导致错误指针转换和崩溃。 |
||||||
|
|
||||||
|
### 6.4 不增加自动桥接 |
||||||
|
|
||||||
|
三套模型之间不进行隐式对象身份转换。自动装箱/拆箱会隐藏分配、复制、RC 和 GC root |
||||||
|
变化,也会使编译器边界不再可靠。 |
||||||
|
|
||||||
|
允许的转换必须具有明确语义: |
||||||
|
|
||||||
|
- Std Container 转 PHP array:复制容器数据; |
||||||
|
- Native Object 的 `toArray()` 等实体方法:由用户定义并显式复制数据; |
||||||
|
- 高精度类型的显式标量转换:产生新的 PHP 标量值; |
||||||
|
- 普通 Zend Object 不会自动变成 Native Object。 |
||||||
|
|
||||||
|
## 7. 编译器实现约束 |
||||||
|
|
||||||
|
后续修改必须保持以下不变量: |
||||||
|
|
||||||
|
1. 先根据静态类型确定对象模型,再选择代码生成路径;不得在运行时猜测三者之一。 |
||||||
|
2. Native Object 不得因通用 fallback 被包装成 `php::Var` 或传入 ZendVM。 |
||||||
|
3. Box 的具体类型恢复必须校验 resource 类型和 concrete C++ 类型/类型 ID。 |
||||||
|
4. Zend Object 优化不得改变 Zend 对象身份、生命周期或动态可见性。 |
||||||
|
5. 三种模型的参数 ABI 不得混用:`php::Object`、Box-bearing `php::Var`、`NativeClass *` |
||||||
|
分别代表不同所有权和类型约束。 |
||||||
|
6. 跨模型转换必须显式,并在文档和生成代码中体现分配或复制成本。 |
||||||
|
7. 若一个新特性需要牺牲所有 Native Class 热路径来获得少量动态兼容,应优先在编译期禁止。 |
||||||
|
8. 若一种新的 C++ 泛型类型需要穿过 Zend value 边界,应优先评估 Box,而不是扩大 Native |
||||||
|
Object 的动态边界。 |
||||||
|
9. 若一个值需要完整 PHP 对象语义,应使用 Zend Object,不能把 Box 当作简化的 PHP class。 |
||||||
|
|
||||||
|
## 8. 代码位置 |
||||||
|
|
||||||
|
主要实现入口: |
||||||
|
|
||||||
|
```text |
||||||
|
普通 Zend Object |
||||||
|
compiler/src/Parser/* |
||||||
|
phpx/include/phpx.h Object / Variant / Zend API wrappers |
||||||
|
|
||||||
|
PHPX Box 与 Std Container |
||||||
|
phpx/include/phpx.h Box / StdContainerBox<T> |
||||||
|
phpx/src/core/base.cc Box resource registration and destructor |
||||||
|
compiler/src/Parser/StdContainerTrait.php |
||||||
|
|
||||||
|
Native Class Object |
||||||
|
compiler/src/NativeClass/ |
||||||
|
compiler/src/Transform/NativeClassAttributeLowering.php |
||||||
|
phpx/include/phpx_native_gc.h |
||||||
|
phpx/src/core/native_gc.cc |
||||||
|
phpx/thirdparty/wren-gc/ |
||||||
|
``` |
||||||
|
|
||||||
|
详细规则分别见 [STD_CONTAINERS.md](STD_CONTAINERS.md)、 |
||||||
|
[NATIVE_CLASS_OBJECT.md](NATIVE_CLASS_OBJECT.md) 和 |
||||||
|
[NATIVE_CLASS_IMPLEMENTATION_AUDIT.md](NATIVE_CLASS_IMPLEMENTATION_AUDIT.md)。 |
||||||
|
|
||||||
|
## 9. 当前决策 |
||||||
|
|
||||||
|
当前阶段不实施以下重构: |
||||||
|
|
||||||
|
- 不移除 Wren GC; |
||||||
|
- 不把 Native Object 改为 Box 或自定义 Zend Object; |
||||||
|
- 不给 Native Object 增加通用 `toObject()` 动态恢复机制;Native Class 自定义的 |
||||||
|
`toObject(): object` 仍是普通的确定 Native Call; |
||||||
|
- 不把 Std Container 改为无法跨签名表达类型的裸指针 ABI; |
||||||
|
- 不尝试用单一统一 wrapper 覆盖三种对象模型。 |
||||||
|
|
||||||
|
未来只有在 PHP 语言层能够稳定表达泛型参数、或者有经过 benchmark 和完整 GC 正确性验证 |
||||||
|
的新 ABI 时,才重新评估这些边界。在此之前,三套机制的共存是有意的架构选择。 |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
class ArrayDefClassMapKeyType {} |
||||||
|
class ArrayDefClassMapKeyBox |
||||||
|
{ |
||||||
|
#[ArrayDef(ArrayDefClassMapKeyType::class, Type::String)] |
||||||
|
public array $value = []; |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<?php |
||||||
|
class ArrayDefInvalidMapKey |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::Bool, Type::String)] |
||||||
|
public array $value = []; |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
<?php |
||||||
|
class ArrayDefMapAppend |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::Int, Type::String)] |
||||||
|
public array $value = []; |
||||||
|
} |
||||||
|
function arrayDefMapAppend(ArrayDefMapAppend $box): void |
||||||
|
{ |
||||||
|
$box->value[] = 'bad'; |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
<?php |
||||||
|
#[Native] |
||||||
|
class ArrayDefNativeValue {} |
||||||
|
class ArrayDefNativeValueBox |
||||||
|
{ |
||||||
|
#[ArrayDef(ArrayDefNativeValue::class)] |
||||||
|
public array $values = []; |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<?php |
||||||
|
class ArrayDefNoArguments |
||||||
|
{ |
||||||
|
#[ArrayDef] |
||||||
|
public array $value = []; |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<?php |
||||||
|
class ArrayDefNonArrayProperty |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::Int)] |
||||||
|
public string $value = ''; |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
<?php |
||||||
|
class ArrayDefExpectedUser {} |
||||||
|
class ArrayDefOtherUser {} |
||||||
|
class ArrayDefClassMismatchBox |
||||||
|
{ |
||||||
|
#[ArrayDef(ArrayDefExpectedUser::class)] |
||||||
|
public array $users = []; |
||||||
|
} |
||||||
|
function arrayDefStaticClassMismatch(ArrayDefClassMismatchBox $box): void |
||||||
|
{ |
||||||
|
$box->users[] = new ArrayDefOtherUser(); |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
<?php |
||||||
|
class ArrayDefStaticKeyMismatch |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::Int, Type::String)] |
||||||
|
public array $value = []; |
||||||
|
} |
||||||
|
function arrayDefStaticKeyMismatch(ArrayDefStaticKeyMismatch $box): void |
||||||
|
{ |
||||||
|
$box->value['bad'] = 'value'; |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
#[Native] |
||||||
|
class ArrayDefStaticValueMismatch |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::String)] |
||||||
|
public array $value = []; |
||||||
|
} |
||||||
|
function arrayDefStaticValueMismatch(ArrayDefStaticValueMismatch $box): void |
||||||
|
{ |
||||||
|
$box->value[] = 123; |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
class ArrayDefStdContainerValueBox |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::Any)] |
||||||
|
public array $values = []; |
||||||
|
} |
||||||
|
function arrayDefStdContainerValue(ArrayDefStdContainerValueBox $box): void |
||||||
|
{ |
||||||
|
$values = std::vector(Type::Int); |
||||||
|
$box->values[] = $values; |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<?php |
||||||
|
class ArrayDefTooManyArguments |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::Int, Type::String, Type::Bool)] |
||||||
|
public array $value = []; |
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
function localLiteralDeclarationInitializer(): void |
||||||
|
{ |
||||||
|
$integer = 42; |
||||||
|
$negative = -7; |
||||||
|
$floating = 1.25; |
||||||
|
$boolean = true; |
||||||
|
$string = 'hello'; |
||||||
|
$nullValue = null; |
||||||
|
|
||||||
|
if ($boolean) { |
||||||
|
$nested = 9; |
||||||
|
} |
||||||
|
|
||||||
|
$computed = 40 + 2; |
||||||
|
|
||||||
|
var_dump( |
||||||
|
$integer, |
||||||
|
$negative, |
||||||
|
$floating, |
||||||
|
$boolean, |
||||||
|
$string, |
||||||
|
$nullValue, |
||||||
|
$nested, |
||||||
|
$computed, |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class InvalidNativeToObjectParameters |
||||||
|
{ |
||||||
|
public function toObject(string $class): object |
||||||
|
{ |
||||||
|
return new stdClass(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new InvalidNativeToObjectParameters(); |
||||||
|
$value->toObject(stdClass::class); |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class InvalidNativeToObjectReturn |
||||||
|
{ |
||||||
|
public function toObject(): array |
||||||
|
{ |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new InvalidNativeToObjectReturn(); |
||||||
|
$value->toObject(); |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeToObject |
||||||
|
{ |
||||||
|
public function toObject(): object |
||||||
|
{ |
||||||
|
return new stdClass(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeToObject(); |
||||||
|
$value->toObject(); |
||||||
|
} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
final class ArrayDefTest extends \BaseTest |
||||||
|
{ |
||||||
|
public function testArrayDefDeclarationAndDirectWriteDiagnostics(): void |
||||||
|
{ |
||||||
|
$this->exec('ArrayDef can only be applied to properties declared as array', 'array-def-non-array-property.php'); |
||||||
|
$this->exec('ArrayDef expects one or two type arguments', 'array-def-no-arguments.php'); |
||||||
|
$this->exec('ArrayDef expects one or two type arguments', 'array-def-too-many-arguments.php'); |
||||||
|
$this->exec('ArrayDef map keys must use Type::Int or Type::String', 'array-def-invalid-map-key.php'); |
||||||
|
$this->exec('ArrayDef map keys must use Type::Int or Type::String', 'array-def-class-map-key.php'); |
||||||
|
$this->exec('ArrayDef map properties do not support append writes', 'array-def-map-append.php'); |
||||||
|
$this->exec('expects key of type int, string given', 'array-def-static-key-mismatch.php'); |
||||||
|
$this->exec('expects value of type string, int given', 'array-def-static-value-mismatch.php'); |
||||||
|
$this->exec('expects value of type ArrayDefExpectedUser, ArrayDefOtherUser given', 'array-def-static-class-mismatch.php'); |
||||||
|
$this->exec('Native class types cannot be used in ArrayDef', 'array-def-native-class-value.php'); |
||||||
|
$this->exec('Std Container values cannot be stored in ArrayDef properties', 'array-def-std-container-value.php'); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
use TypePhp\CompilerTest; |
||||||
|
|
||||||
|
final class LocalVariableInitializerTest extends \BaseTest |
||||||
|
{ |
||||||
|
public function testTopLevelLiteralAssignmentsInitializeDeclarations(): void |
||||||
|
{ |
||||||
|
global $translator; |
||||||
|
|
||||||
|
$compiler = CompilerTest::create(ROOT_PATH); |
||||||
|
$translator = $compiler; |
||||||
|
$source = ROOT_PATH . '/phpunit/code/local-literal-declaration-initializer.php'; |
||||||
|
$compiler->addFiles([$source]); |
||||||
|
$compiler->prepareFile($source); |
||||||
|
$generated = $compiler->convertFile($source); |
||||||
|
$code = file_get_contents($generated); |
||||||
|
|
||||||
|
self::assertIsString($code); |
||||||
|
self::assertStringContainsString('php::Var integer = 42L;', $code); |
||||||
|
self::assertStringContainsString('php::Var negative = -7L;', $code); |
||||||
|
self::assertStringContainsString('php::Var floating = 1.25;', $code); |
||||||
|
self::assertStringContainsString('php::Var boolean = true;', $code); |
||||||
|
self::assertMatchesRegularExpression('/php::Str string = _literal_strings\[\d+\];/', $code); |
||||||
|
self::assertStringContainsString('php::Var nullValue = php::null;', $code); |
||||||
|
|
||||||
|
self::assertStringContainsString('php::Var nested;', $code); |
||||||
|
self::assertStringContainsString('nested = 9L;', $code); |
||||||
|
self::assertStringContainsString('php::Var computed;', $code); |
||||||
|
self::assertStringContainsString('computed = ((40L) + (2L));', $code); |
||||||
|
|
||||||
|
$afterDeclaration = substr( |
||||||
|
$code, |
||||||
|
strpos($code, 'php::Var integer = 42L;') + strlen('php::Var integer = 42L;'), |
||||||
|
); |
||||||
|
self::assertStringNotContainsString('integer = 42L;', $afterDeclaration); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,291 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP. |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\ArrayDef; |
||||||
|
|
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\Node\Expr; |
||||||
|
use PhpParser\NodeAbstract; |
||||||
|
use TypePhp\Entity\PropertyDef; |
||||||
|
use TypePhp\Transform\CompileTimeAttribute; |
||||||
|
use TypePhp\Type; |
||||||
|
|
||||||
|
/** |
||||||
|
* Compile-time-only ArrayDef declaration parsing and direct-write checking. |
||||||
|
* |
||||||
|
* This deliberately does not attempt to guard array values that escape into |
||||||
|
* ZendVM calls. ArrayDef is a static contract for syntax the compiler owns. |
||||||
|
*/ |
||||||
|
trait ArrayDefSupportTrait |
||||||
|
{ |
||||||
|
protected function parseArrayDefinition(Node\Stmt\Property|Node\Param $property): ?ArrayDefinition |
||||||
|
{ |
||||||
|
$attribute = CompileTimeAttribute::find($property, 'ArrayDef'); |
||||||
|
if ($attribute === null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if (!$property->type instanceof Node\Identifier |
||||||
|
|| strtolower($property->type->toString()) !== 'array' |
||||||
|
) { |
||||||
|
$this->fatalError($property, 'ArrayDef can only be applied to properties declared as array'); |
||||||
|
} |
||||||
|
|
||||||
|
$count = count($attribute->args); |
||||||
|
if ($count < 1 || $count > 2) { |
||||||
|
$this->fatalError($attribute, 'ArrayDef expects one or two type arguments'); |
||||||
|
} |
||||||
|
|
||||||
|
$types = []; |
||||||
|
foreach ($attribute->args as $arg) { |
||||||
|
if ($arg->name !== null || !$arg->value instanceof Expr\ClassConstFetch) { |
||||||
|
$this->fatalError($arg, 'ArrayDef arguments must be Type::* or ClassName::class constants'); |
||||||
|
} |
||||||
|
$class = $arg->value->class; |
||||||
|
$constant = $arg->value->name; |
||||||
|
if (!$class instanceof Node\Name || !$constant instanceof Node\Identifier) { |
||||||
|
$this->fatalError($arg, 'ArrayDef arguments must be Type::* or ClassName::class constants'); |
||||||
|
} |
||||||
|
$resolved = $class->getAttribute('resolvedName') ?? $class; |
||||||
|
$resolvedClass = ltrim($resolved->toString(), '\\'); |
||||||
|
if (strcasecmp($constant->toString(), 'class') === 0) { |
||||||
|
if ($this->isNativeObjectClass($resolvedClass)) { |
||||||
|
$this->fatalError($arg, 'Native class types cannot be used in ArrayDef'); |
||||||
|
} |
||||||
|
$types[] = $resolvedClass; |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (strcasecmp($resolvedClass, 'Type') !== 0) { |
||||||
|
$this->fatalError($arg, 'ArrayDef arguments must be Type::* or ClassName::class constants'); |
||||||
|
} |
||||||
|
$types[] = $this->resolveArrayDefType($constant->toString(), $arg); |
||||||
|
} |
||||||
|
|
||||||
|
if ($count === 1) { |
||||||
|
return new ArrayDefinition(null, $types[0]); |
||||||
|
} |
||||||
|
if (!in_array($types[0], [Type::INT, Type::STR], true)) { |
||||||
|
$this->fatalError($attribute, 'ArrayDef map keys must use Type::Int or Type::String'); |
||||||
|
} |
||||||
|
return new ArrayDefinition($types[0], $types[1]); |
||||||
|
} |
||||||
|
|
||||||
|
private function resolveArrayDefType(string $name, NodeAbstract $errorNode): string |
||||||
|
{ |
||||||
|
$type = match (strtolower($name)) { |
||||||
|
'int' => Type::INT, |
||||||
|
'float' => Type::FLOAT, |
||||||
|
'bool' => Type::BOOL, |
||||||
|
'string' => Type::STR, |
||||||
|
'array' => Type::ARRAY, |
||||||
|
'object' => Type::OBJECT, |
||||||
|
'any' => Type::VAR, |
||||||
|
'stream' => Type::STREAM, |
||||||
|
'bigint' => Type::BIGINT, |
||||||
|
'bigfloat' => Type::BIGFLOAT, |
||||||
|
'decimal' => Type::DECIMAL, |
||||||
|
default => null, |
||||||
|
}; |
||||||
|
if ($type === null) { |
||||||
|
$this->fatalError($errorNode, "Unsupported ArrayDef type Type::{$name}"); |
||||||
|
} |
||||||
|
return $type; |
||||||
|
} |
||||||
|
|
||||||
|
protected function prepareArrayDefDirectWrite( |
||||||
|
Expr\ArrayDimFetch $left, |
||||||
|
Expr $right, |
||||||
|
string $value, |
||||||
|
): ?ArrayDefWritePlan |
||||||
|
{ |
||||||
|
$def = $this->getNativePropertyDef($left->var); |
||||||
|
$arrayDef = $def?->arrayDef; |
||||||
|
if ($arrayDef === null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
$value = $this->validateArrayDefWriteValue($left->var, $right, $value, $arrayDef->valueType, 'value'); |
||||||
|
if ($left->dim === null) { |
||||||
|
if (!$arrayDef->isList()) { |
||||||
|
$this->fatalError($left, 'ArrayDef map properties do not support append writes'); |
||||||
|
} |
||||||
|
return new ArrayDefWritePlan(true, null, $value); |
||||||
|
} |
||||||
|
|
||||||
|
if ($arrayDef->isList() && $this->isArrayDefCountAppend($left->dim, $left->var)) { |
||||||
|
return new ArrayDefWritePlan(true, null, $value); |
||||||
|
} |
||||||
|
|
||||||
|
$expectedKey = $arrayDef->keyType ?? Type::INT; |
||||||
|
$key = $this->parseExprAsValue($left->dim); |
||||||
|
$key = $this->validateArrayDefWriteValue($left->var, $left->dim, $key, $expectedKey, 'key'); |
||||||
|
if ($arrayDef->isList()) { |
||||||
|
$array = $this->parseWritableIdentifier($left->var); |
||||||
|
$key = 'php::safeIndex(' . $key . ', ' . $array . '.length())'; |
||||||
|
} |
||||||
|
|
||||||
|
return new ArrayDefWritePlan(false, $key, $value); |
||||||
|
} |
||||||
|
|
||||||
|
private function validateArrayDefWriteValue( |
||||||
|
NodeAbstract $property, |
||||||
|
Expr $expr, |
||||||
|
string $code, |
||||||
|
string $expected, |
||||||
|
string $part, |
||||||
|
): string |
||||||
|
{ |
||||||
|
$actual = $this->detectTypeOfExpr($expr); |
||||||
|
$stdContainerValue = $this->isVarExpr($expr) |
||||||
|
&& $this->isStdContainer($this->parseIdentifier($expr)); |
||||||
|
if ($part === 'value' && ($stdContainerValue || $this->isStdContainerType($actual))) { |
||||||
|
$this->fatalError($expr, 'Std Container values cannot be stored in ArrayDef properties'); |
||||||
|
} |
||||||
|
if ($expected === Type::VAR) { |
||||||
|
return $code; |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->isArrayDefClassType($expected)) { |
||||||
|
$actualClass = $this->detectClassOfExpr($expr); |
||||||
|
if ($actualClass !== '') { |
||||||
|
if (!$this->isObjectClassStaticallyAssignableTo($actualClass, $expected)) { |
||||||
|
$this->fatalError( |
||||||
|
$expr, |
||||||
|
'ArrayDef property ' . $this->getObjectPropertyTypeCheckDisplayName($property) |
||||||
|
. ' expects ' . $part . ' of type ' . $expected |
||||||
|
. ', ' . $actualClass . ' given', |
||||||
|
); |
||||||
|
} |
||||||
|
return $code; |
||||||
|
} |
||||||
|
|
||||||
|
if ($actual !== Type::VAR && $actual !== Type::OBJECT) { |
||||||
|
$this->fatalError( |
||||||
|
$expr, |
||||||
|
'ArrayDef property ' . $this->getObjectPropertyTypeCheckDisplayName($property) |
||||||
|
. ' expects ' . $part . ' of type ' . $expected |
||||||
|
. ', ' . $this->arrayDefTypeName($actual) . ' given', |
||||||
|
); |
||||||
|
} |
||||||
|
return 'php::toObjectExact(' |
||||||
|
. $code . ', ' . $this->getClassEntryPtr($expected) . ', ' |
||||||
|
. $this->genCharPtr( |
||||||
|
$this->getObjectPropertyTypeCheckDisplayName($property) . ' ArrayDef ' . $part, |
||||||
|
true, |
||||||
|
) |
||||||
|
. ')'; |
||||||
|
} |
||||||
|
|
||||||
|
if ($actual !== Type::VAR) { |
||||||
|
if ($actual !== $expected) { |
||||||
|
$this->fatalError( |
||||||
|
$expr, |
||||||
|
'ArrayDef property ' . $this->getObjectPropertyTypeCheckDisplayName($property) |
||||||
|
. ' expects ' . $part . ' of type ' . $this->arrayDefTypeName($expected) |
||||||
|
. ', ' . $this->arrayDefTypeName($actual) . ' given', |
||||||
|
); |
||||||
|
} |
||||||
|
return $code; |
||||||
|
} |
||||||
|
|
||||||
|
$display = $this->genCharPtr( |
||||||
|
$this->getObjectPropertyTypeCheckDisplayName($property) . ' ArrayDef ' . $part, |
||||||
|
true, |
||||||
|
); |
||||||
|
$helper = match ($expected) { |
||||||
|
Type::INT => 'php::toIntExact', |
||||||
|
Type::FLOAT => 'php::toFloatExact', |
||||||
|
Type::BOOL => 'php::toBoolExact', |
||||||
|
Type::STR => 'php::toStringExact', |
||||||
|
Type::ARRAY => 'php::toArrayExact', |
||||||
|
Type::OBJECT => 'php::toObjectExact', |
||||||
|
Type::STREAM => 'php::toStreamExact', |
||||||
|
Type::BIGINT => 'php::toBoxExact<php::BigInt>', |
||||||
|
Type::BIGFLOAT => 'php::toBoxExact<php::BigFloat>', |
||||||
|
Type::DECIMAL => 'php::toBoxExact<php::Decimal>', |
||||||
|
default => null, |
||||||
|
}; |
||||||
|
if ($helper === null) { |
||||||
|
return $code; |
||||||
|
} |
||||||
|
$boxType = in_array($expected, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true) |
||||||
|
? ', ' . $this->genCharPtr($this->arrayDefTypeName($expected), true) |
||||||
|
: ''; |
||||||
|
return $helper . '(' . $code . ', ' . $display . $boxType . ')'; |
||||||
|
} |
||||||
|
|
||||||
|
private function arrayDefTypeName(string $type): string |
||||||
|
{ |
||||||
|
return match ($type) { |
||||||
|
Type::INT => 'int', |
||||||
|
Type::FLOAT => 'float', |
||||||
|
Type::BOOL => 'bool', |
||||||
|
Type::STR => 'string', |
||||||
|
Type::ARRAY => 'array', |
||||||
|
Type::OBJECT => 'object', |
||||||
|
Type::STREAM => 'stream', |
||||||
|
Type::BIGINT => 'BigInt', |
||||||
|
Type::BIGFLOAT => 'BigFloat', |
||||||
|
Type::DECIMAL => 'Decimal', |
||||||
|
Type::VAR => 'any', |
||||||
|
default => $type, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private function isArrayDefClassType(string $type): bool |
||||||
|
{ |
||||||
|
return !in_array($type, [ |
||||||
|
Type::INT, |
||||||
|
Type::FLOAT, |
||||||
|
Type::BOOL, |
||||||
|
Type::STR, |
||||||
|
Type::ARRAY, |
||||||
|
Type::OBJECT, |
||||||
|
Type::VAR, |
||||||
|
Type::STREAM, |
||||||
|
Type::BIGINT, |
||||||
|
Type::BIGFLOAT, |
||||||
|
Type::DECIMAL, |
||||||
|
], true); |
||||||
|
} |
||||||
|
|
||||||
|
private function isArrayDefCountAppend(Expr $dim, Expr $property): bool |
||||||
|
{ |
||||||
|
if (!$dim instanceof Expr\FuncCall |
||||||
|
|| !$dim->name instanceof Node\Name |
||||||
|
|| strtolower(ltrim($dim->name->toString(), '\\')) !== 'count' |
||||||
|
|| count($dim->args) !== 1 |
||||||
|
|| !$dim->args[0] instanceof Node\Arg |
||||||
|
) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return $this->isSameArrayDefProperty($dim->args[0]->value, $property); |
||||||
|
} |
||||||
|
|
||||||
|
private function isSameArrayDefProperty(Expr $left, Expr $right): bool |
||||||
|
{ |
||||||
|
if ($left instanceof Expr\StaticPropertyFetch && $right instanceof Expr\StaticPropertyFetch) { |
||||||
|
return $left->class instanceof Node\Name |
||||||
|
&& $right->class instanceof Node\Name |
||||||
|
&& $left->name instanceof Node\Identifier |
||||||
|
&& $right->name instanceof Node\Identifier |
||||||
|
&& strcasecmp($left->class->toString(), $right->class->toString()) === 0 |
||||||
|
&& strcasecmp($left->name->toString(), $right->name->toString()) === 0; |
||||||
|
} |
||||||
|
if (!$left instanceof Expr\PropertyFetch || !$right instanceof Expr\PropertyFetch |
||||||
|
|| !$left->name instanceof Node\Identifier || !$right->name instanceof Node\Identifier |
||||||
|
|| strcasecmp($left->name->toString(), $right->name->toString()) !== 0 |
||||||
|
) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return $left->var instanceof Expr\Variable |
||||||
|
&& $right->var instanceof Expr\Variable |
||||||
|
&& is_string($left->var->name) |
||||||
|
&& $left->var->name === $right->var->name; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP. |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\ArrayDef; |
||||||
|
|
||||||
|
final readonly class ArrayDefWritePlan |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
public bool $append, |
||||||
|
public ?string $key, |
||||||
|
public string $value, |
||||||
|
) { |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP. |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\ArrayDef; |
||||||
|
|
||||||
|
final readonly class ArrayDefinition |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
public ?string $keyType, |
||||||
|
public string $valueType, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
public function isList(): bool |
||||||
|
{ |
||||||
|
return $this->keyType === null; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,76 @@ |
|||||||
|
--TEST-- |
||||||
|
ArrayDef supports class value types, subclasses, aliases and dynamic checks |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
namespace App { |
||||||
|
class User |
||||||
|
{ |
||||||
|
public function __construct(public string $name) {} |
||||||
|
} |
||||||
|
|
||||||
|
class Admin extends User {} |
||||||
|
class Other {} |
||||||
|
} |
||||||
|
|
||||||
|
namespace Demo { |
||||||
|
use App\User as Member; |
||||||
|
|
||||||
|
class UserCollection |
||||||
|
{ |
||||||
|
#[\ArrayDef(Member::class)] |
||||||
|
public array $list = []; |
||||||
|
|
||||||
|
#[\ArrayDef(\Type::String, \App\User::class)] |
||||||
|
public array $map = []; |
||||||
|
} |
||||||
|
|
||||||
|
function putList(UserCollection $collection, any $value): void |
||||||
|
{ |
||||||
|
$collection->list[] = $value; |
||||||
|
} |
||||||
|
|
||||||
|
function putMap(UserCollection $collection, any $key, any $value): void |
||||||
|
{ |
||||||
|
$collection->map[$key] = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
namespace { |
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$collection = new Demo\UserCollection(); |
||||||
|
$collection->list[] = new App\User('user'); |
||||||
|
$collection->list[] = new App\Admin('admin'); |
||||||
|
$collection->map['owner'] = new App\Admin('owner'); |
||||||
|
Demo\putList($collection, new App\Admin('dynamic-list')); |
||||||
|
Demo\putMap($collection, 'dynamic', new App\User('dynamic-map')); |
||||||
|
|
||||||
|
foreach ($collection->list as $user) { |
||||||
|
echo $user->name, "\n"; |
||||||
|
} |
||||||
|
foreach ($collection->map as $key => $user) { |
||||||
|
echo $key, '=', $user->name, "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
Demo\putList($collection, new App\Other()); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "list class checked\n"; |
||||||
|
} |
||||||
|
try { |
||||||
|
Demo\putMap($collection, 'bad', new stdClass()); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "map class checked\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
user |
||||||
|
admin |
||||||
|
dynamic-list |
||||||
|
owner=owner |
||||||
|
dynamic=dynamic-map |
||||||
|
list class checked |
||||||
|
map class checked |
||||||
@ -0,0 +1,84 @@ |
|||||||
|
--TEST-- |
||||||
|
ArrayDef checks dynamic compound and high-precision value types |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ArrayDefCompoundBox |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::Array)] |
||||||
|
public array $arrays = []; |
||||||
|
|
||||||
|
#[ArrayDef(Type::Object)] |
||||||
|
public array $objects = []; |
||||||
|
|
||||||
|
#[ArrayDef(Type::BigInt)] |
||||||
|
public array $bigInts = []; |
||||||
|
|
||||||
|
#[ArrayDef(Type::BigFloat)] |
||||||
|
public array $bigFloats = []; |
||||||
|
|
||||||
|
#[ArrayDef(Type::Decimal)] |
||||||
|
public array $decimals = []; |
||||||
|
} |
||||||
|
|
||||||
|
function putCompound(ArrayDefCompoundBox $box, int $slot, any $value): void |
||||||
|
{ |
||||||
|
if ($slot === 0) { |
||||||
|
$box->arrays[] = $value; |
||||||
|
} elseif ($slot === 1) { |
||||||
|
$box->objects[] = $value; |
||||||
|
} elseif ($slot === 2) { |
||||||
|
$box->bigInts[] = $value; |
||||||
|
} elseif ($slot === 3) { |
||||||
|
$box->bigFloats[] = $value; |
||||||
|
} else { |
||||||
|
$box->decimals[] = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$box = new ArrayDefCompoundBox(); |
||||||
|
putCompound($box, 0, [1, 2]); |
||||||
|
putCompound($box, 1, new stdClass()); |
||||||
|
putCompound($box, 2, std::bigInt(3)); |
||||||
|
putCompound($box, 3, std::bigFloat('4')); |
||||||
|
putCompound($box, 4, std::decimal('5')); |
||||||
|
|
||||||
|
var_dump($box->arrays[0]); |
||||||
|
var_dump($box->objects[0] instanceof stdClass); |
||||||
|
echo std::bigInt($box->bigInts[0])->toString(), "\n"; |
||||||
|
echo std::bigFloat($box->bigFloats[0])->toString(), "\n"; |
||||||
|
echo std::decimal($box->decimals[0])->toString(), "\n"; |
||||||
|
|
||||||
|
try { |
||||||
|
putCompound($box, 0, 'not-array'); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "array checked\n"; |
||||||
|
} |
||||||
|
try { |
||||||
|
putCompound($box, 1, []); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "object checked\n"; |
||||||
|
} |
||||||
|
try { |
||||||
|
putCompound($box, 2, std::decimal('6')); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "BigInt checked\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
int(1) |
||||||
|
[1]=> |
||||||
|
int(2) |
||||||
|
} |
||||||
|
bool(true) |
||||||
|
3 |
||||||
|
4 |
||||||
|
5 |
||||||
|
array checked |
||||||
|
object checked |
||||||
|
BigInt checked |
||||||
@ -0,0 +1,131 @@ |
|||||||
|
--TEST-- |
||||||
|
ArrayDef enforces direct list and map writes for Zend and Native classes |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ZendArrayDefBox |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::String)] |
||||||
|
public array $names = []; |
||||||
|
|
||||||
|
#[ArrayDef(Type::Int, Type::String)] |
||||||
|
public array $labels = []; |
||||||
|
|
||||||
|
#[ArrayDef(Type::String, Type::Int)] |
||||||
|
public static array $staticCounters = []; |
||||||
|
} |
||||||
|
|
||||||
|
class PromotedArrayDefBox |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
#[ArrayDef(Type::Int)] public array $values = [], |
||||||
|
) { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeArrayDefBox |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::Int)] |
||||||
|
public array $values = []; |
||||||
|
|
||||||
|
#[ArrayDef(Type::String, Type::Int)] |
||||||
|
public array $counters = []; |
||||||
|
} |
||||||
|
|
||||||
|
function writeDynamicList(ZendArrayDefBox $box, any $key, any $value): void |
||||||
|
{ |
||||||
|
$box->names[$key] = $value; |
||||||
|
} |
||||||
|
|
||||||
|
function writeDynamicMap(NativeArrayDefBox $box, any $key, any $value): void |
||||||
|
{ |
||||||
|
$box->counters[$key] = $value; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$zend = new ZendArrayDefBox(); |
||||||
|
$zend->names[] = 'first'; |
||||||
|
$zend->names[count($zend->names)] = 'second'; |
||||||
|
$zend->names[0] = 'changed'; |
||||||
|
$zend->labels[10] = 'ten'; |
||||||
|
ZendArrayDefBox::$staticCounters['writes'] = 1; |
||||||
|
|
||||||
|
$promoted = new PromotedArrayDefBox(); |
||||||
|
$promoted->values[] = 13; |
||||||
|
|
||||||
|
$native = new NativeArrayDefBox(); |
||||||
|
$native->values[] = 7; |
||||||
|
$native->values[count($native->values)] = 8; |
||||||
|
$native->values[1] = 9; |
||||||
|
$native->counters['ok'] = 11; |
||||||
|
|
||||||
|
writeDynamicList($zend, 1, 'dynamic'); |
||||||
|
writeDynamicMap($native, 'dynamic', 12); |
||||||
|
|
||||||
|
var_dump($zend->names, $zend->labels, ZendArrayDefBox::$staticCounters, $promoted->values, $native->values, $native->counters); |
||||||
|
|
||||||
|
try { |
||||||
|
writeDynamicList($zend, '1', 'bad-key'); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "list key type checked\n"; |
||||||
|
} |
||||||
|
try { |
||||||
|
writeDynamicList($zend, 0, 123); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "list value type checked\n"; |
||||||
|
} |
||||||
|
try { |
||||||
|
writeDynamicMap($native, 1, 12); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "map key type checked\n"; |
||||||
|
} |
||||||
|
try { |
||||||
|
writeDynamicMap($native, 'bad', '12'); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "map value type checked\n"; |
||||||
|
} |
||||||
|
try { |
||||||
|
writeDynamicList($zend, 20, 'out'); |
||||||
|
} catch (Error $error) { |
||||||
|
echo "list bounds checked\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(7) "changed" |
||||||
|
[1]=> |
||||||
|
string(7) "dynamic" |
||||||
|
} |
||||||
|
array(1) { |
||||||
|
[10]=> |
||||||
|
string(3) "ten" |
||||||
|
} |
||||||
|
array(1) { |
||||||
|
["writes"]=> |
||||||
|
int(1) |
||||||
|
} |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
int(13) |
||||||
|
} |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
int(7) |
||||||
|
[1]=> |
||||||
|
int(9) |
||||||
|
} |
||||||
|
array(2) { |
||||||
|
["ok"]=> |
||||||
|
int(11) |
||||||
|
["dynamic"]=> |
||||||
|
int(12) |
||||||
|
} |
||||||
|
list key type checked |
||||||
|
list value type checked |
||||||
|
map key type checked |
||||||
|
map value type checked |
||||||
|
list bounds checked |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
--TEST-- |
||||||
|
Top-level literal local assignments preserve PHP values |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function values(): array |
||||||
|
{ |
||||||
|
$integer = 42; |
||||||
|
$negative = -7; |
||||||
|
$floating = 1.25; |
||||||
|
$boolean = true; |
||||||
|
$string = 'hello'; |
||||||
|
$nullValue = null; |
||||||
|
|
||||||
|
return [$integer, $negative, $floating, $boolean, $string, $nullValue]; |
||||||
|
} |
||||||
|
|
||||||
|
var_dump(values()); |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(6) { |
||||||
|
[0]=> |
||||||
|
int(42) |
||||||
|
[1]=> |
||||||
|
int(-7) |
||||||
|
[2]=> |
||||||
|
float(1.25) |
||||||
|
[3]=> |
||||||
|
bool(true) |
||||||
|
[4]=> |
||||||
|
string(5) "hello" |
||||||
|
[5]=> |
||||||
|
NULL |
||||||
|
} |
||||||
Loading…
Reference in new issue