Skip to content

Releases: debuss/attribute-routing

2.0.0

Choose a tag to compare

@debuss debuss released this 15 Sep 20:50
3c4aad9

2.0.0

This major release redesigns the attributes to follow common PHP conventions, makes route discovery stricter and fixes several bugs of the route loader. It contains breaking changes: see the upgrade guide below.

⚠️ Breaking changes

Attributes renamed

1.x 2.0
#[Controller] #[AsController]
#[ApiController] removed
#[BaseController] removed
#[HttpGet] #[Get]
#[HttpPost] #[Post]
#[HttpPut] #[Put]
#[HttpPatch] #[Patch]
#[HttpDelete] #[Delete]
#[HttpHead] #[Head]
#[HttpOptions] #[Options]
HttpMethod (abstract) #[Route] (concrete)
  • Controller conflicted with the base Controller class provided by many frameworks: importing both in the same namespace made controllers silently extend the attribute. AsController follows the As* naming convention used by Symfony.
  • ApiController and BaseController were plain aliases. BaseController also conflicted with CodeIgniter's base class. Create your own attribute extending AsController instead (see below).
  • The Http* prefix came from ASP.NET; short names (Get, Post, …) are the common convention in PHP libraries.

Stricter route discovery

  • #[AsController] is now required. Classes without #[AsController] (or an attribute extending it) are ignored, even if their methods have route attributes.
  • #[AsController] on an interface, a trait, an enum or an abstract class throws a LogicException, as only concrete classes can be controllers. PHP attributes are not inherited: each concrete controller must have its own #[AsController]. Routes of abstract parents and traits are still inherited by the concrete controllers extending or using them, with the prefix of the concrete controller.
  • A route attribute on a non-public method throws a LogicException, as no dispatcher can call it.
  • AttributeRouteLoader throws an InvalidArgumentException when the given path is not an existing directory (instead of an UnexpectedValueException from RecursiveDirectoryIterator).

Attributes API

  • Route (formerly HttpMethod) constructor signature is now Route(string|array $methods, string $path = '', string $name = '', int $priority = 0).
  • HTTP methods are normalized to uppercase ('get' becomes 'GET'), and an empty list of methods throws an InvalidArgumentException.
  • The properties of AsController and Route are now readonly.

✨ New features

  • #[Route] for several HTTP methods on the same action:
    #[Route(['GET', 'POST'], '/search', name: 'users.search')]
  • Optional path: #[Get(name: 'users.index')] on a controller prefixed with /users maps to /users.
  • Custom attributes: AsController and Route can be extended, for instance to define a controller attribute with a predefined prefix:
    #[Attribute(Attribute::TARGET_CLASS)]
    class AsApiController extends AsController
    {
        public function __construct(int $priority = 0)
        {
            parent::__construct('/api', $priority);
        }
    }

🐛 Bug fixes

  • The priority of a controller leaked to the routes of the classes loaded after it when they had no controller attribute.
  • Routes defined on private and protected methods were registered.
  • Routes defined in abstract classes and enums were registered with a non-instantiable handler.
  • The class name was built with str_replace() on the whole file path, which could alter paths containing the base directory or .php more than once.

📚 Documentation

  • New sections: discovery rules, route priority, name conflicts with frameworks, custom controller attributes.

🧰 Tooling

  • declare(strict_types=1) in every file.
  • PHPUnit test suite (100% line coverage): composer test.
  • PHPStan at level max: composer analyse.
  • GitHub Actions CI running composer validate, PHPStan and PHPUnit on PHP 8.3, 8.4 and 8.5.

⬆️ Upgrade guide

  1. Replace the imports and attributes following the table above:
    -use Routing\Attribute\{Controller, HttpGet, HttpPost};
    +use Routing\Attribute\{AsController, Get, Post};
    
    -#[Controller(prefix: '/users')]
    +#[AsController(prefix: '/users')]
     class UserController
     {
    -    #[HttpGet('/{id}', name: 'users.show')]
    +    #[Get('/{id}', name: 'users.show')]
         public function show(int $id): void {}
     }
  2. Replace #[ApiController] and #[BaseController] with #[AsController], or with your own attribute extending AsController.
  3. Add #[AsController] to every controller that did not have a controller attribute, otherwise its routes will no longer be loaded.
  4. Make public the methods holding route attributes, or remove their attributes.
  5. Move #[AsController] from abstract classes to their concrete controllers.
  6. If you extended HttpMethod, extend Route instead and update the call to the parent constructor: parent::__construct($methods, $path, $name, $priority).
  7. If a short attribute name conflicts with a class of your application (e.g. Options), import the namespace with an alias:
    use Routing\Attribute as Http;
    
    #[Http\Options('/users')]

Full Changelog: 1.1.0...2.0.0

1.1.0

Choose a tag to compare

@debuss debuss released this 11 Apr 11:38
927ad57

What's Changed

  • upd(priority): returned route definition are sorted by priority by @debuss in #1

New Contributors

  • @debuss made their first contribution in #1

Full Changelog: 1.0.0...1.1.0

1.0.0

Choose a tag to compare

@debuss debuss released this 10 Apr 14:51