Laravel

提供:senooken JP Wiki
2024年11月14日 (木) 11:06時点におけるSenooken (トーク | 投稿記録)による版 (Log)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)

About

人気の理由

Laravelの人気を大検証 何が凄いの? | テクフリ

CakePHPのほうが早い。

  • 簡単にマスター可能
  • 自由度が高い

情報源

Getting Started

Configuration

Configuration - Laravel 5.8 - The PHP Framework For Web Artisans

Environment Configuration

DotEnvライブラリーを使っており、ルートディレクトリーの.env.exampleを.envにリネームするとこの設定を利用する。

Accessing Configuration Values

configグローバルヘルパー関数で、アプリ内のどこからでも設定値を参照できる。

$value = config('app.timezone');

実行時に設定もできる。

config(['app.timezone' => 'America/Chicago']);
Configuration Caching

config:cacheで設定をキャッシュすることができるらしい。ただし、このコマンドを実行すると、.envファイルは読み込まれないので、env関数は設定ファイル内でしか使ってはいけない。

つまり、基本は設定値の参照はconfig関数で行う。

Additional Directory

php - Laravel 5 - Config sub-folder - Stack Overflow

configディレクトリーに、ディレクトリーを追加することもできる。DotEnvライブラリーの作法と思われる。

config('subfolder.myfile.var');

上記のような命名で、ディレクトリーも解釈される。config('directory')を実行すると、ディレクトリー内の全部が連想配列で取得できる。

Array in .env

.envは環境変数の設定ファイル。そのままだと配列は使えない。

  • コンマ区切り
  • json

やるとしたら上記で記述しておいて、読込後にデコードする。

.env format

.envでSQLのような複雑なテキストを記述する場合。引用符の扱いが重要になる。dotEnvの書式を整理する。

基本はシェルスクリプトと同じだが一部違う。

  • 改行を含める場合、二重引用符で囲む。

コマンド置換$()が使えるのは重要。catでヒアドキュメントで生テキストを書ける→無理。シンプルなコマンドしかパースされない。

しかたないので、JSON内の二重引用符をエスケープする。

RAKURAKU_TABLES="{\"101174\":{\"dbSchemaId\":101174, \"importId\":100577, \"sqlFile\":\"101174.sql\", \"sqlText\":\"
SELECT \"0000010108\" as 消費者コード, \"1\" as 連番, \"111-1111\" as 郵便番号, \"XXXX\" as 住所1, \"X\" as 住所2, \"41\" as 販売店コード, \"111 X\" as 消費者名, \"10\" as メーター数, \"2024-01-01\" as 契約日, \"2024-01-01\" as 建築日, \"地上\" as 供給設備_バルク設置区分, \"1\" as 供給設備_バルク基数, \"2024-01\" as 供給設備_バルク設置年月, \"2024-01\" as 供給設備_バルク製造年月, \"1\" as 供給設備_バルク貯槽番号, \"1\" as 供給設備_ガスメータ種別, \"メーカー\" as 供給設備_ガスメータメーカ, \"型式\" as 供給設備_ガスメータ型式, \"2024-01\" as 供給設備_ガスメータ設置年月
UNION ALL SELECT '0000010109','2','X','X','X','41','111 X','10','2024-01-01','2024-01-01','地上','1','2024-01','2024-01','1','1','メーカー','型式','2024-01'
;
                                                      \"}}"

Directory Structure

Directory Structure - Laravel 5.8 - The PHP Framework For Web Artisans

  • app
  • bootstrap
  • config
  • database
  • public
  • resources
  • routes
  • storage: フレームワークで自動生成される、コンパイル済みのBladeテンプレート、ファイル系セッション、ファイルキャッシュ類を格納。app, framework, logsがある。
    • app: アプリ生成ファイルの格納。
    • framework: フレームワーク生成ファイルとキャッシュ。
    • logs: ログ。
  • tests
  • vendor

Laravelの名前空間と、ディレクトリーは同一ではない。

命名規則の都合だろうと思われる。少々気持ち悪い。

Architecture Concepts

Service Container

Service Container - Laravel 5.8 - The PHP Framework For Web Artisans

Service Class

Serviceクラスは特定のビジネスロジックの管理に重点を置いたクラス。ビジネスロジックに特化しているから、他のクラスと異なり、通常はプロパティーを継承しない。

app/Servicesに配置して、クラス名の末尾にはServiceの接尾辞をつける。

多くの場合、Eloquentモデルにリンクされたロジックの追加で役立つ。例えば、Userモデルに対するUserServiceのような。ただ、Eloquentモデルとは関係なしに、PaymentServiceのように特定の機能 (ビジネスロジック) に合わせて調整したサービスクラスもある。

ビジネスロジックに特化したユーティリティークラスとかに近い。

うまい作りとしては、Controllerではtry-catchを含んだサービスクラスに定義した関数を呼ぶだけ、呼び終わった結果を返すだけにするとか。

コードがモジュール化されて、保守しやすく整理される。

Other

独自ライブラリーの追加

Laravelで独自に作ったライブラリファイルへのパスの通し方 | A Day In The Boy's Life

自前のライブラリーを追加したくなった。追加方法がいくつかある。

composerを使う方法が簡単。

The Basics

Routing

Basic Routing

routes/web.phpでURL別のアクセス時 (ルーティング) の処理 を設定する。

web.phpはwebミドルウェアグループに割り当てられていて、CSRFガードなどが入っている。

Route::get('/user', 'UserController@index');

上記のような書式で、パスとアクションを対応付ける。

Named Routes

ルートに名前を付けて、後で流用・参照できる。

Other

動的アクション

ルートに応じて、DBにアクセスして、値の取得や処理をしたいことがある。

evalでアクションメソッドを呼び出すこともできる。が、パラメーターとして渡して、1個のメソッドで処理するのがスマート。

evalでやるのはいまいち。

Middleware

リクエスト受信後にコントローラー処理の前後に割り込んで行う処理の仕組み。プログラムの基本はコントローラーのアクション。

このアクションに共通処理を一括で仕込む場合、コントローラーのアクション単位で処理が必要になる。そのままだと何回も同じことを書く必要がある。

例えば、フォームの送信チェックやログイン認証など。これらを一括で行うための仕組みがミドルウェア。

Controllers

Controllers - Laravel 5.8 - The PHP Framework For Web Artisans

Resource Controllers
Verb URI Action Route Name
GET /photos index photos.index
GET /photos/create create photos.create
POST /photos store photos.store
GET /photos/{photo} show photos.show
GET /photos/{photo}/edit edit photos.edit
PUT/PATCH /photos/{photo} update photos.update
DELETE /photos/{photo} destroy photos.destroy

create/editは新規作成、編集用の画面表示。実際の処理はstore/updateで行う。

Requests

HTTP Requests - Laravel 5.8 - The PHP Framework For Web Artisans

Retrieving Input
Retrieving All Input Data

allメソッドで入力のすべてを取得できる。

Retrieving An Input Value

inputメソッドで指定した入力 (HTTP ボディー) をキーで参照できる。第二引数になかった場合のデフォルト値を設定できる。

$name = $request->input('name', 'Sally');

$names = $request->input('products.*.name');

$request->input();

引数空だと、連想配列で全部取得できる。

Retrieving Input From The Query String

input同様、queryでURLクエリーを取得できる。

$name = $request->query('name');
$name = $request->query('name', 'Helen');
$request->query();

LaravelでURLのクエリパラメーターを取得する二つの方法と注意点 #PHP - Qiita

Bladeで使う場合、request() (Helpers - Laravel 5.8 - The PHP Framework For Web Artisans) のグローバルヘルパー関数を経由して、Requestインスタンス経由でアクセスする。

Retrieving Input Via Dynamic Properties
$name = $request->name;

input/queryなどを使わなくても、動的にプロパティーに持っている。

Old Input

Laravelは次のリクエストの送信完了まで、前回のリクエストの入力を保存している。これにより、バリデーションエラー時などに入力内容を復元できる。

Retrieving Old Input

Request$ondにより、セッションから前回の入力を取得できる。

$username = $request->old('username');

Bladeでも使える。フォーム要素の前回の値表示として使える。基本は指定するとよいだろう。

<input type="text" name="username" value="{{ old('username') }}">

なお、このBladeのold関数は「Helpers - Laravel 5.8 - The PHP Framework For Web Artisans」のグローバルのヘルパー関数。

Responses

Creating Responses

全てのルートとコントローラーは、応答を返す必要がある。いくつかの方法がある。

Strings & Arrays

基本はテキストで返すこと。フレームワークが自動的にHTTP応答に変換する。

Route::get('/', function () {
    return 'Hello World';
});

文字列の他に、配列も返せる。配列の場合、JSONに変換する。

Route::get('/', function () {
    return [1, 2, 3];
});
Response Objects

文字列と配列以外で返したい場合、Illuminate\Http\Responseインスタンスか、viewsを返せる。 ResponseインスタンスはHTTPステータスコードやヘッダーを完全に返せる。ResponseはSymfony\Component\HttpFoudation\Responseを継承しており、HTTP応答構築のメソッドをいろいろ使える。

Route::get('home', function () {
    return response('Hello World', 200)
                  ->header('Content-Type', 'text/plain');
});
  • header: 1項目ずつ引数指定。
  • withHeaders: 配列でまとめて指定。

Views

ControllerからViewにデータを渡す際、user情報など毎回渡すデータがあったりする。そういうのをControllerとは別の場所で自動処理する仕組みがView Composer。Controllerの処理がすっきりする。

ViewはHTMLを保持している。view関数で、テンプレートを指定して、テンプレートで使用する変数を渡せば、Viewインスタンスを取得する。ViewインスタンスがBladeのHTMLを保有している。

いくつかViewインスタンスのメソッドがある。

first: 指定した配列の最初のテンプレートを表示に使う。基本的にアプリなどでユーザーが上書きするよう。あまり使わない。

return view()->first(['custom.admin', 'admin'], $data);
return view('greetings', ['name' => 'Victoria']);
return view('greeting')->with('name', 'Victoria'); 

引数で渡すほかに、with関数でも渡せる。

Validation

主にPOST系のアクション実行用に、バリデーションというデータの検証の仕組みがある。

ControllerのValidateRequestsトレイトに用意されており、Controllerのメソッドとして使える。

$this->validate($request, [検証設定の配列]);

上記の書式で使う。

ただ、この基本的なバリデーションだとコントローラーに都度記載が必要。できれば、リクエストなどで別でやりたい。

FormRequestという仕組みがある。フォームに関する機能をリクエストに組み込む。コントローラーではなく、リクエストでフォーム処理をやってくれる。

