create-controller

Scaffold a controller with its FormRequest, route, and feature test

SKILL.md

GitHub

New Controller

Create the controller named in $ARGUMENTS (e.g. Chat/DeleteChat, Payment/Invoice).

Read modules/Chat/Infrastructure/Http/Controller/ChatController.php and its request class first, and mirror them.

Files

modules/<Module>/Infrastructure/Http/Request/<Name>Request.php
modules/<Module>/Infrastructure/Http/Controller/<Name>Controller.php
routes/<file>.php                                      # route entry
tests/Feature/<Module>/<Name>ControllerTest.php

FormRequest — all validation lives here

<?php

declare(strict_types=1);

namespace Modules\<Module>\Infrastructure\Http\Request;

use Illuminate\Foundation\Http\FormRequest;
use Override;

final class <Name>Request extends FormRequest
{
    /** @return array<string, mixed> */
    #[Override]
    public function rules(): array
    {
        return [
            // 'txid' => ['required', 'string', 'size:64'],
        ];
    }
}

Never validate inline in the controller.

Controller — three lines of responsibility

<?php

declare(strict_types=1);

namespace Modules\<Module>\Infrastructure\Http\Controller;

use Modules\<Module>\Domain\<Name>ActionInterface;

final readonly class <Name>Controller
{
    public function __construct(
        private <Name>ActionInterface $action,
    ) {
    }

    public function __invoke(<Name>Request $request): RedirectResponse
    {
        $result = $this->action->execute(
            new <Name>Transfer(...$request->validated()),
        );

        return redirect()->route('...');
    }
}

A controller may only: accept a validated request → call an Action or Facade → return a response or view. Anything else belongs in Application/.

Route

Add it under routes/, wired by modules/Shared/RouteServiceProvider.php. Controller mappings only — no closures, no inline logic:

Route::post('/chat/{chat}/delete', DeleteChatController::class)->name('chat.delete');

Verify:

php artisan route:list --path=<segment>

Feature test

Controllers need the framework, so the test goes in tests/Feature/:

use Illuminate\Foundation\Testing\RefreshDatabase;

final class <Name>ControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_post_with_valid_input_redirects(): void { /* … */ }

    public function test_post_with_invalid_input_returns_validation_error(): void { /* … */ }

    public function test_post_without_auth_is_rejected(): void { /* … */ }
}

Cover the happy path, the validation failure, and the auth boundary.

Constraints

  • No Eloquent, no DB::, no business conditionals in the controller — the pre-write hook blocks these
  • Inject Action or Facade interfaces, never concrete classes
  • Rate-limited or auth'd routes get the middleware from modules/Shared/
  • Never register the controller manually — the container resolves it

Gate

composer test