https://d226lax1qjow5r.cloudfront.net/blog/blogposts/type-safety-done-right-php-array-hacking/type-safety-done-right.png

正しい型安全性 - PHPの配列ハッキング

最終更新日 October 31, 2023

所要時間:1 分

大規模なPHPアプリケーションの開発に携わった最初の5、6年間は、バックエンド開発を始めた人がよく目にするような問題の多くを経験し、作り上げてきました。今となっては、特に Vonage PHP SDKを受け継いだ今になってようやく、他の人が同じことをしないように教育できる立場にあることに気づきました。これらの問題は、実は私がPHPをどのように見てきたかに起因しています。 特にフレームワークが使われているかどうかに関係なく。最初の手榴弾を投げてみよう:

PHPの配列はちょっとひどい

OK、彼らはひどくはない。私が言いたいのは よく使われるがひどいということだ。私がPHPを最初の言語として使い始めたのは、(VBAの後に)PHPを使い始めたからです。 VBASQLの後)、他の言語がどのようにデータの集まりを扱うのか知りませんでした。PHPと同じだと思い込んでいたのです:

  • インデックス配列があります。

  • 関連する配列があり、その配列には英数字のキーが定義されています。

どちらも同じタイプの変数とみなされる。 arrayなぜなら、エンジンはあなたが作成したい配列の種類を解決しているからである。

$myAssociatedArray = ['foo' => 'bar']; // array
$myHashedArray = ['foo', 'bar']; // also array

おそらく、他のことはすべてそうなんだろう?

いいえ、そうではありません。PHPだけを使っている私の甘さが、以下の記述に気づかなかったことを意味します:

他のすべての主要なバックエンド言語では、インデックス配列と連想配列は2つ以上の異なる基本クラスに分かれている。

さらに、連想配列は PHP でのみ使われる用語です。今後は 連想配列 ハッシュ配列と呼ぶことにします。

以下は他の言語での動作である:

ノードJS

  • Array(インデックス付き配列)

  • Object(ハッシュ配列)

パイソン

  • List(インデックス付き配列)

  • Dictionary(ハッシュ配列)、リスト理解力を使用

ルビー

  • Array(インデックス付き配列)

  • Hash(ハッシュ配列)

しかし、Hashオブジェクトが存在するため、配列がハッシュとして使用されることは非常に稀です。

  • Slice(インデックス付き配列)

  • Map(ハッシュ配列)

ジャワ

  • Array(インデックス付き配列, プリミティブ)

  • List(インターフェイス、実装は ArrayList)

  • ArrayList(インデックス付き配列、サイズ調整のために新しいオブジェクトを作成する)

  • Hashmap(ハッシュ配列。 Map)

  • LinkedHashMap(ハッシュ化された配列、挿入順序を再修正する)

おわかりのように、Javaはデータ構造の集合に対して非常に厳格である。これらはすべて、値を変更できるという点ではミュータブルだが、ノードに関するオブジェクトのサイズを変更することはできない。そのため ArrayListこれは、変更時に実際に新しいオブジェクトが作成されるからです。

C#

  • Array(インデックス付き配列)

  • List(ジェネリックス付き添字配列、ミュータブル)

  • Dictionary(ハッシュ配列)

つまり、これらの言語の基本APIにはすべて異なるクラスが含まれているという事実は、それらの言語がすべて、より厳格な型の振る舞いを導入する能力を持っていることを意味する。

それがどうした?

PHPにはそれがない。つまり PHPの配列は 何でも.この事実のために、私が何年も読んできたような、次のようなコードが生まれたのだ:

public function updateRecord(int $id, array $options) {
	// do some stuff here
}

何かのIDを更新したい。そして私は $options.オプションだからです。しかし、これは単なるPHPの配列です。中身は何?関数にはわかりません。

このコードの何が問題なのか?個人的な経験なら 4時間xdebugを4時間も繰り返すことになる。それがどのようなものなのか見当もつかない。この 特に私のバグベアは、何年もの間、自分でコーディングしていたものだ、 特に特に「時は金なり」のエージェンシー環境では。もし「クライアントの要求通りに動くかどうか」だけが心配なら、時間通りに出荷するために可能な限り手を抜くだろう。副作用で発生したバグを修正しなければならないのは、あなたの後任者でしょうか?それが技術的負債なのだ。

さて、どうする?

保守的なコードを書くために私が歩んできた道は、型ヒントを使うことだった。 を使うことだ。.これは、もし全体を通して徹底的に行えば、このようなソフトウェア開発になる、 このようなソフトウェア開発は神話に登場するようなセルフ・ドキュメント化されたコードになるはずだ。しかし、私たちがタイプできるのはヒント arrayを入力することしかできない。そこで、3つの選択肢がある:

  • ジェネリック医薬品

  • 配列の形 (ハックされたジェネリックス経由)

  • 価値あるオブジェクト

ジェネリック医薬品

PHPにはジェネリックスはありません。エンジンに制限があるからです。しかし、ジェネリクスという単語が少し出回っているようなので、次のコードを考えてみてください。 TypeScript:

interface UserProfile {
    id: string;
    name: string;
    age: integer;
}

class Collection<Type> {
    private items: Type[] = [];

    add(item: Type) {
        this.items.push(item);
    }

    get(index: number): Type | undefined {
        return this.items[index];
    }
}

const userProfiles = new Collection<UserProfile>();

このPHPを可能な限り堅牢にし、ヴァリリア鋼よりも硬くしろ!」という陣営の人間としては、このようなジェネリックは仕事をする上で深い喜びとなる。この <>は、新しい Collectionオブジェクトを作成する際には には新しいオブジェクトを作成するときは、インスタンス化時に指定された型だけを含むようにしなければならない。つまり、新しい Collection<UserProfile>を作成すると、このオブジェクトには UserProfileオブジェクトしか含むことができない、ということになる。 つまり敵も味方も、これが自己文書化コードなのだ。

