36
16

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.

LaravelAdvent Calendar 2020

Day 7

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

Last updated at Posted at 2020-12-06

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

確認バージョンは Laravel 7.26.1

よく使うコマンド

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

make系


# マイグレーション作成
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系

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

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

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

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

キャッシュを消す

# 全消し
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


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

Routing

ルーディングの書き方について

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

api.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の方が多くなるので)

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を使う
    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固定
        ];
    }

Eloquent

Model

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

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

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

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
class Post extends Model
{
    use SoftDeletes;

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

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
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が機能しない
    User::join('posts', 'users.id', '=', 'posts.user_id')->get();
    select * from users inner join posts on users.id = posts.user_id
  • joinされるテーブルがModelの場合はdeleted_atが機能する
    Post::join('comments', 'posts.id', '=', 'comments.post_id')->get();
    select * from posts inner join comments on posts.id = comments.post_id where posts.deleted_at is null

Relation Query

hasで流れるSQL

    User::has('posts.comments')->get();
[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)"} 
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

    User::join('posts', 'posts.user_id', '=', 'users.id')
        ->join('comments', 'comments.post_id', '=', 'posts.id')
        ->whereNull('posts.deleted_at')
        ->get();
[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"} 
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されないので注意
    User::with('posts.comments')->get();
[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)"} 
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

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を無効にしてみてください。5倍くらい違う!

# テスト実行
./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に出力する

下記をregisterに追加

app/Providers/AppServiceProvider.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

    '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エラーと単純なログをチャンネルごとに分けると便利です

    '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',
        ],
36
16
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
36
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?