本当は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
- Laravelでapiを叩いたときにjsonが返ってこない問題
-
php artisan route:list > route.txt
で吐き出すと楽
ルーディングの書き方について
個人的には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は1階層と2階層両方書く必要はない
- ちなみにwithは1階層と2階層両方書いても実行されるSQLは変わらない
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にもログを残す
- stackのchannelsにslackを追加
- slackのlevelをcritical → debugに変更
- 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',
],