quellabs/annotation-reader

A PHP annotations reader that parses, processes, and caches docblock annotations for classes, properties, and methods.

1.0.10 2025-06-27 05:29 UTC

This package is auto-updated.

Last update: 2025-07-01 12:02:23 UTC


README

Latest Version License Downloads

A powerful PHP annotation reader for parsing, processing, and caching docblock annotations in PHP classes.

Overview

The AnnotationReader component provides robust parsing and caching of PHP docblock annotations, allowing you to define metadata directly within your class docblocks. This approach makes your code more self-documenting and reduces the need for separate configuration files.

Features

  • Annotation parsing: Parse docblock annotations for classes, properties, and methods
  • Import resolution: Automatically resolves class imports for fully qualified annotation names
  • Class constant support: Supports fully qualified class constants in annotation parameters (e.g., ObjectName::class)
  • Performance optimization: Implements smart caching to improve performance
  • Flexible integration: Easy to integrate with your existing projects
  • Error handling: Graceful handling of malformed annotations
  • Collection support: Returns immutable AnnotationCollection objects with array-like access

Installation

composer require quellabs/annotation-reader

Usage

Basic Usage

use Quellabs\AnnotationReader\AnnotationsReader;
use Quellabs\AnnotationReader\Configuration;

// Create configuration
$config = new Configuration();
$config->setAnnotationCachePath(__DIR__ . '/cache');
$config->setUseAnnotationCache(true);

// Create annotation reader
$reader = new AnnotationsReader($config);

// Get annotations for a class
$classAnnotations = $reader->getClassAnnotations(MyClass::class);

// Get annotations for a class, filtered by a specific annotation
$classAnnotations = $reader->getClassAnnotations(MyClass::class, SomeAnnotation::class);

// Get annotations for a property
$propertyAnnotations = $reader->getPropertyAnnotations(MyClass::class, 'propertyName');

// Get annotations for a property, filtered by a specific annotation
$propertyAnnotations = $reader->getPropertyAnnotations(MyClass::class, 'propertyName', SomeAnnotation::class);

// Get annotations for a method
$methodAnnotations = $reader->getMethodAnnotations(MyClass::class, 'methodName');

// Get annotations for a method, filtered by a specific annotation
$methodAnnotations = $reader->getMethodAnnotations(MyClass::class, 'methodName', SomeAnnotation::class);

Working with AnnotationCollection

All annotation reader methods return an AnnotationCollection object that provides array-like access with a clean, flat structure:

$annotations = $reader->getMethodAnnotations(MyClass::class, 'myMethod');

// Array access by class name - returns first annotation of that type
$interceptor = $annotations[InterceptWith::class];

// Array access by numeric index - returns annotation at that position
$firstAnnotation = $annotations[0];
$secondAnnotation = $annotations[1];

// Iterate through all individual annotations
foreach ($annotations as $annotation) {
    // Each iteration gives you a single annotation object
}

// Get all annotations of a specific type (returns AnnotationCollection)
$allInterceptors = $annotations->all(InterceptWith::class);

// Check if multiple annotations of same type exist
if ($annotations->hasMultiple(InterceptWith::class)) {
    // Process multiple interceptors
}

// Collection methods
$count = count($annotations);
$isEmpty = $annotations->isEmpty();
$firstAnnotation = $annotations->first();
$lastAnnotation = $annotations->last();

// Filtering returns a new AnnotationCollection
$filtered = $annotations->filter(function($annotation) {
    return $annotation->isActive();
});

// Chaining operations
$activeInterceptors = $annotations
    ->all(InterceptWith::class)
    ->filter(fn($interceptor) => $interceptor->isActive());

Handling Multiple Annotations

When you have multiple annotations of the same type, the collection provides clean access patterns:

/**
 * @InterceptWith("AuthValidator")
 * @InterceptWith("LoggingInterceptor") 
 * @Route("/api/users")
 */
public function getUsers() { /* ... */ }
$annotations = $reader->getMethodAnnotations(MyClass::class, 'getUsers');

// Get first InterceptWith annotation
$firstInterceptor = $annotations[InterceptWith::class]; 

// Get all InterceptWith annotations as a collection
$allInterceptors = $annotations->all(InterceptWith::class);

// Check for multiple InterceptWith annotations
if ($annotations->hasMultiple(InterceptWith::class)) {
    foreach ($allInterceptors as $interceptor) {
        // Process each interceptor
    }
}

// Get single Route annotation
$route = $annotations[Route::class];

// Iterate through all annotations (individual objects)
foreach ($annotations as $annotation) {
    // Gets: AuthValidator, LoggingInterceptor, Route
}

Filtered Results

When filtering annotations, the result maintains the same clean structure:

// Filter by specific annotation class
$interceptors = $reader->getMethodAnnotations(MyClass::class, 'myMethod', InterceptWith::class);

// Or filter with custom logic
$activeAnnotations = $annotations->filter(fn($annotation) => $annotation->isActive());

// All results are AnnotationCollection with consistent access
$first = $interceptors[0];              // First annotation
$count = count($interceptors);          // Total count
foreach ($interceptors as $annotation) {
    // Iterate individual annotations
}

// Chain operations
$result = $annotations
    ->filter(fn($a) => $a->isActive())
    ->all(InterceptWith::class);

Array Conversion Methods

The AnnotationCollection provides three different methods to convert the collection to standard PHP arrays, each serving different use cases:

toArray() - Mixed Key Format

The toArray() method creates an array with hybrid indexing that provides both class-name access for the first occurrence of each annotation type and numeric indexing for duplicates:

/**
 * @Route("/api/users")
 * @InterceptWith("AuthValidator")
 * @InterceptWith("LoggingInterceptor")
 * @Cache(ttl=3600)
 */
