2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHPStanAdvent Calendar 2023

Day 20

パラメータのConditional Types

Last updated at Posted at 2023-12-20

メリークリスマス! :tada: PHPStan Advent Calendarも20日目になりましたね!

以前、PHPStan 1.6.0(2022年4月リリース)で条件付き戻り値型(Conditional return types)が使えるという話をしました。

これなんですが、戻り値(@return)だけでなく、パラメータ(@param)でも使えます。

問題設定

本を検索する関数を考えてみましょう。

/**
 * @phpstan-param 'asc'|'desc'|'custom' $sort
 * @phpstan-return list<Book>
 */
function searchBooks(string $word, string $sort, Closure $callback = null): array
{
	return [];
}
  • 文字列を渡して本を検索する
  • ソートの種類として'asc''desc''custom'の3種類を受け付ける
  • 'custom' のときだけ、クロージャを渡して何かをカスタムできるようにする

そういう関数で考えてみることにします。

設定がガバいので何をどうカスタムできるのかは決めていません。
まあ、外部の検索エンジンに投げるクエリかなんかでしょう。

sort\callback Closure null
asc エラー 有効
desc エラー 有効
custom 有効 エラー

エラーになる組み合わせはPHPUnitなどでチェックできるようにしてもいいのですが、PHPStanの機能だけでも静的にチェックできます。

解法

/**
 * @phpstan-param 'asc'|'desc'|'custom' $sort
 * @phpstan-param ($sort is 'custom' ? Closure : null) $callback
 * @phpstan-return list<Book>
 */
function searchBooks(string $word, string $sort, Closure $callback = null): array
{
	return [];
}

スクリーンショット 2023-12-20 22.43.36.png

エラーにすべきパターンできちんとエラーになっています

  • 'desc'Closureを渡している
  • 'custom'nullを渡している

もちろん、文字列定数ではなくenumを使っても大丈夫です。

enum Sort
{
	case ASC;
	case DESC;
	case CUSTOM;
}

/**
 * @phpstan-param ($sort is Sort::CUSTOM ? Closure : null) $callback
 * @phpstan-return list<Book>
 */
function searchBooks(string $word, Sort $sort, Closure $callback = null): array
{
	return [];
}

もうちょっと複雑な例

では、もうちょっと複雑な例を考えてみましょう。

enum BookSearchBy
{
	case AUTHOR;
	case NAME;
	case PUBLISHER;
}

/**
 * @return list<Book>
 */
function search(BookSearchBy $query, array $query): array
{
	return [];
}

検索の種類(著者名、書名、出版社)によって異なるパラメータを渡さなければいけないとしましょう。

  • BookSearchBy::AUTHOR で渡すときは ['type' => 'author', 'author' => $string]
  • BookSearchBy::NAME で渡すときは ['type' => 'name', 'name' => $string]
  • BookSearchBy::PUBLISHER で渡すときは ['type' => 'publisher', 'publisher' => $string]

高レベルな関数としては使いにくい設計ですが、外部のデータベースに問い合わせることにこのような変換を要することは、まああるんじゃないでしょうか。

つまりこうです。

/**
 * @phpstan-param (
 *   $by is BookSearchBy::AUTHOR ? array{type: 'author', author: string} :
 *   $by is BookSearchBy::NAME ? array{type: 'name', name: string} :
 *   $by is BookSearchBy::PUBLISHER ? array{type: 'publisher', publisher: string} :
 *   never) $query
 * @return list<Book>
 */
function search(BookSearchBy $by, array $query): array
{
	return [];
}

やってみましょう。

$search_by = filter_var($_GET['search_by']);
$word = filter_var($_GET['word'], options: FILTER_NULL_ON_FAILURE) ?? '';

$books = match ($search_by) {
    'author' => search(BookSearchBy::AUTHOR, [
		'type' => 'author',
		'author' => $word,
	]),
    'name' => search(BookSearchBy::PUBLISHER, [
		'type' => 'name',
		'name' => $word,
	]),
    'publisher' => search(BookSearchBy::NAME, [
		'type' => 'publisher',
		'publisher' => $word,
	]),
	default => throw new Exception(),
};

このコードではわざとBookSearchBy::PUBLISHERBookSearchBy::NAMEをとりちがえていますが、きちんと警告が出ているのがわかりますでしょうか。

スクリーンショット 2023-12-21 0.06.24.png

スクリーンショット 2023-12-20 23.49.41.png

きっちりと型がついてエラーも出ていないことがおわかりいただけますでしょうか。

現状の課題

将来的にクエリが複雑になることを見越してクエリビルダークラスに分割してみましょうか。

$params = match ($search_by) {
    'author' => [BookSearchBy::AUTHOR, new AuthorQueryBuilder()],
    'name' => [BookSearchBy::PUBLISHER, new NameQueryBuilder()],
    'publisher' => [BookSearchBy::NAME, new PublisherQueryBuilder()],
	default => throw new Exception(),
};

$books = search($params[0], $params[1]->build($word));

このコードもわざと間違っているのですが、これは残念なことに検知されていません。

俺たちの戦いはこれからだ!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?