FormRequestを派生させたクラスを作成し、適用するURLパスとルールを定義。使用するコントローラーのアクションの引数に、Reqeustではなく作ったFormRequest派生クラスに変更するとOK。これだけ。

エラーメッセージもFormRequest派生クラスで作れる。

Logging

Logging - Laravel 5.8 - The PHP Framework For Web Artisans

Other

Laravelで便利なログ出力方法がいくつかある。

  • ヘルパー関数 (Helpers - Laravel 5.8 - The PHP Framework For Web Artisans)
    • dd: dump and die。引数の変数をその場で表示して終了。
    • dump: 引数の変数をその場で表示。
  • Log::debug(): ログファイルstorage/logsに出力 (use Illuminate\Support\Facades\Log;)。
    • Log::info(print_r($user, true));
    • Log::info(json_encode($user));
    • オブジェクト類はjson_encodeがいい。
Allowed memory size of 134217728 bytes exhausted (tried to allocate 90181632 bytes)

なお、print_r($request, true) などをすると、以下のエラーが出る。

[2017-09-06 15:19:44] production.ERROR: Symfony\Component\Debug\Exception\FatalErrorException: Allowed memory size of 134217728 bytes exhausted (tried to allocate 90181632 bytes) in [path to file reducted] Stack trace: #0 {main}

[print_r($request) causes out of memory error.] にあるように、print_rは継承元も再帰的に出力し、Laravelはたくさん継承しているからいっぱいになるらしい。

これはせずに、dumpなどを使う。

Helpers

Helpers - Laravel 5.8 - The PHP Framework For Web Artisans

デバッグに役立つグローバルなヘルパー関数がいくつかある。Bladeでもそのまま使用できる。

  • info: Log::info相当。第二引数に配列データも渡せる。
  • logger: Log::debug相当。引数を空にするとloggerインスタンスを取得できて、そこから個別のエラーレベルに出力もできる。

Log::info/Log::debugはuse宣言が必要で面倒なので、info/loggerを使ったほうがいい。

logger('Debug message');
logger('User has logged in.', ['id' => $user->id]);
logger()->error('You are not allowed here.');
debugbar

DebugbarというLaravelの開発デバッグにかなり便利なツールがある。

導入方法

composer require barryvdh/laravel-debugbar --dev

.envでAPP_DEBUG=trueの場合に機能する。

クエリーの発行数、メモリー使用量、実行時間。このあたりが特に重要と思われる。

Frontend

Blade Templates

Blade Templates - Laravel 5.8 - The PHP Framework For Web Artisans

@section/@yield: Template Inheritance

Bladeでは、継承とセクションの2種類のテンプレートの流用方法がある。

Layoutはページ全体のテンプレート。セクションは区画単位のテンプレート。

セクションは@section/@yieldを使って実現する。

@sectionは@showか@endsectionで終わる。

