19
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DiverseAdvent Calendar 2019

Day 14

[Laravel]RegisterController.phpさん、抽象化ダイエット大作戦!

Last updated at Posted at 2019-12-14

Diverse Advent Calendar 2019 14日目の記事です。

こんにちは! @gatapon です!
エンジニア歴1年未満ですが無事生きてます。元気にしてます!
業務では使っているわけではないのですが、Laravelについて書いてみようと思います。

概要

少し設計に興味を持った私がRegisterController.phpをLaravelの機能を利用したり、レイヤー化させてみました。

やったこと

  • RegisterController.phpの処理の分離
  • Service層の導入
  • Repository層の導入

はじめに

 「どうしてこんな記事を書くのか」という経緯をポエムっぽく書きます。

 4月からエンジニアとして実務を経験してきましたが、入社前の私は個人で小さなポートフォリオを作った程度でした。実際に仕事で触れるプロダクトはポートフォリオと比べ物にならないくらい大きく、ちょっとした機能の修正であっても「どこに記述するべきか」、「どう記述するべきか」という事に悩む事が多かったです。

エンジニアになって3,4ヶ月程経ち、少しずつですが経験を重ね「ここはこう書いたほうが良いよな」みたいな勘が、なんとなくではありますが自分の中で形成されていきました。しかしそれはあくまで勘でしかなく、「なんとなく正しい」と思ってるだけであり、客観的に正しいかどうか自分には判断できませんでした。自分の中で言語化できておらず他人に伝えることも、なかなか大変でした。

 そういった事が原因か判りませんが、抽象的なものを読みたいという欲求に駆られていろいろ読み物を漁りました。コードを『どこに記述するべきか』『どう記述するべきか』について読み漁っていくうちに「設計」を勉強することがが近道ではないかなと感じ、少しずつ興味を持つようになりました。

以前、Laravelを使ってポートフォリオを作っていたのもあり、たまたま持っていた本もLaravelのクリーンアーキテクチャについて詳細に記述されていたのでLaravelを使って勉強してみることにしました。

その中でもLaravelのRegisterController.phpの書かれ方が気になったので、いろいろいじってみました。
それが割と面白かったので、簡単ではありますが内容をお伝えしたいと思います。

以下、出てくるソースは実装をフェイズごとに区切っていますが、勉強時にディレクトリ構造やファイル名、テーブル名など命名にも試行錯誤重ね、度々変更していました。記事中のファイル名などに不整合が起こっているかもしれません。統一するよう見直しましたが、漏れがあったらご指摘下さい。

RegisterController.phpを手直し

まずはそのままのRegisterController.phpを見てみます。

app/Http/Controllers/Auth/RegisterController.php

