PHP
Symfony
symfony3

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

More than 1 year has passed since last update.

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

まずはisActivedoneメソッドです。

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

DoneDateTaskエンティティ以外でも使われたので、Genericというディレクトに入れてます。次に作るときは、例えばCommonという名前のほうがいいかなと思います。

そして、書いていて気がついたのですが、次のような構造にすればいいのか…

Entity/
├── Common
│   ├── DoneDate.php
│   ├── EntityTrait.php
│   └── EnumTrait.php
├── Project
│   ├── Project.php
│   └── ...
└── Task
    ├── Task
    ├── TaskDate.php
    ├── TaskRepository.php
    ├── TaskStatus.php
    └── Task.php

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