@show/@endsectionの違い (Bladeテンプレートの@showと@endsectionを間違えないようにする #Laravel - Qiita)。親テンプレートで定義する場合は@show。親でも@endsectionを使うと、sectionの内容が消える。sectionは本来、元テンプレートから継承したものを埋め込むためのものなので、ベーステンプレートだと埋め込み元がないので消えるのだと思う。だから、@showを使う必要があると思われる。

@component/@slot: Components & Slots

テンプレートよりも細かい部品単位の流用方法がcomponent。ヘッダーやフッター、ボタンなどの部品単位で流用できる。

viewsディレクトリー以下に格納する。一般的にはviews/components/ok.blade.phpなどのように配置し、components.okなどで、コンポーネント名を指定して読み込む。

component定義側は、通常のBladeと同じだが、コンポーネント内で変数のプレースホルダーを使用できる。これは、利用側の@slotのブロックで引き渡す。

<div class="message">
                                                                                                                                  <p class="msg_title">{{$msg_title}}</p>
 <p class="msg_content">{{$msg_content}}</p>
 {{$slot}}
</div>

component利用側で組み込むために工夫する。

@component(名前)
  @slot('msg_title')
    title
  @endslot
    <strong>Whoops!</strong> Something went wrong!
@endcomponent

$slot変数には、@componentsのテキスト、@slotブロック以外が入る。

Laravelに複数の在不明のcomponentを順番に適用させたい場合componentFirstを使う。

@componentFirst(['custom.alert', 'alert'])
    <strong>Whoops!</strong> Something went wrong!
@endcomponent

@slot以外に、変数を渡すこともできる。

@component('alert', ['foo' => 'bar'])
    ...
@endcomponent

@component('alert')
  @slot('foo', 'bar')
@endcomponent

slotの設定方法は複数ある。@slot/@endslotよりかは@slot()で設定するほうが短い。が、@component内は$slotのデフォルト値を入れるとしたほうがわかりやすいかもしれない。

ただし、@endcomponentは省略できない。

名前付きslotのデフォルト値設定はない。やりたければ、??や@if/@issetで自分でやっておく。

デフォルトの$slotは、@componentの配列式 (@component(, ['slot']) では指定できないが、view関数で呼ぶ際は['slot']で指定できる。

基本は短くできるので、component内では$slotを使うほうがいい。

なお、$slotは扱いに注意が必要。使う側で指定がなかったら、nullではないが見えない変な値が入っている。変数として使う際は"$slot"のように二重引用符で囲んで、値がない場合に確実に空にしておく。

@include: Including Sub-Views

レイアウトやコンポーネントのように、変数の引き渡しなど複雑なことをしない場合、単純な定形固定文字列を読み込むような場合、Sub-Viewsというのを使うこともできる。

これは@includeでテンプレートファイルをそのまま読み込むのが基本。親の変数もそのまま使える。他に、引数で変数を渡すこともできる。

@include('view.name', ['some' => 'data'])
@includeIf('view.name', ['some' => 'data'])
@includeFirst(['custom.admin', 'admin'], ['some' => 'data'])

@includeで指定したテンプレートが不在の場合、Laravelはエラーを投げる。このエラーを回避したい場合、@includeIf指令を使う。@includeIfのバリエーションの一種で、配列のテンプレート名で存在する最初のものを使う場合、@includeFirstを使う。

条件がtrueなら読み込む場合、@includeWhenがある。

@include($boolean, 'view.name', ['some' => 'data'])

@ifよりもシンプル。

【Laravel】bladeの@includeと@componentの違い #PHP - Qiita

includeはcomponentと違って@endincludeがいらない。

componentはslotで渡すこともできる。$slotを使える点が大きな違い。

複雑で長いHTMLなどを、引き渡して使いたい場合、componentのほうがいい。

@each: Rendering Views For Collections

意外と使用頻度が高いのが繰り返し表示。例えば、リストの項目、テーブルの行など。これようの指令が@each

 @each('components.item', $data, 'item');

$data配列の要素をコンポーネントのitem変数に渡す。

// components/item.blade.php
<li>{{$item['name']}} [{{$item['mail']}}]</li>

Displaying Data

Displaying escaped Data

Bladeでデータを表示する際は、二重波括弧を使う。

Route::get('greeting', function () {
    return view('welcome', ['name' => 'Samantha']);
});
Hello, {{ $name }}.

二重波括弧は自動的にhtmlspecialcharsでXSS対策してくれる。変数の他に、PHP関数の結果も表示できる。

Displaying Unescaped Data

なお、生のHTMLなどのデータをそのまま表示させたい場合、二重波括弧の代わりに{!! !!}で囲む。

Hello, {!! $name !!}.
Hello, {!! e($name) !!}.

もっというと、e()でエスケープしておくと安心 (Bladeで変数に入れたhtml文字列を表示させる #Laravel - Qiita)。

Helpers - Laravel 5.8 - The PHP Framework For Web Artisans

混乱するが、二重波括弧内はPHP扱い、外はHTML扱い。二重波括弧内で表示文字列扱いしたいなら、文字列にする必要がある。

Rendering JSON

JavaScript変数として、配列をJSONとして描画したいことがあるだろう。

<script>
                                                                              var app = <?php echo json_encode($array); ?>;
                                                                              var app = {{json_encode($array)}};
      var app = {!! json_encode($array) !!};
     var array = JSON.parse('{{ json_encode($theArray) }}');
</script>

手動でjson_encodeを呼ぶ代わりに、@json指令がある。

<script>
    var app = @json($array);
 
    var app = @json($array, JSON_PRETTY_PRINT);
</script>

なお、@jsonはjson_encodeと同じではない。以下のコードで{{json_encode部分を@jsonに変えるとうまくいかない。

<button type="button" onclick="
         ajaxGetAndSetTable('/ajax/楽楽販売_器具交換履歴_最新取得?消費者コード=' + document.getElementById('消費者コード').value, '#facilityHistory tbody', 
         {{json_encode($names)}}, '部屋番号');
     "
     >楽楽販売最新取得 (未実装)</button>

{{json_encode($array)}}で良いと思われる。

{{}} だとエスケープされて二重引用符が"になって、扱いにくいことがある、{!! !!}でエスケープ解除すると問題ない。

Blade & JavaScript Frameworks

JavaScriptフレームワークの中に、二重波括弧をプレースホルダーとしてそのまま使うものがある。そういう場合、@を波括弧に前置するとそのまま表示する。

<h1>Laravel</h1>
 
Hello, @{{ name }}.
The @verbatim Directive

JavaScript変数を表示する場合、波括弧の前に@をたくさん置かないで済むように、@verbatimで囲める。

@verbatim
    <div class="container">
                                                                                                                                             Hello, {{ name }}.
    </div>
@endverbatim
Control Structure/Blade Directives
If Statements
@if (count($records) === 1)
    I have one record!
@elseif (count($records) > 1)
    I have multiple records!
@else
    I don't have any records!
@endif

@unless (Auth::check())
    You are not signed in.
@endunless

@isset($records)
    // $records is defined and is not null...
@endisset
 
@empty($records)
    // $records is "empty"...
@endempty

@auth
    // The user is authenticated...
@endauth
 
@guest
    // The user is not authenticated...
@endguest

@auth('admin')
    // The user is authenticated...
@endauth
 
@guest('admin')
    // The user is not authenticated...
@endguest

@hasSection('navigation')
    <div class="pull-right">
        @yield('navigation')
    </div>
 
    <div class="clearfix"></div>
@endif

特に@isset/@emptyをよく使うかも。ただ、これらには@elseはないので注意が必要かもしれない。やるなら、

@if(isset())
@else
@endif

@isset()
@endisset
@empty()
@endempty
Switch Statements
Loops

PHPの反復構造に近いものがある。重要。

@for ($i = 0; $i < 10; $i++)
    The current value is {{ $i }}
@endfor
 
@foreach ($users as $user)
    <p>This is user {{ $user->id }}</p>
@endforeach
 
@forelse ($users as $user)
    <li>{{ $user->name }}</li>
@empty
    <p>No users</p>
@endforelse
 
@while (true)
    <p>I'm looping forever.</p>
@endwhile

公式マニュアルに記載がないが、foreachはPHPのforeachと同じく foreach($array as $key => $value) 形式にも対応している。

The Loop Variable

Blade Templates - Laravel 5.8 - The PHP Framework For Web Artisans

ループ内で使用可能な$loop変数があり、これに便利なプロパティーがある。

Property Description
$loop->index The index of the current loop iteration (starts at 0).
$loop->iteration The current loop iteration (starts at 1).
$loop->remaining The iterations remaining in the loop.
$loop->count The total number of items in the array being iterated.
$loop->first Whether this is the first iteration through the loop.
$loop->last Whether this is the last iteration through the loop.
$loop->even Whether this is an even iteration through the loop.
$loop->odd Whether this is an odd iteration through the loop.
$loop->depth The nesting level of the current loop.
$loop->parent When in a nested loop, the parent's loop variable.

first/last/even/oddあたりは特に便利だろう。

Additional Attributes

Blade Templates - Laravel 9.x - The PHP Framework For Web Artisans

Laravel v9から使用可能。

@disabled

Comments

php - Laravel - Blade comments , blade rendering causing page to crash - Stack Overflow

Bladeテンプレートファイル内でのコメントには注意が必要。

{{-- code --}} これが基本
PHPコード扱いでのコメントアウトも便利。
@php
/* */
@endphp
<?php /* */ ?>

Bladeの{{-- code --}}は内部の{{}}が変数展開として解釈される。内部に波括弧がある場合は、phpコード扱いでコメントアウトしたほうが安全。

PHP

Bladeテンプレート内でPHPのコードをそのまま記述する際には、専用の指令を使う。

@php
@endphp

これを使うと、「Laravelのbladeで変数を定義する – FUNBREW」にあるように、変数を定義してBlade内で使うことができる。変数の定義や空チェックなどを最初にできるので、シンプルになる。

@php
$newValue = $somethingValue;
if (empty($newValue)) {
    $newValue = 'Not Defined';
}
@endphp

<div>{{ $newValue }}</div>
Forms
CSRF Field

アプリ内でHTMLフォームを使用する場合、CSRFトークンフィールドを記述する。

具体的には、form要素の冒頭に@csrfを指定する。

<form method="POST" action="/profile">
    @csrf
 
    ...
</form>
@stack/@push/@prepend: Stacks

名前付きのスタックに、他の場所で使用するビューやレイアウトを格納して、流用できる。

まず@pushする。使いたいか所で@stackすると取り出せる。

使い方としては、componentで@pushでscript要素やstyle要素を記述しておいて、レイアウトで@stackで呼び出す感じ。

@push('scripts')
    <script src="/example.js"></script>
@endpush
<head>
  
      @stack('scripts')
  </head>

順番が大事な場合、@prependでpushより先に詰め込める。

扱いは、sectionに似ている。componentのためのsectionのような感じだと思う。

pushしたものは描画のたびにstackで表示される。後のバージョンで@onceというのが登場したので、これを使えば1個だけになる。それまでは自作が必要。

push/stackを使わない場合、そのページに必要なくても、使う可能性のあるcss/jsを親で全部読み込んでおかないといけない。それを回避できる。

コンポーネントやテンプレート固有で必要なものを、同じファイルに記載しておいて、反映だけstackでまとめてできる。これが利点だろう。

Blade Templates - Laravel 9.x - The PHP Framework For Web Artisans

Blade Templates - Laravel 7.x - The PHP Framework For Web Artisans

Laravel 9.xで@pushOnce。Laravel 7.xで@onceがある。

componentにcssやscriptを含めたい場合に@pushを使う。

Component

コンポーネントのパターンがある。

table

Table | Components | BootstrapVue

BootstrapVueを真似する。

{{--  
    fields = [field1, field2, field3]
    items = [[col1, col2, col3], {col1, col2, col3}]

    fields = [['label' => field1, 'class' => '', 'style' => ''], ['label' => field1, 'class' => '', 'style' => '']]
    items = [['label' => '', 'class' => '', 'style' => ''], ['label' => '', 'class' => '', 'style' => '']]
                                                                                                                  --}}
<table class="table table-sm table-bordered">
                                                                                                                       <thead class="bg-info text-center">
                                                                                                                           @foreach ($fields as $field)
                                                                                                                               @if (!is_array($field))
                                                                                                                                   <th>{{ $field }}</th>
            @else
                <th class="{{empty($field['class']) ? '' : $field['class']}}"
                                                                                                                                       style="{{empty($field['class']) ? '' : $field['style']}}">
                                                                                                                                       {{empty($field['label']) ? '' : $field['label']}}</th>
            @endif
        @endforeach
    </thead>
    <tbody>
                                                                                                                           @foreach ($items as $row)
                                                                                                                               <tr>
                @foreach ($row as $column)
                    @if (!is_array($column))
                        <td>{{ $column }}</td>
                    @else
                        <td class="{{empty($column['class']) ? '' : $column['class']}}"
                                                                                                                                               style="{{empty($column['style']) ? '' : $column['style']}}">
                            {{empty($column['label']) ? '' : $column['label']}}</td>
                    @endif
                @endforeach
            </tr>
        @endforeach
    </tbody>
</table>
Other
Bladeのレンダー結果の取得

場合によっては、componentにBladeの描画結果を埋め込みたいことがある。

view関数でViewインスタンス作成後に、render/renderContents/getContentsを呼ぶと描画後のHTML文字列を取得できる。

これを使う。{!! $var !!} のような感じ。必要に応じてe()でエスケープする。renderを実行しなくても、Viewインスタンスをそのまま渡すとHTMLになっている。けど、エラーが出たときよくわからないのでrender()しておいたほうがいい。

// index.blade.php

	@component('components.tab', [
		'tabs' => [
			'物件' => view('c211000.form')->render(),
			'代表オーナー' => view('c212100.代表オーナー')->render(),
			'括りオーナー' => view('c212200.括りオーナー')->render(),
		],
	])
	@endcomponent
// tab.blade.php
<section class="tab-wrap">
     @foreach($tabs as $label => $content)
         <label class="tab-label">{{$label}}<input type="radio" name="tab" class="tab-switch" {{$loop->first ? "checked=checked" : ""}} /></label>
        <div class="tab-content">{!! $content !!}</div>
    @endforeach
</section>
パスの取得

現在表示ビューのパスを取得したいことがある。いくつか方法がある。

  • Request::path()
  • Route::current()->uri()

Request::path()がシンプル。

子ビューの変数の使用

基本は不能。全体で共有する方法ならある。

$slotの型

Bladeのcomponentなどで使用するslotはHtmlString。だから、これをstring扱いで、old($name) などに使うとエラーになる。oldは内部で、array_key_existsなどで、inputのキーなどをチェックしている。

$slotを他のプロパティーなどに引き渡す際に、toHtmlで文字列に変換して渡すとよい。

PHPDoc

Bladeのコンポーネントで何の変数が使用可能かわかりにくくなる。対策としてPHPDocの記述がある。

単に、<?phpや@phpのPHPブロック内でPHPDocを記述するだけでいい。

@extends('layouts.app')

@php
/** @var App\Entity\User[] $users */
@endphp

@section('content')
  <h1>ユーザ一覧</h1>
  <ul>
               @foreach($users as $user)
                 <li>{{ $user->getName() }}</li>
  @endforeach
  </ul>
@endsection
<?php /** @var \Illuminate\View\ComponentAttributeBag $attributes */ ?>
<?php /** @var \Illuminate\Support\HtmlString $slot */ ?>

どちらでも問題ない。

Digging Deeper

Artisan Console

Artisan Console - Laravel 5.8 - The PHP Framework For Web Artisans

Laravelでのartisanコマンドやコマンドラインアプリケーションの作成方法が記載されている。

Writing Commands

app/Console/Commandsディレクトリーにコマンドは一般的に格納される。が、別の場所にも配置できる。

Generating Commands

make:commandで新規コマンドを作成できる。

php artisan make:command SendEmails

app/Console/Commandsに指定したコマンド名でファイルが作られる。

なお、作成新するコマンドの名前は、「framework/src/Illuminate/Foundation/Console/StorageLinkCommand.php at b9cf7d3217732e9a0fa4f00e996b3f9cc5bf7abd · laravel/framework」を見る限lり名詞:動詞で、ファイル名は「名詞動詞Command.php」になっている。

Command Structure

signatureとdescriptionプロパティーの記入が必要。これらのプロパティーはartisan listで表示される。handleメソッドにコマンド実行時の処理を配置する。

<?php
 
namespace App\Console\Commands;
 
use App\User;
use App\DripEmailer;
use Illuminate\Console\Command;
 
class SendEmails extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'email:send {user}';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Send drip e-mails to a user';
 
    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }
 
    /**
     * Execute the console command.
     *
     * @param  \App\DripEmailer  $drip
     * @return mixed
     */
    public function handle(DripEmailer $drip)
    {
        $drip->send(User::find($this->argument('user')));
    }
}

signatureは特に重要。後で詳述する。

Defining Input Expectations

signatureプロパティーでユーザーからの想定入力を定義する。コマンド名、引数、オプションなどを定義できる。

最低限コマンド名 (例だとemail:send) は記載する。

コロンはなくてもいい。signature='some' だとphp artisan someで実行できる。

Arguments

引数がある場合、波括弧で指定できる。

 protected $signature = 'email:send {user}';

この波括弧にはいくつか気法がある。

  • email:send {user?}: オプション引数。
  • email:send {user=default}: デフォルト引数ありのオプション引数。

? =がないと、必須引数。

Options

オプションはハイフン2個--を前置して指定する。オプション引数の有無で2系統ある。オプション引数がない場合、スイッチのような意味合い。

protected $signature = 'email:send {user} {--queue}';

上記の例では--queueがスイッチ系のオプション。--queueが渡されたらtrueになる。それ以外はfalse。

引数がある場合、末尾を=にする。

protected $signature = 'email:send {user} {--queue=}';

以下のように実行する。

php artisan email:send 1 --queue=default

=の後にデフォルト値の指定も可能。

email:send {user} {--queue=default}

短縮形。

オプションの短縮形も指定できる。

email:send {user} {--Q|queue}

短縮形の場合、ハイフン1個で実行できる。また、短縮形は先頭に書かないと認識しない模様。

email:send -Q
Input Descriptions

入力引数とオプションに、:を使って説明を指定できる。

protected $signature = 'email:send
                        {user : The ID of the user}
                        {--queue= : Whether the job should be queued}';

長い説明などで行をまたぎもOK。:の前後にはスペースが必要。

以下のような感じになる。

protected $signature = 'rakuraku:import {table : .envのRAKURAKU_TABLESで指定する、楽楽販売のAPI連携に必要な情報を連想配列のキー名}';
docker exec -i docker-php-1 php ../artisan help rakuraku:import 
Description:
  楽楽販売との連携コマンド。ガス基幹システムから楽楽販売にデータをインポートする。

Usage:
  rakuraku:import <table>

Arguments:
  table                 .envのRAKURAKU_TABLESで指定する、楽楽販売のAPI連携に必要な情報を連想配列のキー名

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Command I/O

Retrieving Input

引数やオプションはそれぞれargument/arguments|option/optionsで参照できる。

$userId = $this->argument('user');
$arguments = $this->arguments();
$queueName = $this->option('queue');
$options = $this->options();

なお、これらのメソッドはインスタンスメソッドなので、__constructでは使用不能。

options()は以下のような利用可能なオプション一覧とその値を返す。

array (
  'check' => false,
  'import' => NULL,
  'export' => '管理会社',
  'help' => false,
  'quiet' => false,
  'verbose' => false,
  'version' => false,
  'ansi' => false,
  'no-ansi' => false,
  'no-interaction' => false,
  'env' => NULL,
)

現在指定中のオプションやその個数を見たければ、array_filterを使う。

count(array_filter($this->options()))
Writing Output

コンソールへの出力には、line/info/comment/question/errorメソッドを使う。

  • info: 緑色。
  • error: 赤色。
  • line: 色なし。

Other

コマンド

外部プログラム類を実行したいことがある。

storage以下に配置する。か、app_pathなどでパスを参照する。

  • shell_exec
  • $schedule->exec
  • Symfony\Component\Process\Process

同じ場所に配置して実行すればいいと思う。

グループ

Artisanコマンドはartisan some:function {argument} {--option}の書式で定義される。

が、別にコロンはなくてもいい。コロンをつけると、同じプレフィクスのコマンドをグループ化して、ヘルプなどで取り扱ってくれる。わかりやすい。

処理を共通化したければ、クラスにして継承するか、Traitにする。

シンプルなものなら、オプションか引数で分けるというのもありだろう。

File Storage

File Storage - Laravel 5.8 - The PHP Framework For Web Artisans

Laravelでファイル入出力の仕組み。S3など、ストレージシステムを抽象化して扱える。ただし、これは基本的に一般公開用ディレクトリーを念頭に置いている。

File

公式マニュアルに記載がないが、Fileファサードが存在する。これは、PHPのファイル入出力処理のラッパー。Storageと異なり、ディスクシステムの考慮などはしていない。比較的シンプルなファイルIO用。

侍エンジニアブログくらいしか解説がない。

vendor/laravel/framework/src/illuminate/Filesystem/Filesystem.phpがソースコード。

Illuminate\Support\Facades\Fileが名前空間。

  • File::extension
  • File::size
  • File::get
  • File::put
  • File::copy
  • File::delete
  • File::move
  • File::isDirectory
  • File::copyDirectory: 中も全部。
  • File::deleteDirectory: 中も全部。

Helpers

Paths

パス取得関係のヘルパー関数がいくつかある。

  • app_path: appの絶対パス。
  • base_path: アプリのルートパス。
  • config_path: config
  • database_path: database
  • mix
  • public_path
  • resource_path
  • storage_path

特にbase_pathは重要に感じる。

Task Scheduling

Task Scheduling - Laravel 5.8 - The PHP Framework For Web Artisans

Introduction

cronでタスクスケジューリングできるが、cronでやる場合、タスクが増えるたびに設定が必要になる。

LaravelではLaravelのスケジュールコマンドを1個cronに追加するだけで、Laravelのタスクを全部管理できる。

タスクスケジュールはapp/Console/Kernel.phpのscheduleメソッドで定義できる。

Starting The Scheduler

スケジューラー使用時に、以下の項目をcrontabに登録する。

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

これで毎分実行される。

Defining Schedules

Defining Schedules

App\Console\Kernelクラスのscheduleメソッドに、全タスクを定義する。

<?php
 
namespace App\Console;
 
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
 
class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        //
    ];
 
    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->call(function () {
            DB::table('recent_users')->delete();
        })->daily();
    }
}

