はじめに
こんばんは。
最近やっとフレームワークからドメイン(関心事)の切り離しがわかってきた僕です。
切り離しに関して僕的に一番めんどくさいのがORM(データベース)です。
LaravelだとEloquentです。
EloquentデータからEntityへのマッピングで結構泥臭くさくやらないとだめですよね。
新原さんのコアレイヤパターンとかでも以下みたいな感じで、入れ直しています。
データの数が少ない場合は問題ないんですが、10〜15個とか膨大な数になっちゃうとケアレスミスも起きやすいし、何よりめんどくさい。
この解決方法ないかな〜と考えていたら、自分の師匠である @polidog さんが作ってくれました。
やってみた
例えばこんなEntityクラスがあるとします。
User.php
<?php namespace Kojirock5260\Entity; class User { /** * @var int */ private $id; /** * @var string */ private $email; /** * @var UserProfile */ private $userProfile; }
UserProfile.php
<?php namespace Kojirock5260\Entity; class UserProfile { /** * @var int */ private $id; /** * @var int */ private $userId; /** * @var string */ private $name; /** * @var \DateTime */ private $birthday; /** * @var int */ private $sex; }
先程あげたコアレイヤパターン的な感じで行くならば、 User.php に以下みたいなメソッドを生やす感じになると思います。
public static function ofByArray(array $values) { return new self( $values['id'] ?? 0, $values['email'] ?? '', UserProfile::ofByArray($values['userProfile'] ?? []) ); }
このあたりを object-mapperで書き直すとこうなります。
<?php $parser = new \Helicon\ObjectTypeParser\Parser(); $resolver = new \Helicon\TypeConverter\Resolver(); $hydrator = new \Zend\Hydrator\ReflectionHydrator(); $resolver->addConverter(new \Helicon\TypeConverter\TypeCaster\ScalarTypeCaster()); $resolver->addConverter(new \Helicon\TypeConverter\TypeCaster\DateTimeCaster()); $resolver->addConverter(new \Helicon\TypeConverter\TypeCaster\ClassTypeCaster($resolver, $parser, $hydrator)); $converter = new \Helicon\TypeConverter\Converter($resolver); $mapper = new \Helicon\ObjectMapper\ObjectMapper($converter, $parser, $hydrator); $user = ($mapper)($data, \Kojirock5260\Entity\User::class);
結果
$ php example.php /path/to/example.php:30: class Kojirock5260\Entity\User#61 (3) { private $id => int(100) private $email => string(17) "example@gmail.com" private $userProfile => class Kojirock5260\Entity\UserProfile#45 (5) { private $id => int(1) private $userId => int(1) private $name => string(8) "kojirock" private $birthday => class DateTime#64 (3) { public $date => string(26) "1990-01-01 00:00:00.000000" public $timezone_type => int(3) public $timezone => string(3) "UTC" } private $sex => int(2) } }
実質的なマッピングは以下の行です。
$user = ($mapper)($data, \Kojirock5260\Entity\User::class);
前段でもろもろ準備が必要ですけど、その点はうまくメソッド化すれば問題ないと思います。
DocCommentでマッピングが解決できるのがとにかく素敵ですよね。
拡張してみた(Carbon編)
拡張性が高いもの魅力の一つです。
例えば \DateTime
ではなく Carbonを使いたいとなった場合、オリジナルのTypeCasterを作成することで解決できます。
CarbonTypeCaster.php
<?php namespace Kojirock5260\Caster; use Carbon\Carbon; use Carbon\CarbonImmutable; use Helicon\TypeConverter\TypeCaster\TypeCasterInterface; class CarbonTypeCaster implements TypeCasterInterface { public function convert($value, string $type) { return new $type($value); } public function supports(string $type): bool { return Carbon::class === $type || CarbonImmutable::class === $type; } }
UserProfile.php
の $birthday
のDocCommentを変更します。
/** * @var \DateTime */ private $birthday; ↓ /** * @var CarbonImmutable */ private $birthday;
セットしていたDateTimeTypeCasterをCarbonTypeCasterへ差し替えます。
$resolver->addConverter(new \Helicon\TypeConverter\TypeCaster\DateTimeCaster()); ↓ $resolver->addConverter(new \Kojirock5260\Caster\CarbonTypeCaster());
結果
$ php example.php /path/to/example.php:30: class Kojirock5260\Entity\User#61 (3) { private $id => int(100) private $email => string(17) "example@gmail.com" private $userProfile => class Kojirock5260\Entity\UserProfile#45 (5) { private $id => int(1) private $userId => int(1) private $name => string(8) "kojirock" private $birthday => class Carbon\CarbonImmutable#64 (3) { public $date => string(26) "1990-01-01 00:00:00.000000" public $timezone_type => int(3) public $timezone => string(3) "UTC" } private $sex => int(2) } }
$birthday
がCarbonImmutableに差し替わっています。
拡張してみた(ValueObject編)
例えば email
とかは、ValueObjectのように最小単位の値クラスを作って管理したいです。
以下みたいなValueObjectがあるとします。
MailAddress.php
<?php namespace Kojirock5260\ValueObject; class MailAddress implements ValueObjectInterface { /** * @var string */ private $value; /** * MailAddress constructor. * @param string $value * @throws \InvalidArgumentException */ public function __construct(string $value) { $this->value = $value; if (!$this->valid()) { throw new \InvalidArgumentException('invalid mail address'); } } /** * @return string */ public function value():string { return $this->value; } /** * @return bool */ public function valid():bool { $result = filter_var($this->value, FILTER_VALIDATE_EMAIL); return $result === false ? false : true; } }
この辺どうやるのだろう...。
と思って polidog師匠に相談すると F1レベルの速さで返事がコード付きで返ってきました。
こちらもCarbon同様拡張でどうにでもなるようです。
ValueObjectTypeCaster.php
<?php namespace Kojirock5260\Caster; use Helicon\ObjectTypeParser\Parser; use Helicon\TypeConverter\Resolver; use Helicon\TypeConverter\TypeCaster\TypeCasterInterface; use Kojirock5260\ValueObject\ValueObjectInterface; use Zend\Hydrator\ReflectionHydrator; class ValueObjectTypeCaster implements TypeCasterInterface { /** * @var Resolver */ private $resolver; /** * @var Parser */ private $parser; /** * @var ReflectionHydrator */ private $reflectionHydrator; /** * @param Resolver $resolver * @param Parser $parser * @param ReflectionHydrator $reflectionHydrator */ public function __construct(Resolver $resolver, Parser $parser, ReflectionHydrator $reflectionHydrator) { $this->resolver = $resolver; $this->parser = $parser; $this->reflectionHydrator = $reflectionHydrator; } /** * @param $value * @param string $type * @return mixed|object|\Zend\Hydrator\object * @throws \ReflectionException */ public function convert($value, string $type) { $refClass = new \ReflectionClass($type); $schemas = ($this->parser)($type); $type = $schemas['value']['type']; $convertedValue = $this->resolver->resolve($type)->convert($value, $type); return $this->reflectionHydrator->hydrate([ 'value' => $convertedValue, ], $refClass->newInstanceWithoutConstructor()); } /** * @param string $type * @return bool * @throws \ReflectionException */ public function supports(string $type): bool { $refClass = new \ReflectionClass($type); if ($refClass->isSubclassOf(ValueObjectInterface::class)) { return true; } return false; } }
User.php
の email
のDocCommentを修正します。
/** * @var string */ private $email; ↓ /** * @var MailAddress */ private $email;
先程作ったValueObjectTypeCasterをConverterとして追加します。
$resolver->addConverter(new \Kojirock5260\Caster\ValueObjectTypeCaster($resolver, $parser, $hydrator));
結果
$ php example.php /path/to/example.php:31: class Kojirock5260\Entity\User#62 (3) { private $id => int(100) private $email => class Kojirock5260\ValueObject\MailAddress#49 (1) { private $value => string(18) "example@gmail.com" } private $userProfile => class Kojirock5260\Entity\UserProfile#54 (5) { private $id => int(1) private $userId => int(1) private $name => string(8) "kojirock" private $birthday => class Carbon\CarbonImmutable#68 (3) { public $date => string(26) "1990-01-01 00:00:00.000000" public $timezone_type => int(3) public $timezone => string(3) "UTC" } private $sex => int(2) } }
email
が指定のValueObjectのクラス MailAddress
としてマッピングされています!
終わりに
こんな感じでマッピングが狙ったとおりにきれいにマッピングされるのが object-mapperです。
早速自分のプロジェクトでも推してみようと思います。
今回試したサンプルプロジェクトはgithubに上げておきます。
polidog師匠ありがとうございました。