Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Valinor Bundle Laravel Package

cuyz/valinor-bundle

View on GitHub
Deep Wiki
Context7
2.3.0

Notable changes

HTTP request mapping support

The bundle provides automatic mapping of HTTP request values to controller arguments using attributes. This feature leverages Valinor's mapping capabilities to handle route parameters, query values, and request body data.

Lean more about HTTP request mapping in the library documentation.

Note that Symfony provides a similar built-in solution, which makes use of attributes like #[MapQueryString] and #[MapRequestPayload]. This bundle can bring some additional features:

  • Ability to map advanced types like non-empty-string, positive-int, int<10, 100> and more.
  • Precise error messages when a request contains invalid values.
  • Easy customization of the mapping process using mapper configurators.
  • And, in the end, any other feature provided by Valinor's mapping system.
Basic usage

Using the #[MapRequest] on a controller's method enables automatic mapping.

Arguments can be mapped from different sources:

  • Route parameters — using #[FromRoute] attribute
  • Query parameters — using #[FromQuery] attribute
  • Request body — using #[FromBody] attribute

Basic example:

use CuyZ\Valinor\Mapper\Http\FromQuery;
use CuyZ\Valinor\Mapper\Http\FromRoute;
use CuyZ\ValinorBundle\Http\MapRequest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

#[AsController]
final class ListArticles
{
    /**
     * GET /api/authors/{authorId}/articles?status=X&page=X&limit=X
     *
     * [@param](https://github.com/param) positive-int $page
     * [@param](https://github.com/param) int<10, 100> $limit
     */
    #[Route('/api/authors/{authorId}/articles', methods: 'GET')]
    #[MapRequest]
    public function __invoke(
        // Comes from the route
        #[FromRoute] string $authorId,

        // All come from query parameters
        #[FromQuery] string $status,
        #[FromQuery] int $page = 1,
        #[FromQuery] int $limit = 10,
    ): Response { /* … */ }
}
Per-controller mapper configuration

You can customize the mapper behavior for a specific controller by passing mapper configurators to the #[MapRequest] attribute:

use CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase;
use CuyZ\Valinor\Mapper\Http\FromBody;
use CuyZ\ValinorBundle\Http\MapRequest;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

#[AsController]
final class CreateAuthor
{
    #[Route('/api/authors/new', methods: 'POST')]
    #[MapRequest(new ConvertKeysToCamelCase())]
    public function __invoke(
        #[FromBody] string $name,
        #[FromBody] DateTimeInterface $birthDate,
    ): Response { /* … */ }
}

APIs often need to define rules concerning the keys cases passed in the request; this can be defined using the following configurators:

Custom request mapping attribute

When multiple controllers share the same mapper configuration (date formats, key case rules, etc.), a custom attribute can be created to avoid repeating the same configurators on every controller.

This is done by implementing the MapRequestAttribute interface directly:

use Attribute;
use CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase;
use CuyZ\Valinor\Mapper\Configurator\RestrictKeysToSnakeCase;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\ValinorBundle\Http\MapRequestAttribute;

#[Attribute(Attribute::TARGET_METHOD)]
final class MyAppMapRequest implements MapRequestAttribute
{
    public function __construct(
        /** [@var](https://github.com/var) list<non-empty-string> */
        private array $dateFormats = ['Y-m-d', 'Y-m-d H:i:s'],
        private bool $allowSuperfluousKeys = false,
    ) {}

    public function configureMapperBuilder(MapperBuilder $builder): MapperBuilder
    {
        $builder = $builder->configureWith(
            // Always restrict keys to `snake_case`
            new RestrictKeysToSnakeCase(),
            // Always convert keys to `camelCase`
            new ConvertKeysToCamelCase(),
        );

        $builder = $builder->supportDateFormats(...$this->dateFormats);

        if ($this->allowSuperfluousKeys) {
            $builder = $builder->allowSuperfluousKeys();
        }

        return $builder;
    }
}

It can then be used in place of #[MapRequest] on any controller method:

use CuyZ\Valinor\Mapper\Http\FromBody;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

#[AsController]
final class CreateComment
{
    #[Route('/api/comments', methods: 'POST')]
    #[MyAppMapRequest(
        dateFormats: ['d/m/Y'],
        allowSuperfluousKeys: true,
    )]
    public function __invoke(
        #[FromBody] string $author,
        #[FromBody] string $content,
    ): Response { /* … */ }
}
Error handling

When mapping fails, the bundle throws an HttpRequestMappingError exception with a 422 Unprocessable Entity status code. The error message includes all validation errors. Example:

