SymfonyのORMと言えばDoctrine2です。
フレームワークの勉強のため、Symfony3でタスク管理アプリ作ってみたのパート2です。
今回のタスク管理の中では一番複雑なTaskクラスを使って説明します。
確かDoctrine2のモデル生成ツールを使った記憶があります… プロパティとしてはid, status, title, details, doneBy, doneAtがあります。それぞれにゲッターとセッターが作成されたエンティティクラスが出来あがりました。長いので、一部を抜き出すと↓の感じ。
/**
* Task
*
* @ORM\Table(name="task_task")
* @ORM\Entity(repositoryClass="AppBundle\Repository\Tasks\TaskRepository")
*/
class Task
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="status", type="string", length=16)
*/
private $status = TaskStatus::ACTIVE;
/**
* @var \DateTime
*
* @ORM\Column(name="done_by", type="datetime", nullable=true)
*/
private $doneBy;
// ... 大量のプロパティ ...
/**
* @return int
*/
public function getId() {
return $this->id;
}
/**
* @param string $status
* @return Task
*/
public function setStatus($status) {
$this->status = $status;
return $this;
}
/**
* @return string
*/
public function getStatus() {
return $this->status;
}
/**
* @param \DateTime $doneBy
* @return Task
*/
public function setDoneBy($doneBy) {
$this->doneBy = $doneBy;
return $this;
}
/**
* @return \DateTime
*/
public function getDoneBy() {
return $this->doneBy;
}
// ... 大量のゲッターとセッター ...
このエンティティを変更してゆきます。参考にしたのは「【改訂版】PHP7 で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計 / PHP Conference 2016」です。
Value Objectの導入
意味のある値については、Value Object(値オブジェクト)として管理します。例えばステータス。
TaskStatus
ステータスとしては、有効と完了、の2つの値を持てることとしました。英語だとActiveとDone。まずはステータス用の値オブジェクトに定数を定義します:
TaskStatus {
const ACTIVE = 'active';
const DONE = 'done';
}
次は、オブジェクトとして値を保持できるようにします。次のような使い方です。
$status = new TaskStatus(TaskStatus::ACTIVE);
これを実現するためTaskStatus
にコードを追加するのですが、面倒なのでEnumTraitを作ることにしました。
namespace AppBundle\Entity\Tasks\Task;
use AppBundle\Entity\EnumTrait;
class TaskStatus
{
use EnumTrait;
const ACTIVE = 'active';
const DONE = 'done';
protected $choices = [
self::ACTIVE => 'Active',
self::DONE => 'Done',
];
/**
* @return array
*/
public function getChoices()
{
return $this->choices;
}
}
すると、次のように使えます。
$status = new TaskStatus(TaskStatus::ACTIVE);
echo $status; // active
echo $status->label(); // Active
echo $status->is(TaskStatus::ACTIVE); // true
これを使って先のセッターとゲッターを変更します。モデル内ではstatusは生の値でないと問題がでる(と思う)ので、ゲッターとセッターでTaskStatusオブジェクトに変換します。
class Task
{
/**
* @var string
* @ORM\Column(name="status", type="string", length=16)
*/
private $status = TaskStatus::ACTIVE;
public function getStatus() {
return new TaskStatus($this->status);
}
public function setStatus(TaskStatus $status) {
$this->status = (string) $status;
}
}
TaskDate
タスクの日付表示を例にして、もうちょっと複雑なValue Objectについて説明してみます。
タスクのdoneBy(完了目標日あるいは作業予定日)ですが、設定しないケースもあります。単なるToDoという扱いの場合です。一方、完了したタスクを表示した場合は、完了日(doneAt)を表示したいと考えました。まとめると、
- ステータスが完了なら、doneAtを表示。
- ステータスが有効で、doneByがあればdoneByを表示。
- それ以外は日付を表示しない。
となります。
これをValue Objectを使って表現します。
class TaskDate
{
private $status;
private $doneBy;
private $doneAt;
/**
* @param TaskStatus $status
* @param DateTime $doneBy
* @param DateTime $doneAt
*/
public function __construct(TaskStatus $status, $doneBy, $doneAt) {
$this->status = $status;
$this->doneBy = $doneBy;
$this->doneAt = $doneAt;
}
/**
* @param string $format
* @return string
*/
public function format($format = 'm/d')
{
$date = $this->status->is(TaskStatus::DONE)
? $this->doneAt
: $this->doneBy;
if ($date) {
return $date->format($format);
}
return '';
}
}
考えたことを、実直にコードで表してます。
ところで、日付表示ですが年は必要ない場合が多いです。そんな先の話はあまりないですから。なので、基本の日付表示はm/d
としたいところです。でも、来年の話や過去(やり残し)の場合は、年表示がないと混乱します。まぁY/m
で表示するとしましょう。
この仕様もValue Objectとして実装してみました。DoneDate
というクラスです。説明は省略しますが、やはり考えたロジックを忠実にコードにしています。
セッター削除
話変わりますが、セッターが余り好きではないです。そもそもエンティティは操作するものという気がしてます。なので、セッターを削除してしまいました。先のsetStatusもざっくりです。
そのかわり、操作するメソッドを追加しました。
isActive, done
まずはisActive
とdone
メソッドです。
Task {
public function isActive() {
return $this->status === TaskStatus::ACTIVE;
}
public function done(\DateTime $doneAt = null) {
$this->doneAt = $doneAt ?: new \DateTime();
$this->status = TaskStatus::DONE;
return $this;
}
}
としました。
done
でタスクを完了させると、ステータスを変更するとともに完了日付(doneAt)も必ず設定します。こういう整合性を保たせるには、セッターを使わないのが簡単と思います。
フォームからの情報修正
あとはエンティティの情報を設定するメソッドです。フォームでの登録/修正に対応しますが、これも操作の一つと考えました。
$task = new Task();
$task->fill($_POST); // ちゃんとバリデーションすること!
と出来るようにしました。これは、他のエンティティでも使うのでEntityTraitとして実装してあります。fill
という名前は、Eloquentから取りました。
ディレクトリ構造
Value Objectクラスの置き場所ですが、エンティティごとにまとめて置きたいところです。こんな感じになりました。
Entity/
├── EntityTrait.php
├── EnumTrait.php
└── Tasks
├── Generic
│ └── DoneDate.php
│...
├── Task
│ ├── TaskDate.php
│ ├── TaskRepository.php
│ └── TaskStatus.php
└── Task.php
DoneDate
はTask
エンティティ以外でも使われたので、Generic
というディレクトに入れてます。次に作るときは、例えばCommon
という名前のほうがいいかなと思います。
そして、書いていて気がついたのですが、次のような構造にすればいいのか…
Entity/
├── Common
│ ├── DoneDate.php
│ ├── EntityTrait.php
│ └── EnumTrait.php
├── Project
│ ├── Project.php
│ └── ...
└── Task
├── Task
├── TaskDate.php
├── TaskRepository.php
├── TaskStatus.php
└── Task.php
最初に作ったときに、妙なディレクトリだなと思ったのですが、こういう構造を想定してたんですね。またレポジトリも移動しましたが、元の場所でも良かったのかも、と思うようになりました。