callメソッドで、PHPコードを指定できる。

この例では、毎日深夜にClosureを実行している。Closureでテーブルを削除している。__invokeを定義済みのinvokableオブジェクトなら以下でシンプルにかける。

$schedule->call(new DeleteRecentUsers)->daily();
Scheduling Artisan Commands

ArtisanコマンドやOSのコマンドも指定できる。

その場合、commandメソッドを使う。

$schedule->command('emails:send Taylor --force')->daily();
 
$schedule->command(EmailsCommand::class, ['Taylor', '--force'])->daily();

Artisanコマンドの場合、コマンド名かクラスを指定する。

Scheduling Shell Commands

execメソッドはシェルコマンドの発行に使える。

$schedule->exec('node /home/forge/script.js')->daily();
Schedule Frequency Options

php - Laravel schedule - hourly, daily - understanding when exactly start - Stack Overflow

dailyは ["0 0 * * *" ] 相当。

Method Description
->cron('* * * * *'); Run the task on a custom Cron schedule
->everyMinute(); Run the task every minute
->everyFiveMinutes(); Run the task every five minutes
->everyTenMinutes(); Run the task every ten minutes
->everyFifteenMinutes(); Run the task every fifteen minutes
->everyThirtyMinutes(); Run the task every thirty minutes
->hourly(); Run the task every hour
->hourlyAt(17); Run the task every hour at 17 mins past the hour
->daily(); Run the task every day at midnight
->dailyAt('13:00'); Run the task every day at 13:00
->twiceDaily(1, 13); Run the task daily at 1:00 & 13:00
->weekly(); Run the task every week
->weeklyOn(1, '8:00'); Run the task every week on Monday at 8:00
->monthly(); Run the task every month
->monthlyOn(4, '15:00'); Run the task every month on the 4th at 15:00
->quarterly(); Run the task every quarter
->yearly(); Run the task every year
->timezone('America/New_York'); Set the timezone

daily()=dailyAt('00:00')。

Preventing Task Overlaps

デフォルトだと、直前のタスクが実行中でも無視して次のタスクが実行される。withoutOverlappingを使うと、排他処理で他のタスクをさせない。

実行時間が大幅に異なるタスクがあって、終了予定時間が予測できない場合に便利。

デフォルトだと24時間でロックは解除される。引数で分単位で解除時間を指定できる。

タスクが異常終了したらロックファイルが残る。

Laravelタスクスケジュールの多重起動制御のロックを解除する - ハマログ

php artisan schedule:clear-cache

上記のコマンドでロックファイルを削除できる。

Running Tasks On One Server

memcachedかredisでキャッシュサーバーと同期していることが前提。

アプリが複数サーバーで稼働させてスケーリングしている場合、そのままだと全部のサーバーで重複実行される。

処理の最後に->onOneServer()を指定すると、別の場所で実行済みだとスキップするらしい。

Database

LaravelのDBの取得結果は、以下のような行ごとの連想配列になっている。

[
['column1' => 1, 'column2' => 2],
['column1' => 3, 'column2' => 4],
]

Getting Started

Database: Getting Started - Laravel 5.8 - The PHP Framework For 1Web Artisans

クエリービルダー

use Illuminate\Support\Facades\DB;

上記のクラスメソッドで生SQLを使えeる。

Running Raw SQL Queries

LaravelのクエリビルダでSQL文を直接実行(select,insert,update,delete,その他) #Laravel - Qiita

DB::select/insert/update/deleteなどよく使うCRUD操作はメソッドがある。そういうのとは関係なしに、SQLを直実行する場合DB::statementを使う。

DB::statement('drop table users');

なお、DB::statementは1文ずつしか実行できない。複数実行する場合、1文ごとにDB::statementを記載する。

Query Builder

Database: Query Builder - Laravel 5.8 - The PHP Framework For Web Artisans

Retrieving Results

Aggregates

集約メソッドがある。

  • count: レコード数を返す。テーブルの行数などの把握で頻出。データの可否確認には、exists/doesntExistもある。がcountでいい気もする。
  • max
  • min
  • avg
  • sum
Joins
$users = DB::table('users')
            ->join('contacts', 'users.id', '=', 'contacts.user_id')
            ->join('orders', 'users.id', '=', 'orders.user_id')
            ->select('users.*', 'contacts.phone', 'orders.price')
            ->get();

php - Laravel join queries AS - Stack Overflow

なお、joinの第1引数でテーブル指定の部分はASで別名指定が可能な模様。

Query Builder: multiple joins based on array

複数のjoinを行う場合、loopで行う。

$builder = DB::table($table);

foreach($leftJoins as $leftJoin) {
	$builder->leftJoin(...$jeftJoin);
}

$results = $builder->get();
  • join: 内部結合 (集合積)。
  • leftJoin/rightJoin: 外部結合 (集合和)
  • crossJoin: クロス結合。

Where Clauses

重要。いくつか書き方がある。

  • ->where('vote','=', 100);
  • ->where('vote', 100);: 演算子を省略すると=扱い。
  • ->where([['status', '=', '1'], ['subscribed', '<>', '1'],]);: 二次元配列にすれば一度に複数条件渡せる。キー・演算子・バリューのペア。
分割実行

php - Is it possible to split query builder in Laravel? - Stack Overflow

問題ない。

public function getStatuses($dates)
{
    $query = DB::table('tickets');
    if ($dates['from'])
        $query->where('from', $dates['from']);
    if ($dates['to'])
        $query->where('to', $dates['to']);
    $query->select('Active');
    return $query->get()->toArray();
}

whereの戻り値を変数に入れて流用しても問題ない。

Insert

Upsert

Laravel 8からupsertメソッドが登場した。 Laravel 8未満の場合、自分でDB::statementなどで行うしかない。

GitHub - yadakhov/insert-on-duplicate-key」が例。

Transaction

DB::transactionに処理の無名関数を指定してやると、DB処理が失敗したら自動的にロールバックする。

try {
$result = DB::transaction(function () use ($a, $b) {
    DB::table('users')->update(['votes' => 1]);
 
    DB::table('posts')->delete();
    return true;
});
} catch (Exception $e) {
    Log::error($e->getMessage());
    return;
}

2引数でデッドロック用のリトライ回数を指定できる。基本はいらない?

