Laravel 4.2から5.0へのアップグレード

# 概要

L4.2で稼働しているシステムを5.0にアップグレードした際のトラブル事例集。
基本的には [Upgrading To 5.0 From 4.2](https://laravel.com/docs/5.3/upgrade#upgrade-5.0) に従えば良いのだけど、メジャーアップデートだけあって細々いくつか問題が起きた。

# 移行対象プロジェクト

https://github.com/naomichi-y/monelytics

# 発生した問題

## Case 1: Class 'Predis\Client' not found

### 詳細
セッションドライバをRedisに変更したらエラーが起きた。

```..env
-) SESSION_DRIVER=file
+) SESSION_DRIVER=redis
```

### 解決策
`predis/predis` パッケージをインストール。

```
composer require predis/predis
```

## Case 2: ReflectionException in Container.php

### 詳細

`IndexController` が見つからない。

```
ReflectionException in Container.php line 779:
Class App\Http\Controllers\App\Controllers\IndexController does not exist
```

### 解決策

エラーメッセージをよくよく見ると、名前空間が怪しい。
L5ではコントローラのデフォルト名前空間が `RouteServiceProvider` に定義されている。

```app/Providers/RouteServiceProvider.php
protected $namespace = 'App\Http\Controllers';
```

L4の時はPSR-4に従いコントローラに名前空間を指定していたので、`routes.php` は次のような書き方をしていた。

```
Route::get('/', array('uses' => 'App\ontrollers\IndexController@getIndex', 'as' => 'home'));
```

デフォルトの名前空間に `uses` で指定された名前空間が結合されてしまってるのが原因だった。
ルーティングパスから名前空間を取り除くことで解決。

```
Route::get('/', array('uses' => 'IndexController@getIndex', 'as' => 'home'));
```

## Case 3: Trait 'App\Models\SoftDeletingTrait' not found

### 詳細
EloquentのSoft Deleteを利用してる場合、`SoftDeletingTrait` が無いと言われる。

### 解決策

SoftDeleteの名前空間とトレイトの変更が必要。

```App/Models/BaseModel.php
-) use Illuminate\Database\Eloquent\SoftDeletingTrait;
+) use Illuminate\Database\Eloquent\SoftDeletes;
class BaseModel extends \Eloquent {
-) use SoftDeletingTrait
+) use SoftDeletes;
```

## Case 4: Redisの接続に失敗する

接続設定は正しいはずなのに繋がらない。

```
ConnectionException in AbstractConnection.php line 155:
`SELECT` failed: ERR invalid DB index [tcp://172.16.238.7:6379]
```

以下設定。

```config/database.php
    'redis' => [

        'cluster' => false,

        'default' => [
            'host'     => env('REDIS_HOST', '127.0.0.1'),
            'port'     => env('REDIS_PORT', 6379),
            'database' => env('REDIS_DATABASE', 0),
        ],

    ],
```

```..env
REDIS_HOST=172.16.238.7
REDIS_PORT=6379
REDIS_DATABASE=0
```

### 解決策
`env('REDIS_DATABASE')` が空文字を返してしまう。0番を使いたい場合は文字列形式で指定が必要。

```..env
REDIS_DATABASE="0"
```

## Case 5: カスタムバリデータの移行

### 詳細

L5が提供するサービスプロバイダ形式に変更する。

### 解決策

`app\Validators` の下にカスタムバリデータを作成。

```app/Validators/NotExistsValidator.php
<?php
namespace App\Validators;

use DB;

class NotExistsValidator extends \Illuminate\Validation\Validator
{
    /**
     * @param $attribute
     * @param $value
     * @param $parameters
     * @return int
     */
    public function validateNotExists($attribute, $value, $parameters)
    {
        $count = DB::table($parameters[0])
            ->where($parameters[1], '=', $value)
            ->whereNull('delete_date')
            ->count();
        if ($count) {
            return false;
        }
        return true;
    }
}
```

バリデータをプロバイダに登録。

```app\Providers\ValidatorServiceProvider.php
<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class ValidatorServiceProvider extends ServiceProvider
{
    /**
     * @return void
     */
    public function boot()
    {
        \Validator::resolver(function($translator, $data, $rules, $messages) {
            return new \App\Validators\NotExistsValidator($translator, $data, $rules, $messages);
        });
    }
    /**
     * @return void
     */
    public function register()
    {}
}
```

プロバイダを読み込むよう設定。

```config/app.php
'providers' => [
    'App\Providers\ValidatorServiceProvider',
]
```

## Case 6: Class '\App\User' not found

### 詳細

ユーザ認証時にL5のAuthエラーが起こる。

### 解決策

モデルの設置場所を `App/Models` に変更していたので、認証モデルのパスも変更が必要。

```config/auth.php
-) 'model' => 'App\User',
+) 'model' => 'App\Models\User',
```

## Case 7: class 'Illuminate\Database\Eloquent\Collection' does not have a method 'getTotal'

### 詳細

Bladeのページャでエラー。

```
call_user_func_array() expects parameter 1 to be a valid callback,
class 'Illuminate\Database\Eloquent\Collection' does not have a method 'getTotal'
(View: /data/resources/views/summary/daily/index.blade.php)
```

### 解決策

メソッドの呼び出し方が変わってた。

```
-) @if ($activities->getTotal())
+) @if ($activities->total())
```

## Case 8: Route::resouce()でミドルウェアを利用したい

### 詳細

`routes.yml` 上で特定のコントローラにミドルウェアを適用したい。

### 解決策

`Route::group()` が使える。

```
Route::group(['middleware' => 'auth'], function() {
    Route::resource('dashboard', 'DashboardController');
});
```

## Case 9: AJAX通信時にTokenMismatchExceptionが発生

### 詳細

AJAXでPOSTやDELETEメソッドを実行するとCSRFチェックでエラーが起こる。

```
production.ERROR: exception 'Illuminate\Session\TokenMismatchException'
in /data/src/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php:46
```

### 解決策

L5からはCSRFチェックがすべてのページで有効になったので、AJAX通信時も `_token` パラメータを送信する必要がある。
すべての通信で `_token` を追加するのは面倒なので (jQuery限定の対策ではあるが)、`<meta>` タグにトークンを記述し、AJAX通信時はjQuery経由でMETAからトークンを取得及びサーバへ送信する手段を用いた。

```
<meta name="csrf-token" content="{{ csrf_token() }}">
```

```
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});
```

## Case 10:　PHPUnitでPOST送信時に "Failed asserting that Symfony\Component\HttpFoundation\Response Object" エラー

### 詳細

レスポンスオブジェクトをデバッグしてみると、CSRFエラーが起きていることがわかる。

```
Failed asserting that Symfony\Component\HttpFoundation\Response Object (...)
is an instance of class "Illuminate\Http\RedirectResponse".
```

### 解決策

リクエストパラメータにCSRFトークンを追加してあげれば良い。

```
$params = [
    'contact_name' => 'test',
    'email' => 'test@monelytics.me',
    'contact_type' => '1',
    'contact_message' => 'test',
    '_token' => csrf_token()
];

$this->call('POST', '/contact/send', $params);
$this->assertRedirectedTo('/contact/done');
```

`csrf_token()` を利用するに辺り、セッションを開始しておく必要がある。

```tests/TestCase.php
public function setup()
{
    parent::setup();

    \Session::start();
}
```

※L5.2ではテスト時にトークンチェックは無視されるようになりました。

## Case 11: PHPUnitでPOST送信時に "InvalidArgumentException: An uploaded file must be an array or an instance of UploadedFile." エラー


### 詳細

問題のコード。

```
$this->call(
    'DELETE',
    '/cost/constant/1',
    [],
    [],
    ['HTTP_REFERER' => $http_referer]
);
```

### 解決策

[call()](https://laravel.com/api/5.0/Illuminate/Foundation/Testing/TestCase.html#method_call) メソッドの引数4番目に `$cookies` が追加されてた。

```L4.2
Response call(
    string $method,
    string $uri,
    array $parameters = array(),
    array $files = array(),
    array $server = array(),
    string $content = null,
    bool $changeHistory = true
)
```


```L5.0
Response call(
    string $method,
    string $uri,
    array $parameters = array(),
    array $cookies = array(),
    array $files = array(),
    array $server = array(),
    string $content = null
)
```

修正後のコード。

```
$this->call(
    'DELETE',
    '/cost/constant/1',
    [],
    [],
    [],
    ['HTTP_REFERER' => $http_referer]
);
```