Laravelを触って1年経ったのでTIPS

本当は2年くらいかもしれない

確認バージョンは Laravel 7.26.1

## よく使うコマンド
ネームスペースとか考慮するとコマンドから作った方が良いです。でもコピペしちゃう

make系

```bash

# マイグレーション作成
php artisan make:migration create_xxxxx_table

# モデル＆ファクトリー＆シーダー作成(app/)
php artisan make:model Models/Xxxx -fs

# コントローラー作成(app/Http/Controllers/)
php artisan make:controller Api/XxxxController --model=Models/Xxxx --api

# フォームリクエスト作成(app/Http/Requests/)
php artisan make:request Api/Xxxx/Store

# コマンド作成(app/Console/Commands/)
php artisan make:command Xxxx

### ide-helperを使う場合
# ide-helperインストール
COMPOSER_MEMORY_LIMIT=-1 composer require --dev barryvdh/laravel-ide-helper

# ide_helper:generate
php artisan ide-helper:generate

# ide_helper:models
php artisan ide-helper:models -N
```

db系

```bash
# マイグレーション実行
php artisan migrate

# マイグレーションを一つ戻す
php artisan migrate:rollback --step=1

# DB初期化＆DatabaseSeeder実行
php artisan migrate:fresh --seed

# シーダー実行
php artisan db:seed --class=UserSeeder
```

## キャッシュを消す

```bash
# 全消し
php artisan optimize:clear

# optimize:clearの中身の実態は下記コマンド
php artisan view:clear && \
php artisan cache:clear && \
php artisan route:clear && \
php artisan config:clear && \
php artisan clear-compiled
```

ちなみに間違ってもローカル開発環境で`php artisan config:cache`は使わないよう注意してください。
phpunitを走らせた際にキャッシュ化した.envの環境変数が使われてしまいます。
間違えた場合は `php artisan config:clear`してください。

公式にも書いてる！
https://readouble.com/laravel/7.x/ja/configuration.html

## Factory

公式Faker
https://github.com/fzaninotto/Faker

日本語ソース
https://github.com/fzaninotto/Faker/blob/master/src/Faker/Provider/ja_JP

```bash

# ランダム英数字生成（unique使っても衝突してしまう場合）
$faker->unique()->regexify('[a-zA-Z0-9]{1,20}')
```