無名関数内でreturnすると、返せる。DB::transactionで失敗時の処理を個別にしたい場合、内部でthrowしてtry-catchをする。

Pagination

Database: Pagination - Laravel 5.8 - The PHP Framework For Web Artisans

テーブルの内容を全表示する場合などで、単純にallやgetでデータを取得・表示させようとすると、データ量が多すぎてメモリーアウトする可能性がある。

それを回避しつつ全データを表示させる仕組みがページネーション。データを一定間隔で分割・表示することでメモリーアウトを防止しながら大量データを表示する。

Introduction

基本的な使い方。

  1. DBやEloquentでのデータ取得時に最後にpaginate(表示数)のメソッドを追加する。
  2. Bladeで{{ $users->links() }}のメソッドを配置。

基本は以上。

<?php
 
namespace App\Http\Controllers;
 
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
 
class UserController extends Controller
{
    /**
     * Show all of the users for the application.
     *
     * @return Response
     */
    public function index()
    {
        $users = DB::table('users')->paginate(15);
 
        return view('user.index', ['users' => $users]);
    }
}
<div class="container">
     @foreach ($users as $user)
         {{ $user->name }}
    @endforeach
</div>
 
{{ $users->links() }}

Paginator Instance Methods

paginateなどが返すPaginatorインスタンスに役立つメソッドがある。

Method Description
$results->count() Get the number of items for the current page.
$results->currentPage() Get the current page number.
$results->firstItem() Get the result number of the first item in the results.
$results->getOptions() Get the paginator options.
$results->getUrlRange($start, $end) Create a range of pagination URLs.
$results->hasMorePages() Determine if there are enough items to split into multiple pages.
$results->items() Get the items for the current page.
$results->lastItem() Get the result number of the last item in the results.
$results->lastPage() Get the page number of the last available page. (Not available when using simplePaginate).
$results->nextPageUrl() Get the URL for the next page.
$results->onFirstPage() Determine if the paginator is on the first page.
$results->perPage() The number of items to be shown per page.
$results->previousPageUrl() Get the URL for the previous page.
$results->total() Determine the total number of matching items in the data store. (Not available when using simplePaginate).
$results->url($page) Get the URL for a given page number.

ヒット件数総数を表示するtotalが特に重要か。

Migrations

Database: Migrations - Laravel 5.8 - The PHP Framework For Web Artisans

マイグレーションはデータベースのバージョン管理のようなもの。データベーススキーマを簡単に修正・共有を可能にする。

Generating Migrations

make:migrationコマンドでマイグレーションファイルを作れる。

php artisan make:migration create_users_table

php artisan make:migration add_votes_to_users_table --table=users

CRUD_テーブル名_tableなどのような命名規則にしておくとわかりやすい。

実行すると、database/migrationsディレクトリーにマイグレーションファイルが生成される。

オプションで--create=テーブル名、--table=テーブル名がある。

現在のテーブルの状況などで、マイグレーションファイルのひな形が変わる。ある程度マイグレーション名から推測してくれるが、日本語などが入るときかなくなる。オプションを指定したほうが無難。

なお、このマイグレーションの名前は非常に重要。この名前がクラス名になるので、既存と被ってはいけない。変更内容が分かるように一意にする必要がある。

Laravel 8からは匿名マイグレーションで名前を意識しなくてもよくなる。

return new class extends Migration
{
    //
};

Migration Structure

コマンド実行で生成されたテンプレートには、upとdownメソッドが用意されている。ここにテーブルの更新と、巻戻の処理を記載する。

Running Migrations

以下のコマンドで用意済みのマイグレーションを実行する。

php artisan migrate
Forcing Migrations To Run In Production
Rolling Back Migrations

マイグレーションを打ち消すようにrollbackコマンドがある。

php artisan migrate:rollback

最後のバッチを自動判別してそのブロックで巻き戻す。--stepでステップ数を指定できる。

php artisan migrate:rollback --step=5

migrate:resetでアプリのマイグレーションを全部戻せる。

php artisan migrate:reset
Rollback & Migrate In Single Command

マイグレーションファイルやシーダーの更新などで、ロールバックとマイグレーションをやり直したいことがある。そういうコマンドがmigrate:refresh。--seedも指定するとシーダー登録もやってくれる。これはよく使う。

php artisan migrate:refresh
// php artisan migrate:reset && php artisan migrate // 相当と思われる
 
// Refresh the database and run all database seeds...
php artisan migrate:refresh --seed
// こちらは最後にphp artisan db:seed相当

これも--stepで指定できる。

Drop All Tables & Migrate

migrate:freshコマンドは、全テーブルを削除してmigrateを実行する。これは、マイグレーションで管理していないテーブルも含むので、危険。基本は使わないほうがいい。

php artisan migrate:fresh
 
php artisan migrate:fresh --seed

Tables

Renaming/Dropping Tables
Schema::rename($from, $to);
Schema::drop('users');
 
Schema::dropIfExists('users');

Columns

Creating Columns
Command Description
$table->bigIncrements('id'); Auto-incrementing UNSIGNED BIGINT (primary key) equivalent column.
$table->bigInteger('votes'); BIGINT equivalent column.
$table->binary('data'); BLOB equivalent column.
$table->boolean('confirmed'); BOOLEAN equivalent column.
$table->char('name', 100); CHAR equivalent column with an optional length.
$table->date('created_at'); DATE equivalent column.
$table->dateTime('created_at'); DATETIME equivalent column.
$table->dateTimeTz('created_at'); DATETIME (with timezone) equivalent column.
$table->decimal('amount', 8, 2); DECIMAL equivalent column with a precision (total digits) and scale (decimal digits).
$table->double('amount', 8, 2); DOUBLE equivalent column with a precision (total digits) and scale (decimal digits).
$table->enum('level', ['easy', 'hard']); ENUM equivalent column.
$table->float('amount', 8, 2); FLOAT equivalent column with a precision (total digits) and scale (decimal digits).
$table->geometry('positions'); GEOMETRY equivalent column.
$table->geometryCollection('positions'); GEOMETRYCOLLECTION equivalent column.
$table->increments('id'); Auto-incrementing UNSIGNED INTEGER (primary key) equivalent column.
$table->integer('votes'); INTEGER equivalent column.
$table->ipAddress('visitor'); IP address equivalent column.
$table->json('options'); JSON equivalent column.
$table->jsonb('options'); JSONB equivalent column.
$table->lineString('positions'); LINESTRING equivalent column.
$table->longText('description'); LONGTEXT equivalent column.
$table->macAddress('device'); MAC address equivalent column.
$table->mediumIncrements('id'); Auto-incrementing UNSIGNED MEDIUMINT (primary key) equivalent column.
$table->mediumInteger('votes'); MEDIUMINT equivalent column.
$table->mediumText('description'); MEDIUMTEXT equivalent column.
$table->morphs('taggable'); Adds taggable_id UNSIGNED BIGINT and taggable_type VARCHAR equivalent columns.
$table->uuidMorphs('taggable'); Adds taggable_id CHAR(36) and taggable_type VARCHAR(255) UUID equivalent columns.
$table->multiLineString('positions'); MULTILINESTRING equivalent column.
$table->multiPoint('positions'); MULTIPOINT equivalent column.
$table->multiPolygon('positions'); MULTIPOLYGON equivalent column.
$table->nullableMorphs('taggable'); Adds nullable versions of morphs() columns.
$table->nullableUuidMorphs('taggable'); Adds nullable versions of uuidMorphs() columns.
$table->nullableTimestamps(); Alias of timestamps() method.
$table->point('position'); POINT equivalent column.
$table->polygon('positions'); POLYGON equivalent column.
$table->rememberToken(); Adds a nullable remember_token VARCHAR(100) equivalent column.
$table->set('flavors', ['strawberry', 'vanilla']); SET equivalent column.
$table->smallIncrements('id'); Auto-incrementing UNSIGNED SMALLINT (primary key) equivalent column.
$table->smallInteger('votes'); SMALLINT equivalent column.
$table->softDeletes(); Adds a nullable deleted_at TIMESTAMP equivalent column for soft deletes.
$table->softDeletesTz(); Adds a nullable deleted_at TIMESTAMP (with timezone) equivalent column for soft deletes.
$table->string('name', 100); VARCHAR equivalent column with a optional length.
$table->text('description'); TEXT equivalent column.
$table->time('sunrise'); TIME equivalent column.
$table->timeTz('sunrise'); TIME (with timezone) equivalent column.
$table->timestamp('added_on'); TIMESTAMP equivalent column.
$table->timestampTz('added_on'); TIMESTAMP (with timezone) equivalent column.
$table->timestamps(); Adds nullable created_at and updated_at TIMESTAMP equivalent columns.
$table->timestampsTz(); Adds nullable created_at and updated_at TIMESTAMP (with timezone) equivalent columns.
$table->tinyIncrements('id'); Auto-incrementing UNSIGNED TINYINT (primary key) equivalent column.
$table->tinyInteger('votes'); TINYINT equivalent column.
$table->unsignedBigInteger('votes'); UNSIGNED BIGINT equivalent column.
$table->unsignedDecimal('amount', 8, 2); UNSIGNED DECIMAL equivalent column with a precision (total digits) and scale (decimal digits).
$table->unsignedInteger('votes'); UNSIGNED INTEGER equivalent column.
$table->unsignedMediumInteger('votes'); UNSIGNED MEDIUMINT equivalent column.
$table->unsignedSmallInteger('votes'); UNSIGNED SMALLINT equivalent column.
$table->unsignedTinyInteger('votes'); UNSIGNED TINYINT equivalent column.
$table->uuid('id'); UUID equivalent column.
$table->year('birth_year'); YEAR equivalent column.
Column Modifiers

カラム種類に加え、いくつかのカラム修飾がある。

Schema::table('users', function (Blueprint $table) {
    $table->string('email')->nullable();
});

以下が特に。

デフォルトだとnullable(false)相当なので、nullを許容するならnullable()の指定が必要。

Modifying Columns

既存カラムの改名や、データ型変更も行えるが、癖がある。

なお、int(10) のような数値型の幅などは、MySQLの独自拡張で、DB独自拡張はマイグレーションのAPIで未対応。

php - Define property zerofill and size on field schema migration with laravel - Stack Overflow

DB::statementで生SQLで対応するしかない。

Prerequisites

事前に依存関係を追加する。

composer require doctrine/dbal
Updating Column Attributes

既存のカラムを別の型に変更する場合、changeメソッドを使う。

Schema::table('users', function (Blueprint $table) {
    $table->string('name', 50)->change();
});
Schema::table('users', function (Blueprint $table) {
    $table->string('name', 50)->nullable()->change();
});

nullableの付与も可能。