app/Http/Controllers/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------     
    | Register Controller
    |-------------------------------------------------------------------------- 
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name'  => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array $data
     * @return \App\User
     */
    protected function create(array $data)
    {
        return User::create([
            'name'  => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}

ユーザーの新規登録の処理がコントローラー内にまとめられています。これは各プロジェクトごとに拡張しやすいように、良い感じにしてくれているのでしょう。RegisterController.phpにはvalidation, ユーザーの作成する処理はありますが、実体となるregisterメソッドはここにはありません。

route/web.php
Route::post('auth/register', 'Auth\RegisterController@register');

しかし、それでもRouteで呼ぶことができています。
それはregisterメソッドを持つRegistersUsersトレイトを継承しているからです。

framework/src/illuminate/Foundation/Auth/RegistersUsers.php

これらソースをもとに下準備を行い記述します。
一部修正というか、別のディレクトリにてファイルを作り直しています。

app/Http/Controllers/Api/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Profile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Registered;
use Illuminate\Foundation\Auth\RegistersUsers;

class RegisterController extends Controller
{

    use RegistersUsers;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * register
     *
     * @param  Request
     * @return JsonResponse
     */
    public function register(Request $request)
    {
        $validate = $this->validator($request->all());
        if ($validate->fails()) {
            return new JsonResponse($validate->errors());
        }
        event(new Registered($user = $this->create($request->all())));
        if (empty($user)) {
            return new JsonResponse('Error');
        }
        return new JsonResponse($user);
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name'     => ['required', 'string', 'max:255', 'unique:users'],
            'email'    => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    /**
     * Transaction for create a new user and profile, after a valid registration.
     *
     * @param  array $data
     * @return \App\Models\User
     */
    protected function create(array $data)
    {

        DB::beginTransaction();
        try {
            $user = User::create([
                'name'     => $data['name'],
                'email'    => $data['email'],
                'password' => Hash::make($data['password']),
            ]);
            Profile::create([
                'user_id'  => $user->id,
                'country'  => '',
                'question' => '',
                'answer'   => '',
            ]);

            DB::commit();
            return $user;

        } catch(\PDOException $e) {
            DB::rollBack();
            Log::Debug('Transaction Error: '. print_r($e, true));
        }
    }
}

app/Http/Controllers/Api/RegisterController.phpを作成し、以下を実施しました。

  • RegistersUsersregisterメソッドをもってくる。
  • 手軽に検証できるようにregisterメソッドはJsonを返すように修正
  • createメソッドにユーザー新規作成と同時にProfileも作成するトランザクションを追加

いい感じに肥えて来ましたね!
その他、Profileモデルの実装やテーブル等の準備はありますが端折ります。

作成したRegisterController.phpのダイエット

準備ができました。
早速以下2つを行いRegisterController.phpをスッキリさせます。

  • LaravelのFormRequest機能を利用
  • Service層を作成して処理層を分離

これらを行うことで、コントローラーは「リクエストを受け取り、要求されたデータを返す」だけのシンプルな形になります。

app/Http/Controllers/Api/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\UserRegistPost;
use App\Services\UserRegisterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Registered;
use Illuminate\Foundation\Auth\RegistersUsers;

class RegisterController extends Controller
{

    use RegistersUsers;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct(UserRegisterService $service)
    {
        $this->middleware('guest');
        $this->service = $service;
    }

    /**
     * register
     * @param  UserRegistPost
     * @return JsonResponse
     */
    public function register(UserRegistPost $request)
    {
        # userRegisterTransactionについては後述
        event(new Registered($user = $this->service->userRegisterTransaction($request->all())));
        if (!$user) {
            return new JsonResponse('Error');
        }
        return new JsonResponse($user);
    }
}

FormRequest

LaravelにあるFormRequestはとても便利です。バリデーションのロジックをコントローラーから分離できます。
また、コントローラーのメソッドに依存注入することでコントローラーがリクエストを受け取った時点でバリデーションが行われます。
registerメソッド内にバリデーションエラー処理も記述しなくて済み、コントローラーがスッキリしました。

app/Http/Requests/UserResistPost.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\Traits\ApiFormRequestTrait;

class UserRegistPost extends FormRequest
{
    use ApiFormRequestTrait;

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name'  => ['required', 'string', 'max:255', 'unique:users'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ];
    }
}

今回のAPIのFormRequestのValidationエラーの実装は、以下リンクを参考にさせてもらいました。これ以外の方法もあるようです。脇道にそれるので、ここでは記述しません。

【Laravel5】FormRequestのバリデーション結果をJSON APIで返す

Service層の導入

MVCの下に作成されたフレームワークにはFatモデル、Fatコントローラー問題があります。
Service層の導入はその問題を緩和することが出来ます。
モデルやコントローラーからビジネスロジックを分離させることができるので、本来の責務だけ任せることが出来ます。

app/Services/UserRegisterService.php
<?php

namespace App\Services;

use App\Models\User;
use App\Models\Profile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;

class UserRegisterService
{
    /**
     * Transaction for create a new user and profile, after a valid registration.
     *
     * @param  array $data
     * @return User
     */
    public function userRegisterTransaction(array $data)
    {
        DB::beginTransaction();
        try {
            $user = User::create([
                'name'     => $data['name'],
                'email'    => $data['email'],
                'password' => Hash::make($data['password']),
            ]);
            Profile::create([
                'user_id'  => $user->id,
                'country'  => '',
                'question' => '',
                'answer'   => '',
            ]);

            DB::commit();
            return $user;

        } catch(\PDOException $e) {
            DB::rollBack();
            Log::Debug('Transaction Error: '. print_r($e, true));
        }
    }
}

メソッド名をuserRegisterTransactionに変更していますが、ただ単にapp/Services/UserRegisterService.phpにお引越ししてきただけです。
これでRegisterController.phpのダイエットは完了です。

UserRegisterService.phpさんにもダイエットしてもらおう

Repository層の導入

Service層はビジネスロジックを分離できるメリットがあるとお話しましたが、移植してきたものの内容を見てみるとデータの操作ぐらいしか行っていません。と言うか、ビジネスロジックに集中したいService層でデータアクセスを直接行っています。この書き方はもう少し変えていきたいと思います。
ビジネスロジックからデータ操作を切り離すことで、テスト容易性や、保守性、拡張性が保証されます。そこでドメイン駆動開発にも紹介されているRepository層を導入してみようと思います。


注意
実際問題、めったにデータソースが変わることがないサーバーにRepository層を装する必要は、あまりないかもしれません。レイヤー化、抽象化を目的とした設計なら他の方法があるかもしれませんね。
参考:“Repositoryによる抽象化の理想と現実/Ideal and reality of abstraction by Repository - Speaker Deck”


抽象クラス(RepositoryInterface)と具象クラス(Repository)の実装

私は今のところ、Repository層の最大の目的はデータの永続性だと理解しています。データソースが変更された時(MySQL→NoSQLなど)、移行の対応を極力抑えることが出来ます。各RepositoryInterfaceを継承した具象クラスを実装するだけなので、ビジネスロジックに手を加える必要がなくなります。

User, Profileそれぞれの今回使うメソッドのみの実装を行っていきます。

RepositoryInterfaceの実装

app/Repositories/Interfaces/UserRepositoryInterface
<?php

namespace App\Repositories\Interfaces;

interface UserRepositoryInterface
{
    public function register(array $data)
}
app/Repository/Interfaces/ProfileRepositoryInterface.php
<?php

namespace App\Repositories\Interfaces;

interface ProfileRepositoryInterface
{
    public function create(int $user_id);
}

インターフェイスはあくまで骨組みだけになります。各データソースごとに、このInterfaceを継承した具象クラスを作成することで変更の対応が容易になります。(後に述べるInterfaceとのバインディングを変更する必要があります)
今回のデータ操作はLaravelのORM、Eloquentを利用していきます。
クエリビルダ、SQLを使うことも出来ます。

Repositoryの実装

app/Repositories/UserRepository.php

<?php

namespace App\Repositories;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use App\Repositories\Interfaces\UserRepositoryInterface;

class UserRepository implements UserRepositoryInterface
{
    protected $user;

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

    /**
     * Create User
     * 
     * @param  $data
     * @return \App\Models\User
     */
    public function register(array $data)
    {
        return $this->user->create([
            'name'     => $data['name'],
            'email'    => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}
app/Repository/Interfaces/ProfileRepository.php
<?php

namespace App\Repositories;

use App\Models\Profile;
use App\Repositories\Interfaces\ProfileRepositoryInterface;

class ProfileRepository implements ProfileRepositoryInterface
{
    protected $profile;

    public function __construct(Profile $profile)
    {
        $this->profile = $profile;
    }

    /**
     * Create Profile
     *
     * @param  int $user_id
     * @return void
     */
    public function create(int $user_id)
    {
        $this->profile->create([
            'user_id'  => $user_id,
            'country'  => '',
            'question' => '',
            'answer'   => '',
        ]);
    }
}

Repository層を実装したUserRegisterService.phpは以下になります。
userRegisterTransactionメソッドに依存注入されているのは具象クラスでなく抽象クラスであるInterfaceであることに注意して下さい。

app/Services/UserRegisterService.php
<?php

namespace App\Services;

use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Repositories\Interfaces\UserRepositoryInterface;
use App\Repositories\Interfaces\ProfileRepositoryInterface;

class UserRegisterService
{
    protected $user;
    protected $profile;

    public function __construct(UserRepositoryInterface $user, ProfileRepositoryInterface $profile)
    {
        $this->user    = $user;
        $this->profile = $profile;
    }

    /**
     * Transaction for create a new user and profile, after a valid registration.
     *
     * @param  array $data
     * @return \App\Models\User
     */
    public function userRegisterTransaction(array $data)
    {
        DB::beginTransaction();
        try {

            $newUser = $this->user->register($data);
            $this->profile->create($newUser->id);

            DB::commit();
            return $newUser;

        } catch(\PDOException $e) {
            DB::rollBack();
            Log::Debug('Transaction Error: '. print_r($e, true));
        }
    }
}

RepositoryとInterfaceをバインディング

新しくRepository用のServiceProviderを作成するので、config/app.phpに追加します。

config/app.php

〜〜
    'providers' => [

     /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
        App\Providers\RepositoryServiceProvider::class, # 追加

    ],

RepositoryServiceProvider.phpを作成し、抽象クラスと具象クラスをバインディングすれば実装完了です。データソースが違う具象クラスを利用する場合、こちらでInterfaceとバインディングされた具象クラスを変更します。

app/Providers/RepositoryServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            \App\Repositories\Interfaces\ProfileRepositoryInterface::class,
            \App\Repositories\ProfileRepository::class
        );

        $this->app->bind(
            \App\Repositories\Interfaces\UserRepositoryInterface::class,
            \App\Repositories\UserRepository::class
        );
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

実装してみて

一年ぐらい前、エンジニアになるためにLaravelを勉強し始めたのですが、その時とコードの見た目が全く違うなあという印象です。コンストラクタにてDIを利用しクラスをセットしているので、依存クラスがひと目で分かりますね。実装時はディレクトリ構成とクラス名を考えるのに苦戦しました。実際にクラスを増やしていって見ていかないと、よりベターなディレクトリ構成やクラス名を付けれそうにないですね...
しかし機能やレイヤーをきっちり分けると、どこに何を書くべきかが判断しやすくなりました。面白かったので引き続き勉強していきたいと思います。


参考にした記事

参考にした書籍

19
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?