A7-tech/laminas-attribute-controller — use PHP attributes for controllers in Laminas

Hi everyone!

I want to share a small but useful package I’ve made:

a7-tech/laminas-attribute-controller

This package lets you use PHP 8 attributes (like #[Route], #[Autowire], etc.) to define your Laminas controllers in a cleaner and more modern way — no need to write routes as arrays anymore, and possible to use like #MapRequestPayload to serialize and validate before action.

Key Features:

  • #[Route] — define routes right on controllers or methods, auto-registered by the module
  • #[QueryParam] — extract and validate GET query params via Symfony Validator (constraints, required flag)
  • #[MapRequestPayload] — map POST/PUT/PATCH request body into a typed DTO and validate it automatically
  • #[MapQueryString] — map GET query string into a typed DTO with validation rules
  • #[Autowire] — inject services automatically by type (or alias) from the container
  • #[IsGranted] — restrict access by roles (e.g. FULLY_AUTHENTICATED)
  • #[CurrentUser] — inject the authenticated user object directly into your action method

Unlimited customization is also possible via custom ParameterResolverInterface implementations


use LaminasAttributeController\AttributeActionController;
use LaminasAttributeController\Routing\Route;
use LaminasAttributeController\Validation\MapRequestPayload;
use LaminasAttributeController\Validation\MapQueryString;
use LaminasAttributeController\Validation\QueryParam;
use LaminasAttributeController\Security\CurrentUser;
use LaminasAttributeController\Security\IsGranted;

#[Route('/api/users', name: 'users')]
final class UserController extends AttributeActionController
{
    #[Route('/create', methods: ['POST'])]
    public function create(
        #[MapRequestPayload] UserCreateRequest $data
    ): array {
        // $data DTO is populated and validated
        return ['id' => $this->userService->create($data)];
    }

    #[Route('/search', methods: ['GET'])]
    public function search(
        #[MapQueryString] UserSearchRequest $criteria,
        #[QueryParam('token', required: true)] string $token
    ): array {
        // $criteria is filled and validated
        return [
          'criteria' => $criteria,
          'token' => $token,
          'results' => $this->userService->find($criteria)
        ];
    }

    #[Route('/profile', methods: ['GET'])]
    #[IsGranted('FULLY_AUTHENTICATED')]
    public function profile(#[CurrentUser] User $user): array {
        return ['user' => $user];
    }
}

Why use it:

If you like the clean, declarative style of frameworks like Symfony, but you use Laminas — this package can help improve your developer experience.

I’d love to hear your feedback!
I’m also planning to add more features like caching, OpenAPI support, and more.

Let me know what you think!

1 Like

I’m actively collecting feedback on compatibility with different Laminas‑based projects. :raising_hands: I’d love to hear from anyone planning to or already using this package in real applications—especially to learn how it fits with existing Laminas setups. Your insights will help improve stability and compatibility.

Please remember:

Clean? If I first have to search all controllers or request handlers for the defined routes, then I personally do not consider this to be clean or simple. :wink:

Doctrine as a required dependency?

Hi.
If you have a single central route config, that’s great. But in many projects routes are split across modules and nested arrays, which can be hard to find route. In those cases, attributes + IDE support often make it easier to find routes.

About Doctrine I fully agree - it’s only used for a couple of optional features like validation and automatically find record by id in route. I plan to move it to suggest to avoid dependencies it in by default. :+1: