Symfonyでタスク管理アプリ作ってみた（エンティティ編）

SymfonyのORMと言えばDoctrine2です。

> フレームワークの勉強のため、[Symfony3でタスク管理アプリ作ってみた](http://qiita.com/asaokamei/items/8468aa366914664c518c)のパート２です。

今回のタスク管理の中では一番複雑な[Taskクラス](https://github.com/asaokamei/TaskComplete/blob/34133b86a31753be690df68de0e8907ef3549cb1/src/AppBundle/Entity/Tasks/Task.php)を使って説明します。

確かDoctrine2の[モデル生成ツール](http://symfony.com/doc/current/doctrine.html#generating-getters-and-setters)を使った記憶があります…　プロパティとしてはid, status, title, details, doneBy, doneAtがあります。それぞれに[ゲッターとセッターが作成されたエンティティクラス](https://github.com/asaokamei/TaskComplete/blob/34133b86a31753be690df68de0e8907ef3549cb1/src/AppBundle/Entity/Tasks/Task.php)が出来あがりました。長いので、一部を抜き出すと↓の感じ。

```php
/**
 * 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](https://speakerdeck.com/twada/php-conference-2016-revised)」です。

Value Objectの導入
----

意味のある値については、Value Object（値オブジェクト）として管理します。例えばステータス。

### TaskStatus

ステータスとしては、有効と完了、の2つの値を持てることとしました。英語だとActiveとDone。まずはステータス用の値オブジェクトに定数を定義します：

```php
TaskStatus {
    const ACTIVE = 'active';
    const DONE = 'done';
}
```

次は、オブジェクトとして値を保持できるようにします。次のような使い方です。

```php
$status = new TaskStatus(TaskStatus::ACTIVE);
```

これを実現するため`TaskStatus`にコードを追加するのですが、面倒なので[EnumTrait](https://github.com/asaokamei/TaskComplete/blob/611aa2011f99fa3bc4238488e2b72d7efb59e2b1/src/AppBundle/Entity/EntityTrait.php)を作ることにしました。

```php
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;
    }
}
```

すると、次のように使えます。

```php
$status = new TaskStatus(TaskStatus::ACTIVE);
echo $status; // active
echo $status->label(); // Active
echo $status->is(TaskStatus::ACTIVE); // true
```

これを使って先のセッターとゲッターを変更します。モデル内ではstatusは生の値でないと問題がでる（と思う）ので、ゲッターとセッターでTaskStatusオブジェクトに変換します。

```php
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を使って表現します。

```php
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`メソッドです。

```php
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）も必ず設定します。こういう整合性を保たせるには、セッターを使わないのが簡単と思います。

### フォームからの情報修正

あとはエンティティの情報を設定するメソッドです。フォームでの登録/修正に対応しますが、これも操作の一つと考えました。

```php
$task = new Task();
$task->fill($_POST); // ちゃんとバリデーションすること！
```

と出来るようにしました。これは、他のエンティティでも使うので[EntityTrait](https://github.com/asaokamei/TaskComplete/blob/611aa2011f99fa3bc4238488e2b72d7efb59e2b1/src/AppBundle/Entity/EntityTrait.php)として実装してあります。`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
```

> 最初に作ったときに、妙なディレクトリだなと思ったのですが、こういう構造を想定してたんですね。またレポジトリも移動しましたが、元の場所でも良かったのかも、と思うようになりました。