RitoLabo

LaravelでRepositoryパターンを実装する

  • 公開:
  • 更新:
  • カテゴリ: PHP Laravel
  • タグ: Laravel,Repository,Architecture

Laravelは自由度の高いPHPフレームワークですが、ノー設計プランでアプリケーション開発を行うと、規模が大きくなってきた時に多々辛みポイントが発生してきます。 肥大化したソースコードに整理されていないクラス群などなど、、機能追加の前に要リファクタリングなんて状況にもなりかねず、設計は大事な要素の1つだと思い知らされます。

今回は、設計手法の1つとして、Repositoryパターンでの実装をLaravelで行います。

開発環境

今回の開発環境は以下の通りです。

  • MySQL 8.0
  • PHP 7.2
  • Laravel 5.8

今回は、Laravelで予め用意されているUserのモデルやテーブルを用いて実装を行って行きます。マイグレーションを行い、予めテーブルを作成してください。

尚、Laravelのルートディレクトリを「laravel/」としています。

Repositoryパターンとは

Repository(リポジトリ)パターンは、ドメイン駆動型設計の一つとして導入されたもので、データソースへのアクセスの抽象化を提供するものです。

それぞれ異なる性質を持つデータソースに対して共通のインターフェイスを定義しそれをリポジトリとして実装する事で、 ビジネスロジックからデータアクセスを分離(疎結合)させ、機能としての拡張性・保守性を向上させる事ができます。

簡単に言うと、データソースへのアクセス・操作に関して「SELECT」「INSERT」「UPDATE」「DELETE」が思い浮かびますが、 例えばRDBMSとNoSQLで文法の違いがあるので、これらの操作の為に私達が書くコードにはそれぞれ違いがあります。 これらを共通のインターフェイスを持たせる事で、ビジネス側ではどのデータソースかを意識せずにデータ側を扱う事ができるようになる。という感じです。

これは機能を疎結合に保つことから、例えば仕様変更などが合った場合でも容易にデータアクセスを変更したり、追加したりする事ができます。 また、データ側をモック化する事でテストが容易に行えるなどの保守性も向上します。

今回の実装例

今回は、クエリビルダとEloquentを用いたデータ取得機能をRepositoryパターンで実装し、切り替えてみようと思います。

Seeder&Fakerを用いてデータ投入

今回はLaravelに最初から定義されているUserテーブルを使うので、まずはデータを投入しておきます。 データ投入には、Fakerを用いてダミーデータを挿入します。

まずはダミーデータの定義の為にFactoryクラスが必要ですが、Userモデルの場合は既に用意されています。

laravel/database/factories/UserFactory.php
<?php

use App\User;
use Illuminate\Support\Str;
use Faker\Generator as Faker;

$factory->define(User::class, function (Faker $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'remember_token' => Str::random(10),
];
});

今回はこのまま使用します。

尚、User以外で行う場合は以下のartisanコマンドでFactoryクラスを生成し、定義する必要があります。

php artisan make:factory HogeFactory

次に、シーダクラスを生成します。以下のartisanコマンドを叩きます。

# Laravelルートディレクトリへ移動
cd /path/to/laravel

# シーダ生成
php artisan make:seeder UsersTableSeeder

# 実行結果
[demo@localhost laravel]$ php artisan make:seeder UsersTableSeeder
Seeder created successfully.

laravel/database/seeds 配下に UsersTableSeeder.php が生成されます。

laravel
├─ database
│ ├─ seeds
│   │   └─
UsersTableSeeder.php
laravel/database/seeds/UsersTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
public function run()
{
}
}

SeederクラスをFactoryクラスを用いて以下のように実装します。

laravel/database/seeds/UsersTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
public function run()
{
factory(App\User::class, 10000)->create();
}
}

今回は10,000件挿入します。

Seederクラス定義したら、DatabaseSeederクラスへの登録も忘れずに

laravel/database/seeds/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
public function run()
{
$this->call(UsersTableSeeder::class);
}
}

そしてComposerのオートローダを再生成します。

composer dump-autoload

ちなみにオートローダを再生成しないと、シーディングを実行した際に
Class Database\Seed\UsersTableSeeder does not exist
と怒られるので、忘れずに叩きます。

それではデータを投入します。

# Laravelルートディレクトリへ移動
cd /path/to/laravel

# Seeding実行
php artisan db:seed

# 実行結果
[demo@localhost laravel]$ php artisan db:seed
Seeding: UsersTableSeeder
Database seeding completed successfully.

MySQLに入って確認します。

mysql> SELECT COUNT(id) FROM `users`;
+----------+
| count(id)|
+----------+
| 10000 |
+----------+
1 row in set (0.00 sec)

10,000件挿入された事を確認しました。

Repositoryパターン実装

データの準備ができたので、Repositoryパターンを実装していきます。

まずはインターフェイスを定義します。以下のディレクトリ階層(適宜作成)で、UserDataAccessRepositoryInterface.phpを作成します。

laravel
├─ app
│ ├─ Repositories
│ │ └── User
│   │   └─
UserDataAccessRepositoryInterface.php

