実現したい事
クラスのプロパティ(配列)に特定の型のオブジェクトが入っている事を保証したい。
解決方法
PHP7から利用出来るようになった、... による可変長引数により解決する。
サンプルコード
以下のクラスを用意する。
- BlackCompany(ブラック会社)
- Human(人間の基底クラス)
- CompanySlave(社畜、Humanクラスを継承)
- Nomad(ノマド、Humanクラスを継承)
- Neet(ニート、Humanクラスを継承)
<?php
// ブラック会社を表すクラス
class BlackCompany
{
private $companySlaves;
/**
* BlackCompany constructor.
*
* @param CompanySlave[] ...$companySlaves
*/
public function __construct(CompanySlave ...$companySlaves)
{
$this->companySlaves = $companySlaves;
}
/**
* @return CompanySlave[]
*/
public function getCompanySlaves(): array
{
return $this->companySlaves;
}
/**
* 社長の暴言
*
* @param int $number
* @return string
* @throws \Exception
*/
public function companyGreeting(int $number)
{
$companySlave = $this->companySlaves[$number];
if ($companySlave instanceof CompanySlave === false) {
throw new \Exception('オレの会社にはそんな奴隷はいない。');
}
$greetingFormat = '%sはオレの奴隷だ。働け。';
$greeting = sprintf(
$greetingFormat,
$this->companySlaves[$number]->getName()
);
return $greeting;
}
}
// 人間の基底クラス
abstract class Human
{
private $name;
/**
* Human constructor.
*
* @param string $name
*/
public function __construct(string $name)
{
$this->name = $name;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
}
// 社畜を表すクラス
class CompanySlave extends Human
{
// ゴマすり
public function polisher()
{
return '一生社長について行きます!!';
}
}
// ノマドを表すクラス
class Nomad extends Human
{
public function noSyachiku()
{
return 'いや社畜とかマジ無理だから。';
}
}
// ニートを表すクラス
class Neet extends Human
{
public function noSyachiku()
{
return '働いたら負けっしょ?';
}
}
ブラック会社クラス(BlackCompany)は常に人員不足なので、コンストラクタで人間オブジェクトをまとめて渡して欲しい。
しかし、従順な社畜以外はいても社長の気分を損ねるので、社畜オブジェクトのみを受け取りそれ以外はエラーにしたい。
そこでコンストラクタの部分で以下のように定義する。
public function __construct(CompanySlave ...$companySlaves)
{
$this->companySlaves = $companySlaves;
}
CompanySlaveの型を指定している点と...がポイント、この$companySlavesの中身をvar_dumpで出力すると以下のような結果が得られる。
// 中略
$companySlaveA = new \CompanySlave('可哀想な社畜A');
$companySlaveB = new \CompanySlave('可哀想な社畜B');
$companySlaveC = new \CompanySlave('可哀想な社畜C');
$companySlaves = [
$companySlaveA,
$companySlaveB,
$companySlaveC,
];
$blackCompany = new BlackCompany(...$companySlaves);
var_dump($blackCompany->getCompanySlaves());
array(3) {
[0] =>
class CompanySlave#1 (1) {
private $name =>
string(19) "可哀想な社畜A"
}
[1] =>
class CompanySlave#2 (1) {
private $name =>
string(19) "可哀想な社畜B"
}
[2] =>
class CompanySlave#3 (1) {
private $name =>
string(19) "可哀想な社畜C"
}
}
ノマドやニートを渡そうとすると以下のように\TypeErrorという例外がthrowされる。
$companySlaveA = new \CompanySlave('可哀想な社畜A');
$nomadA = new \Nomad('ノマドA');
$neetA = new \Neet('ニートA');
$companySlaves = [
$companySlaveA,
$nomadA,
$neetA,
];
try {
$blackCompany = new BlackCompany(...$companySlaves);
} catch (\TypeError $e) {
var_dump($e->getMessage());
}
string(181) "Argument 3 passed to BlackCompany::__construct() must be an instance of CompanySlave, instance of Neet given, called in /home/vagrant/laravel-api-sample/BlackCompany.php on line 114"
エラーメッセージをざっくり訳すと、「奴隷以外はいらねーよ」 CompanySlaveのインスタンスを期待していたが、Neet型が渡されているよとなる。
主なメリット
BlackCompany(ブラック会社)側は配列の中身が、社畜(CompanySlave)である事が保証される為、社畜を好きなように使う事が出来る。
これが保証されていないと、CompanySlaveしか持っていないメソッドpolisher()を呼び出すまで社畜以外のオブジェクトが社内に入り込んでいる事に気が付かない。
また開発する側も間違って、社畜以外のオブジェクトを渡した時にすぐ例外がthrowされるので間違いに気が付きやすい。
※instanceof 等で常時チェックすれば良いが型を指定したほうがシンプルで堅牢であると思う。
おまけ
PHPマニュアルを見ていたらスカラー型を宣言出来るようになっている事が分かった。
<?php
function sumOfInts(int ...$ints)
{
return array_sum($ints);
}
var_dump(sumOfInts(2, '3', 4.1));
// 出力結果
int(9)
ただしデフォルトは弱い型チェックのようで、例えばintだと 11も"11"も両方通してしまう模様。
厳密な型チェックを行う為には、ファイルの先頭で declare(strict_types=1);
と記述すれば厳密な型チェックが行われるようになる。
PHPの新規開発を結構久しぶりにやっているので他にも新しい発見がないか探ってみようと思う。