dentelis/php7-attribute-reader

Library for reading php8 attributes from legacy php7 code

Maintainers

Package info

github.com/dentelis/php7-attribute-reader

pkg:composer/dentelis/php7-attribute-reader

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

dev-main 2026-03-10 15:11 UTC

This package is auto-updated.

Last update: 2026-03-10 15:11:31 UTC


README

A fast and simple library that brings PHP 8 attribute syntax to PHP 7.2+. Read and parse PHP 8 style attributes from comments in legacy PHP codebases.

PHP Version License

๐ŸŽฏ Why This Library?

PHP 8 introduced native attributes (also known as annotations in other languages), but many projects are still running on PHP 7.x. This library allows you to:

  • Write modern PHP 8 attribute syntax that works in PHP 7.2+
  • Future-proof your codebase - attributes written with this library work as comments in PHP 7 and as native attributes in PHP 8
  • Gradually migrate from PHP 7 to PHP 8 without rewriting your metadata
  • Use attributes for frameworks that don't support PHP 8 yet

๐Ÿ“ฆ Installation

composer require dentelis/php7-attribute-reader

๐Ÿš€ Quick Start

Basic Usage with Plain Arrays

use AttributeReader\AttributeReader;

class UserController
{
    #[Route('/api/users', 'GET')]
    #[Auth('admin')]
    public function getUsers()
    {
        // Your code here
    }
}

// Read attributes as plain arrays
$attributes = AttributeReader::getPlainAttributes(
    UserController::class,
    'getUsers'
);

// Result:
// [
//     ['name' => 'Route', 'arguments' => ['/api/users', 'GET']],
//     ['name' => 'Auth', 'arguments' => ['admin']]
// ]

Advanced Usage with DTO Classes (Recommended)

Define your attribute classes:

namespace App\Attributes;

class Route
{
    public $path;
    public $method;

    public function __construct(string $path, string $method = 'GET')
    {
        $this->path = $path;
        $this->method = $method;
    }
}

class Auth
{
    public $role;

    public function __construct(string $role)
    {
        $this->role = $role;
    }
}

Use them in your code:

class UserController
{
    #[Route('/api/users', 'POST')]
    #[Auth('admin')]
    public function createUser($data)
    {
        // Your code here
    }
}

// Create reader with attribute mapping
$reader = new AttributeReader([
    'Route' => \App\Attributes\Route::class,
    'Auth' => \App\Attributes\Auth::class,
]);

// Get attribute instances
$attributes = $reader->getAttributes(UserController::class, 'createUser');

// Work with strongly-typed objects
foreach ($attributes as $attribute) {
    if ($attribute instanceof \App\Attributes\Route) {
        echo "Path: {$attribute->path}, Method: {$attribute->method}\n";
        // Output: Path: /api/users, Method: POST
    }

    if ($attribute instanceof \App\Attributes\Auth) {
        echo "Required role: {$attribute->role}\n";
        // Output: Required role: admin
    }
}

๐ŸŽจ Features

Named Arguments Support

Use PHP 8 named arguments syntax:

class ProductController
{
    #[Route(path: '/products', method: 'GET')]
    #[Cache(ttl: 3600, enabled: true)]
    public function list()
    {
        // Your code here
    }
}

$reader = new AttributeReader([
    'Route' => \App\Attributes\Route::class,
    'Cache' => \App\Attributes\Cache::class,
]);

$attributes = $reader->getAttributes(ProductController::class, 'list');

Mixed Positional and Named Arguments

#[Route('/api/users', method: 'POST')]  // First arg positional, second named
#[Cache(ttl: 3600, maxSize: 100)]       // All named
#[Auth('admin')]                        // Positional

Multiple Attributes

You can use multiple attributes per method:

// Separate lines
#[Route('/admin')]
#[Auth('admin')]
#[RateLimit(100)]
public function adminDashboard() {}

// Or comma-separated in one line
#[Route('/admin'), Auth('admin'), RateLimit(100)]
public function adminDashboard() {}

Unmapped Attribute Handling

Control what happens when an attribute isn't mapped to a class:

// Ignore unmapped attributes (default)
$reader = new AttributeReader($map, AttributeReader::UNMAPPED_IGNORE);

// Throw exception for unmapped attributes
$reader = new AttributeReader($map, AttributeReader::UNMAPPED_THROW);

๐Ÿ“– Complete Example: Building a Router

Here's a practical example of building a simple router using attributes:

<?php

namespace App\Attributes;

class Route
{
    public $path;
    public $method;
    public $middleware;