## Routing
- [Laravelでapiを叩いたときにjsonが返ってこない問題](https://qiita.com/okdyy75/items/fc6f5ce004f7a0bd39b3)
- `php artisan route:list > route.txt`で吐き出すと楽

#### ルーディングの書き方について
個人的にはHTTPメソッドごとにアクションを指定した方がいいかと思います。
Route::resourceを使ってデフォルトアクションを指定できるんですが、ワナがあります。
下記のように公開・下書きで投稿の処理を丸ごと分けたいなどした時に下記のようなルーティングになるかと思います。

api.php

```php
Route::resource('publish/posts', 'Publish\PostController')->only([
    'index',
    'store',
    'show',
    'update',
    'destroy',
]);

Route::resource('draft/posts', 'Draft\PostController')->only([
    'index',
    'store',
    'show',
    'update',
    'destroy',
]);
```

処理的には問題ないですが、`php artisan route:cache`（本番環境での高速化に必要）した際にname被りでエラーになります。
なのでシンプルにメソッドごとに書いた方が良いかと思います（いずれresourceで表現できないURIの方が多くなるので）

```php
Route::get('/publish/posts', 'Publish\PostController@index');
Route::post('/publish/posts', 'Publish\PostController@store');
Route::get('/publish/posts/{id}', 'Publish\PostController@show');
Route::patch('/publish/posts/{id}', 'Publish\PostController@update');
Route::delete('/publish/posts/{id}', 'Publish\PostController@destroy');

Route::get('/draft/posts', 'Draft\PostController@index');
Route::post('/draft/posts', 'Draft\PostController@store');
Route::get('/draft/posts/{id}', 'Draft\PostController@show');
Route::patch('/draft/posts/{id}', 'Draft\PostController@update');
Route::delete('/draft/posts/{id}', 'Draft\PostController@destroy');
```

## Controller
- ルートモデルバインディングはFatControllerになる危険性があるので、使用は注意

## FormRequest
- exists使う際にはdeleted_atを考慮する必要あり
- min、max、sizeなどは型指定で動きが変わるので注意
- existsの逆はuniqueを使う


```php
    public function rules()
    {
        return [
            'id'    => 'required|integer|exists:posts,id,deleted_at,NULL',
            'str1'  => 'required|string|max:10',   // 10文字以内
            'str2'  => 'required|string|min:10',   // 10文字以上
            'str3'  => 'required|string|size:10',  // 10文字固定
            'num1'  => 'required|integer|max:10',  // 10以内
            'num2'  => 'required|integer|min:10',  // 10以上
            'num3'  => 'required|integer|size:10', // 10固定
        ];
    }
```

- [validationのエラーメッセージ「The given data was invalid.」をカスタマイズする](https://qiita.com/okdyy75/items/8002e45a0195e3e01069)

## Eloquent
### Model
- 論理削除を有効にしたい場合は`use SoftDeletes`
- 全モデルでguardedにidを指定しておくと良い。予期せぬキーの登録・更新を防げる。
- created_atカラムを作成しなかった場合は`CREATED_AT = null`
- updated_atカラムを作成しなかった場合は`UPDATED_AT = null`

```php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Xxxxx extends Model
{
    use SoftDeletes;

    const CREATED_AT = null;
    const UPDATED_AT = null;

    protected $guarded = [
        'id',
    ];
}
```

### Relation
こんなモデルがあったとして

```php
class User extends Authenticatable
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
```

```php
class Post extends Model
{
    use SoftDeletes;

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
```

```php
class Comment extends Model
{
    const UPDATED_AT = null;

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

```

#### リレーションメソッドでアクセスした時
- hasOneの戻り値は Model or null
- hasManyの戻り値はCollection
- belongsToの戻り値は Model or null

### QueryBuilder
- クエリビルダの場合はjoinするテーブルのdeleted_atが機能しない

```php
    User::join('posts', 'users.id', '=', 'posts.user_id')->get();
```

```sql
    select * from users inner join posts on users.id = posts.user_id
```

- joinされるテーブルがModelの場合はdeleted_atが機能する

```php
    Post::join('comments', 'posts.id', '=', 'comments.post_id')->get();
```

```sql
    select * from posts inner join comments on posts.id = comments.post_id where posts.deleted_at is null
```

### Relation Query

- [hasは1階層と2階層両方書く必要はない](https://qiita.com/okdyy75/items/f5120c5b8efe9de5f6bb)
- ちなみにwithは1階層と2階層両方書いても実行されるSQLは変わらない

#### hasで流れるSQL

```php
    User::has('posts.comments')->get();
```

```log
[2020-12-05 23:39:48] local.DEBUG: SQL {"time":"12.82 ms","sql":"select * from `users` where exists (select * from `posts` where `users`.`id` = `posts`.`user_id` and exists (select * from `comments` where `posts`.`id` = `comments`.`post_id`) and `posts`.`deleted_at` is null)"} 
```

```sql
select * from users
where exists (
    select * from posts
    where users.id = posts.user_id
    and exists (
        select * from comments
        where posts.id = comments.post_id
    )
    and posts.deleted_at is null
)
```

#### joinで流れるSQL

```php
    User::join('posts', 'posts.user_id', '=', 'users.id')
        ->join('comments', 'comments.post_id', '=', 'posts.id')
        ->whereNull('posts.deleted_at')
        ->get();
```

```log
[2020-12-06 00:00:13] local.DEBUG: SQL {"time":"12.11 ms","sql":"select * from `users` inner join `posts` on `posts`.`user_id` = `users`.`id` inner join `comments` on `comments`.`post_id` = `posts`.`id` where `posts`.`deleted_at` is null"} 
```

```sql
select * from users
inner join posts on posts.user_id = users.id
inner join comments on comments.post_id = posts.id
where posts.deleted_at is null
```

#### withで流れるSQL
- Eager Loadingではlimit使用時、withごとにlimitされないので注意

```php
    User::with('posts.comments')->get();
```

```log
[2020-12-05 23:49:37] local.DEBUG: SQL {"time":"11.54 ms","sql":"select * from `users`"} 
[2020-12-05 23:49:37] local.DEBUG: SQL {"time":"0.70 ms","sql":"select * from `posts` where `posts`.`user_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) and `posts`.`deleted_at` is null"} 
[2020-12-05 23:49:37] local.DEBUG: SQL {"time":"0.41 ms","sql":"select * from `comments` where `comments`.`post_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"} 
```

```sql
select * from users
select * from posts where posts.user_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) and posts.deleted_at is null
select * from comments where comments.post_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
```


## Console
##### 複数時間でタスクスケジューリングしたい場合(8:30、11:30、18:30など)はcron形式にする。

app/Console/Kernel.php

```
->cron('30 8,11,18 * * *');
```

##### コンソール出力時にログにも残す
トレイトを作成してコマンドクラスで`use PrependsOutput;`してあげればOK
普通に`$this->info()`や`$this->error()`するだけでログに残せます。
テスト時に実行時間を出力していない理由は、コンソール出力値もテストしているためです。

app/Console/Commands/PrependsOutput.php

```php
<?php

namespace App\Console\Commands;

use Carbon\CarbonImmutable;

trait PrependsOutput
{
    /**
     * コンソール出力に追加
     *
     * @param  string  $string
     * @param  string|null  $style
     * @param  int|string|null  $verbosity
     * @return void
     */
    public function line($string, $style = null, $verbosity = null)
    {
        if (\App::environment() === 'testing') {
            parent::line($string, $style, $verbosity);
        } else {
            parent::line(CarbonImmutable::now()->format('[Y-m-d H:i:s] ').$string, $style, $verbosity);
        }
        logger(CarbonImmutable::now()->format('[Y-m-d H:i:s] ').$string);
    }
}

```


## PHPUnitテスト
- テストが遅い時はxdebugを無効にしてみてください。５倍くらい違う！

```bash

# テスト実行
./vendor/bin/phpunit

# 対象のファイルでテスト実行
./vendor/bin/phpunit tests/Feature/XxxxxTest.php

# 対象のファイルの対象の関数でテスト実行
./vendor/bin/phpunit tests/Feature/XxxxxTest.php --filter=xxxxxxxx

# テスト結果をログに残す
./vendor/bin/phpunit --testdox-text=test.txt
```

## 他いろいろ

#### 実行SQLを確認したい
下記を参考にしてちょっと直しました
[LaravelでSQL文をlaravel.logに出力する](https://qiita.com/tmf16/items/f82c1af0d535b2337f61)

下記をregisterに追加

app/Providers/AppServiceProvider.php

```php
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // SQL Log
        \Illuminate\Support\Facades\DB::listen(function ($query) {
            $sql = preg_replace('/"(.*?)"/', "'$1'", $query->sql);
            for ($i = 0; $i < count($query->bindings); $i++) {
                $bindValue = $query->bindings[$i];
                if (is_bool($bindValue)) {
                    $bindValue = $bindValue ? 'true' : 'false';
                } else {
                    $bindValue = "'".(string)$bindValue."'";
                }
                $sql = preg_replace("/\?/", $bindValue, $sql, 1);
            }
            logger("SQL", ["time" => sprintf("%.2f ms", $query->time), "sql" => $sql]);
        });
    }
```

#### Laravelログ＋Slackにもログを残す
1. stackのchannelsにslackを追加
2. slackのlevelをcritical → debugに変更
3. envのLOG_SLACK_WEBHOOK_URLにslackのwebhookUrlを指定

config/logging.php

```php
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => [
                'single',
                'slack', // 追加
            ],
            'ignore_exceptions' => false,
        ],
        〜
        'slack' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel Log',
            'emoji' => ':boom:',
            'level' => 'debug', // critical → debugに変更
        ],
```

.env

```
LOG_SLACK_WEBHOOK_URL=xxxxxx
```

stackのchannelsには複数のdriverが使えるので、fatalエラーと単純なログをチャンネルごとに分けると便利です

```php
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => [
                'single',
                'slack-critical',
                'slack-debug',
            ],
            'ignore_exceptions' => false,
        ],
        〜
        'slack-critical' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel critical Log',
            'emoji' => ':boom:',
            'channel' => env('LOG_SLACK_CHANNEL_ALERT'),
            'level' => 'critical',
        ],
        'slack-debug' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel debug Log',
            'emoji' => ':memo:',
            'channel' => env('LOG_SLACK_CHANNEL_DEBUG'),
            'level' => 'debug',
        ],
```