- 書: 解耦PHP——清淨六角之構造,適應框架之長存
- 亦余所撰: 思乎蓋亂 (二書之集) — 蓋亂程式設計之全覽 + 蓋亂六角之構造
- 吾之項目: 赫爾墨斯IDE |GitHub — 一款供开发者使用,配合Claude Code及其他AI编程工具之集成开发环境
- 吾: xgabriel.com | GitHub
上月与吾言谈之团队,有千四百人mixed 返类型于其 Laravel 11 代码之林。PHPStan 级别六为青。测试无碍。一初学者重构一助函数,三控制器于测试环境返 HTTP 500。无物示警。无物可察。
mixed 乃 PHP 类型系统言“吾已竭力。” Laravel 自身之文载此意于四处。PHPStan 容之于级别七。汝之代码林默然积之。每mixed乃隐于下流之断裂,非目可见,直至其时始现。
五类混凝土之崩坏,继以五十行脚本,寻汝之最凶者。
mixed之真意何在(及何故"anything"为患)
mixed乃PHP 8.0所增,为"无类型"之显式版本。其纳int,string,array。object,null,false,閉包,資源。凡PHP所能容者,皆在焉。此乃語言之至型也.
然有陷阱:至型無以示人。若函數歸mixed,則呼者必須為每支每較每法皆懷惡境。編譯器(PHP之例,則為靜態分析器)無可依據。
八点前之变,乃失其返类型,靜工器视之如一。八点無新患,唯舊患賦名。名無所助,反使記述易而標識難。
失一:IDE自动补全功能每用必死mixed归也
汝书之$order = $repo->find($id);且击之->,PHPStorm于汝无益。甚者,乃予汝项目诸类之法,悉以字母序列。汝卷过__construct,accept,acknowledge,actAs……
// Repository.php: the kind of code that ages badly
final class OrderRepository
{
public function find(int $id): mixed // <-- the crime
{
$row = DB::table('orders')->find($id);
if (!$row) {
return null;
}
return Order::fromRow($row);
}
}
其签署曰"可任一物"。故 PHPStorm 以为然。汝失"定位于定义"之能。汝失于Order字段之重命名重构。汝失"汝于->customerId上调用null"之警示。汝于二零二六年而书 PHP 5.6.
其解乃机巧耳:
public function find(int $id): ?Order
{
$row = DB::table('orders')->find($id);
return $row ? Order::fromRow($row) : null;
}
一言而已。IDE苏醒,重命名重构遍及应有之处。后之开发者读之find()能知其返,不须检其文。
失二:PHPStan等级六以上,不能捕获空引用错误于mixed
此乃生产中噬人之败式。PHPStan可证诸君之码甚多,然mixed乃墙也。
public function totalForCustomer(int $customerId): float
{
$orders = $this->cache->get("orders.$customerId"); // returns mixed
$sum = 0.0;
foreach ($orders as $order) {
$sum += $order->total;
}
return $sum;
}
于此运行PHPStan第七级。其通过。foreach于mixed,->total之访问于mixed。PHPStan不能推理二者,因其无上限。运行时捕获之:Cannot access property total on mixed,然惟缓存未命中时方得之,此乃测试所不及也。
第八级,PHPStan之善止焉。及第八级,于mixed唤法,乃违。迭mixed,亦违。mixed[]之取,亦违。其解,在以型注缓存之裹:
final class OrderCache
{
/** @return list<Order> */
public function getOrders(int $customerId): array
{
$raw = $this->store->get("orders.$customerId");
if (!is_array($raw)) {
return [];
}
return array_map(fn($r) => Order::fromArray($r), $raw);
}
}
PHPDoc之泛型与边界处之运行时校验,乃成其功。至于余下之代码库中,$cache->getOrders($id)乃一也list<Order>不可。mixed无空值引用。PHPStan级别8保持绿色。
失三:泛型包装器Collection<mixed>隐无物
Laravel(拉aravel)之Collection乃 Laravel 代码库中最常用之类,亦为最被欺瞒者。若不添 PHPDoc,其默认类型为Collection<int, mixed>:
public function activeOrders(): Collection
{
return Order::where('status', 'active')->get();
}
PHPStan视之为Illuminate\Database\Eloquent\Collection<int, Model>至多耳Collection<mixed>若尔用基Collection锁链之呼下,皆运行于mixed:
$activeOrders
->filter(fn ($o) => $o->customerId === $id) // mixed->customerId
->map(fn ($o) => $o->total) // mixed->total
->sum();
凡矢皆为臆测。凡重构(更名customerId为customer_id,于?加total)皆默失此呼点。
其解乃PHPDoc之泛:
/** @return Collection<int, Order> */
public function activeOrders(): Collection
{
return Order::where('status', 'active')->get();
}
PHPStan與PHPStorm皆遵此註解。->filter之回調今得有型之Order。->map返Collection<int, float>。->sum乃型之float。型之信息流於管線。
larastan/larastan使此效於Eloquent集合於六級以上。其難在:汝必自書泛型。其預設(一Collection(无注解)即Collection<mixed>。此乃众代码库所栖之地。
失败之四:重构于下游三处崩坏
一微小更名,足显累积之费。汝有一getConfig()之助函数,返mixed:
function getConfig(string $key): mixed
{
return config($key);
}
// Three callers, all using it differently:
$ttl = getConfig('cache.default.ttl'); // expects int
$drivers = getConfig('queue.connections'); // expects array
$timeout = getConfig('http.timeout'); // expects ?int
汝决之getConfig甚愚(盖config()已存也),宜删助件。IDE之重命名重构,不识诸调用处之殊,以其皆耗mixed也。尔删助件,调用处遂直归config(),然三者今用其值,非其所宜。
尤甚:尔复欲增验。getConfig 当失其键则应抛之:
function getConfig(string $key): mixed
{
$value = config($key);
if ($value === null) {
throw new ConfigMissing($key);
}
return $value;
}
今凡用?int于此函数者,其约已然更易。PHPStan莫能助,盖mixed既纳int亦纳?int。诸试皆通。然生产之境,每于部署后首遇缓存未命中而抛之。
此非泛型之弊,乃小而专之型助也。
final class CacheConfig
{
public function ttl(string $store): int
{
$value = config("cache.stores.$store.ttl");
if (!is_int($value)) {
throw new ConfigMissing("cache.stores.$store.ttl");
}
return $value;
}
}
不可重构型约,破型系统则不可。此即型之要义也。
失败五:mixed于型约方法签中,必使每用者扩型。
此微妙。一法有mixed返签之毒,害及诸班。
trait HasMetadata
{
public function getMetadata(string $key): mixed
{
return $this->metadata[$key] ?? null;
}
}
final class Order
{
use HasMetadata;
public function shippingAddress(): Address
{
$raw = $this->getMetadata('shipping'); // mixed
return Address::fromArray($raw); // PHPStan: ok, mixed accepts array
}
}
PHPStan等级八标志Address::fromArray($raw)故也$raw是。mixed且fromArray期之array欲绝其声,则神人加以运行时之变。assert(is_array($raw))固可行,然为类型系统之失,实乃运行时之税也。
其真解,在于去mixed于特性,以PHPDoc使特性为泛型:
/**
* @template TValue
*/
trait HasMetadata
{
/** @var array<string, TValue> */
private array $metadata = [];
/** @return TValue|null */
public function getMetadata(string $key)
{
return $this->metadata[$key] ?? null;
}
}
final class Order
{
/** @use HasMetadata<string|array<string, string>> */
use HasMetadata;
}
PHPStan敬重@template于特性。消费者固类型。mixed 已绝于调用之域,无运行时之断言。
之性,mixed 积聚最速,盖因其框架之质,复用之谓也。每复用,弱之签章随之流布。
更替之阶:mixed 而至联合,而至界面,而至具体之型
当于码中见 mixed,循此阶而上之:
-
具体类型:若能书
Order,则当书Order。毋甘于不足 -
。可空具体类型:
?Order,若法可合法地返回无物 -
。联合类型:
Order|Refund|null每当函数返回若干物时。别异之合乃PHP之谓也。"此中择一" -
接口:
Refundable若重行止,非重类之别 -
PHPDoc泛型:
list<Order>,array<string, Money>,Collection<int, Order>。PHPStan洞悉此理。 -
mixed唯于信界之畔(反序列化JSON,自中读取)$_POST,先击中缓存而后验之。继而立时收束,勿使值流他处。
经验之则:mixed宜存于汝之码库,仅一语耳。其后之语,当渐狭。
五十行审计脚本,寻汝之最恶mixed罪人
初度无需PHP-Parser。以适切之式grep,已得九成功矣。此式录为bin/audit-mixed.sh:
#!/usr/bin/env bash
# audit-mixed.sh: rank files by mixed-density
set -euo pipefail
ROOT="${1:-src}"
TMP=$(mktemp)
# count mixed occurrences per file, ignoring vendor/tests
find "$ROOT" -name '*.php' \
-not -path '*/vendor/*' \
-not -path '*/tests/*' \
-print0 \
| while IFS= read -r -d '' file; do
# match `: mixed` returns, `mixed $var` params, `@param mixed`,
# `@return mixed`, and `@var mixed` PHPDoc tags
count=$(grep -cE '(\:\s*mixed\b|mixed\s+\$|@(param|return|var)\s+mixed)' "$file" || true)
lines=$(wc -l < "$file")
if [ "$count" -gt 0 ]; then
# density = mixed count per 100 lines
density=$(awk "BEGIN { printf \"%.2f\", ($count * 100) / $lines }")
printf '%s\t%d\t%d\t%s\n' "$density" "$count" "$lines" "$file" >> "$TMP"
fi
done
echo "DENSITY COUNT LINES FILE"
echo "-----------------------------"
sort -rn "$TMP" | head -30
rm "$TMP"
运行之:./bin/audit-mixed.sh src。得三十最劣者,依mixed-每百行密度排序。密度重于原数。二千行之模,八mixed 之康健,逾于五十行之简牍,兼有六
。若论 PHP-Parser 之版本(精微,察动态属性之取用,略去注解),则 nikic/php-parser 5.x 之外,更添一 NodeVisitorAbstract ,能数 Identifier 节点中,凡名唤 mixed 者于 Stmt\ClassMethod 之内,而Stmt\Function_。bash之版式,乃CI检之所需。增行,使构建若顶文件之密度越阈,则败之(初定3.0,月降之)。
PHPStan级8,乃执行之严。
审计示汝所处。PHPStan级8,使汝恒守之。
于phpstan.neon:
parameters:
level: 8
paths:
- src
ignoreErrors: []
treatPhpDocTypesAsCertain: true
checkMissingIterableValueType: true
checkGenericClassInNonGenericObjectType: true
第八级禁止:
- 方法调用于
mixed - 属性访问于
mixed - 数组访问于
mixed - 遍历于
mixed - 从函数返回
mixed而其声明的返回更为具体
它允许mixed 自诩为既定之型(PHP 许之,PHPStan 亦不与之争)。然则必先收束,而后得用。合以审计之脚本,则渐进而无止:PHPStan 阻止新 mixed 以不安全之法加之;审计之压力,迫使团队删去旧者。
吾所言之团队,已迁 1,400。mixed 返归二百八十之数于四分之一。其HTTP五百之率因类型错误而降至近于无。PHPStan之基线文件每周皆有所减。无人增新验证之码。彼等唯书旧有之文而已。
mixed 乃最易书之类型。然持之则最费。
君尝承最恶之 mixed 耶?弃其密度之率而audit-mixed.sh 言于注脚.
乎mixed 之弊,乃症结也:框架之常,恃动态之变,渗入域码,非其所宜。其解,在框架层与类型核心间,界之清明耳. 脱节之PHP者,汝之代码基架,逾越框架之常则,所求之架构层也:仓库之接口,值物之对象,永不见mixed之用例是也。
。Kindle、Paperback、Hardcover皆可购。英文、德文、日文版已出,葡萄牙文、西班牙文版将即至。