後から変更可能な型は以下。

bigInteger, binary, boolean, date, dateTime, dateTimeTz, decimal, integer, json, longText, mediumText, smallInteger, string, text, time, unsignedBigInteger, unsignedInteger and unsignedSmallInteger

Renaming Columns
Dropping Columns

downで戻す場合に使ったりする。

Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('votes');
    $table->dropColumn(['votes', 'avatar', 'location']);
});

なお、非常に紛らわしいのだが、removeColumnメソッドもある (database - What is the difference between removeColumn and dropColumn methods on Laravel Framework? - Stack Overflow)。こちらはBlueprintから削除するだけで、実テーブルからは削除しない。使うことはほぼない。

Indexes

Creating Indexes

schema builder laravel migrations unique on two columns - Stack Overflow

$table->string('email')->unique();

$table->unique('email');
$table->index(['account_id', 'created_at']);
$table->unique('email', 'unique_email');
$table->unique(['account_id', 'email']);
$table->dropUnique(['account_id', 'email']);

複合UNIQUE制約をつけたい場合、uniqueメソッドの第一引数を配列にする。

Other

Command
  • migrate:rollback: マイグレーションをロールバックする。デフォルトだと最後の1回分。--step=Nで回数を指定できる。
  • migrate:reset: 全部ロールバックする。
  • migrate:refresh: rollback+migrate。
  • migrate:fresh: ロールバックではなく、テーブルを削除してから戻す。
既存DBの途中からの管理

今後の変更分だけ管理するということもできる。

なお、中途半端にLaravelのマイグレーションファイルがある場合、migrationsテーブルにデータを手動で登録しておくと、マイグレーション済みと認識できる。

作ってあげるデータは、id,migration, batchを持つデータで、idは1からインクリメント、migrationはマイグレーションのファイル名(.phpは除く)、batchを1(たぶんこれは何度マイグレーションを実行したかを保っておくもので状態を1つ戻すときに使われるのでは?)に設定すればよい。

id migration batch
1 2017_09_01_134421_create_prefectures_table 1
2 2017_09_01_134422_create_cities_table 1

SQLだと以下相当。

insert into migrations(migration, batch) values('2015_12_08_134409_create_tables_script',1);
insert into migrations(migration, batch) values('2015_12_08_134410_create_foreign',1);
insert into migrations(migration, batch) values('2015_12_08_134411_create_index',1);
SQLでのマイグレーション

database - Laravel migration - is it possible to use SQL instead of schema commands to create tables and fields etc? - Stack Overflow

upメソッドにDB::updateなどで生のSQLをそのまま書けばOK。

class AddColumnsToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('name');
            $table->string('age');
            $table->timestamps();
        });

        DB::update('update users set age = 30 where name = ?', ['John']);
    }
}

downメソッドにはDropIfExistsで削除すればOK。

Cannot declare class TableName, because the name is already in use

マイグレーションファイル作成時に生成されるクラスが、既存のマイグレーションファイルのクラス名と被っているのが原因の一つ。

マイグレーションのファイル名/クラス名は固有にしないといけない。

Laravel 8からは匿名マイグレーションで名前を意識しなくても済むらしい (php - Error migrations: Cannot declare class X, because the name is already in use - Stack Overflow)。

return new class extends Migration {
  //
};
Doctrine\DBAL\Exception  : Unknown column type "mediuminteger" requested.
Doctrine\DBAL\Exception  : Unknown column type "mediuminteger" requested. Any Doctrine type that you use has to be registered with \Doctrine\DBAL\Types\Type::addType(). You can get a list of all the known types with \Doctrine\DBAL\Types\Type::getTypesMap(). If this error occurs during database introspection then you might have forgotten to register all database types for a Doctrine Type. Use AbstractPlatform#registerDoctrineTypeMapping() or have your custom types implement Type#getMappedDatabaseTypes(). If the type name is empty you might have a problem with the cache or forgot some mapping information.

Laravel 5.8のmigrationでmediumintergerが使えない。Doctrineが対応していないから。しかたないからintegerにする。

"Unknown column type "mediumInteger" requested" error after downgrading doctrine/dbal to 2.* · Issue #36509 · laravel/framework · GitHub」によると、Laravel 8.0、Doctrine DBAL 3.0以上で解決しているとのこと。

Seeding

Database: Seeding - Laravel 5.8 - The PHP Framework For Web Artisans

マイグレーションで用意したDBのデフォルトデータの用意の話。

デフォルトデータをシードと呼んでおり、シードを用意する機能をシーディングと呼んでいる。

シードを作成するためのスクリプト (シーダー) ファイルを生成して、記述して実行する流れ。

Writing Seeders

以下のコマンドでシーダーを作成する。

php artisan make:seeder UsersTableSeeder

database/seedsにファイルが作成される。

中身はrunメソッドがあるだけ。run内でinsertするだけ。

Calling Additional Seeders

追加したシーダーはそのままでは認識されない。

DatabaseSeederに登録する。DatabaseSeeder.phpのrunのcallメソッド内に追加する。

public function run()
{
    $this->call([
        UsersTableSeeder::class,
        PostsTableSeeder::class,
        CommentsTableSeeder::class,
    ]);
}

Running Seeders

追加したシーダーを認識させるために以下のコマンドでautoloadを更新する。

composer dump-autoload

シーダーを用意したらコマンドを実行して作成する。

php artisan db:seed

--classでシーダー名を指定もできる。あとからシーダーを追加する場合は--classで指定するイメージ。

Other

Error handling

DB関係の処理で、失敗した場合の扱いが、公式文書に記載がなくて混乱する。

基本はtry-catchでやればいい。

use Exception;

public function result(Request $request)
{
    try {
        $user = User::findOrFail($request->user_id);

        return view('result', compact('user'));
    } catch(Exception $exception) {
        $message = $exception->getMessage();

        if($exception instanceof ModelNotFoundException) {
            $message = 'User with ID: '.$request->user_id.' not found!';
        }

        return back()->withError($message)->withInput();
    }
}

Using Docker I get the error: "SQLSTATE[HY000] [2002] No such file or directory"

php - Using Docker I get the error: "SQLSTATE[HY000 [2002] No such file or directory" - Stack Overflow]

PHPのPDOをDockerコンテナ内で使おうとしたところ、"No such file or directory" エラーが発生した話 #docker-compose - Qiita

dockerで.envのDB_HOSTの指定間違い。dockerのservice名をホスト名に指定する必要がある。

local.ERROR: could not find driver

[2024-07-02 15:35:42] local.ERROR: could not find driver (SQL: select * from `sessions` where `id` = 0cDH7URcrsFzC7hPYlbAQcezNVjdDM16OJh1aCSZ limit 1) {"exception":"[object] (Illuminate\\Database\\QueryException(code: 0): could not find driver (SQL: select * from `sessions` where `id` = 0cDH7URcrsFzC7hPYlbAQcezNVjdDM16OJh1aCSZ limit 1) at /var/www/vendor/laravel/framework/src/Illuminate/Database/Connection.php:664, Doctrine\\DBAL\\Driver\\PDO\\Exception(code: 0): could not find driver at /var/www/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/Exception.php:18, PDOException(code: 0): could not find driver at /var/www/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:40)

このエラーはphpのモジュール不足。LaravelはPDOを使う。mysqliではなく。

RUN docker-php-ext-install pdo_mysql

これが必要。

Log

Laravel SQLの実行クエリログを出力する #PHP - Qiita

方法が2種類ある。

  1. toSqlメソッド。getBindingsでプレースホルダーの値。シンプルな場合に有効。
  2. DB::enableQueryLog/DB::getQueryLog()。複雑な場合と実行時間も。

Eloquent

Getting Started

Eloquent: Getting Started - Laravel 5.8 - The PHP Framework For Web Artisans

Eloquent ORM。

Defining Models

make:modelコマンドでモデルインスタンスを作成できる。

php artisan make:model Flight

-mで新規作成のマイグレーションファイルも一緒に作れる。

Eloquent Model Conventions
Table Names

Laravelで「Base table or view not found: 1146 Table」エラーが出るときの対処法 #PHP - Qiita

Eloquentのモデルでは、デフォルトでクラス名を複数形のスネークケースにしたものをテーブル名とみなす。

SQLSTATE[42S02]: Base table or view not found: 1146 Table 'your_db.your_models' doesn't exist (SQL: select * from `your_models`)

不在の場合、上記のエラーが出る。

デフォルト以外のテーブル名を使いたければ、$tableにテーブル名を指定する。

<?php
 
namespace App;

use Illuminate\Database\Eloquent\Model;
 
class Flight extends Model
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'my_flights';
}

プロジェクトでテーブル名に日本語を使う場合など、クラスメイとテーブル名が同一なら、以下のような親クラスを使って、これを継承すると同じ処理を記載しなくていい。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class BaseModel extends Model
{
    function __construct()
    {
        // デフォルトだと複数形のスネークケースのテーブルを探すため、クラス名=テーブル名で上書き。
        $this->table = (new \ReflectionClass($this))->getShortName();
    }
}
Timestamps

LaravelのEloquentモデルでupdated_atがないテーブルを使う方法 #PHP - Qiita

php - Laravel Unknown Column 'updated_at' - Stack Overflow

デフォルトで、Eloquentはcreated_atとupdated_atカラムを期待する。使っていないならば、以下のエラーが出る。

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'updated_at' in 'field list' at /var/www/vendor/laravel/framework/src/Illuminate/Database/Connection.php:664, Doctrine\\DBAL\\Driver\\PDO\\Exception(code: 42S22): SQLSTATE[42S22]: Column not found: 1054 Unknown column 'updated_at' in 'field list' at /var/www/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/Exception.php:18, PDOException(code: 42S22): SQLSTATE[42S22]: Column not found: 1054 Unknown column 'updated_at' in 'field list' at /var/www/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:82)
[stacktrace]

$timestamps=falseにしておく。

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
 
class Flight extends Model
{
    /**
     * Indicates if the model should be timestamped.
     *
     * @var bool
     */
    public $timestamps = false;
}

Retrieving Models

Eloquentのモデルは、強力なクエリビルダーと考えてもいい。Eqloquentのallメソッドはモデルテーブルの全結果を返す。これはクエリービルダーとして、各モデルで提供されているので、条件を追加するなら、getメソッドを使う。

つまり、whereなどを使うなら、最後の取得はgetメソッドを使う必要がある。

Eloquentのモデルはクエリービルダーなので、クエリービルダーの全メソッドが使用可能。

モデル名::メソッドの他、select/where/findなども同じものを返すので、メソッドチェーンで呼び出せる。

Retrieving Single Models / Aggregates

