339 lines
7.1 KiB
Markdown
339 lines
7.1 KiB
Markdown
|
|
# Controller Entrypoint (`asController`)
|
||
|
|
|
||
|
|
## Scope
|
||
|
|
|
||
|
|
Use this reference when exposing an action through HTTP routes.
|
||
|
|
|
||
|
|
## Recap
|
||
|
|
|
||
|
|
- Documents controller lifecycle around `asController(...)` and response adapters.
|
||
|
|
- Covers routing patterns, middleware, and optional in-action `routes()` registration.
|
||
|
|
- Summarizes validation/authorization hooks used by `ActionRequest`.
|
||
|
|
- Provides extension points for JSON/HTML responses and failure customization.
|
||
|
|
|
||
|
|
## Recommended pattern
|
||
|
|
|
||
|
|
- Route directly to action class when appropriate.
|
||
|
|
- Keep HTTP adaptation in controller methods (`asController`, `jsonResponse`, `htmlResponse`).
|
||
|
|
- Keep domain logic in `handle(...)`.
|
||
|
|
|
||
|
|
## Methods provided (`AsController` trait)
|
||
|
|
|
||
|
|
### `__invoke`
|
||
|
|
|
||
|
|
Required so Laravel can register the action class as an invokable controller.
|
||
|
|
|
||
|
|
```php
|
||
|
|
$action($someArguments);
|
||
|
|
|
||
|
|
// Equivalent to:
|
||
|
|
$action->handle($someArguments);
|
||
|
|
```
|
||
|
|
|
||
|
|
If the method does not exist, Laravel route registration fails for invokable controllers.
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Illuminate\Routing\RouteAction
|
||
|
|
protected static function makeInvokable($action)
|
||
|
|
{
|
||
|
|
if (! method_exists($action, '__invoke')) {
|
||
|
|
throw new UnexpectedValueException("Invalid route action: [{$action}].");
|
||
|
|
}
|
||
|
|
|
||
|
|
return $action.'@__invoke';
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
If you need your own `__invoke`, alias the trait implementation:
|
||
|
|
|
||
|
|
```php
|
||
|
|
class MyAction
|
||
|
|
{
|
||
|
|
use AsAction {
|
||
|
|
__invoke as protected invokeFromLaravelActions;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function __invoke()
|
||
|
|
{
|
||
|
|
// Custom behavior...
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Methods used (`ControllerDecorator` + `ActionRequest`)
|
||
|
|
|
||
|
|
### `asController`
|
||
|
|
|
||
|
|
Called when used as invokable controller. If missing, it falls back to `handle(...)`.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function asController(User $user, Request $request): Response
|
||
|
|
{
|
||
|
|
$article = $this->handle(
|
||
|
|
$user,
|
||
|
|
$request->get('title'),
|
||
|
|
$request->get('body')
|
||
|
|
);
|
||
|
|
|
||
|
|
return redirect()->route('articles.show', [$article]);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `jsonResponse`
|
||
|
|
|
||
|
|
Called after `asController` when request expects JSON.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function jsonResponse(Article $article, Request $request): ArticleResource
|
||
|
|
{
|
||
|
|
return new ArticleResource($article);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `htmlResponse`
|
||
|
|
|
||
|
|
Called after `asController` when request expects HTML.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function htmlResponse(Article $article, Request $request): Response
|
||
|
|
{
|
||
|
|
return redirect()->route('articles.show', [$article]);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `getControllerMiddleware`
|
||
|
|
|
||
|
|
Adds middleware directly on the action controller.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function getControllerMiddleware(): array
|
||
|
|
{
|
||
|
|
return ['auth', MyCustomMiddleware::class];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `routes`
|
||
|
|
|
||
|
|
Defines routes directly in the action.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public static function routes(Router $router)
|
||
|
|
{
|
||
|
|
$router->get('author/{author}/articles', static::class);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
To enable this, register routes from actions in a service provider:
|
||
|
|
|
||
|
|
```php
|
||
|
|
use Lorisleiva\Actions\Facades\Actions;
|
||
|
|
|
||
|
|
Actions::registerRoutes();
|
||
|
|
Actions::registerRoutes('app/MyCustomActionsFolder');
|
||
|
|
Actions::registerRoutes([
|
||
|
|
'app/Authentication',
|
||
|
|
'app/Billing',
|
||
|
|
'app/TeamManagement',
|
||
|
|
]);
|
||
|
|
```
|
||
|
|
|
||
|
|
### `prepareForValidation`
|
||
|
|
|
||
|
|
Called before authorization and validation are resolved.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function prepareForValidation(ActionRequest $request): void
|
||
|
|
{
|
||
|
|
$request->merge(['some' => 'additional data']);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `authorize`
|
||
|
|
|
||
|
|
Defines authorization logic.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function authorize(ActionRequest $request): bool
|
||
|
|
{
|
||
|
|
return $request->user()->role === 'author';
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
You can also return gate responses:
|
||
|
|
|
||
|
|
```php
|
||
|
|
use Illuminate\Auth\Access\Response;
|
||
|
|
|
||
|
|
public function authorize(ActionRequest $request): Response
|
||
|
|
{
|
||
|
|
if ($request->user()->role !== 'author') {
|
||
|
|
return Response::deny('You must be an author to create a new article.');
|
||
|
|
}
|
||
|
|
|
||
|
|
return Response::allow();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `rules`
|
||
|
|
|
||
|
|
Defines validation rules.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function rules(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'title' => ['required', 'min:8'],
|
||
|
|
'body' => ['required', IsValidMarkdown::class],
|
||
|
|
];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `withValidator`
|
||
|
|
|
||
|
|
Adds custom validation logic with an after hook.
|
||
|
|
|
||
|
|
```php
|
||
|
|
use Illuminate\Validation\Validator;
|
||
|
|
|
||
|
|
public function withValidator(Validator $validator, ActionRequest $request): void
|
||
|
|
{
|
||
|
|
$validator->after(function (Validator $validator) use ($request) {
|
||
|
|
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
|
||
|
|
$validator->errors()->add('current_password', 'Wrong password.');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `afterValidator`
|
||
|
|
|
||
|
|
Alternative to add post-validation checks.
|
||
|
|
|
||
|
|
```php
|
||
|
|
use Illuminate\Validation\Validator;
|
||
|
|
|
||
|
|
public function afterValidator(Validator $validator, ActionRequest $request): void
|
||
|
|
{
|
||
|
|
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
|
||
|
|
$validator->errors()->add('current_password', 'Wrong password.');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `getValidator`
|
||
|
|
|
||
|
|
Provides a custom validator instead of default rules pipeline.
|
||
|
|
|
||
|
|
```php
|
||
|
|
use Illuminate\Validation\Factory;
|
||
|
|
use Illuminate\Validation\Validator;
|
||
|
|
|
||
|
|
public function getValidator(Factory $factory, ActionRequest $request): Validator
|
||
|
|
{
|
||
|
|
return $factory->make($request->only('title', 'body'), [
|
||
|
|
'title' => ['required', 'min:8'],
|
||
|
|
'body' => ['required', IsValidMarkdown::class],
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `getValidationData`
|
||
|
|
|
||
|
|
Defines which data is validated (default: `$request->all()`).
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function getValidationData(ActionRequest $request): array
|
||
|
|
{
|
||
|
|
return $request->all();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `getValidationMessages`
|
||
|
|
|
||
|
|
Custom validation error messages.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function getValidationMessages(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'title.required' => 'Looks like you forgot the title.',
|
||
|
|
'body.required' => 'Is that really all you have to say?',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `getValidationAttributes`
|
||
|
|
|
||
|
|
Human-friendly names for request attributes.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function getValidationAttributes(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'title' => 'headline',
|
||
|
|
'body' => 'content',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `getValidationRedirect`
|
||
|
|
|
||
|
|
Custom redirect URL on validation failure.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function getValidationRedirect(UrlGenerator $url): string
|
||
|
|
{
|
||
|
|
return $url->to('/my-custom-redirect-url');
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `getValidationErrorBag`
|
||
|
|
|
||
|
|
Custom error bag name on validation failure (default: `default`).
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function getValidationErrorBag(): string
|
||
|
|
{
|
||
|
|
return 'my_custom_error_bag';
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `getValidationFailure`
|
||
|
|
|
||
|
|
Override validation failure behavior.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function getValidationFailure(): void
|
||
|
|
{
|
||
|
|
throw new MyCustomValidationException();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `getAuthorizationFailure`
|
||
|
|
|
||
|
|
Override authorization failure behavior.
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function getAuthorizationFailure(): void
|
||
|
|
{
|
||
|
|
throw new MyCustomAuthorizationException();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Checklist
|
||
|
|
|
||
|
|
- Route wiring points to the action class.
|
||
|
|
- `asController(...)` delegates to `handle(...)`.
|
||
|
|
- Validation/authorization methods are explicit where needed.
|
||
|
|
- Response mapping is split by channel (`jsonResponse`, `htmlResponse`) when useful.
|
||
|
|
- HTTP tests cover both success and validation/authorization failure branches.
|
||
|
|
|
||
|
|
## Common pitfalls
|
||
|
|
|
||
|
|
- Putting response/redirect logic in `handle(...)`.
|
||
|
|
- Duplicating business rules in `asController(...)` instead of delegating.
|
||
|
|
- Assuming action route discovery works without `Actions::registerRoutes(...)` when using in-action `routes()`.
|
||
|
|
|
||
|
|
## References
|
||
|
|
|
||
|
|
- https://www.laravelactions.com/2.x/as-controller.html
|