a7-tech / laminas-attribute-controller
Package for Laminas Framework to use attributes sugar in controller.
Requires
- php: ^8.2
- beberlei/assert: ^3.3
- doctrine/orm: ^2.0 | ^3.0
- jms/serializer: ^3.30
- laminas/laminas-mvc: ^3.0
- symfony/validator: ^5.4 | ^6.0 | ^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.75
- phpstan/phpstan: ^1.12
- phpunit/phpunit: ^9.0
This package is auto-updated.
Last update: 2025-06-02 19:13:01 UTC
README
Description
This package provides attribute-based controllers for use in Laminas applications. It simplifies route management, dependency injection, and input handling in controller arguments.
Installation
Requirements
- PHP >= 8.0
- Laminas MVC >= 3.0
- Composer
Instructions
- Install the package:
composer require a7-tech/laminas-attribute-controller
- Set up the module in your Laminas application by adding it to the
application.config.php
:return [ 'modules' => [ // other modules... LaminasAttributeController\Module::class, ], ];
Usage
Example Controller with Attributes
use LaminasAttributeController\AttributeActionController; use LaminasAttributeController\Routing\Route; #[Route('/user', name: 'user')] final class UserController extends AttributeActionController { #[Route('/list', name: 'user_list', methods: ['GET'])] public function listAction() { return ['users' => []]; } #[Route('/add', name: 'user_add', methods: ['POST'])] public function addAction() { return ['status' => 'success']; } }
Route Registration
Routes are automatically registered through attributes when the module is enabled.
Attributes Documentation
Route
The Route
attribute defines routing for controller methods.
use LaminasAttributeController\Routing\Route; #[Route('/api/users', name: 'users_route', methods: ['GET', 'POST'])] public function usersAction() { // Method implementation }
Parameters:
path
(string): The URL path for the routename
(string|null): Optional name for the routemethods
(array): HTTP methods allowed for this route, defaults to ['GET']
QueryParam
The QueryParam
attribute extracts and validates query parameters from the request.
use LaminasAttributeController\Validation\QueryParam; use Symfony\Component\Validator\Constraints as Assert; public function searchAction( #[QueryParam('query', constraints: [new Assert\NotBlank(), new Assert\Length(min: 3)], required: true)] string $searchQuery ) { // Use $searchQuery which has been extracted from the request query parameters return ['results' => $this->searchService->search($searchQuery)]; }
Parameters:
name
(string): The name of the query parameterconstraints
(array): Array of Symfony validator constraints to applyrequired
(bool): Whether the parameter is required, defaults to false
Autowire
The Autowire
attribute enables automatic dependency injection.
use LaminasAttributeController\Injection\Autowire; use App\Service\UserService; public function listAction( #[Autowire] UserService $userService, #[Autowire('custom.service.alias')] $customService ) { $users = $userService->getAllUsers(); // Method implementation }
Parameters:
alias
(string|null): Optional service alias for the container, defaults to null (uses the type hint)
IsGranted
The IsGranted
attribute provides auth-based access control for controller methods.
use LaminasAttributeController\Security\IsGranted; #[IsGranted('FULLY_AUTHENTICATED')] public function authorizedAction() { // Only accessible to authenticated users return ['message' => 'authorized area']; }
Parameters:
role
(string): The role required to access the method
MapRequestPayload
The MapRequestPayload
attribute maps the request body to a parameter.
use LaminasAttributeController\Validation\MapRequestPayload; use App\DTO\UserCreateRequest; public function createAction( #[MapRequestPayload] UserCreateRequest $request ) { // $request is populated from the request body return ['id' => $this->userService->createUser($request)]; }
This attribute automatically deserializes the request body into the specified type.
Example with Multiple Fields and Validators
use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; class UserCreateRequest { #[Assert\NotBlank(message: "Username is required")] #[Assert\Length(min: 3, max: 50, minMessage: "Username must be at least {{ limit }} characters", maxMessage: "Username cannot be longer than {{ limit }} characters")] public string $username; #[Assert\NotBlank(message: "Email is required")] #[Assert\Email(message: "The email '{{ value }}' is not a valid email")] public string $email; #[Assert\NotBlank(message: "Password is required")] #[Assert\Length(min: 8, minMessage: "Password must be at least {{ limit }} characters")] #[Assert\Regex( pattern: "/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/", message: "Password must include at least one uppercase letter, one lowercase letter, and one number" )] public string $password; #[Assert\NotBlank(message: "Age is required")] #[Assert\Range( min: 18, max: 120, notInRangeMessage: "You must be between {{ min }} and {{ max }} years old" )] public int $age; }
Using this DTO with the MapRequestPayload
attribute will automatically validate all fields according to the specified constraints.
MapQueryString
The MapQueryString
attribute maps the query string parameters to a typed object.
use LaminasAttributeController\Validation\MapQueryString; use App\DTO\UserSearchRequest; public function searchAction( #[MapQueryString] UserSearchRequest $searchCriteria ) { // $searchCriteria is populated from the query string parameters return ['results' => $this->userService->search($searchCriteria)]; }
This attribute automatically deserializes the query string parameters into the specified type, making it easy to work with structured query parameters in GET requests.
Example with Multiple Fields and Validators
use Symfony\Component\Validator\Constraints as Assert; use JMS\Serializer\Annotation as Serializer; class UserSearchRequest { #[Assert\Length(min: 2, max: 50, minMessage: "Search term must be at least {{ limit }} characters")] public ?string $searchTerm = null; #[Assert\Range( min: 1, max: 100, notInRangeMessage: "Results per page must be between {{ min }} and {{ max }}" )] public int $limit = 20; #[Assert\PositiveOrZero(message: "Page number cannot be negative")] public int $page = 0; #[Assert\Choice(choices: ["asc", "desc"], message: "Sort direction must be either 'asc' or 'desc'")] public string $sortDirection = "asc"; }
Using this DTO with the MapQueryString
attribute will automatically validate all query parameters according to the specified constraints. This example would handle a query like ?searchTerm=john&limit=50&page=2&sortDirection=desc
.
CurrentUser
The CurrentUser
attribute injects the current authenticated user into a parameter.
For implementation, you need to have a user entity and a service that retrieves the current user from the session or security context implemented LaminasAttributeController\Security\GetCurrentUser
use LaminasAttributeController\Security\CurrentUser; use App\Entity\User; public function profileAction( #[CurrentUser] User $user ) { // $user contains the currently authenticated user return ['user' => $user]; }
This attribute provides easy access to the authenticated user without manual retrieval.
Advanced Configuration
Custom Resolvers
The package allows you to create and configure your own parameter resolvers. This is useful when you need custom logic for resolving controller action parameters.
To create a custom resolver:
- Create a class that implements
ParameterResolverInterface
:
use LaminasAttributeController\ParameterResolverInterface; use LaminasAttributeController\ResolutionContext; final class MyCustomResolver implements ParameterResolverInterface { public function resolve(ResolutionContext $context): mixed { // Access parameter information $parameter = $context->parameter; // Access route match information $routeMatch = $context->routeMatch; // Access parameter attributes $attributes = $context->getAttributes(); // Implement your custom resolution logic // ... // Return the resolved value return $resolvedValue; } }
- Register your resolver in the configuration:
// In your module.config.php or any other configuration file return [ 'laminas-attribute-controller' => [ 'resolvers' => [ // Default resolvers FromRouteResolver::class, MapRequestPayloadResolver::class, MapQueryStringResolver::class, QueryParamResolver::class, AutowireResolver::class, AutoInjectionResolver::class, CurrentUserValueResolver::class, DefaultValueResolver::class, // Your custom resolver MyCustomResolver::class, ], ], 'service_manager' => [ 'factories' => [ // Factory for your custom resolver MyCustomResolver::class => function (ContainerInterface $container) { return new MyCustomResolver(/* dependencies */); }, ], ], ];
The order of resolvers in the configuration is important as they are tried in sequence until one returns a non-null value.
Default Resolvers
The package comes with the following default resolvers:
FromRouteResolver
: Resolves parameters from route matchesMapRequestPayloadResolver
: Maps request body to a parameter (POST, PUT, PATCH, etc)MapQueryStringResolver
: Maps request to a parameter from query params (GET request)QueryParamResolver
: Extracts and validates query parametersAutowireResolver
: Provides automatic dependency injectionAutoInjectionResolver
: Provides automatic dependency injectionCurrentUserValueResolver
: Injects the current authenticated userDefaultValueResolver
: Uses default parameter values
You can customize which resolvers are used by modifying the configuration:
return [ 'laminas-attribute-controller' => [ 'resolvers' => [ // Only include the resolvers you need FromRouteResolver::class, MapRequestPayloadResolver::class, MapQueryStringResolver::class, QueryParamResolver::class, AutowireResolver::class, AutoInjectionResolver::class, CurrentUserValueResolver::class, DefaultValueResolver::class, ], ], ];