laravel周り全般tips
enumについて
laravelとMySQLでENUMを扱う際に、laravel-enumを使う.
laravel-enumではphp artisan make:UserType
とすることで、ENUMの定義ファイルを作成できる。
<?php
namespace App\Enums;
use BenSampo\Enum\Enum;
final class UserType extends Enum
{
const Administrator = 0;
const Moderator = 1;
const Subscriber = 2;
const SuperAdministrator = 3;
}
ここで問題になるのが、const Administrator = 0
。
MySQLではsql_modeがStrictの時にENUM型で整数値0を許容しないというのがあるらしい。
確認するにはmysql
コマンドでログインしてSELECT @@sql_mode;
と打つと確認できる。
mysql> SELECT @@sql_mode;
+-----------------------------------------------------------------------------------------------------------------------+
| @@sql_mode |
+-----------------------------------------------------------------------------------------------------------------------+
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+-----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
ここでSTRICT_TRANS_TABLES
とあると、厳格モード。
この時に整数値0が許容されない。
したがって、ENUMを設定する場合は、以下のように 1
からスタートするようにするべきなのではないのかな。
<?php
namespace App\Enums;
use BenSampo\Enum\Enum;
final class UserType extends Enum
{
const Administrator = 1;
const Moderator = 2;
const Subscriber = 3;
const SuperAdministrator = 4;
}
slug などを用いたルートモデルバインディングについて
基本的にルートは以下のようにして行う。
<?php
Route::get('/post/{id}', [PostController::class, 'show'])->name('post.show');
ここで、モデルバインディングといった機能があり、idに紐付けてモデルに関連するデータをController側で受け取るがことができる。
さらにデフォルトではこのバインディングする際のキーが idとなっているが、slugなどに変更することが可能である。
<?php
Route::get('/post/{post:slug}', [PostController::class, 'show'])->name('post.show');
これの利点は、(....以下説明ちゃんと書く)
そして問題になるのが、resoueceに対してid以外のキーを指定する場合である。
この場合は以下にすると大丈夫
<?php
Route::resource('post', PostController::class)->parameters([
'post' => 'post:slug'
]);
以上。
Laravel と Vueの組み合わせ(部分的導入)で注意すべき点
例えば以下のようにbladeで記述して、app.jsでvueをマウントするとする
<div id="app">
<h1>タイトル</h1>
<vue-component :text="stringAttr" boolProps="false" />
<section>
<!-- 省略 -->
</section>
</div>
import { createApp } from 'vue'
import App from './App'
if (document.getElementById('app')) {
createApp({
components: {
app: App,
},
}).mount('#app')
}
vueの記法的には Component を <vue-component />
と書いても良いのでそうすると、
その下に続く section が表示されなくなる。
この場合、<vue-component></vue-component>
と閉じることで表示されるようになる
Laraveでどうdebugするか
Laravel、特にモノリスで構築するLaravelではフロント、バックエンド両方の開発をすることがあり、バックエンドについてもMVCだけでなくcommandやtest, queueなど様々なシーンの開発を行う。
このため、開発のシーンによってデバッグしやすい方法も変わり、これを把握しているかどうかは開発スピードに大きな影響を及ぼすだろう。
デバッグの種類を列挙し、どの開発シーンで生きるかについて説明できればと思う。
echo
print_r
var_dump
フロント
dd
フロント
eval(\Psy\sh());
全般的に有効
しかし最近のlaravelではphp artisan serve
はこのブレイクポイントを自動的に閉じてしまうため
リクエストに応じた処理をするならこれがいい
log
全般、特にqueue(job, redis)などは、ターミナルでも見えにくいのでここを参照するといい
tail -f でログを垂れ流してみてるのも便利。Macユーザーならコンソールアプリケーションで確認することもできる
tinker
プレイグラウンド的に実行するのにちょうどいい。
tinker内部でも様々なコマンドが用意されており、奥深い
laravel-debugbar
お馴染み。何かと便利。特にセッション周りやDBのクエリをさくっと確認したい場合はこれを使う。
リダイレクトに弱い。
deubgbar の config をいじるだけでさらに便利になるのでちゃんとカスタマイズしよう。
この辺の解説記事が全然ない気がする
こんな感じで、ログインしているユーザー情報とかロケーションの設定、conifg, laravelのバージョンなども出力できるって知ってた?知らんかったでしょ
laravel clockwork
APIのデバッグをする時に有効。軽く触ったが、結構良い。
xdebug
PHP使いなら一度は聞いたことがあるとおもう。あまり使わない
Query builderの戻り値に関して
save, update などの戻り値についてまとまっている
メソッド | 返り値(型) | 返り値(例) | コメント |
---|---|---|---|
save | boolean | true, false | 成功の結果 |
create | object | {id:1,name: テスト} | 新規作成したレコード |
insert | boolean | true, false | 成功の結果 |
update | integer | 0, 1, 10など | 影響を与えたレコード数 |
delete | integer | 0, 1, 10など | 影響を与えたレコード数 |
first | object | {id:1,name:テスト} | レコード1件 / 無ければ 5.1→NULL, 5.3→{} |
BladeからVueにbooleanをpropsする際のTips
bladeに部分的にVueを採用しているパターンにおいて、booleanをpropsに渡そうとすると以下のようになる。
<div class="wrapper">
<boolean-props-form
:is-truthy="{{ $isTruthry }}"
></boolean-props-form>
</div>
[Vue warn]: Template compilation error: v-bind is missing expression.
laravelからgetで吐き出されるhtmlを見ると、:is-truthy=""
となり空になってしまう
<php?
<div class="wrapper">
<boolean-props-form
- :is-truthy="{{ $isTruthry }}"
+ :is-truthy="{{ json_encode($isTruthry) }}"
></boolean-props-form>
</div>
ネストした連想配列を一括でCollectionに変換する方法
こんな感じでネストされた配列があったとする。
$array = [
"token" => "b9938784-85bf-4dc6-af1b-75336d204e28",
"selling_plan_group" => [
"token" => "3c65fb70-b64c-4903-ab87-accf9c277205",
"selling_plans" => [
[
"token" => "70ee18ee-a7c5-4a5e-b1a8-0d3e3acb5885",
],
[
"token" => "1a3e293c-34bf-4d55-922a-e698c8313205",
],
]
]
これをCollectionに変換したい場合、
$collection = collect($array);
上辺ではcollectionに変換されたように見える。
しかし、ネストした値を取り出そうとすると、ネストされた値はすべて配列として取得される。
メソッドチェーンで値を取得したい場合は、これは少し厄介。
従って、下記のように一旦 jsonに変換したのちに取得するといい。
$courseCollection = collect(json_decode(json_encode($course['course_variants'])));
ide-helper, laravel-ide-helper について
morphTo
リレーションの phpdoc の書き方について
morphTo
リレーションを行なっているクラスで ide-helper を走らせると以下のような問題にぶち当たる
静的には morph が返すモデルが確定していないため、その上位の Model
が割り当てられたんだと思う(要出典)。
通常、mono
, poly
なリレーションに関わらず、リレーションは返却されるモデルは固定されているはず。
なので、この場合は愚直に出力された phpdoc をユニオンで記述して上書きしても良いと思っている。
**** 中略
// * @property-read \App\Models\Post|\App\Modesl\Video $commentable
**** 中略
public function commentable()
{
return $this->morphTo();
}
追記:よく読んだら、この人と同じやり方かも
認証周りの話の理解
ログイン後のリダイレクト方法について
ログインしたら通常、ログインページへリダイレクトされる前のページに戻りたいのが世の常いうものである。
ところが、調べてみると redirectTo
などをいじることでなんとかする記事が多い。
違う。そうじゃない。正しくはこうだ。
public function login(Request $request): Response|RedirectResponse
{
// ログイン処理....中略
return redirect()->intended($this->redirectTo); // intended メソッドを活用する
}
また、会員登録フロー(register -> メール認証)などを行なった際に、最後に 「元のページに戻る」などをbladeで記述したい場合はこうなる。
<a href="{{ session()->pull('url.intended', '/') }}">元のページに戻る</a>
やっていることは、 redirect()->intended($this->redirectTo)
の intended
メソッドと同様である。
(メソッドのコードを読めば、 session()->pull()が呼び出されているのがわかる。)
より動的に元いたページに戻るには
redirect()->intended()
で元いたページに戻るように変更を加えた。
このメソッドの呼び出して、sessionに保存されている url.intended
を読み取り、存在すればそのページに飛ばしている。
そもそも、url.intended
はいつセットされるのか?これは以下の記事を読むとわかりやすい。
url-intendedはいつどこでセットされたのか?
url.intended
は基本的に auth
のミドルウェアが設定されているルーティングで、ログインしていない場合に設定(session set
)される。
例えば別のページからnavバー経由でログインページに訪れた場合、url.intended
はセットされない。
この場合、url.intended
が設定されていないため、ログイン後にLoginController
のredirectTo
に設定されたページに飛ばされてしまう。
これを回避するために、ログインページを表示するメソッドで以下の処理を追加すれば良い
public function showLoginForm(): View
{
if (url()->previous()) {
$this->redirector->setIntendedUrl(url()->previous());
}
return view('chefrepi.auth.login');
}
これでどのページからログインページにアクセスしても元いたページに戻すことが可能になる
LoginController の利用時の注意点
LoginControllerの AuthenticatesUsers
に事前にログイン処理で必要な処理が事前に定義されている。
これらを使い回すのは非常に便利であるが、注意しないといけない部分もある。
例えば、複数タイプのアカウント (User, Admin など) を管理する場合、ロジック内で guard
を一意に指定したいが AuthenticatesUsers では一意に指定できていないということだ。
この中で定義されている guard
メソッドで Auth::guard()
を返しており guard
の指定を行なっていない。
このため各種guardを指定したい場合は、LoginController上でtraitを上書きするように実装する。
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard()
{
return Auth::guard('admin');
}
既にある配列をもとに、配列の並び替えを行う方法
こういうユーザー情報が入った配列があったとする
$users = [
[
'age' => 10,
'name' => '太郎',
],
[
'age' => 25,
'name' => '五郎',
],
[
'age' => 30,
'name' => '三郎',
],
];
これを単純な年齢のソートや文字コードによるソートなどを行わず、
事前に用意された「五郎、太郎、三郎」の順番を並び替えたい時、usort
と array_search
を使う。
$users = [
[
'age' => 10,
'name' => '太郎',
],
[
'age' => 25,
'name' => '五郎',
],
[
'age' => 30,
'name' => '三郎',
],
];
$sort = ['五郎', '太郎', '三郎'];
usort($users, function ($a, $b) use ($sort) {
return array_search($a['name'], $sort) - array_search($b['name'], $sort);
});
// => [
// [
// "age" => 25,
// "name" => "五郎",
// ],
// [
// "age" => 10,
// "name" => "太郎",
// ],
// [
// "age" => 30,
// "name" => "三郎",
// ],
// ]
時間経過を伴うテストを行いたい時
有効期限付きの処理など、ある時刻以降での状態をチェックしたい場合がある。
Laravelで標準的に利用する日時処理系のパッケージ Carbon
では、アプリケーションのロード時に時刻を改変する機能がある。
<?php
namespace App\Providers;
use Carbon\Carbon;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
// 省略
public function boot()
{
// 15日後に移動「したものとして」実行
$target_dt = today()->addDays(15);
Carbon::setTestNow($target_dt);
// Laravel上のデフォルトをCarbonからCarbonImmutableに変更している場合は、
// CarbonImmutable::setTestNow($target_dt);
}
}
多対多リレーションの紐付けに関して
$post = Post::first();
// 投稿に対してユーザーのいいね(user id が1,2,3)
$post->userLikes()->sync([1,2,3]);
laravelの多対多リレーションは上記のように sync
を使うと簡単に行える。
ただしこの sync
メソッドは与えられた配列のみを多対多リレーションするので、DBには存在するが、引数に渡した配列に存在しないuser id は削除されてしまう。
$post = Post::first();
// 投稿に対してユーザーのいいね(user id が1,2,3)
$post->userLikes()->sync([1,2,3]);
$post->userLikes()->sync([4]);
上記では、user_idが4のみがDBに保存される形となる。
これを回避するために attach
を使う方法がある
$post->userLikes()->attach(1);
$post->userLikes()->attach(2);
$post->userLikes()->attach(3);
$post->userLikes()->attach(4);
これで強制的に外されることはない。
ただし、ここで問題なのが以下のようなもの
$post->userLikes()->attach(4);
$post->userLikes()->attach(4); // SQL の重複エラーが発生
このようにattachでは重複があるとエラーになる。
このような状況の場合は、 sync
の第二引数に false
を設定するか、 syncWithoutDetaching
メソッドを用いる
ちなみにsyncWithoutDetaching
は sync
の第二引数にfalseを設定したショートハンドである
ポリシーの利用について
Laravelの認可については、ポリシーとゲートの2種類が存在する。
主に使い分ける基準は「対象が特定のモデルに対するアクションかどうか」だと考えて良いと思っていて、
基本的にモデルに対する話は、ポリシーで対処すると良いと考える。
ポリシーの認可では、各種メソッドでboolの戻り値を用いて、権限があるかどうかの判定を行うが、これのみだと認可ができない場合が複数パターンあるときに表現力に乏しい。
このためExceptionを活用する。
namespace App\Exceptions;
use Exception;
class NotPremiumMemberException extends Exception
{
protected $message = 'プレミアム会員ではないため購入できません';
}
class ProductNotAvailableException extends Exception
{
protected $message = 'この商品は現在利用できません';
}
public function view(User $user, Product $product)
{
if (!$user->is_premium_member) {
throw new NotPremiumMemberException;
}
if (!$product->is_available) {
throw new ProductNotAvailableException;
}
return true;
}
public function show(Product $product)
{
try {
$this->authorize('view', $product);
} catch (\App\Exceptions\NotPremiumMemberException $e) {
return back()->with('error', $e->getMessage());
} catch (\App\Exceptions\ProductNotAvailableException $e) {
return back()->with('error', $e->getMessage());
}
// ...
}
認証関連
Multi Auth (認証を複数モデルに付与する実装)
Laravel では1つのプロジェクト内で認証(ログインやユーザーごとの制限権限付与)可能なモデルは1つにすることが推奨されているが、現実のアプリケーションにおいてはそれは結構難しく、ほとんどの場合で対応が必要になる。
Multi Authでログアウトすると、全アカウントからログアウトされる問題について
基本的に Laravel は Multi Auth をあまり推奨はしていない
ログアウト時にセッションを削除するため、ログアウト時には全アカウントからログアウトされてしまう。
これはログアウトの処理で invalidate()
を無効化することで回避可能ではある。
$request->session()->invalidate();
$request->session()->regenerateToken();
セッション固定攻撃を防ぐために、必ずログインログアウト時にはセッションを更新するべき。
結論としては、Laravel自体が個別でログアウトする機能をサポートしていないので、現状はできないと考えて良い。
やり方があるとすれば、各ガードごとに異なるセッションを用いるようにすることなのだと思う
Multi Auth の際に複数の認証可能なモデルごとにエラーページ(404.blade.php)を用意する方法
結論
app/Exceptions/Handler.php
の getHttpExceptionView
メソッドに表示するエラーページを指定する
詳しく
デフォルトの app/Exceptions/Handler.php
の実装は以下の通りかと思う。
ここで、 Handler
クラスは Illuminate\Foundation\Exceptions\Handler
に依存しているので、ここを眺めているとエラーの処理を見ることができる。
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
}
Illuminate\Foundation\Exceptions\Handler
に対応するファイルは、 vendor
以下で次のように見ることができるが、その中でエラー時に表示するページを以下のメソッドで定義しているようである。
/**
* Render the given HttpException.
*
* @param \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function renderHttpException(HttpExceptionInterface $e)
{
$this->registerErrorViewPaths();
if ($view = $this->getHttpExceptionView($e)) {
return response()->view($view, [
'errors' => new ViewErrorBag,
'exception' => $e,
], $e->getStatusCode(), $e->getHeaders());
}
return $this->convertExceptionToResponse($e);
}
したがって、ここを上書きするような実装を、app/Exceptions/Handler.php
にて行えば良い。
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
/**
* Override default method.
* Get the view used to render HTTP exceptions.
*/
protected function getHttpExceptionView(HttpExceptionInterface $e): ?string
{
- $view = 'errors::'.$e->getStatusCode();
+ // getHttpExceptionView を記述しつつ、
+ // エラーが発生したルーティングで表示するページを雑に切り分ける(PHP8.0.0以降はmatch 文で制御してもいいかも)
+ $view = request()->is('admin/*')
+ ? 'errors::admin.' . $e->getStatusCode()
+ : 'errors::' . $e->getStatusCode();
if (view()->exists($view)) {
return $view;
}
$view = substr($view, 0, -2) . 'xx';
if (view()->exists($view)) {
return $view;
}
return null;
}
}
TODO
errors::
の記法について、解説
match 文を使った分岐について解説
リクエストのパスで分岐するのではなく、namespace で分岐する方法を解説
20230816 追記
request()->routeIs() を用いて、ルーティングのネームスペースで分岐を行いたい場合、404のようなエラーでは動作しない。request()->routeIs() の実装は、 https://github.com/laravel/framework/blob/9.x/src/Illuminate/Http/Request.php#L224 にある通りだが、Kernel.php でグローバルに登録されている Middleware レベルで発生するエラーについては、
request()->route()
に関する解決が行われていないため、request()->routeIs()
で実行されている$this->route()
がnull
になる。これにより判定が常にfalse で返ってくる。
これに対応するために致し方なく、request()->server->get('***')
などを使う手もある。
request()->server では symfonyのServerBagを読んでおり、これに紐づくデータが取得できる。
Auth::user() とかは、必ずクラスで管理するようにした方が良い。
こんな感じで管理してれば、型をつけることができる。
以前はRepositoryに記載していたが、Repositoryではセッションの操作は責務範囲外なので分離したいところ。
<?php
declare(strict_types=1);
namespace App\Services\Laravel\Auth;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Authenticatable;
use App\Models\Customer\Customer;
/**
* @mixin \Illuminate\Auth\AuthManager
* @mixin \Illuminate\Auth\SessionGuard
*/
final class AuthCustomer extends AbstractAuth
{
public static function user(): ?Customer
{
return static::guard('customer')->user();
}
public static function userOrFail(): Customer
{
$user = static::guard('customer')->user();
if (is_null($user)) {
throw new AuthenticationException('user failed login');
}
return $user;
}
/**
* @param array<mixed> $credentials
* @param bool $remember
*
* @return bool
*/
public static function attempt(array $credentials = [], $remember = false): bool
{
return static::guard('customer')->attempt($credentials, $remember);
}
/**
* @param bool $remember
*
* @return void
*/
public static function login(Authenticatable $user, $remember = false): void
{
static::guard('customer')->login($user, $remember);
}
}
PHP_CS_FIXER のバグっぽかった挙動について
$this->handler($a, $b, );
上記のようなコードをCS_FIXERにかけたら、Fixがうまくできないとコンソールに表示された。
おそらく以下の内容が原因なんだろう
サブドメインルーティングについて
Laravelではサブドメインルーティンが行える。
これは以下のように定義すればOK
Route::domain('sub.domain.com')
->middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web/app.php'));
サブドメインルーティングをする場合、
sub.domain.com
を登録するが、このとき、元々存在するメインのドメイン domain.com
についても Route::domain('domain.com')
として登録しなければならない。
ドメインを設定していない場合、相対バスで解決されてしまい
domain.com/pages
のパスが sub.domain.com/pages
でもアクセスできてしまう問題が発生する。
request()->route() が null になる場合、これが問題
vscode extension [laravel goto view] について
- vite ディレクティブに対応してほしい
- URL::temporarySignedRoute など、URLクラスでネームスペースで指定するものに対応してほしい
使えるLinter類
- PHPStan (Larastan)
- Rector
- Deptrac
- PHP_CodeSniffer
- Pint (Laravel 謹製)
- PHPMD
- PHP Insightis
DDDについて(やや脱線)
DBカラム定義について
Laravelではmigrationでテーブル定義を行う際にタイムスタンプを以下のようにすることが一般的
$table->timestamps()
php artisan make:migration
でもデフォでこれが入っているが、なんとnullableで登録されるのである。
created_at, updated_atを登録するからにはnullableになるようなケースは考えにくいのだが、nullableで指定されるのは非常に謎。
nullableにするなら必要ないのでは。。。。
自分はこのようにしている。
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
stubで定義すると便利
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('{{ table }}', function (Blueprint $table) {
$table->id();
// $table->timestamps(); は nullable となるため利用しない。
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('{{ table }}');
}
};