Doctrine Annotations and Attributes Living Together in Peace
In previous post How to Refactor Custom Doctrine Annotations to Attributes we looked on how to make the @annotation
to #[Attribute]
transition.
Last week, we started refactoring in my favorite long-term project, and we came to a challenging situation. When we started to move all annotations to attributes at once, we lost control over the results. It was also impossible because 3rd party annotations were not attributes ready.
We had to support annotations and attributes at the same time. Do you have plenty of custom annotations yourself? In this post, you'll learn to build a bridge with both annotations and attributes on board.
"United we stand,
Divided we fall"
Safety First
Before we start, we'll make sure we have instant feedback about code. We'll prepare a special kind of test designed for refactoring, where we need 2 mechanism to work at once - a bridge testing. In linked post we'll build a test that we use here, so if you're coding with me, be sure to read it first.
Teaching Annotation Attributes
We already know the first steps from previous post - add #[Attribute]
:
/**
* @Annotation()
*/
#[Attribute]
class AttentionPrice
{
// ...
public function __construct(array $values)
{
$this->amount = $values['amount'];
}
// ...
}
The problem is that annotation requires an array in the constructor - array $values
.
But attributes accept specific value:
/**
* @AttentionPrice(1000) // array with int
*/
#[AttentionPrice(1000)] // int
public $publicProperty;
What can we do about different construction needs?
2 Different Classes?
An annotation class with array
contract:
/**
* @Annotation()
*/
class AttentionPrice
{
public function __construct(array $values)
{
// ...
}
}
And an attribute class with int
contract:
#[Attribute]
class AttentionPrice
{
public function __construct(int $amount)
{
// ...
}
}
2 Constructors?
public function __construct($values)
{
// annotatoin
if (is_array($values)) {
$this->amount = $values['amount'];
// attribute
} elseif (is_int($values)) {
$this->amount = $values;
} else {
// exception
}
}
Try both options, and you'll soon see that they both smell:
- First is nice and clean but gives you extra work with duplicated classes ❌
- Second is a headache to write and read. A that's only one value, imagine there are 2 or 3 values ❌
Hmm, what can we do now?
@NamedArgumentConstructor
to the Rescue
We're lucky, doctrine/annotations
got us covered since 1.12
. I've learned this trick from Koriym - thanks!
+use Doctrine\Common\Annotations\NamedArgumentConstructor;
/**
* @Annotation()
+ * @NamedArgumentConstructor()
*/
+#[Attribute]
class AttentionPrice
{
private int $amount;
- public function __construct(array $values)
+ public function __construct(int $amount)
{
$this->amount = $amount;
}
// ...
}
Thanks to the @NamedArgumentConstructor
, the constructors are now the same for both annotation and attribute ✅
Big thanks to Alexander M. Turek, who contributed this feature for doctrine/annotations
and Vincent who added support for default value unwrap.
Now we can use both annotation and attributes in our code with the same result:
/**
* @AttentionPrice(1000) // int
*/
#[AttentionPrice(1000)] // int
public $publicProperty;
Happy coding!
Have you find this post useful? Do you want more?
Follow me on Twitter, RSS or support me on GitHub Sponsors.