Laravel5.2から5.3へアップグレードする

9/12(月)にLaravel Meetup Tokyo vol.8に参加しました。その時に@localdiskさんがLaravel5.3へのアップグレードについて話されていて、そろそろかなと思って早速アップデートしてみました。仕事では5.2をこれまで使っていてプロジェクトの大きなリリースが来月あるのでテストを踏まえるとちょうど良い頃合いでした。以下はアップグレードした際の備忘録です。

参考にしたURL

大まかな手順

最新のLaravelのソースをエディタで開きつつ、ガイドに沿って変更箇所を確かめながらコピペして動作確認して、というのを繰り返しながらアップグレードしました。

ガイドには2~3時間とあるんですが、ぼくの場合はちまちま確認してたら4時間以上かかってしまいました。行った変更は下記です。

  • composer.jsonのlaravel/frameworkを5.3.*にしcomposer update
  • ServiceProviderの変更(bootメソッドの引数)
  • routes.phpの分割(routes/**.php)
  • AuthControllerの分割(RegisterControllerとLoginController)
  • PasswordControllerの分割(ForgotPasswordControllerとResetPasswordController)
  • PasswordControllerのメールで利用する通知をカスタマイズ
  • BroadcastServiceProviderの追加
  • FormRequestの例外ハンドリングの変更(HttpResponseExceptionからValidationException)
  • Middlewareの変更(SubstituteBindings)
  • queue.phpの変更(retry_after)
  • routes/console.phpの読み込み
  • database queueで利用するjobs/failed_jobsテーブルの変更

composer.jsonのlaravel/frameworkを5.3.*にしcomposer update

そのままですが、composer.jsonを書き換えてcomposer updateします。

     "type": "project",
      "require": {
          "php": ">=5.5.9",
 -        "laravel/framework": "5.2.*",
 +        "laravel/framework": "5.3.*",
          "predis/predis": "^1.0",

composerやはり便利ですね。updateするとアプリケーションが軒並み動かなくなるはずです。

ServiceProviderの変更(bootメソッドの引数)

動かない原因としてServiceProviderのメソッドシグネチャが変わってるというのがあります。まずこれを変更してやります。具体的にはbootメソッドの引数がなくなってます。少なくともAuthServiceProvider, EventServiceProvider, RouteServiceProviderのbootメソッドを変えてやる必要があります。ガイドだけでなく最新のソースと見比べたりコピペして修正していくと効率が良いと思います。RouteServiceProviderに関してはmapメソッドも引数がなくなっています。

引数がなくなった事によって、その引数を使っていた場合は処理が書けなくなってしまうんですが、その場合はその設定に該当するものをファサードを使ってやりましょうという事です。たとえばぼくの場合はAuthServiceProviderで少し変則的なポリシーの設定をしていたんですが、下記のようにGateファサードを使って書き換えました。

 -    public function boot(GateContract $gate)
 +    public function boot()
      {
 -        $this->registerPolicies($gate);
 +        $this->registerPolicies();
  
          // モデルを引数にとらないポリシーに関しては下記のように指定する
          // http://laravel-tricks.com/tricks/using-51-authorization-without-models
          foreach (get_class_methods(new MemberPolicy) as $method) {
              $callback = 'App\Policies\MemberPolicy@' . $method;
 -            $gate->define($method, $callback);
 -            $gate->define(Str::snake($method, '-'), $callback);
 +            \Gate::define($method, $callback);
 +            \Gate::define(Str::snake($method, '-'), $callback);
          }
      }

routes.phpの分割(routes/**.php)

Laravel5.3からは/routes/web.phpという形でルーティング定義を行う場所が変わっています。5.2までは/app/Http/routes.phpでここに一緒くたにしてたと思うんですが、折角なので5.3風に変えました。RouteServiceProviderで下記のようにすることで読み込み箇所が変わります。

      public function map(Router $router)
      {
 -        $this->mapWebRoutes($router);
 +        $this->mapWebRoutes();
 +        $this->mapApiRoutes();
      }
     
 -    protected function mapWebRoutes(Router $router)
 +    protected function mapWebRoutes()
 +    {
 +        Route::group([
 +            'middleware' => ['web'],
 +            'namespace' => $this->namespace . '\Web',
 +            'prefix' => '/',
 +        ], function ($router) {
 +            require base_path('routes/web.php');
 +        });
 +    }


mapWebRoutesで指定しているmiddlewareはKernel.phpに定義があります。namespaceはぼくの場合、Http/Controllers/Web, Http/Controllers/Api等のように分けているので上記のように指定してます。prefixは実際のURLのprefixです。

ルーティング側の定義は下記のような感じです。


 +Route::get('/login', 'AuthController@showLoginForm');
 +Route::post('/login', 'AuthController@login');
 +Route::get('/logout', 'AuthController@logout');

ルーティング定義は/routesフォルダにあったほうがより設定っぽくて良いですね。分かりやすくなった気がします。

AuthControllerの分割(RegisterControllerとLoginController)

まず、Controllerに共通で使われていたAuthorizesResourcesというtraitがAuthorizesRequestsに統合されてなくなったようなので、削除します。

 -    use AuthorizesRequests, AuthorizesResources, DispatchesJobs, ValidatesRequests;
 +    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

AuthControllerがRegisterControllerとLoginControllerの2つに分けます。これは普通なんでもないんですが、MultiAuth(通常のUserの他にAdministrator等を設ける)を使っていると少しハマるかもしれません。

ぼくの場合、下記のような感じになりました。下記はMultiAuthのデフォルトじゃないguardの場合の例です。

<?php
namespace App\Http\Controllers\Admin\Auth;
use App\Http\Controllers\Admin\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $redirectTo = '/admin/home';

    public function __construct()
    {
        $this->middleware('guest:admin', ['except' => 'logout']);
    }

    public function showLoginForm()
    {
        return view('admin.auth.login');
    }

    protected function guard()
    {
        return \Auth::guard('admin');
    }

    public function logout(Request $request)
    {
        $this->guard()->logout();
        $request->session()->flush();
        $request->session()->regenerate();
        return redirect('/admin/login');
    }
}
<?php
namespace App\Http\Controllers\Admin\Auth;
use App\Models\Administrator;
use Validator;
use App\Http\Controllers\Admin\Controller;
use Illuminate\Foundation\Auth\RegistersUsers;
class RegisterController extends Controller
{
    use RegistersUsers;

    protected $redirectTo = '/admin/home';

    public function __construct()
    {
        $this->middleware('guest');
    }

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:administrators',
            'password' => 'required|min:6|confirmed',
        ]);
    }

    protected function create(array $data)
    {
        return Administrator::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }

    public function showRegistrationForm()
    {
        return view('admin.auth.register');
    }

    protected function guard()
    {
        return \Auth::guard('admin');
    }
}

参考までにauth.phpは下記のような感じです。

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'api' => [
            'driver' => 'token',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'session',
            'provider' => 'admin',
        ],
    ],
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'admin' => [
            'driver' => 'eloquent',
            'model' => App\Models\Administrator::class,
        ],

PasswordControllerの分割(ForgotPasswordControllerとResetPasswordController)

AuthControllerの時と同じようにこれも2つのControllerに分割します。これもMultiAuthの場合は少し面倒でした。参考までにデフォルトじゃないguardの場合の例を載せておきます。

<?php
namespace App\Http\Controllers\Admin\Auth;
use App\Http\Controllers\Admin\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
class ForgotPasswordController extends Controller
{
    use SendsPasswordResetEmails;

    public function __construct()
    {
        $this->middleware('guest:admin');
    }
    public function showLinkRequestForm()
    {
        return view('admin.auth.passwords.email');
    }
    public function broker()
    {
        return \Password::broker('admin');
    }
}
<?php
namespace App\Http\Controllers\Admin\Auth;
use App\Http\Controllers\Admin\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
class ResetPasswordController extends Controller
{
    use ResetsPasswords;

    protected $redirectTo = '/admin/home';

    public function __construct()
    {
        $this->middleware('guest:admin');
    }
    public function showResetForm(Request $request, $token = null)
    {
        return view('admin.auth.passwords.reset')->with(
            ['token' => $token, 'email' => $request->email]
        );
    }
    protected function guard()
    {
        return \Auth::guard('admin');
    }
    public function broker()
    {
        return \Password::broker('admin');
    }
}

PasswordControllerのメールで利用する通知をカスタマイズ

パスワードリセットのメールが5.3からNotificationという新機能を使ったものに変わっています。必要なのは、AuthenticableなモデルにNotifiableというtraitをuseするようにする事と、通知の具合があまり良くなければsendPasswordResetNotificationというメソッドをオーバーライドして調整する事です。ぼくの場合は、リセットするURLを通常のものと変えたかったためオーバーライドしてカスタマイズする事になりました。

<?php
namespace App\Models;
use App\Notifications\ResetAdminPasswordNotification;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class Administrator extends Authenticatable
{
    use Notifiable;

    public function sendPasswordResetNotification($token)
    {
        $this->notify(new ResetAdminPasswordNotification($token, $this->email));
    }
}

下記が自分で拡張したNotificationクラスです。

<?php
namespace App\Notifications;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Notifications\Messages\MailMessage;
class ResetAdminPasswordNotification extends ResetPassword
{
    private $email;
    public function __construct($token, $email)
    {
        parent::__construct($token);
        $this->email = $email;
    }
    public function toMail()
    {
        return (new MailMessage)
            ->line('You are receiving this email because we received a password reset request for your account.')
            ->action('Reset Password', url('admin/password/reset/' . $this->token . '?email=' . $this->email))
            ->line('If you did not request a password reset, no further action is required.');
    }
}

下記のような通知がメールで飛びます。

NewImage

Notificationはけっこうカジュアルに使えそうですね。

BroadcastServiceProviderの追加

今開発しているプロジェクトはWebSocketsの連携にBroadcasting Eventsを使っています。ちなみにこのBroadcastServiceProviderを追加しなくても5.2同様に動きます。ここでProviderを設定しているのは、Authorization Routesというものを設定するのに必要になるようです。

この機能は対応予定がまだないので、コピペして追加したものの、bootメソッドの中身はコメントアウトしています。いちおう読み込むようにconfig/app.phpにも設定を入れます。

FormRequestの例外ハンドリングの変更(HttpResponseExceptionからValidationException)

FormRequestでバリデーションエラーになった時の例外クラスがValidationExceptionに変わりました。通常のValidatorは本来ValidationExceptionを送出していたのになぜかFormRequestの時だけHttpResponseExceptionが返ってたんですよね…。ぼくの場合はFormRequestのabstractクラスでこのHttpResponseExcetionをValidationExceptionに変えるという事をしていたんですが、それが不要になりました。

Middlewareの変更(SubstituteBindings)

5.3だと、middlewareGroupにSubstituteBindingsというものがあるのでこれを追加します。あと、canというエイリアスで使われていた\Illuminate\Foundation\Http\Middleware\Authorize::classが\Illuminate\Auth\Middleware\Authorize::classに変わっているので書き換えます。

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
		(省略)
        'admin' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
+           \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
+       'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
-       'can' => \Illuminate\Foundation\Http\Middleware\Authorize::class,
+       'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    ];

ちなみにSubstituteBindingsですが、これはRoute Model Bindingを行うのに必要なようです。

queue.phpの変更(retry_after)

expireやt/lがretry_afterに統一になったようです。

routes/console.phpの読み込み

最新のソースだとroutes/console.phpというものがありますが、これはクロージャを渡すだけの簡易なCommandを作るのに利用できるようです。Console/Kernel.phpで読み込むようです。

 +    /**
 +     * Register the Closure based commands for the application.
 +     *
 +     * @return void
 +     */
 +    protected function commands()
 +    {
 +        require base_path('routes/console.php');
 +    }

database queueで利用するjobs/failed_jobsテーブルの変更

database queueを利用する場合はテーブルにカラム削除/カラム追加があるようです。マイグレーションを作成しました。

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class DropColumnOnJobsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('jobs', function (Blueprint $table) {
            $table->dropIndex('jobs_queue_reserved_reserved_at_index');
            $table->dropColumn('reserved');
            $table->index(['queue', 'reserved_at']);
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('jobs', function (Blueprint $table) {
            $table->dropIndex('jobs_queue_reserved_at_index');
            $table->tinyInteger('reserved')->after('attempts')->unsigned();
            $table->index(['queue', 'reserved', 'reserved_at']);
        });
    }
}
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddColumnOnFailedJobsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('failed_jobs', function (Blueprint $table) {
            $table->text('exception')->after('payload')->nullable();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('failed_jobs', function (Blueprint $table) {
            $table->dropColumn('exception');
        });
    }
}

と、こんな感じでした。Broadcasting Eventsの新機能などまだ試せてないものがあるので最新に追随しながらまた見ていきたいと思います。