どこに作るかはもちろん自由です。(ファイル名長くなりがちなのが最近の悩み)

laravel/app/Repositories/User/UserDataAccessRepositoryInterface.php
<?php

namespace App\Repositories\User;

interface UserDataAccessRepositoryInterface
{
public function getAll();
}

今回は、先に挿入した10000件のデータ全件を取得するメソッドのみを宣言します。

続いて、このインターフェイスを実装します。

こちらも以下のディレクトリ階層(適宜作成)で、クエリビルダと、EloquentORMを用いたデータ取得クラスを作成します。

laravel
├─ app
│ ├─ Repositories
│ │ └── User
│   │   ├─ UserDataAccessRepositoryInterface.php
│   │   ├─
UserDataAccessQBRepository.php
│   │   └─
UserDataAccessEQRepository.php

今回はリポジトリの切り替えがメインなのでその先のサービスクラスとかはひとまず置いておき、両方ともシンプルにデータを全件取得し返します。

laravel/app/Repositories/User/UserDataAccessQBRepository.php
<?php

namespace App\Repositories\User;
use DB;

class UserDataAccessQBRepository implements UserDataAccessRepositoryInterface
{
protected $table = 'users';

public function getAll()
{
return DB::table($this->table)->get();
}
}
laravel/app/Repositories/User/UserDataAccessEQRepository.php
<?php

namespace App\Repositories\User;

use App\User;

class UserDataAccessEQRepository implements UserDataAccessRepositoryInterface
{
public function getAll()
{
return User::all();
}
}

実装できたら、これをサービスプロパイダに登録します。 記述先はAppServiceProviderでも良いのですが、「Repository」を強調したいので、 RepositoryServiceProviderを作成して登録します。

# Laravelルートディレクトリへ移動
cd /path/to/laravel

# サービスプロパイダ作成
php artisan make:provider RepositoryServiceProvider

# 実行結果
[demo@localhost laravel]$ php artisan make:provider RepositoryServiceProvider
Provider created successfully.

artisanコマンドを叩いて生成したら、以下を定義します。

laravel/app/Providers/RepositoryServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
\App\Repositories\User\UserDataAccessRepositoryInterface::class,
\App\Repositories\User\UserDataAccessQBRepository::class
);
}

public function boot()
{
}
}

まずはクエリビルダでのデータ取得クラスを登録しました。

忘れずに、設定ファイルにサービスプロバイダを追加します

laravel/config/app.php
'providers' => [
/*
.
省略
.
.
.
*/
// 追加
App\Providers\RepositoryServiceProvider::class,

],

一旦ここで、リポジトリクラス周りの実装は完了です。

コントローラ&ルーティング

最後にルーティングと、コントローラからの使用を前提に処理を定義します。 せっかくなので、全件取得した時のクエリビルダとEloquentORMでどれくらい時間がかかるのかを測ってもみたいと思います。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Repositories\User\UserDataAccessRepositoryInterface AS UserDataAccess;

class SampleController extends Controller
{
protected $User;

public function __construct(UserDataAccess $UserDataAccess)
{
$this->User = $UserDataAccess;
}

public function index()
{
$start = microtime(true);
$memory = memory_get_usage();

// ここでデータ取得を行う
$data = $this->User->getAll();

$result = [
// どちらのリポジトリを使用しているかわかるように
'name' => get_class($this->User),
// 実行時間
'time' => microtime(true) - $start,
// 使用メモリ
'memory' => (memory_get_peak_usage() - $memory) / (1024 * 1024)
];

// 結果出力
var_dump($result);
}
}

インターフェイスをuseし、コンストラクタインジェクションでメンバ変数へデータ取得機能を代入しています。 あとは、データ取得を行いつつ、時間とメモリを計測して結果をダンプする。といった流れです。

最後にルーティング

laravel/routes/web.php
Route::get('/sample', 'SampleController@index');

動作確認とリポジトリの変更

一通り実装できたので、実際に動作を確認してみます。

クエリビルダでのデータ取得結果画面

現在はクエリビルダでのデータ取得を登録しているので、こちらでデータ取得が行われている事が確認できます。

ここで、データ取得をEloquentORMで行う方針変換があったとします。

リポジトリを切り替えて今度はEloquentORMでのデータ取得に切り替えてみます。

laravel/app/Providers/RepositoryServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
\App\Repositories\User\UserDataAccessRepositoryInterface::class,
// \App\Repositories\User\UserDataAccessQBRepository::class
// ↓ 変更
\App\Repositories\User\UserDataAccessEQRepository::class
);
}

public function boot()
{
}
}

ブラウザからアクセスして確認します。

EloquentORMでのデータ取得結果画面

今度はEloquentORMでのデータ取得クラスに切り替わっている事が確認できました。

まとめ

このようにして、Repositoryパターンを用いるとビジネスロジック側に変更を加えずに機能の切り替えが容易に行えるようになります。 また、先述した通り、ビジネス側とデータ側が分離されたので、テストも行い易くなっているはずです。

ちなみにせっかくなので触れると、クエリビルダの方がEloquentより4倍弱早かったですね。

ぜひ試してみてください。
サンプルコード