Edited at

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


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