Open6

Laracon US 2024 まとめ

MaruMaru

2024/08/27 - 2024/08/28

Nuno Maduro - Introducing Pest 3.0

https://www.youtube.com/watch?v=BNhbgcNJyAk

PEST V3

2021 v1
2023 v2

ここまで1億8000万のダウンロードされた。

3つの新機能

1. タスクマネジメントメント

todo(assignee: 'nunomaduro', issue: 11)
でGitHubに連携できる!!
えぐい!

pest --asinnee=taylor
でアサイニーで絞ってtodoを確認可能

2. アーキテクチャテスト

いろんなチームで使ってもらいたいからデフォルトルールを用意した。

PHPプリセット

arch()->preset()->php()

die()は使えないようにする

arch()->preset()->security()

md5(), eval()が使われているのを検知する

laravel用プリセット

arch()->preset()->laravel();

  • リソースコントローラーでindex, showなどのリソース操作メソッド以外が使われていないかチェック
arch()->preset()->strict()
arch()->preset()->relaxed()

好みに合わせて異なるタイプのプリセットがある。finalをつけることを強制するかどうかなど。

3. ミューテーションテスト

pest --mutateでテストしていないコードを検知できる(単純にカバレッジを測るのとは異なる。)

例えば、、ソースコードについて各行を削除してテストが失敗しないかを計測する。
テストがちゃんと書かれていれば失敗するはず。

PEST3は来週リリース!

Pinkaryは今日オープンソース化

MaruMaru

Daniel Coulbourne "Verbs for Laravel"

https://www.youtube.com/watch?v=tX6nBpSvh4s

https://verbs.thunk.dev/

Event Sourcingとは?

Event Sourcingは、アプリケーション内のすべての状態を「イベント」として記録し、その履歴を基にアプリケーションの現在の状態を再構築する手法。
このアプローチでは、データベースに「現在の状態」だけを保存するのではなく、状態がどのように変化したか(イベント履歴)を記録するため、変更の追跡や過去の状態の復元が可能になる。

Verbsパッケージについて

  • 開発者: Chris Morrellと複数の開発者(Daniel Colbornなど)が手掛けている。
  • 目的: Event Sourcingの技術的な複雑さを軽減し、Laravelアプリケーションで簡単に利用できるようにするパッケージ。
  • 特徴: Event Sourcingの「難しい部分」を隠蔽して、開発者がスムーズに使えるように設計されている。

発表の内容

講演では、Event Sourcingの威力を示すためのデモが行われた。

  1. ゲームの構築:
  • 発表者は観客が参加できるゲームを作成
  • ゲーム内に意図的にバグがあり、観客の一部がそのバグを利用して不正をした。
  1. Event Sourcingの力をデモ:
  • 通常のLaravelアプリケーションであれば、不正があってもそれを検出するのは難しい(データが単に「今の状態」を反映しているだけだから)。
  • しかし、Event Sourcingを利用している場合、アプリケーションの「すべての履歴(イベント)」を追跡できる。これにより、不正がどのように行われたのかを特定し、その結果を取り消すことが可能。
  1. 具体的な効果:
  • 不正を行ったユーザーを特定し、勝者から除外することができた。
  • まるでその不正なイベントが「存在しなかった」かのように状態を再構築可能。

ポイント

  1. Event Sourcingは、アプリケーションの現在の状態を単に保存するのではなく、その状態がどうやって構築されたかを追跡できる。
  2. これにより、不正やエラーを容易に特定し、状態を安全に再構築できる。
  3. Verbsは、この強力な機能をLaravelアプリケーションで簡単に使えるようにするためのツール。

結論

  • Verbsは、Event Sourcingの難しい部分を解消し、Laravel開発者が簡単に使えるようにしたパッケージ。
  • Event Sourcingは特に履歴の追跡や不正検出、エラー回復が必要なシステムに非常に有用。
MaruMaru

Taylor Otwell - Laravel Keynote

https://www.youtube.com/watch?v=AwWepVU5uWM

First Party VS Code 拡張

VSCode用の公式拡張を今秋にリリース予定

  • config()やenv()のジャンプ
  • route=>Controllerアクションへのジャンプ
  • trans()の翻訳文字列へのジャンプ
  • bladeのシンタックスは依頼と
  • Inertiaサポート
  • Eloquent
    • リレーションの補完
    • フィールド名の補完
  • Test
    • Playボタンの追加

バックエンドのアップデート

ベータは来週使える!

local temporaryUrl

Storage::temporaryUrl()
localドライバーで動くように!!

Container Attribute

DIで解決されるstringなどのプリミティブな値を渡す際、Attributeでデフォルト値が設定が可能に!!

Relation charperone

N+1の解決

deferで非同期処理

defer()
Queue workerを使わずに非同期処理を実装できる。

PHP fpmなどのアプリケーションサーバーの機能を使っている

cacheの改良

Cache::flexible('metrics', [5, 10], finction () {
時間のかかる処理
})

deferを使ってバックグラウンドでキャッシュ値を再計算するため、ユーザーが待つことがなくなる!

並列処理

Concurrencyファサードで簡単に並列処理を実行できるように

$values = Concurrency::run([
 fn () => Metrics::get(),
 fn () => Metrics::get(),
 fn () => Metrics::get(),
 fn () => Metrics::get(),
 fn () => Metrics::get(),
]);

Concurrencyファサードでdeferも使える

$values = Concurrency::defer([
 fn () => Metrics::get(),
 fn () => Metrics::get(),
 fn () => Metrics::get(),
 fn () => Metrics::get(),
 fn () => Metrics::get(),
]);

Inertia 2.0

6つの新機能
10月にリリース予定

1. Async Requests

全てのリロードリクエストがデフォルトで非同期に

2. Polling

usePollでポーリングを簡単に実装可能に

3. WhenVisible

Inertia::optional()
<WhenVislble>

でページに表示された時に処理を開始する。

4. Infinite Scrolling

Inertia::merge()で無限スクロールを簡単に実装可能に

5. Prefetching

<Link href="/" prefetch>で
データの事前取得ができるように
例: ボタンホバー時にデータ取得を始める

6 Deferred Props

<Deffered data="user_count">でコンポーネントごとにデータの遅延表示が可能に

Laravel Cloud

ワンクリックでLaravelアプリケーションを公開できる!
LarvelがServeするのでAWSなどの連携も不要。Laravel版Vercel

MaruMaru

Luke Downing - Lessons From the Framework

https://www.youtube.com/watch?v=Njkxzc-aqts&t=1413s

Laravelは私達にいろいろなことを教えてくれる。

3行コメント

laravelソースコードのコメントは3行でかつ3文字下げになるようになっている。

config/app.php
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Application Name
    |--------------------------------------------------------------------------
    |
    | This value is the name of your application, which will be used when the
    | framework needs to place the application's name in a notification or
    | other UI elements where an application name needs to be displayed.
    |
    */

    'name' => env('APP_NAME', 'Laravel'),

コメントの形式がバラバラでも動作にはもちろん影響しないが、美しいのだ。
このこだわりは変数名やメソッド名など可読性を高くすることにこだわることに通じる。
プログラミングが単なるコーディングではないことを教えてくれる。

ルーティング

Laravel開発者が以下のルーティングファイルを見たとき、アプリケーションについて色々なことを読み取れる。

  • auth・verifiedがすでに組み込まれている
  • parrotについてCRUD処理が実装されている
  • parrotモデル, parrotsテーブルがある
  • userは複数のparrotsに紐づいている
  • Laravel breezeを使っている
    など
routes/web.php
<?php

