Laravel/データベースレイヤーの再考

Posted: 2015-05-31 02:02 |  laravel PHP全般 
テストを書くにあたり、より良い設計を考えて実装していくのはフレームワークの機能ではなく、
開発している方次第です。
Eloquentに依存しているシステムなども今一度考えながらリファクタリングを目指してみましょう。
ということで、今回は巷で言われているリポジトリーパターン風ではなく、
スタンダードなリポジトリ+エンティティをLaravelのデータベースコンポーネントの
クエリービルダーを使って実装するサンプルです。
まず一つ、コントローラにEloquentなどのデータベースを用いる処理を乗せればMVCじゃん!
という意識を少し変える必要があります。
今回の流れは リポジトリ->エンティティ->サービス->コントローラ として実装します。
モデルという言葉はどこにも出てきませんが、
この流れを理解するにはモデル=なんとかテーブルという意識を変える必要があり、
データ層を司るものがモデルで、EloquentはたまたまORMなのでデータベースに依存するデータ層を操作するもの、
として捉えます。
= リポジトリはデータ層を操作するものですので、
それに利用するものはデータベースやNoSQL、何かのオブジェクトやなんでも、それに依存しない層です。
データベースは前回のものと同じく

    public function up()
    {
        Schema::create($this->table, function (Blueprint $table) {
            $table->increments('entry_id');
            $table->string('title')->index('entry_title');
            $table->longText('content');
            $table->timestamps();
        });
    }
です。

エンティティを

エンティティはデータベースなどのデータストレージを問わず
なんらかのデータを表現するためのただのオブジェクトです。
データベースを利用した例なので、データベースのカラムを表現したクラスにします。
Eloquentのfillableやguardedのようなイメージです。
namespace App\Entities;

/**
 * Class EntryEntity
 * @package App\Entities
 */
class EntryEntity implements Entityable
{

    /** @var int */
    private $entry_id;

    /** @var string  */
    public $title;

    /** @var string */
    public $content;

    /** @var string */
    public $created_at;

    /** @var string */
    public $updated_at;

    /**
     * @return int
     */
    public function getId()
    {
        return $this->entry_id;
    }
}
ただのクラスですね

リポジトリ

namespace App\Repositories;

use App\Entities\EntryEntity;

/**
 * Interface EntryRepository
 * @package App\Repositories
 */
interface EntryRepositoryInterface
{

    /**
     * @param EntryEntity $item
     * @return EntryEntity
     */
    public function save(EntryEntity $item);

    /**
     * @param $id
     * @return EntryEntity|null
     */
    public function find($id);

    /**
     * @return mixed
     */
    public function findAll();
}
↑のようなインターフェースを用意します。
実装する具象クラスは、操作に必要な手段はなんでもあっても構わずに
操作だけを担当しますので、下記のように実装してみます。
namespace App\Repositories;

use App\Entities\EntryEntity;
use App\Repositories\Criteria\Entryable;

/**
 * Class EntryRepository
 * @package App\Repositories
 */
class EntryRepository implements EntryRepositoryInterface
{

    /** @var Entryable  */
    protected $entryable;

    /**
     * @param Entryable $entryable
     */
    public function __construct(Entryable $entryable)
    {
        $this->entryable = $entryable;
    }

    /**
     * @param EntryEntity $item
     * @return EntryEntity
     */
    public function save(EntryEntity $item)
    {
        $this->entryable->save($item);
        return $item;
    }

    /**
     * @param $id
     * @return EntryEntity|null
     */
    public function find($id)
    {
        return $this->entryable->find($id);
    }

    /**
     * @return mixed
     */
    public function findAll()
    {
        return $this->entryable->findAll();
    }
}
エンティティを利用してsaveするとなります。
ここでもデータベース関連は一切関心を持たないので直接記述することもありません。
コンスタラクタインジェクションでタイプヒンティングされているものはインターフェースで、
そのインターフェースを実装したクラスがデータ操作を司るなにかということになります。

操作を司るなにか

操作はデータベースかもしれませんし、mongodbかもしれませんし、他のなにかかもしれません
ので、共通のインターフェースとして下記のものを用意します。
namespace App\Repositories\Criteria;

use App\Entities\EntryEntity;

/**
 * Interface Entryable
 * @package App\Repositories\Criteria
 */
interface Entryable
{

    /**
     * @param EntryEntity $entity
     * @return mixed
     */
    public function save(EntryEntity $entity);

    /**
     * @param $id
     * @return mixed
     */
    public function find($id);

    /**
     * @return mixed
     */
    public function findAll();

}
インターフェースの名前があまりよくないですが、Entry関連を操作するもの、ということです。

