Laravelでの開発でいつもやってること
php7.4.3、Laravel 8.49.0で検証
ログのローテーション
デフォルトではstolage/logs/laravel.log
の1ファイルに全て出力されるので特に本番環境などではローテーションとログの保存期間について注意する。また、permissionを環境に合わせて設定する
LOG_CHANNEL=daily
・・・
'channels' => [
'daily' => [
'driver' => 'daily',
'days' => 90,
'permission' => 0664,
],
],
エラー通知
例外が発生したときに通知を送りたい。スキップしたい例外は$dontReportに追加する。
class Handler extends ExceptionHandler
{
protected $dontReport = [
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Validation\ValidationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
];
・・・
public function report(Throwable $e)
{
if ($this->shouldReport($e)) {
// 必要な情報をメールやSlackに通知する
// $e->getMessage()、$e->getCode()、$e->getFile()、$e->getLine();
}
parent::report($exception);
}
}
https対応
httpsなサービスで、ロードバランサー配下にLaravelアプリがあってhttpでアクセスされる場合などは特に注意が必要。リンクやリダイレクト、authまわりの認証付きURLの挙動の確認が必要
APP_SCHEME=https
class AppServiceProvider extends ServiceProvider
{
・・・
public function boot(UrlGenerator $url)
{
if (env('APP_SCHEME') == 'https') {
$url->forceScheme('https');
$this->app['request']->server->set('HTTPS', true);
}
}
※$this->app['request']->server->set('HTTPS', true)
については、ここやここを参考
APIのエラーレスポンスのjson化
404エラー
class Handler extends ExceptionHandler
{
・・・
public function render($request, Throwable $exception)
{
if (!$request->is('api/*')) {
// API以外は何もしない
return parent::render($request, $exception);
} else if ($exception instanceof ModelNotFoundException) {
// Route Model Binding でデータが見つからない
return response()->json(['error' => 'Not found'], 404);
} else if ($exception instanceof NotFoundHttpException) {
// Route が存在しない
return response()->json(['error' => 'Not found'], 404);
} else {
return parent::render($request, $exception);
}
}
}
その他エラー
LaravelではhttpリクエストヘッダーAccept: application/json
の有無でエラー時のレスポンス形式を判定しているのでapiへのリクエストでは強制的にこのヘッダーを付与する
class Kernel extends HttpKernel
{
・・・
protected $middlewareGroups = [
'api' => [
\App\Http\Middleware\AcceptJson::class,
・・・
],
];
}
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AcceptJson
{
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}
APIのレスポンスはエスケープしない
class Kernel extends HttpKernel
{
・・・
protected $middlewareGroups = [
'api' => [
\App\Http\Middleware\UnescapeJsonResponse::class,
・・・
],
];
}
<?php
namespace App\Http\Middleware;
use Illuminate\Http\JsonResponse;
class UnescapeJsonResponse
{
public function handle($request, \Closure $next)
{
$response = $next($request);
// JSON以外は何もしない
if (!$response instanceof JsonResponse) {
return $response;
}
// エンコードオプションを追加して設定し直す
$newEncodingOptions = $response->getEncodingOptions() | JSON_UNESCAPED_UNICODE;
$response->setEncodingOptions($newEncodingOptions);
return $response;
}
}
日本語化とタイムゾーン変更
'timezone' => 'Asia/Tokyo',
'locale' => 'ja',
$ php -r "copy('https://readouble.com/laravel/8.x/ja/install-ja-lang-files.php', 'install-ja-lang.php');"
$ php -f install-ja-lang.php
$ php -r "unlink('install-ja-lang.php');"
任意の時間でシミュレート
この例ではシミュレートしたい日時を.env
で管理。非プログラマーが検証するケースがある場合はテーブルで管理するのもあり
DEBUG_DATE=2021-11-01
web用
class Kernel extends HttpKernel
{
・・・
protected $middlewareGroups = [
'web' => [
・・・
\App\Http\Middleware\SetSimulationDateTime::class,
],
];
}
<?php
namespace App\Http\Middleware;
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
class SetSimulationDateTime
{
public function handle(Request $request, Closure $next)
{
if (env('APP_ENV') == 'local') {
if (env("DEBUG_DATE")) {
// Too Many Request対策で時分秒は変わるように
Carbon::setTestNow(new Carbon(
env("DEBUG_DATE") . ' ' . date("H:i:s")
));
}
}
return $next($request);
}
}
batch用
baseクラスを作って、app/Console/Commands
以下のクラスではこれを継承させる
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
abstract class BaseCommand extends Command
{
public function __construct()
{
parent::__construct();
if (env('APP_ENV') == 'local') {
if (env("DEBUG_DATE")) {
Carbon::setTestNow(env("DEBUG_DATE"));
}
}
}
}
DBレプリケーション設定
DB_MASTER_HOST=127.0.0.1
DB_MASTER_USER=master_user
DB_MASTER_PASS=master_password
DB_SLAVE_HOST=127.0.0.1
DB_SLAVE_USER=slave_user
DB_SLAVE_PASS=slave_password
'connections' => [
・・・
'mysql' => [
'driver' => 'mysql',
'read' => [
'host' => [
env('DB_SLAVE_HOST'),
],
'username' => env('DB_SLAVE_USER'),
'password' => env('DB_SLAVE_PASS'),
'port' => env('DB_PORT', '3306'),
],
'write' => [
'host' => [
env('DB_MASTER_HOST'),
],
'username' => env('DB_MASTER_USER'),
'password' => env('DB_MASTER_PASS'),
'port' => env('DB_PORT', '3306'),
],
'sticky' => true,
・・・
],
・・・
],
エラーページ
以下のコマンドでresources/views/errors
にデフォルトで用意されているエラーページがコピーされる。特定のエラーコードの場合にLaravelデフォルトのエラーページが表示されてしまうことを避けるために最初にやってしまう
$ php artisan vendor:publish --tag=laravel-errors
アプリとマイグレーションでリポジトリを分ける
運用ルールによるが、私はgitのmasterブランチにマージしたものはいつでもデプロイ可能なものとしている。プログラムとマイグレーションをセットでデプロイしたいケースで、マイグレーション完了前に、プログラムが動くとエラーになる場合を回避するためにリポジトリを分けている
$ cd /path/to/マイグレーション用プロジェクト
$ git pull
$ php artisan migrate
$ cd /path/to/アプリ用プロジェクト
$ git pull
デプロイ用にEnvoyをセットアップ
.env
は環境ごとに.env.develop
.env.production
などを準備する。
$ composer require laravel/envoy
@servers(['web' => ['web1', 'web2'], 'migrate' => ['web1']])
@story('deploy', ['confirm' => true])
deploy_migrate
deploy_web
@endstory
@task('deploy_web', ['on' => 'web'])
cd /path/to/アプリ用プロジェクト
git pull
composer install
cp .env.production .env
@endtask
@task('deploy_migrate', ['on' => 'migrate'])
cd /path/to/マイグレーション用プロジェクト
git pull
cp .env.production .env
composer install
php artisan migrate
@endtask
$ php vendor/bin/envoy run deploy
便利なプラグインの導入
必須じゃないけどこの辺は導入を検討。本番環境では無効化しなければいけないものは注意が必要
N+1の検知
httpリクエストのロギング
sqlのロギング
おわりに
今回は非コンテナ環境を想定
Discussion