tonybogdanov / mockable-annotations
Mockable Doctrine annotations
Requires
- php-64bit: ^7.3
- doctrine/annotations: ^1.10
Requires (Dev)
- ext-pcov: ^1.0
- jaschilz/php-coverage-badger: ^2.0
- phpunit/phpunit: ^9.1
This package is auto-updated.
Last update: 2024-12-03 21:37:40 UTC
README
Introduction
Doctrine Annotations are a powerful tool that allows specifying all sorts of meta information for classes, methods and properties, in a clean, semantic & predictable way.
One feature lacking in the default annotation readers is mock-ability, or the ability to inject annotations at runtime.
This can be particularly useful when the annotated classes are part of a third party library that you have no control
over. The default strategy in this case would be to extend or override the entire class, but that is not always
possible and is definitely not maintainable.
This package introduces a mock-able annotation reader, with support for various ways of "instructing" it how to read annotations. It supports overriding and merging class, method and property level annotations, and also exposes a lot of interfaces for you to introduce your own functionality.
It works as a wrapper to an annotation reader instance of your choice, so you can still benefit from its features (such as caching for example) with minimal effort.
Installation
composer require tonybogdanov/mockable-annotations
* By default the reader has dependency only on the doctrine/annotations
package, so if you intend to use
Doctrine's cached reader, make sure to also add doctrine/cache
to your dependencies.
Providers
The included MockableAnnotationReader
mocks annotations using annotation providers for class, method and property
level annotations respectively. Use any of the [clear|get|set|add][Class|Method|Property]MockProvider(s)
methods
to register your providers. The methods expect instances of the respective interfaces, so you can even build your
own provider if required.
The package comes with 3 default providers: ClassMockProvider
, MethodMockProvider
and PropertyMockProvider
with
support for override and merge strategies, optional filtering and priority (higher priority values are executed later).
Examples
The following class definition:
/** * @TestAnnotation("hello") */ class TestClass {}
Can have its class annotations mocked, so that the reader actually sees this:
/** * @AnotherTestAnnotation("world") */ class TestClass {}
With the following example code:
$newAnnotation = new AnotherTestAnnotation(); $newAnnotation->value = "world"; $reader = new MockableAnnotationReader( new AnnotationReader() ); $reader->addClassMockProvider( new ClassMockProvider( [ $newAnnotation ], new OverrideStrategy(), new ClassNameFilter( TestClass::class ) ) ); $reader->getClassAnnotations( TestClass::class ); // ...
Here we are using the OverrideStrategy
, which ignores the original annotations and replaces them with the custom ones
passed to the ClassMockProvider
. You can also use a MergeStrategy
, which will merge the original annotations with
the new ones instead. You can of course implement and pass your own strategy.
Additionally we are using a ClassNameFilter
, so that the provider will only be applied if the class being read is
an instance of TestClass
.
* If you need the filter to check if the class being read is exactly an instance of TestClass
and not an
instance of a class extending TestClass
, you can pass true
as a second argument to the ClassNameFilter
filter.
Mocking methods and properties follows the same principles with a very similar API.
Take a look at the source for reference.
Convenience Methods
Registering providers can be quite a verbose task, so for most common scenarios the reader exposes a couple of convenience methods instead.
-
[override|merge][Class|Method|Property]Annotations
- accepts a class name, (a method or property name), an array of annotations and an optional priority, registering a simple class / method / property annotations provider, only applicable in case the class / method / property being read matches the specified one. -
[override|merge]Alias[Class|Method|Property]Annotations
- accepts two pairs of class names, (and method or property names), where the first pair targets the class / method / property being read, and the second one targets a corresponding class / method / property who's annotations will override / be merged with the annotations of the source.
This can be useful when you don't want to specify annotations programmatically and want to get them from an "Alias" or "Dummy" class / method / property. -
[override|merge]AliasAnnotations
- probably the most convenient of all, this method accepts a source class name and a target class name, then overrides / merges all class / method & property annotations from the target class with the source class.
Keep in mind that only methods & properties in the source class will be inspected and matched against the target class. Annotations on methods and properties from the target (alias) class, which do not exist in the source one, will be ignored.
Mocking & Inheritance
Doctrine annotations are not inheritable by design. Meaning that if you have class A extending class B, and you define some class level annotations on B, retrieving annotations for A will not include them.
* Keep in mind that the above is only true for annotations. Methods and properties of parent classes are still visible and their annotations will be inspected in child classes.
The above, reflecting onto mocking, means that you would (in theory) need to separately mock both child and parent classes, since the reader will traverse them individually.
The MockableReader
can help alleviate this problem by respecting inheritance to some degree. When you mock class
B, all class level annotations will be applied (unless the strict
flag is set to TRUE
on the ClassNameFilter
)
when the reader inspects any class that inherits from B, even if you did not explicitly mock it.
If you did explicitly mock it, then the inherited annotations will be ignored.