在PHP中实现空安全
前提 (Chinese:
PHP >= 8.0
PHPStan 版本: 1.2.0
空安全是什么?
-
- 簡単にいうと、null が原因で実行時エラーにならない仕組みのこと。
-
- コンパイラや静的解析ツールによって nullable 型とnon-nullable型を区別し、必要な null チェックが機械的に強制されることで、null を安全に扱うことができる。
-
- PHP では null に対してメソッド呼び出しを行うとエラーになるので null 安全ではない。
- null に対するプロパティアクセスはエラーにならないものの、想定外の挙動であることが大半だと思うのでバグを引き起こす可能性がある。
<?php
$user = null;
echo $user->getAddress()->getCountry(); // Error: Call to a member function address() on null
var_dump($user->address->country); // null
通过使用PHPStan来实现null安全。
- PHPの静的解析ツールである PHPStan を使うと、nullable 型に対して直接プロパティアクセスやメソッド呼び出しを行なっている箇所をエラーとして検出する事ができます(解析レベル 8 以上)。
<?php
class User
{
public function __construct(private ?Order $order)
{
}
public function echoOrder(): void
{
echo $this->order->id; // Cannot access property $id on Order|null.
echo $this->order->createdAt(); // Cannot call method createdAt() on Order|null.
}
}
class Order
{
public function __construct(public string $id, public DateTimeImmutable $createdAt)
{
}
public function createdAt(): string
{
return $this->createdAt->format('Y-m-d');
}
}
phpstan 游乐场
- これで null ハンドリングが漏れていることに気づけるようになりました。
空(Null)处理(从Null对象模式到安全空(null safe)操作符)
- null をハンドリングする際にnull チェックが重なると辛いため、これを回避するためのパターンに Null Object パターンがありますが、使うのが適切でないケースもあります。
想要区分空对象和非空对象的类型
-
- 注文した商品を生産する工場を例に挙げます。
- 工場(=Factory クラス)の取得処理では、工場が存在しない場合を Null Object パターンを使って NullFactory として表現してみます。
<?php
class Factory
{
public function __construct(private string $code)
{
}
public function isNull(): bool
{
return false;
}
public function code(): string
{
return $this->code;
}
}
class NullFactory extends Factory
{
public function isNull(): bool
{
return true;
}
public function code(): string
{
return '';
}
}
interface FactoryRepository
{
public function find(string $code): Factory; // 存在しない場合は NullFactory を返す
}
class SomeUseCase
{
public function __construct(private FactoryRepository $factoryRepository)
{
}
public function factoryCode(): string
{
return $this->factoryRepository->find('9000')->code();
}
}
- すると、注文クラス(=Order)では存在する工場を割り当てたいので型としては Factory ですが、中身が Null Object かどうかをチェックしなければなりません。
<?php
class Order
{
private Factory $factory;
public function __construct(Factory $factory)
{
$this->setFactoryCode($factory);
}
private function setFactoryCode(Factory $factory)
{
if ($factory->isNull()) { // Null Object かどうかチェック !?
throw new \InvalidArgumentException('工場が存在しない');
}
$this->factory = $factory;
}
}
-
- Factory 型で受け取っているにも関わらず Null Object かどうかをチェックしなければならない分、型としての強制力が弱くなってしまっています。
-
- 他のクラスでもFactory 型を使う度に、存在しない工場も受け入れるのか、それとも存在する工場だけ受け入れるのかを考えて Null Object かどうかのチェックを入れるかどうか、人間が判断し続けなければなりません。
-
- Null Object パターンは null かどうかを判別せずに共通したインターフェースで実行可能にするものでしたが、ここではそれが裏目に出ている感があります。
- この状態では、Null Object パターンを導入するデメリットが大きいため、存在する工場だけを Factory で表現し、存在しない工場は null で表現した方がいいと考えます。
除了上述之外,通常被认为 Null 对象模式是在定义 Null 类不存在时的行为,但希望根据使用的上下文而有不同行为的案例。
使用空安全运算符。
- PHP8 未満で存在しないものを null で表現する時、ネストになり読みづらくなってしまう場合があります。
<?php
$country = null;
if ($session !== null) {
$user = $session->user;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->country;
}
}
}
- PHP8.0 ではそれを解決するために null safe 演算子が導入され、スッキリ書くことができるようになりました。
<?php
$country = $session?->user?->getAddress()?->country;
- null safe 演算子を使って先ほど Null Object パターンで実装したコードを書き換えてみましょう。
<?php
interface FactoryRepository
{
public function find(string $code): ?Factory; // 存在しない場合は null を返す
}
class SomeUseCase
{
public function __construct(private FactoryRepository $factoryRepository)
{
}
public function factoryCode(): string
{
return $this->factoryRepository->find('9000')?->code() ?? '';
}
}
- これで null Object パターンを使わなくても、null をハンドリングする辛さがだいぶ軽減されますね。
错误地将非空类型处理为空值
-
- PHPStan は nullable 型において null ハンドリングを強制するだけでなく、non-nullable 型を誤って null ハンドリングしている所を除外するよう強制することがほぼほぼできています。
-
- ただ、non-nullable 型のプロパティに対して null合体演算子を使用している所ではエラーを起こせておらず、これは PHPStan のバグだと思われます。
- これが見過ごされても実行時エラーにはなりませんが、null を考慮している部分がデッドコードになることで可読性が落ちるので直ると嬉しいですね。
<?php
class User
{
public function __construct(private Order $order2)
{
}
public function echoOrder(): void
{
echo $this->order2?->id; // PHPStan Error: Using nullsafe property access on non-nullable type Order
echo $this->order2->createdAt()?->format('Y-m-d'); // PHPStan Error: Using nullsafe method call on non-nullable type DateTimeImmutable
echo $this->order2->id ?? ''; // string 型に対して null を考慮した書き方になっているが、PHPStan エラーにはならない
$this->order2->createdAt() ?? new DateTimeImmutable(); // PHPStan Error: Expression on left side of ?? is not nullable
}
}
class Order
{
public function __construct(public string $id, public DateTimeImmutable $createdAt)
{
}
public function createdAt(): DateTimeImmutable
{
return $this->createdAt;
}
}
PHPStan 游乐场
总结
-
- PHPStan で null安全にしつつ、null safe 演算子を使うことで nullハンドリングをやり易くすることができました。
- PHP も周辺ツールも日々進化しているので、新機能を上手く取り込みながら効率的に開発していきたいですね。
请阅读以下内容。
PHP官方文档:null 安全操作符
RFC:null 安全操作符