PHPは他の方法でこれを行うことができますか?

よくぞ聞いてくれた!これが箇条書きの2番目、アレイの定義につながる。これは バカバカしいでも、Vonageに入社するまで、SPLのコア部分を拡張できるなんて思いもしなかった。 コア SPLオブジェクトをPHP API内で拡張できるなんて思いもしませんでした。つまり 配列を拡張できる!

class Collection extends \ArrayObject {

	protected function __construct(...$args) {
		parent::__construct(...args);
	}
}

現在のところ、これは親クラスにスプラット演算子で引数を渡す以外には何もしない。 ArrayObject.しかし、我々はこれを拡張し、振る舞いをオーバーロードできるようにした!

ここでやるべきことは2つある:

  • TypeScriptと同じように型を設定する。

  • メソッドをオーバーロードします。 offsetSet()メソッドをオーバーロードします。このメソッドは、配列に値を追加するための SPL メソッドです (インスタンス化後に値を追加するときでも、コンストラクタで値を生成するときでもかまいません)。

では、これに対処しよう:

class Collection extends \ArrayObject {  

	protected $typeError = 'Only UserProfile objects can be added!'
	
    public function __construct(...$args) {  
    
        foreach ($args as $arg) {  
            if (!$arg instanceof UserProfile) {  
                throw new \TypeError($this->typeError);  
            }  
        }  
        
        parent::__construct(...$args);  
    }  
  
    public function offsetSet($key, $value): void  
    {  
  
        if (!$value instanceof UserProfile) {  
            throw new \TypeError($this->typeError);  
        }  
  
        parent::offsetSet($key, $value);  
    }  
}

そうだ。 offsetSetがオーバーロードされ UserProfileオブジェクトだけを追加できる。 Collectionオブジェクトであれば、引数はいくつでも渡すことができる。 UserProfileオブジェクトである限り、いくつでも引数を渡すことができる。

バリュー・オブジェクト

配列を投げまくる問題には、もっと単純な解決策があります。私は、Vonage PHP Core SDK で私の前任者たちが行った作業を引き継ぎました。 クライアントメソッドに渡されるものはすべて値オブジェクトです。

そう、これにはデメリットもある。クラスのプロパティを定義する必要があり、ゲッターとセッターのメソッドを追加する必要があるということだ。コードが増えるということだ。しかし、ネイティブクラスやコンストラクタのプロパティプロパティを使用することで、 最近の PHP ではコードの肥大化を抑えることができます。 enumを使用することで、 コードの肥大化を抑えることもできます。

のPHP SDK実装をコーディングしたときのことだ。 Verify v2.検証リクエストの送信は、値オブジェクトがない場合はこのようになります:

$payload = [
	'locale' => 'en_us',
	'channel_timeout' => 300,
	'client_ref' => 'a-reference',
	'code_length' => 4,
	'workflow' => [
		'channel' => 'sms',
		'to' => '123456789'
	]
];

$myVonageClient = new Client('apiKey', 'apiSecret');
$myVonageClient->verify2()->startVerification($payload);

クライアント・オブジェクトは、渡された配列について何も知らない。キーが間違っていたら?キーがなかったら?コードの中には、その動作について教えてくれるものは何もない。 もしバリデーションを startVerification()メソッドにバリデーションを入れたとしたら、私の頭の中に何があるのかを知るためには、私のコードを探し回らなければならないだろう。

その代わりに、値オブジェクトを使用することで、ロジックを適切に型付けされたPHPに移行します。それは、自己文書化され、設計によって強化された 設計によって.

class SMSRequest extends BaseVerifyRequest  
{
	public function __construct(  
	    protected string $to,  
	    protected string $brand,  
	    protected ?VerificationLocale $locale = null,  
	) {  
	    if (!$this->locale) {  
	        $this->locale = new VerificationLocale();  
	    }  
	  
	    $workflow = new VerificationWorkflow(VerificationWorkflow::WORKFLOW_SMS, $to);  
	  
	    $this->addWorkflow($workflow);  
	}
}

このオブジェクトは startVerification()メソッドに渡される。渡されたオブジェクトでできること、できないことは、コードの最下層で定義できるようになった。例えば、ここに VerificationLocaleオブジェクトが渡されます。

class VerificationLocale  
{  
    private array $allowedCodes = [  
        'en-us',  
        'en-gb',  
        'es-es',  
        'es-mx',  
        'es-us',  
        'it-it',  
        'fr-fr',  
        'de-de',  
        'ru-ru',  
        'hi-in',  
        'pt-br',  
        'pt-pt',  
        'id-id',  
    ];  
  
    public function __construct(protected string $code = 'en-us')  
    {  
        if (! in_array($code, $this->allowedCodes, true)) {  
            throw new \InvalidArgumentException('Invalid Locale Code Provided');  
        }  
    }  
  
    public function getCode(): string  
    {  
        return $this->code;  
    }  
  
    public function setCode(string $code): static  
    {  
        $this->code = $code;  
  
        return $this;  
    }  
}

これで完成だ。セルフ・ドキュメント化されたコードだ。確かに、すべてのオブジェクトを作成し、それらをつなぎ合わせる必要がありますが、私たちのSDKがどのように動作するかは、厳密かつ明示的です。

シェア:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeシニアPHPデベロッパー

スタンダップ・コメディーの学位論文を持つ俳優の訓練を受け、ミートアップ・シーンを経てPHP開発に携わるようになった。技術について話したり書いたり、レコード・コレクションから変わったレコードを再生したり買ったりしています。