と言いつつも今回はデータベースを利用していますので、
このインタフェースを実装したデータベースのクラスを用意します。
下記のようなシンプルなものです。
namespace App\Repositories\Criteria;

use App\Entities\EntryEntity;
use Illuminate\Database\DatabaseManager;

/**
 * Class EntryDataAccessObject
 * @package App\Repositories\Criteria
 */
class EntryDataAccessObject extends FluentObject implements Entryable
{

    /** @var DatabaseManager */
    protected $db;

    /** @var string */
    protected $table = 'entries';

    /** @var string  */
    protected $identity = 'entry_id';

    /**
     * @param DatabaseManager $db
     */
    public function __construct(DatabaseManager $db)
    {
        $this->db = $db;
    }

    /**
     * @param EntryEntity $item
     * @return int
     */
    public function save(EntryEntity $item)
    {
        if (is_null($item->getId())) {
            return $this->db->connection()
                ->table($this->table)->insertGetId($this->data($item));
        }
        return $this->db->connection()
            ->table($this->table)
            ->where($this->identity, $item->getId())->update($this->data($item));
    }

    /**
     * @param $id
     * @return EntryEntity
     */
    public function find($id)
    {
        $data = $this->db->connection()
            ->table($this->table)->where($this->identity, $id)->first();
        if(is_null($data)) {
            return null;
        }
        return $this->getData($data, new EntryEntity);
    }

    /**
     * @return array|static[]
     */
    public function findAll()
    {
        return $this->db->connection()
            ->table($this->table)->get();
    }

}
では全体ができたところで利用のイメージですが、

というイメージです。
ビジネスロジックを記述するクラスはサービスとして下記のように実装してみます。
*簡単なサンプルなのでサービスにインターフェース用意したい方はどうぞ!
namespace App\Services;

use Carbon\Carbon;
use App\Entities\EntryEntity;
use App\Repositories\EntryRepositoryInterface;

/**
 * Class EntryService
 * @package App\Services
 */
class EntryService
{

    /** @var EntryRepositoryInterface  */
    protected $entry;

    /**
     * @param EntryRepositoryInterface $entry
     */
    public function __construct(EntryRepositoryInterface $entry)
    {
        $this->entry = $entry;
    }

    /**
     * @param $id
     * @return \App\Entities\EntryEntity|null
     */
    public function getEntry($id)
    {
        return $this->entry->find($id);
    }

    /**
     * @param array $params
     * @return EntryEntity
     */
    public function setEntry(array $params)
    {
        $entry = new EntryEntity;
        $entry->content = $params['content'];
        $entry->title = $params['title'];
        $datetime = Carbon::now()->toDateTimeString();
        $entry->created_at = $datetime;
        $entry->updated_at = $datetime;
        return $this->entry->save($entry);
    }
}
リポジトリのインターフェースをタイプヒンティングで指定し、
サービス層からもデータベースであろうと何であろうと関心を持たない、何にも依存しないようになります。
エンティティは共通の表現として利用しますので、そのまま利用しています。
コンテナへの登録は下記とします。
        $this->app->bind(
            'App\Repositories\EntryRepositoryInterface',
            'App\Repositories\EntryRepository'
        );
        $this->app->bind(
            'App\Repositories\Criteria\Entryable',
            'App\Repositories\Criteria\EntryDataAccessObject'
        );
あとはビジネスロジックを利用するコントローラで
    /** @var EntryService  */
    protected $entry;

    /**
     * @param EntryService $entry
     */
    public function __construct(EntryService $entry)
    {
        $this->entry = $entry;
    }

    /**
     * @param $id
     * @return \App\Entities\EntryEntity|null
     */
    public function show($id = null)
    {
        return response()->json($this->entry->getEntry($id));
    }

    /**
     * @return \App\Entities\EntryEntity
     */
    public function store()
    {
        $this->entry->setEntry([
            'title' => 'hello Laravel5.1',
            'content' => 'Laravel makes connecting with databases and running queries extremely simple.',
        ]);
    }

のように利用するだけです。
面倒臭さが増したように感じられるかもしれませんが、
開発における大きなヒントになると思いますので是非トライしてみてください。
サンプルはこちら

about ytake

執筆に参加しています


Laravel お役立ち情報

share



このエントリーをはてなブックマークに追加

Categories

laravel 45

DTM 0

music 0

PHP全般 31

0

JAPAN 1

WORLD 1

javascript 4

RDBMS 1

NoSQL 1

NewSQL 1

Recent Posts

Ad

comments powered by Disqus

GitHub

Social Links

Author


クリエイティブ・コモンズ・ライセンス
Yuuki Takezawa 作『Ytake Blog』はクリエイティブ・コモンズ 表示 - 非営利 4.0 国際 ライセンス で提供されています。

© ytake/comnect All Rights Reserved. 2014