HTTP request is invalid, a total of 2 error(s) were found:
- page: value 0 is not a valid positive integer.
- limit: value 150 is not a valid integer between 10 and 100.
Mapping all parameters at once

Instead of mapping individual query parameters or body values to separate parameters, the mapAll option can be used to map all of them at once to a single parameter. This is useful when working with complex data structures or when the number of parameters is large.

use CuyZ\Valinor\Mapper\Http\FromQuery;
use CuyZ\Valinor\Mapper\Http\FromRoute;
use CuyZ\ValinorBundle\Http\MapRequest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

final readonly class ArticleFilters
{
    public function __construct(
        public string $status,
        /** [@var](https://github.com/var) positive-int */
        public int $page = 1,
        /** [@var](https://github.com/var) int<10, 100> */
        public int $limit = 10,
    ) {}
}

#[AsController]
final class ListArticles
{
    /**
     * GET /api/authors/{authorId}/articles?status=X&page=X&limit=X
     */
    #[Route('/api/authors/{authorId}/articles', methods: 'GET')]
    #[MapRequest]
    public function __invoke(
        #[FromRoute] string $authorId,
        #[FromQuery(mapAll: true)] ArticleFilters $filters,
    ): Response { /* … */ }
}

The same approach works with #[FromBody(mapAll: true)] for body values.

Request object mapping

When a controller needs to access the original request object, it can be directly added as an argument:

use CuyZ\Valinor\Mapper\Http\FromRoute;
use CuyZ\ValinorBundle\Http\MapRequest;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

#[AsController]
final class ListArticles
{
    #[Route('/api/authors/{authorId}/articles', methods: 'GET')]
    #[MapRequest]
    public function __invoke(
        // Request object injected automatically
        Request $request,

        #[FromRoute] string $authorId,
    ): Response {
        if ($request->headers->has('My-Customer-Header')) {
            // …
        }
    }
}

Note — by enabling the valinor.http.convert_request_to_psr configuration, controllers can type-hint a PSR-7 ServerRequestInterface parameter instead of Symfony's Request. The bundle will automatically convert the incoming Symfony request to a PSR-7 instance.

This requires the symfony/psr-http-message-bridge package to be installed.

Features

  • Add support for HTTP request mapping using attributes (3b4c6b)
  • Support upstream library configurator interfaces (af68ac)
2.2.0

Features

  • Add support for Symfony 8.0 (2a0849)

Other

  • Drop support for PHP 8.1 (89a9f8)
  • Drop support for Symfony 5.4 (7ff318)
2.1.0

Features

  • Add support for PHP 8.5 (b4808a)

Bug Fixes

  • Automatically register cache for normalizer builder (583b3f)
2.0.0

First, check out the Valinor 2.0 upgrade guide.

List of backward compatibility breaking changes in the Valinor Bundle:

  • The method MapperBuilderConfigurator::configure() has been renamed to MapperBuilderConfigurator::configureMapperBuilder
1.0.0

1.0.0 (2025-05-29)

First stable release 🎉


Note that this release also includes a breaking change: the feature that allowed attributes to configure a TreeMapper directly during the injection has been removed.

After some thoughts, this implementation was a bad idea that led to more complexity in the bundle code base, for something that should anyway be done differently.

There will be no replacement for this feature, and code that used it should instead either inject an instance of MapperBuilder or use the MapperBuilderConfigurator interface.

⚠ BREAKING CHANGES

  • Remove MapperBuilderConfiguratorAttribute support (ddcf2f)

Features

  • Register ArrayNormalizer and JsonNormalizer as services (dcfd1c)
0.4.1

0.4.1 (2024-11-24)

Bug Fixes

  • Explicitly mark service parameter as nullable (0232d9)
0.4.0

Features

  • Add support for PHP 8.4 (52fe0a)
0.3.0

Features

  • Add alias for MapperBuilder (23e8c6)

Bug Fixes

  • Solve deprecation message regarding warm-up class (2d6bba)

Other

  • Drop support for PHP 8.0 (d631b2)
0.2.3

Bug Fixes

  • Set class name in compiler pass definition (3ef094)

Other

  • Watch files in test environment by default (2abfbb)
0.2.2

Bug Fixes

  • Correctly fetch Kernel environment in services configuration (882778)
0.2.1

Bug Fixes

  • Disable injection autowiring for WarmupForMapper attribute (c5a984)
0.2.0

Features

  • Add support for PHP 8.3 (0c0735)

Bug Fixes

  • Disable injection autowiring for WarmupForMapper attribute (2dc2b9)
  • Run cache warmup even if no class was provided (0fa125)
0.1.0

Initial release 🎉

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor
spatie/laravel-javascript-views