Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why Laravel apps break—Mastering the fundamenta...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Why Laravel apps break—Mastering the fundamentals to keep them maintainable

Avatar for 武田 憲太郎

武田 憲太郎

May 25, 2026

More Decks by 武田 憲太郎

Other Decks in Programming

Transcript

  1. "CPVU.F 8IP*BN • 8FC"QQMJDBUJPO%FWFMPQFS • 1)14JODF-BSBWFM4JODF X: @KentarouTakeda Selected Past

    Talks Conference Title PHPerKaigi 2023 Dr. Strange Laravel PHPerKaigi 2024 Schema driven development with Laravel OpenAPI PHPerKaigi 2025 The Housekeeper and Laravel
  2. lWhat: minimizing design decisions #VUXFDBOTPNFUJNFTUFOEUPBTEFWFMPQFST UPDSFBUFMJLFTPSUPGMJLFWFSZDPNQMFY DBUIFESBMTPGDPNQMFYJUZUIBUBSFOUTPFBTZUP DIBOHF ZPVLOPX EFTQJUFUIFJSDPNQMFYCFBVUZ

    "OETPGUXBSFTIPVMECFBMJUUMFCJUNPSF ZPV LOPX TJNQMFBOEEJTQPTBCMFBOEFBTZUP DIBOHFBOE 5BZMPS0UXFMM8IBU:FBSTPG-BSBWFM5BVHIU.F "CPVU.BJOUBJOBCJMJUZ
  3. $SFBUJOH$36%GPSBBookSFTPVSDF php artisan make:model --api Book Class File Model app/Models/Book.php

    Factory database/factories/BookFactory.php Migration database/migrations/2011_06_09_000000_create_books_table.php Seeder database/seeders/BookSeeder.php Request app/Http/Requests/StoreBookRequest.php Request app/Http/Requests/UpdateBookRequest.php Controller app/Http/Controllers/BookController.php Policy app/Policies/BookPolicy.php
  4. $SFBUJOHDPNQPOFOUTCFZPOE$36% php artisan make:* Command File Description make:test tests/Feature/BookControllerTest.php Test

    make:observer app/Observers/FooObserver.php Invariants and side effects for records and Eloquent models make:scope app/Models/Scopes/FooScope.php Cross-cutting constraints on queries make:component app/View/Components/Foo.php Encapsulating view-side logic make:rule app/Rules/Foo.php Custom validation make:cast app/Casts/AsFoo.php Transparent conversion between columns and fields make:event app/Events/BookUpdated.php Event make:listener app/Listeners/SendEmailToSubscriber.php Event listener
  5. %FDPNQPTFEPSOPU XJSJOHJTNBOVBM public function update(Request $request, int $number): Response {

    $user = $this->authService->authenticate($request); $data = $this->requestValidator->validate($request); $pullRequest = $this->pullRequestFinder->find($number); $this->pullRequestRules->assertUpdatable($pullRequest); $this->authorizer->authorize($user, 'update', $pullRequest); // Main logic $pullRequest->update($data); return new JsonResponse($pullRequest, 200); } • &WFOBGUFSEFDPNQPTJOHJOUPTJOHMFSFTQPOTJCJMJUZDMBTTFT UIFDBMM TFRVFODFTUBZTJOUIFDPOUSPMMFS • 4PNFUIJOHUIBUSFTFNCMFTCPJMFSQMBUFŠNFUIPEOBNFT BSHVNFOUT BOE PSEFSBMMEJGGFSTVCUMZ • 'PSHFUUJOHBOBVUIPSJ[BUJPODBMM SFWFSTJOHUIFPSEFS NJYJOHVQBSHVNFOUT ŠFWFSZPOFBHBUFXBZUPBTFDVSJUZJODJEFOU
  6. 5IFDPOUSPMMFSKVTUXSJUFTJUCBDL class UpdatePullRequest { #[Middleware('auth')] #[Authorize('update', 'pullRequest')] public function __invoke(

    UpdatePullRequestRequest $request, PullRequest $pullRequest, ): PullRequestResource { return tap($pullRequest) ->update($request->validated()) ->toResource(); } }
  7. 7BMJEBUJPOMJWFTJOB'PSN3FRVFTU class UpdatePullRequestRequest extends FormRequest { public function rules(): array

    { return [ 'title' => ['required', 'string', 'max:255'], 'body' => ['nullable', 'string'], 'base_ref' => ['nullable', 'string', new BranchName], ]; } }
  8. "VUIPSJ[BUJPOMJWFTJOB1PMJDZ class PullRequestPolicy { public function update(User $user, PullRequest $pullRequest):

    bool { if (!in_array($pullRequest->state, [State::Open, State::Draft])) { return false; } return $pullRequest->author_id === $user->id || $this->isMaintainer($user, $pullRequest); } }
  9. 6OJU5FTUŠFBDIMBZFSJOJTPMBUJPO // Eloquent in memory — no DB needed $user

    = new User() ->forceFill(['id' => 42]) ->setRelation( 'pivot', new Pivot() ->forceFill(['role' => 'maintainer']) ); $pullRequest = new PullRequest() ->forceFill(['author_id' => 23, 'state' => State::Open]) ->setRelation( 'repository', new Repository() ->setRelation('collaborators', collect([$user])) ); // Policy: pure PHP class — instantiate and call $policy = new PullRequestPolicy(); $this->assertTrue($policy->update($user, $pullRequest)); // ValidationRule: also a pure PHP class $rule = new BranchName(); $rule->validate( 'ref', 'feature/new-branch', fn($msg) => $this->fail($msg) ); // If we reach here, validation passed $this->assertTrue(true);
  10. 'FBUVSF5FTUŠUIFXIPMFQJQFMJOF $this->actingAs($outsider) ->putJson("/api/prs/{$pullRequest->id}", $validData) ->assertStatus(403); // Blocked by Policy $this->actingAs($author)

    ->putJson("/api/prs/{$pullRequest->id}", $invalidData) ->assertStatus(422); // Blocked by FormRequest $this->actingAs($author) ->putJson("/api/prs/{$pullRequest->id}", $validData) ->assertStatus(200); // All pass
  11. ;FSPEFTJHOEFDJTJPOTJOUIFCPJMFSQMBUF • /BNJOHUpdatePullRequestRequest  PullRequestPolicy • 1MBDFNFOUHttp/Requests/ Policies/ Rules/ •

    (FOFSBUJPOphp artisan make:* %FWFMPQFSTDBOGPDVTPOXIBUHPFTJOTJEF SVMFT BOEUIFBVUIPSJ[BUJPOMPHJD
  12. 5IFDPOUSPMMFSTKPCFOETBUEJTQBUDI public function store( StoreOrderRequest $request, PaymentGateway $payment, ): OrderResource

    { return DB::transaction(function () use ($request, $payment) { $order = Order::create($request->validated()); $payment->pay($order); event(new OrderConfirmed($order)); return $order; })->toResource(); }
  13. 4ZODISPOPVT-JTUFOFSTEPPOFUIJOH class UpdateInventory { public function __construct( private Inventory $inventory,

    ) {} public function handle(OrderConfirmed $event): void { $this->inventory->deductFor($event->order); } }
  14. "TZODISPOPVT-JTUFOFSTBGUFSDPNNJU class SendConfirmation implements ShouldQueueAfterCommit { public function handle(OrderConfirmed $event):

    void { Mail::to($event->order->customer) ->send( new OrderConfirmationMail($event->order), ); } }
  15. 5FTUFEJOEFQFOEFOUMZ MJLFCFGPSF // Controller: verify dispatch only Event::fake(); $this->postJson('/api/orders', $valid)

    ->assertStatus(201); Event::assertDispatched( OrderConfirmed::class ); // Listener: isolated from the request $this->mock(Inventory::class) ->shouldReceive('deductFor') ->once() ->with($order); $event = new OrderConfirmed($order); $this->app->make(UpdateInventory::class) ->handle($event);
  16. &OUSZBOEFYJUŠUIFTBNFJEFB • &OUSZBQJQFMJOF .JEEMFXBSFˠ'PSN3FRVFTU ˠ$POUSPMMFS • &YJUFWFOUESJWFO $POUSPMMFSˠ&WFOUˠ -JTUFOFS •

    *ODPNNPOXJSJOH OBNJOH BOEQMBDFNFOUBSF -BSBWFMDPOWFOUJPOT "MMUIBUSFNBJOTPOUIFBQQTJEFJTUIFDPOUFOUT PGFBDISFTQPOTJCJMJUZDMBTT
  17. 5IFCJHQJDUVSF Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener
  18. $36%3FTPVSDF Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener • $36%JTCVOEMFEBTBRoute::resource()SFTPVSDF
  19. 1JQFMJOF Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener "MMQSFQSPDFTTJOHIBQQFOTUSBOTQBSFOUMZBOEOFWFSCMPBUTUIF DPOUSPMMFS
  20. 3PVUF.PEFM#JOEJOH Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener 3PVUF.PEFM#JOEJOHSFTPMWFTUIFNPEFMJOBEWBODF
  21. 5IJO$POUSPMMFS Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener 5IFDPOUSPMMFSKVTUXSJUFTCBDLUIF BMSFBEZSFTPMWFENPEFM return tap($book) ->update($request->validated()) ->toResource();
  22. &WFOU%SJWFOTJEFFGGFDUT Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener 4JEFFGGFDUTBSFJTPMBUFECZFWFOUT 5IFZDBOCFNBEFBTZODISPOPVT UPP event(new BookUpdated($book));
  23. &YQSFTTJWF"DUJWF3FDPSE Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener • 4DPQF $BTU BOE.VUBUPSBCTPSCUIFHBQCFUXFFO"DUJWF 3FDPSEBOEUIFEPNBJO
  24. 1FSTJTUFODFMBZFSTJEFFGGFDUT Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener • 4JEFFGGFDUTPO&MPRVFOUNPEFMTDBOBMTPCFTFQBSBUFEXJUI FWFOUT
  25. -JHIUXFJHIUDPOUSPMMFSUFTUT Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener
  26. *TPMBUFEJOUFSWFOUJPOQPJOUUFTUT Test ApiResource Response ValidationRule CRUD Authenticate Route Model Binding

    Authorize FormRequest Controller Model Table Scope Cast Mutator Policy Event Listener • &BDIJOUFSWFOUJPOQPJOUDBOCFUFTUFEXJUIBTJOHMF SFTQPOTJCJMJUZ