public function getUsers() { /* ... */ }

$annotations = $reader->getMethodAnnotations(MyClass::class, 'getUsers');
$array = $annotations->toArray();

// Result structure:
// [
//     'App\Annotations\Route' => Route("/api/users"),
//     'App\Annotations\InterceptWith' => InterceptWith("AuthValidator"),
//     0 => InterceptWith("LoggingInterceptor"),  // Second InterceptWith uses numeric key
//     'App\Annotations\Cache' => Cache(ttl=3600)
// ]

// Access patterns:
$route = $array[Route::class];                    // First (and only) Route
$firstInterceptor = $array[InterceptWith::class]; // First InterceptWith
$secondInterceptor = $array[0];                   // Second InterceptWith
$cache = $array[Cache::class];                    // Cache annotation

This format is ideal when you need both convenient class-name access and want to preserve all duplicate annotations in a single array structure.

toIndexedArray() - Linear Format

The toIndexedArray() method returns a simple indexed array containing all annotations in their original order:

$annotations = $reader->getMethodAnnotations(MyClass::class, 'getUsers');
$array = $annotations->toIndexedArray();

// Result structure:
// [
//     0 => Route("/api/users"),
//     1 => InterceptWith("AuthValidator"),
//     2 => InterceptWith("LoggingInterceptor"),
//     3 => Cache(ttl=3600)
// ]

// Access patterns:
$firstAnnotation = $array[0];   // Route
$secondAnnotation = $array[1];  // First InterceptWith
$thirdAnnotation = $array[2];   // Second InterceptWith

// Iterate through all annotations
foreach ($array as $index => $annotation) {
    echo "Annotation {$index}: " . get_class($annotation) . "\n";
}

This format is perfect for sequential processing, serialization, or when you need a simple list without any special key handling.

toGroupedArray() - Grouped by Class

The toGroupedArray() method organizes annotations by their class names, with each class name mapping to an array of all annotations of that type:

$annotations = $reader->getMethodAnnotations(MyClass::class, 'getUsers');
$array = $annotations->toGroupedArray();

// Result structure:
// [
//     'App\Annotations\Route' => [
//         0 => Route("/api/users")
//     ],
//     'App\Annotations\InterceptWith' => [
//         0 => InterceptWith("AuthValidator"),
//         1 => InterceptWith("LoggingInterceptor")
//     ],
//     'App\Annotations\Cache' => [
//         0 => Cache(ttl=3600)
//     ]
// ]

// Access patterns:
$routes = $array[Route::class];           // Array of Route annotations
$interceptors = $array[InterceptWith::class]; // Array of InterceptWith annotations

// Process all interceptors
foreach ($array[InterceptWith::class] as $interceptor) {
    // Handle each interceptor
}

// Check if specific annotation type exists
if (isset($array[Cache::class])) {
    $cacheAnnotations = $array[Cache::class];
}

// Get count of specific annotation type
$interceptorCount = count($array[InterceptWith::class] ?? []);

This format is excellent for processing annotations by type, configuration systems that need to handle multiple instances of the same annotation, or when building annotation-driven frameworks.

Choosing the Right Conversion Method

  • Use toArray() when you need convenient access to single annotations by class name but also want to preserve duplicates in the same structure
  • Use toIndexedArray() for simple sequential processing, serialization, or when working with external APIs that expect indexed arrays
  • Use toGroupedArray() when building systems that process annotations by type, handling multiple instances of the same annotation class, or creating configuration arrays

Annotation Format

Annotations are defined in PHP docblocks using the @ symbol followed by the annotation name and optional parameters. The annotation reader supports various parameter formats including strings, numbers, booleans, arrays, and the ::class magic constant.

Basic Annotations

Simple annotations with string, numeric, and boolean parameters:

/**
 * @Table(name="products")
 * @Entity
 * @Cache(ttl=3600, enabled=true)
 */
class Product {
    /**
     * @Column(type="integer", primary=true, autoincrement=true)
     */
    private $id;
    
    /**
     * @Column(type="string", length=255)
     * @Validate("required")
     * @Validate("maxLength", 255)
     */
    private $name;
}

Using Class Constants

Annotations with ::class magic constants for type-safe class references:

use App\Models\User;
use App\Services\ValidationService;
use App\Events\UserCreated;

/**
 * @Entity(repository=UserRepository::class)
 * @EventListener(event=UserCreated::class)
 */
class UserService {
    /**
     * @Inject(service=ValidationService::class)
     * @Cache(driver=RedisDriver::class)
     */
    private $validator;
    
    /**
     * @Transform(transformer=UserTransformer::class)
     * @Authorize(policy=UserPolicy::class)
     */
    public function getUser(int $id): User {
        // Method implementation
    }
}

Supported Parameter Types

The annotation reader supports these parameter formats:

  • Strings: "value" or 'value'
  • Numbers: 42, 3.14
  • Booleans: true, false
  • Arrays: {"item1", "item2"} or {key="value"}
  • Class constants: Only ::class magic constant is supported
  • Magic class constant: SomeClass::class
  • Fully qualified names: \App\Models\User::class
  • Imported classes: User::class (when use App\Models\User; is present)

Mixed Parameter Types

You can combine different parameter types within the same annotation:

/**
 * @ComplexAnnotation(
 *     type=User::class,
 *     name="user_service",
 *     priority=10,
 *     enabled=true,
 *     tags={"user", "service"}
 * )
 */
class UserService {
    // Class implementation
}

Configuration

The AnnotationReader requires a Configuration object that specifies:

  • Whether to use annotation caching
  • The path to store annotation cache files

Contributing

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