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

  • 0
    Like
  • 0
    Comment

    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
    

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