Help us understand the problem. What is going on with this article?

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

More than 3 years have 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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした