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"
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.
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?
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)
{
// ...
}
}
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:
Hmm, what can we do now?
@NamedArgumentConstructor
to the RescueWe'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!
Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!