モデルからデータを取得するいくつか主要なメソッドがある。

  • find: プライマリーキーから値を探す。
  • all: 全項目を取得する。
  • where:
  • findOrFail
  • findMany

メソッドチェーン

  • first: 先頭項目を取得する。
  • firstOrFail
  • count
  • max

【laravel】データベースの操作:Eloquent ORM編 #Laravel6 - Qiita

Eloquentのfindは扱いが特殊。引数が配列なら配列で返してくれる。

配列で結果が欲しければ、findManyを使う。findManyで常に配列で返すようにしたほうが、型が揃って扱いやすいかもしれない。

Inserting & Updating Models
Insert

モデルインスタンスを作成し、属性をセットし、最後にsaveを呼ぶ。

<?php
 
namespace App\Http\Controllers;
 
use App\Flight;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
 
class FlightController extends Controller
{
    /**
     * Create a new flight instance.
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request)
    {
        // Validate the request...
 
        $flight = new Flight;
 
        $flight->name = $request->name;
 
        $flight->save();
    }
}
Updates

Eloquent: Getting Started - Laravel 5.8 - The PHP Framework For Web Artisans

$flight = App\Flight::find(1);
 
$flight->name = 'New Flight Name';
 
$flight->save();

saveメソッドはinsertだけでなく、updateにも使える。updated_atも自動で更新される。

クエリーのupdateで配列で複数レコードも一括更新できる。

注意点として、updateの一括更新時は、eloquentで本来発動するsaving/saved/updating/updatedのイベントは発動しない。

Mass Assignment

Add [ to fillable property to allow mass assignment onの解決方法 #初心者 - Qiita]

Add [_token] to fillable property to allow mass assignment on [App\]. 

こんなエラーが出る。

プロパティーの代入で1個ずつ設定するほかに、まとめて1行での保存もできる。ただし、一括割り当ては危険なので、保護されており、fillable属性かguarded属性の指定が必要。

fillメソッドは連想配列でデータをモデルにセットする。

$form = $request->all();
unset($form['_token']);
$flight->fill(['name' => 'Flight 22']);

フォームの項目と対応させておけばそのままセットできる。

fillable

まず、最初に一括割り当て可能な属性を定義する。

fillableプロパティーで、一括割り当て可能なカラムを定義できる。

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
 
class Flight extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];
}
Guarding Attributes

$fillableはホワイトリスト方式。$guardedも使用できる。一括割り当て可能にしたくない属性を指定する。ブラックリスト。 $fillableと$guardedのどちらかが最低限必要。

$fillableは何か増えるたびに修正必要なので手間。$guardedを基本にするとよいと思う。もっというなら、$guardedでpkを指定するといい。pkはAUTO_INCREMENTで自動増分にすればいいから。

Other Creation Methods

insertもupdateもやることはほぼ同じなので、コードの冒頭で以下のような式でモデル生成部分だけ変更したらよいだろう。

        $model = empty($request['オーナーコード'])
            ? new 楽楽販売_オーナーマスタ : 楽楽販売_オーナーマスタ::find($request['オーナーコード']);

と思っていたが、もっといいのがあった。

firstOrCreate/firstOrNew

firstOrNewは自分でsaveできるように、モデルインスタンスを返してくれる (new モデル相当)。

// Retrieve flight by name, or create it if it doesn't exist...
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
 
// Retrieve flight by name, or create it with the name, delayed, and arrival_time attributes...
$flight = App\Flight::firstOrCreate(
    ['name' => 'Flight 10'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);
 
// Retrieve by name, or instantiate...
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
 
// Retrieve by name, or instantiate with the name, delayed, and arrival_time attributes...
$flight = App\Flight::firstOrNew(
    ['name' => 'Flight 10'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

1番目の配列の情報でモデルを検索して、不在ならば、2番目の配列の情報で作成する。

updateOrCreate

// If there's a flight from Oakland to San Diego, set the price to $99.
// If no matching model exists, create one.
$flight = App\Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99, 'discounted' => 1]
);

1番目の配列の情報で検索して、2番目の配列で更新する。1番目の情報で不在なら、全部の情報で更新する。

Mass assignmentの対象。基本はAIのpkをguardedにしておけば問題ないと思う。

ただし、これらのメソッド類は、一度に1レコードずつしか更新できない。大量に行う場合、何回も発動するので遅いかもしれない。そこだけ注意が必要。あまり遅いなら、DBクエリーでやったほうがいいかもしれない。

Laravel で updateOrCreate を使う際に Unknown column 'id' in 'where clause' でハマった #PHP - Qiita

なお、updateOrCreateを使う際は、使用するモデルの主キーの設定をきちんとしておくこと。

Upserts

Laravel 8からupsertメソッドが登場して、一括更新が可能になった。以前から、updateOrCreateがあった。

Relationships

Eloquent: Relationships - Laravel 5.8 - The PHP Framework For Web Artisans

Defining Relationships
One To One
<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
    /**
     * Get the phone record associated with the user.
     */
    public function phone()
    {
        return $this->hasOne('App\Phone');
    }
}

主テーブルに外部キーが埋め込まれていて、シンプルな場合、上記のように外部テーブルをメソッド名にしてhasOneを実行すると全部返せる。非常にシンプル。

プロパティーとしてアクセス可能になる。1対1だから、以下のようにプライマリーキーなどで参照できる。

$phone = User::find(1)->phone;

デフォルトだと、外部キーは、 [モデル名_id] であることを想定している。これじゃないなら、以下のように引数で指定しておく。

return $this->hasOne('App\Phone', 'foreign_key');

そして、外部キーは、元のモデルのidか$primaryKeyと一致する想定になっている。これ以外のキーを使いたければ、hasOneの第3引数で、ローカルキーを指定する。

return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
One To Many

複数の他のモデルを持つ場合。例えば、ブログ投稿は、無限のコメントをもつ。こういう場合のコメント。がOne To Many。これはhasManyで関連付ける。

    public function comments()
    {
        return $this->hasMany('App\Comment');
    }
Many To Many

多対多の関係は、belongsToManyで定義される。

userとroleテーブルがあったとして、テーブル名はアルファベット順でrole_userのようにテーブル名を_で結合する。

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
    /**
     * The roles that belong to the user.
     */
    public function roles()
    {
        return $this->belongsToMany('App\Role');
    }
}

関係定義でroles動的プロパティーを使用してアクセスできる。

$user = App\User::find(1);
 
foreach ($user->roles as $role) {
    //
}

命名規則を定義時にオーバーライドもできる。第2引数でテーブル名を指定する。

return $this->belongsToMany('App\Role', 'role_user');

またキーの列名もカスタマイズ可能。

return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');
Retrieving Intermediate Table Columns

このままで通常は問題ない。リレーションで対応する外部テーブルのモデル・レコードを取得できている。

万が一、直接中間テーブルの参照が必要な場合、pivot属性を間に挟んでアクセスする。

$user = App\User::find(1);
 
foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

デフォルトではモデルキーのみが存在する。

Querying Relations
Querying Relationship Existence

リレーション先のテーブルで絞り込みたい場合、has/wherehasを使う。

ただ、このwherehasはコールバックなどを使わないといけなくて、少々面倒くさい。

joinしてフラットにして、カラム名にプレフィクスをつけたほうがわかりやすい。フラットにしなかったら、入れ子になって扱いが面倒になる。が、たいしてプレフィクスをつけるのと手間は変わらないから。

Eager Loading

Eloquentのリレーションにプロパティーでのアクセス時、リレーションデータは "lazy loaded" になる。これはプロパティーへのアクセスまで、実際には読み込まないことを意味する。つまり、アクセスのたびに読み込みが発生する。Eager Loadが可能になる。

Eagerは、熱心な、せっかちなという形容詞。

例えば、N件のデータを全部表示させたい場合、以下のようなコードを記述する。

<?php
 
namespace App;
 
use Illuminate\Database\Eloquent\Model;
 
class Book extends Model
{
    /**
     * Get the author that wrote the book.
     */
    public function author()
    {
        return $this->belongsTo('App\Author');
    }
}
$books = App\Book::all();
 
foreach ($books as $book) {
    echo $book->author->name;
}

最初に全取得+1、N件のデータを取得+Nで、合計1+N回のクエリーが発行される。

Eager loadによりこれを2回のクエリー発行で収めることができる。withメソッドを使う。

$books = App\Book::with('author')->get();
 
foreach ($books as $book) {
    echo $book->author->name;
}

これは具体的には以下のSQLの実行になる。

select * from books
 
select * from authors where id in (1, 2, 3, 4, 5, ...)

ただ、Eager loadすると、クエリーの数は減ってもモデルのアクセスが増えて、メモリーの使用量と実行速度が遅くなって、逆に悪化することがある (Laravel - Eager loading can be bad! : r/laravelLaravel - Eager loading can be bad! | Personal BlogHow the new 'One Of Many' Laravel relationship made my project 600 times faster - DEV Community)。

特に、hasManyの関係で、1対1じゃなくて、複数のモデルが入っている場合。

Laravel 8.42からLatestOfManyなどのメソッドが追加されており、これを使えば回避できる。これを使わない場合、JOINとMAXなどを駆使する必要があった。

ひとまず、hasManyとか、性能を見て、問題あったらeager loadingしたり工夫することにする。

複数のテーブルと結合したい場合、withに複数指定する。必要な列が決まっているなら、withのテーブル名の後に:で列名指定でOK。

Eager Loading By Default

交差テーブルなど、正規化の都合でテーブルを分離しているだけで、使うときは基本的に常に結合を想定するテーブルもある。 そういうときに毎回withでテーブルを指定するのは面倒だし、テーブルアクセスごとにどのテーブルと連結しているかを指定したり考慮必要なのは面倒。これの回避策として、$withプロパティーがある。ここで関連テーブルを指定すると、常にwithで結合したものを返す。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class Book extends Model
{
    /**
     * The relationships that should always be loaded.
     *
     * @var array
     */
    protected $with = ['author'];
 
    /**
     * Get the author that wrote the book.
     */
    public function author()
    {
        return $this->belongsTo(Author::class);
    }
 
    /**
     * Get the genre of the book.
     */
    public function genre()
    {
        return $this->belongsTo(Genre::class);
    }
}

$withプロパティーがある状態で、結合せずに特定テーブルとの連結が欲しければ、withoutかwithOnlyを指定する。 withoutは$withプロパティーで指定したテーブル名を指定して除外する。withOnlyは$withを上書き。

Constraining Eager Loads
Inserting & Updating Related Models
The Save Method

多対多の関係の保存用に、いくつかのメソッドがある。

$comment = new App\Comment(['message' => 'A new comment.']);
 