    public function __construct(
        string $path,
        string $method = 'GET',
        array $middleware = []
    ) {
        $this->path = $path;
        $this->method = $method;
        $this->middleware = $middleware;
    }
}

class Middleware
{
    public $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

// Your controller
class UserController
{
    #[Route('/api/users', 'GET')]
    #[Middleware('auth')]
    public function list()
    {
        return ['users' => []];
    }

    #[Route(path: '/api/users', method: 'POST')]
    #[Middleware('auth')]
    #[Middleware('admin')]
    public function create($data)
    {
        return ['created' => true];
    }

    #[Route('/api/users/{id}', 'DELETE')]
    #[Middleware('auth')]
    public function delete($id)
    {
        return ['deleted' => true];
    }
}

// Router implementation
class Router
{
    private $reader;
    private $routes = [];

    public function __construct()
    {
        $this->reader = new \AttributeReader\AttributeReader([
            'Route' => Route::class,
            'Middleware' => Middleware::class,
        ]);
    }

    public function registerController(string $controllerClass)
    {
        $reflection = new \ReflectionClass($controllerClass);

        foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
            $attributes = $this->reader->getAttributes(
                $controllerClass,
                $method->getName()
            );

            $route = null;
            $middleware = [];

            foreach ($attributes as $attribute) {
                if ($attribute instanceof Route) {
                    $route = $attribute;
                }
                if ($attribute instanceof Middleware) {
                    $middleware[] = $attribute->name;
                }
            }

            if ($route !== null) {
                $this->routes[] = [
                    'path' => $route->path,
                    'method' => $route->method,
                    'handler' => [$controllerClass, $method->getName()],
                    'middleware' => $middleware,
                ];
            }
        }
    }

    public function getRoutes()
    {
        return $this->routes;
    }
}

// Usage
$router = new Router();
$router->registerController(UserController::class);

print_r($router->getRoutes());

// Output:
// Array
// (
//     [0] => Array
//         (
//             [path] => /api/users
//             [method] => GET
//             [handler] => Array
//                 (
//                     [0] => UserController
//                     [1] => list
//                 )
//             [middleware] => Array
//                 (
//                     [0] => auth
//                 )
//         )
//     [1] => Array
//         (
//             [path] => /api/users
//             [method] => POST
//             [handler] => Array
//                 (
//                     [0] => UserController
//                     [1] => create
//                 )
//             [middleware] => Array
//                 (
//                     [0] => auth
//                     [1] => admin
//                 )
//         )
//     ...
// )

๐Ÿ”ง API Reference

Static Method: getPlainAttributes

Returns attributes as plain arrays without any class instantiation.

public static function getPlainAttributes(
    string $className,
    string $functionName
): array

Returns: Array of associative arrays with name and arguments keys.

[
    ['name' => 'Route', 'arguments' => ['/api/users', 'GET']],
    ['name' => 'Cache', 'arguments' => ['ttl' => 3600]]
]

Instance Method: getAttributes

Returns attribute instances based on provided mapping.

public function getAttributes(
    string $className,
    string $functionName
): array

Returns: Array of instantiated attribute objects.

Constructor

public function __construct(
    array $attributeMap = [],
    string $unmappedStrategy = AttributeReader::UNMAPPED_IGNORE
)

Parameters:

  • $attributeMap - Associative array mapping attribute names to fully qualified class names
  • $unmappedStrategy - Strategy for handling unmapped attributes:
    • AttributeReader::UNMAPPED_IGNORE (default) - Skip unmapped attributes
    • AttributeReader::UNMAPPED_THROW - Throw RuntimeException for unmapped attributes

๐Ÿงช Testing

composer test

๐Ÿ“ Requirements

  • PHP 7.2 or higher
  • Composer

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

๐Ÿ“„ License

This project is licensed under the MIT License.

๐Ÿ™ Credits

Developed by Dim Entelis

๐Ÿ’ก Use Cases

  • Legacy Framework Migration - Gradually introduce modern attribute syntax
  • Custom Framework Development - Build attribute-based routing, validation, caching
  • API Documentation - Generate API docs from attributes
  • Access Control - Define permissions using attributes
  • Event Listeners - Register event handlers via attributes
  • Validation Rules - Define validation schemas with attributes
  • Caching Policies - Configure caching behavior through attributes
  • Dependency Injection - Mark injectable services

๐Ÿ”ฎ Future PHP 8 Compatibility

When you migrate to PHP 8, the syntax remains the same! Your attributes will work as native PHP 8 attributes without any code changes:

// This works in both PHP 7 (via this library) and PHP 8 (natively)
#[Route('/api/users')]
public function getUsers() {}