use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('dashboard', function () {
    return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

Route::middleware('auth')->group(function () {
    Route::get('profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

Route::resource('parrots', ParrotController::class)->except('index', 'show');

Route::get('/users/{user}/parrots', [ParrotController::class, 'index'])->name('users.parrots.index');
Route::get('parrots/{parrot}', [ParrotController::class, 'show'])->name('parrots.show');

こういったことが実現可能なのはLaravelが一般的(ordinary)で明確(obvious)であり続けているから。
Laravel3のルーティングファイルを見ても大きく構造が変わっていないことがわかる。

これはアプリケーションを開発する上で重要な考え。
賢く(clever)あることよりも一般的(ordinary)で明確(obvious)であり続けることが大事。

例:
ControllerにAttributesを設定することでルーティング定義できるライブラリがあるが、それを使っているプロジェクトに新しくシニアLaravelエンジニアが参画したらどうだろう?
本来不必要な説明時間が発生するはず。。

Facades

LaravelではCache, Http, Response, Configなど各種機能にアクセスできるFacadeという仕組みがある。
もしあなたが第三次世界大戦を始める方法を探しているとしたら、最も手っ取り早い方法はTwitterやRedditで世界に向けて「Facadesを使うのが好きだ」と投稿すること。そうすれば、今まで受けたことのない最悪な誹謗中傷を体験できるはず(笑)

それはFacadesがいくつかのルールを破るから。
依存性逆転(IoC)、関心の分離(Separation of Concerns)、単一責任原則(Single Responsibility Principle)など

では、なぜLaravelチームはFacadesを作り、使い、推奨し続けているのか?
=>
『開発者体験(Developer Experience)と生産性(Productivity)の向上』というメリットが、
「いくつかのルールを破ること」によるデメリットを上回ると本気で信じているから。

以下のようなファサードを使ってHttpリクエストをキャッシュする実装をした時

MapImageConrtoller.php
class MapImageController extends Controller
{
    public function __invoke(Request $request, string $lat, string $lng)
    {
        $rawData = Cache::remember(
            "map-images.{$lat}, {$lng}",
            now()->addMonths(),
            fn() => Http::baseUrl("https://api.mapbox.com/styles/v1/mapbox/satellite-v9/static...")
                ->withQueryParameters(['access_token' => Config::get('services.mapbox.access_token')])
                ->get("/{$lng},{$lat},12,0,30/300x300")
                ->body()
        );

        return Response::make($rawData, headers: ['Content-Type' => 'image/png']);
    }

以下のように簡単にテストを書ける

MapImageTest.php
it('will fetch from cache instead of requesting from mapbox when possible', function () {
    get(route('map-images', ['lat' => 0, 'lng' => 0]));
    get(route('map-images', ['lat' => 0, 'lng' => 0]));

    Http::assertSentCount(1);
});

ここから得られる教訓はルールを破ることによって『開発者体験(Developer Experience)と生産性(Productivity)を得られることがあるということ。
そしてルールを破るためにはルールを十分に知っておく必要があるということ。

そもそもLaravelを使っている時点で原則を破っている。

  • SOLID原則に従えば、アプリケーションロジックとフレームワークの機能の間には「境界層(Boundary Layer)」を設けるべき。
    • つまり、コントローラーにロジックを書いてはいけない。
    • Collectionを直接使ってはいけない。
    • Stringableを直接使ってはいけない。
    • インターフェースで抽象化すべき。

全てを守っているプロジェクトは少ないはず。
原則を破ることで、余計な抽象化をせずに開発スピードが上がり、開発者体験も良くなるというメリットがある。
一方Laravelにロックインされるという問題があるが、LaravelからPHPの他のフレームワークに移行する事例は極めて少ない。言語が変わるのであれば全て書き換えが必要なのだからLaravelに依存した方がいい。

ルールを知ることは大切だが、『インターネットの誰かが重要だと言ってたから』という理由だけで、
無条件に従う必要はない。
ルールを理解した上で、『いつそのルールを破るべきか』を知ることが、もっと重要。

まとめ

Laravelの全ての機能・構成からソフトウェア開発について学ぶことができる。

  • なぜLaravelはSymfonyのコンポーネントを含んでいるのか?
  • なぜLaravelのデフォルトDBはSQLiteなのか?
  • なぜLaravel Promptsはフレームワーク非依存なのか?
  • なぜLaravel 11では多くのファイルが削除されたのか?

こういった疑問について答えを探すことで確実に何かを学ぶとができる。
なぜならこれらの機能や構成は数十年の開発経験を持つ数千人の優れたOSS開発者の知識を持って作られたものだから。そしてこれは私達が学ぼうとすれば無料で学べるのだ。

MaruMaru

Livewire Beyond the Basics - Philo Hermans

https://www.youtube.com/watch?v=857ean0JIHE

1. パフォーマンス

Livewireのパフォーマンスについて調べるとRedditなどで「Livewireのパフォーマンスは悪く、小さいコンポーネントでしか機能しない」という意見を見かけるが、実際はそうではない。

1.1 ペイロード・レスポンスサイズ

以下のようなLivewire ビュー・コンポーネントがある時、例えばユーザー数が600いて、ユーザーに紐づくcountryやroleを変更した時画面が固まることがある。

users-overview.blade.php
@foreach($this->users as $user)
    <tr>
        <td>{{ $user['name'] }}</td>
        <td>
            <select wire:change="assignCountry({{ $user['id'] }}, $event.target.value)">
                <option value=""></option>
                @foreach($this->countries as $country)
                    <option value="{{ $country }}">{{ $country }}</option>
                @endforeach
            </select>
        </td>
        <td>
            @foreach($this->roles as $roleId => $roleName)
                <button wire:click="assignRole({{ $user['id'] }}, {{ $roleId }})">
                    @if($roleId === $user['role']) ✅ @endif
                    {{ $roleName }}
                </button>
            @endforeach
        </td>
    </tr>
@endforeach

Livewire コンポーネントクラスの実装は以下のようになっている

UsersOverview.php
class UsersOverview extends Component
{
    public $users;
    public $countries;
    public $roles;

    public function mount()
    {
        $this->users = User::all(['id', 'name', 'role'])->toArray();
        $this->countries = CountriesEnum::toOptions();
        $this->roles = RolesEnum::toOptions();
    }

    public function assignCountry($userId, $country){ ... }

    public function assignRole($userId, $roleId){ ... }
}

これは、Livewireはモーフィングと呼ばれる「 既存のDOM(HTML構造)と新しく生成されたDOMを比較して、差分だけを更新する」処理を行うが、レスポンスのHTMLが大きすぎる(1.6メガバイトなど)と処理が非常に重くなってしまうため。

改善策1
以下のようにComputed Propertyを使うことでフロントと常にデータを同期する必要がなくなる。
リクエストペイロードサイズを大幅に改善できる

UsersOverview.php
class UsersOverview extends Component
{
    #[Computed]
    public function users()
    {
        return User::all(['id', 'name', 'role'])->toArray();
    }

    #[Computed]
    public function countries()
    {
        return CountriesEnum::toOptions();
    }

    #[Computed]
    public function roles()
    {
        return RolesEnum::toOptions();
    }
}

改善策2
以下のようにUI再描画が不要な処理にRenderless 属性を付与することで、UI再描画が避けられ、モーフィングが起きなくなる。
レスポンスサイズ・レスポンスタイムを大幅に削減できる

UsersOverview.php
class UsersOverview extends Component
{
    #[Renderless]
    public function assignCountry($userId, $country){ ... }
}

改善策3
データ更新時にUI再描画が必要な処理については子コンポーネントに切り出し、レンダリング範囲を分離することでレスポンスサイズ・レスポンスタイムを大幅に削減できる

users-overview.blade.php
@foreach($this->users as $user)
    <tr>
        <td>{{ $user['name'] }}</td>
        <td>
            <select wire:change="assignCountry({{ $user['id'] }}, $event.target.value)">
                <option value=""></option>
                @foreach($this->countries as $country)
                    <option value="{{ $country }}">{{ $country }}</option>
                @endforeach
            </select>
        </td>
        <td>
            <livewire:role-assigner :$user />
        </td>
    </tr>
@endforeach
RoleAssigner.php
class RoleAssigner extends Component
{
    public $user;

    public function assignRole($roleId)
    {
        User::find($this->user['id'])->update(['role' => $roleId]);
    }

    #[Computed]
    public function roles()
    {
        return RolesEnum::toOptions();
    }
}
role-assigner.blade.php
@foreach($this->roles as $roleId => $roleName)
    <button wire:click="assignRole('{{ $roleId }}')">
        @if($roleId === $user['role']) ✅ @endif
        {{ $roleName }}
    </button>
@endforeach

1.2 レイテンシー

1.2.1 ルーティングサービス・キャッシュによる改善

ドイツでデプロイしていた組み込み可能なLivewireフォームウィジェットについてUSからアクセスすると遅延が発生することがあった。
以下を実施することで2.5秒 => 100ms以下まで改善した

  • 地理的ロードバランシング + US VPSの設置
  • Cloudflare Argoの導入
  • Redis でEloquent クエリをキャッシュ
  • Laravel Octaneの導入

1.2.2 リードレプリカ導入による改善

データを閲覧する用の管理者サイトではUSからのアクセスで引き続き遅延が発生していた。
RDSリードレプリカをUSに2つ追加したことで3-8秒 => 100 - 200msまで改善した

Laravelでリードレプリカを使うにはServiceProviderに以下のように記述する
ホスト名に報じて接続先DBホストプールを切り替える

AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $dbHost = match (gethostname()) {
            'app-us-east' => config('database.connections.mysql.read_us_east_host_pool'),
            'app-us-west' => config('database.connections.mysql.read_us_west_host_pool'),
            default => config('database.connections.mysql.read_eu_central_host_pool'),
        };

        config()->set('database.connections.mysql.read.host', Arr::wrap($dbHost));
    }
}

config設定例

config/database.php
'connections' => [
    'mysql' => [
        'write' => [
            'host' => env('DB_HOST'),
        ],
        'read_eu_central_host_pool' => implode(',', env('DB_HOST_READONLY_EU_CENTRAL')),
        'read_us_east_host_pool'    => implode(',', env('DB_HOST_READONLY_US_EAST')),
        'read_us_west_host_pool'    => implode(',', env('DB_HOST_READONLY_US_WEST')),
        'sticky' => true,
    ]
]

1.2.3 リードレプリカへの同期遅延による不具合改善

stickyオプションをつけることでwrite直後は同じインスタンスからSELECTすることが保証されるが、
それ以降の別リクエストに紐づくSELECTはリードレプリカから取得されてしまい、同期が間に合っていない場合404となる。

以下のパッケージがこの問題を解決してくれる
https://github.com/mpyw/laravel-cached-database-stickiness
以下のように複数リクエストでもWriteインスタンスを参照するようにできる。(有効期間はconfigで設定可能)

ただ、Writeインスタンスへのアクセス時はレスポンスが遅くなるため、UI側で工夫が必要

1.3 最適化されたUI

処理完了を待たずにUIを変更することでユーザー体験を向上できるケースがある。
以下のようにwire:loading.removeを使うことで、ローディング表示中非表示にすることができる

my-todo.blade.php
<div wire:loading>
    spinner
</div>

@foreach($this->todos as $todo)
    <div wire:key="{{ $todo->id }}"
         wire:loading.remove wire:target="remove({{ $todo->id }})">
        <input wire:click="remove({{ $todo->id }})" type="checkbox">
        <label>{{ $todo['todo'] }}</label>
    </div>
@endforeach

そのほかのUI最適化のtipについては以下の記事がおすすめ
https://tighten.com/insights/optimistic-ui-tips-livewire-alpine/

2. セキュリティ

Liveiwreコンポーネントのpublic プロパティはデフォルトでクライアントから操作可能になっているため、注意する必要がある。
例えばuserIdをpublic プロパティにしているコンポーネントに対して、クライアント側からuserIdを変更すると、別のユーザー情報が見れてしまう。

#[Locked] Attributesを付与することで、クライアントから操作不可能にできる。
デフォルトで全てのプロパティを#[Locked]する以下のパッケージもある。
https://github.com/wire-elements/livewire-strict

3. ソースから学ぶ

Livewireのソースから多くのことを学べる。

3.1 ソースの構成

全ての機能はFeaturesディレクトリにある

  • バックエンド src/Features/**
  • クライアントライブラリ js/features/**

3.2 隠れた機能コンポーネントフック

Livewireコンポーネントにフックを設定してコンポーネントのライフサイクルに処理を挟むことができる。

LivewireStrictServiceProvide.php
class LivewireStrictServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function register(): void
    {
        app('livewire')->componentHook(SupportLockedProperties::class);
    }
}
SupportLockedProperties.php
class SupportLockedProperties extends ComponentHook
{
    public function update($propertyName, $fullPath, $newValue)
    {
        //...
    }
}
MaruMaru

Behind Laravel Octane - Mateus Guimaraes

https://www.youtube.com/watch?v=dOXXL-98Zbs

Laravel Octaneとは?

公式ドキュメントによれば
「Octane boots your application once, keeps it in memory, and then feeds it requests at supersonic speeds」
「Octane はアプリケーションを一度だけ起動し、そのままメモリ上に保持し、超高速でリクエストを処理する。」
この通りで、Octaneとは何なのかや、どう使うかは公式ドキュメントに詳しく書いてある。

このトークでは以下について話す

  • Octane はどんな問題を解決しているのか?
  • もし速いなら、なぜデフォルトではないのか?
  • なぜ PHP はアプリケーションをメモリ上で実行しないのか?
  • 内部的にはどのように動作しているのか?
  • 何を抽象化しているのか?
  • みんなが話している「ノンブロッキング I/O」とは何か?
  • Octane 以外にどんなことを試せるのか?

一般的なLaravelアプリケーション(PHP-FPM)の構成

  1. ユーザーからのリクエストが、まず Nginx または Apache に送られる(リバースプロキシ)
  2. そのリクエストは PHP-FPM に渡される。
  3. PHP-FPM が以下を行う
    • アプリケーションをゼロから起動(Boot)
    • リクエストを処理して、レスポンスを生成
    • アプリケーションを終了(Terminate) し、使用したメモリを解放
    • 最後にレスポンスをNginx経由でユーザーに返す

※Opcacheを有効にしてコードの一部をキャッシュして高速化はできるが、リクエストごとにアプリケーションが再起動されるのは変わらない。

Laravel Octaneはなにを行うか

Laravel アプリケーションと Web サーバーの間のアダプターになる。
このアダプターはアプリケーションを長期間実行し続けるように保つ。
そのために いくつかの異なるランタイム を使用する。対応しているランタイムは以下

  • Swoole
  • Open Swoole(Swoole のフォーク版)
  • FrankenPHP
  • RoadRunner

Octane はこれらと連携するための共通の抽象レイヤーとして機能し、開発者はそれぞれのランタイムの細かい違いを気にすることなく使用できる。

Octaneを使ったLaravelアプリケーションの構成

ユーザーがリクエストを送る前にアプリケーションは起動しており、従来の初期化プロセス(Providerの初期化など)を行わずにただリクエストを処理するだけで済む。
これがOctaneで高速化する理由の一つ

一度起動してずっと動き続ける仕組みはQueue Workerをイメージしてもらうと良い。

PHP-FPMがリクエストを処理する仕組み

PHP-FPMはリクエストを順番に処理する

  1. PHP-FPMはPoolにプロセスを待機させる

  2. リクエストに対してプロセスを割り当てる

  3. プールのプロセス以上にリクエストが来たら

  4. 新しくプロセスを生成可能であれば生成して割り当てる

  5. 生成可能なプロセス数を超えてリクエストが来た場合はリクエストキューに待機させる

つまり、PHP-FPMでは同時に処理できるリクエスト数はプロセス数に依存する。

こういった仕組みを「Shared Nothing Architecture」と呼ぶ。
各リクエストは個別のプロセスによって処理され、全てが独立していて、リクエスト間でメモリ空間を共有することがない。

Shared Nothing Architechtureの利点

  • スケールが簡単: ただプロセスを追加するだけ
  • フォールトトレランス(耐障害性): 各プロセスは独立して障害が発生する
  • デバッグが簡単: 毎回クリーンな状態から始まる
  • 共有される状態が存在しない

Shared Nothing Architechtureの欠点

  • 新規起動によるオーバーヘッドの増加
  • プロセスのコストが高い
  • 永続的な接続が制限され、コネクションプーリングができない
  • プロセス管理のオーバーヘッド
  • 長期間の接続や共有リソースが利用できない

Long-running Architecture

一つのプロセスを動かし続けるアーキテクチャをLong-running Architectureと呼ぶ
馴染みのあるQueueワーカーや