$post = App\Post::find(1);
 
$post->comments()->save($comment);

関係のsaveメソッドから関係先のモデルに挿入できる。

関係に属する

associateメソッドで関係を新しく設定することもできる。

$account = App\Account::find(10);
 
$user->account()->associate($account);
 
$user->save();

削除はdissociate()

$user->account()->dissociate();
 
$user->save();
多対多の関係

取付/取外

多対多の場合は扱いが異なる。

attachメソッドで中間テーブルを更新する。

$user = App\User::find(1);
 
$user->roles()->attach($roleId);

中間テーブルに挿入する追加データも設定できる。

$user->roles()->attach($roleId, ['expires' => $expires]);

中間テーブルのレコード削除はdetach。

// Detach a single role from the user...
$user->roles()->detach($roleId);
 
// Detach all roles from the user...
$user->roles()->detach();

配列で指定もできる。

$user = App\User::find(1);
 
$user->roles()->detach([1, 2, 3]);
 
$user->roles()->attach([
    1 => ['expires' => $expires],
    2 => ['expires' => $expires]
]);

同期

syncメソッドで、指定した配列で上書き・同期することもできる。

$user->roles()->sync([1, 2, 3]);

idの他の列も更新も可能。

$user->roles()->sync([1 => ['expires' => true], 2, 3]);
Other
リレーションの列名エイリアス

Eloquentでリレーションで結合時、カラム名が同じだと困る。

別名をつける方法がいくつかある。

  • .get/.select: ->get(['tags.name AS tag_name', 'products.*'])/ ->select('tags.name AS tag_name', 'products.*')
  • アクセサー

get/selectでやるのがよさそう。

Other

列の存在確認

いくつか方法がある。

  • use Illuminate\Support\Facades\Schema; Schema::hasColumn('テーブル名', '列名'): これが一番シンプル。
  • $model['列名']: インスタンス生成後ならこれ。
Eloquent vs. Query Builder

ORMとクエリービルダー。どこでどう使い分けるか?最初の2個の記事が参考になる。

  • ユーザーフォームのような基本的で簡単なCRUD処理はEloquent
  • 結合を含んだり、大量データの処理が必要な複雑な場合にクエリービルダー。

こういう方針で使い分けるといい。結合とかになると、モデルの範囲外になる。

レコードセットなのか、モデルなのかを意識する。

列名の取得
use Illuminate\Support\Facades\Schema;
$columns = Schema::getColumnListing('users'); // users table
dd($columns); // dump the result and die
        $headings = DB::connection()
            ->getSchemaBuilder()
            ->getColumnListing('colaborators');
::query

EloquentのORMのモデルを使用する際は、<Model>::findなどのメソッドを使う。

が、インスタンスを生成してから、条件に応じて処理を実行したい場合など、都合が悪いことがある。

インスタンス生成用に<Model>::query()がある。<Model>::query()->findなどのように、メソッドの前に挟める。

これを使う形で書いておくと、コードの書き方がきれいになったり、コード補完が効きやすかったりする。

SQL関数

how to Passing MySQL functions to Eloquent ORM query? - deBUG.to

Eloquentのwhere関数内でSQL関数は使えない。工夫が必要。

Database: Query Builder - Laravel 5.8 - The PHP Framework For Web Artisans

DB::rawで書く。

あるいは、selectRaw/whereRawなどを使う。

結果のオブジェクト・配列変換

EloquentのDB取得の結果は、配列になっている。が、DBクエリーの場合、オブジェクトになっている。

型が違っていろいろ困る。DBクエリーのオブジェクトを配列にしたい。

Eloquentを使わずに、配列にしたい場合、mapやtransformで変換必要。

$response['data'] = DB::table('customers')  
    // query conditions, etc
    ->get()
    ->map(function ($item, $key) {
        return (array) $item;
    })
    ->all();

最後のallも必要。これでEloquentと共通化できる。

一括更新

How to set every row to the same value with Laravel's Eloquent/Fluent? - Stack Overflow

マイグレーションなどで、列を切り詰めたり一括更新したいことがある。

Model::query()->update(['confirmed' => 1]);

これがシンプル。

【Laravel】バルクアップデートを行う方法 #MySQL - Qiita」に記載のall()->update()はallが配列を返すのでダメ。

主キーの取得

ChatGPT

モデルの定義時に、protected $primaryKeyで主キーを設定している。インスタンスでこの情報を保有しておりアクセス可能。

  • 主キーのカラム名: getKeyName() または $primaryKey プロパティで取得。
  • 特定レコードの主キーの値: getKey() で取得。

$primaryKeyはprotectedだからアクセスしにくい。(new Model)->getKeyName()などでアクセスするのがよい。

https://chatgpt.com/c/67354314-fbdc-800b-a4e4-65f96d1a8fb2

リレーション先の主キーはgetRelated()でインスタンスを取得すれば同じ方法でアクセスできる。

// Postモデル内
public function user()
{
    return $this->belongsTo(User::class);
}

// 主キー名の取得
$post = Post::find(1);
$primaryKeyName = $post->user()->getRelated()->getKeyName();

echo $primaryKeyName;

Testing

Getting Started

Testing: Getting Started - Laravel 5.8 - The PHP Framework For Web Artisans

Introduction

Laravelはテストを念頭に置いており、デフォルトでPHPHUnitに対応していて、phpunit.xmlもある。

デフォルトで、testsディレクトリーは、Feature Unitの2ディレクトリーを持つ。Unitは関数単位の単体テスト。FeatureはHTTPリクエストとJSONエンドポイントなど、複数のオブジェクトの対話を含む大き目のテスト。

両方のディレクトリーに、ExampleTest.phpがある。

Environment

pupunitのテスト時、phpunit.xmlの<server name="APP_ENV" value="testing"/>の設定により、Laravelは設定環境をtestingにする。また、自動的にセッションとキャッシュをarrayドライバーに自動的に構成する。つまり、テスト中はセッションやキャッシュを保持しない。

テスト実行前に、config:clearで設定キャッシュを削除しておく。

また、.env.testingファイルをプロジェクトルートに作成できる。このファイルは、PUPUnitテスト実行中か、artisanコマンドで--env-testingを指定時に、.envを上書きする。

Creating & Running Tests

テストケースの新規作成には、make:test Artisanコマンドを使う。

// Create a test in the Feature directory...
php artisan make:test UserTest
 
// Create a test in the Unit directory...
php artisan make:test UserTest --unit

--unitの有無で、UnitディレクトリーかFeatureディレクトリーにテストファイルのテンプレートが作られる。

テストが作成されたら、通常のPHPUnit同様に、テストメソッドを定義していく。phppunitコマンドでtestを実行する。

<?php
 
namespace Tests\Unit;
 
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
 
class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $this->assertTrue(true);
    }
}

テストクラス内で、setUp/tearDownメソッドを定義する場合、parent::setUp/parent::tearCownを忘れずに呼ぶこと。

Console Tests

ユーザーの入力を要求するようなコンソールコマンドのテストもできる。

  • expectsQuestionメソッド:、ユーザー入力のモックを簡単に許可する。
  • assertExitCode/expectsOutputメソッドで、終了コードと出力テキストも指定できる。

例えば、以下のコンソールコマンドがあったとする。

Artisan::command('question', function () {
    $name = $this->ask('What is your name?');
 
    $language = $this->choice('Which language do you program in?', [
        'PHP',
        'Ruby',
        'Python',
    ]);
 
    $this->line('Your name is '.$name.' and you program in '.$language.'.');
});

expectsQuestion/assertExitCode/expectsOutputメソッドで、以下のようにテストできる。

/**
 * Test a console command.
 *
 * @return void
 */
public function test_console_command()
{
    $this->artisan('question')
         ->expectsQuestion('What is your name?', 'Taylor Otwell')
         ->expectsQuestion('Which language do you program in?', 'PHP')
         ->expectsOutput('Your name is Taylor Otwell and you program in PHP.')
         ->assertExitCode(0);
}

Database Testing

シーダーでデータの自動登録方法を整理した。ただ、たくさんデータを登録してチェックする場合、ダミーデータを自動生成したい。Factoryというのでダミーデータを作成できる。

Generating Factories
php artisan make:factory PostFactory

database/factoriesに生成される。

--modelオプションで使用するモデル (テーブル名) を指定しておくと、テンプレートも作成してくれる。

Other

Facade

【Laravel】ファサードとは?何が便利か?どういう仕組みか? #初心者 - Qiita

インスタンスメソッドをstaticメソッドのように使うための仕組み。ややこしいだけなのでいらないかなと思う。

Controllerの共通処理

例えば、/city/nycで都市都市の一覧表示。/city/nyc/streetで該当都市の通りの一覧表示。こういうタイプの処理を都市ごとに実装するというようなことがよくある。

ただし、表示処理は共通処理があったりする。

この共通処理をどうまとめて実装するか?いくつか方法がある。

  • 親Controller
  • ファサード/トレイト (関数クラス)/サービスクラス
  • middleware
  • モデルに共通処理を定義して呼び出し。
  • validation/FormRequest

Controllerの前後に挟み込む感じならmiddleware、Controllerの処理の中でやりたいなら親Controllerか、ファサード/トレイト/サービスクラス、DB周りならModel?

基本はmiddlewareの模様。ビジネスロジックになるなら、サービスクラスを使うのがいい。トレイトでやる場合、app/traitsに格納する感じ。

やっぱりサービスクラスに出すのいい。

Coding Style Guide

PSR-2/4に準拠。

ローカル変数名やプロパティーの記法の指定がない。

Laravel本体のソースコードを見るとcamelCaseになっているのでこれに合わせる。

Bladeファイル名 [Laravel naming convention for blade files - Stack Overflow]

チェインケースかcamelCaseを推奨。特に決まってはいない。

framework/src/Illuminate/Foundation/resources/exceptions/renderer/components/theme-switcher.blade.php at 1a1a61068bc3c5594376a559e424ae09ec3fe64a · laravel/framework」にあるように、Laravel本体はチェインケースなので、チェインケースがよいだろう。

component類のslot名はcamelCase [framework/tests/View/Blade/BladeComponentTagCompilerTest.php at 7d26b7ee454a0ccc339db92a641487f668b44331 · laravel/framework]。

コマンド

framework/src/Illuminate/Foundation/Console/StorageLinkCommand.php at b9cf7d3217732e9a0fa4f00e996b3f9cc5bf7abd · laravel/framework

  • 名詞:動詞
  • 名詞動詞Command.php

ViewCacheCommand/ViewClearCommandなど1コマンド1ファイル。

ただし、make:command/CommandMakeCommandなど一部命名規則に外れるものがある。