A powerful PHP annotation reader for parsing, processing, and caching docblock annotations in PHP classes.
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.
- 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
composer require quellabs/annotation-reader
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);
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());
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
}
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);
The AnnotationCollection
provides three different methods to convert the collection to standard PHP arrays, each serving different use cases:
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.
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.
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.
- 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
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.
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;
}
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
}
}
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
(whenuse App\Models\User;
is present)
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
}
The AnnotationReader requires a Configuration object that specifies:
- Whether to use annotation caching
- The path to store annotation cache files
Contributions are welcome! Please feel free to submit a Pull Request.