Sometimes it can be useful to use a callback function or other callable
to prevent a bunch of code duplication.
Let's say you have an event subscriber that encrypts and decrypts data in the life cycle of an entity. When the entity is stored, some data will be encrypted; and when the entity is loaded, the data will be decrypted.
Here is a maybe somewhat contrived but minimal example of this:
final class EncryptionSubscriber
{
public function __construct(private CryptographerInterface $cryptographer) { }
private function getEncryptedProperties(Entity $entity): array {
// some code to detect the encrypted properties of the entity.
return ['encrypted'];
}
public function onSave(Event $event): void {
$entity = $event->getEntity();
foreach ($this->getEncryptedProperties($entity) as $property) {
$value = $entity->getValue($property);
$entity->setValue($property, $this->cryptographer->encrypt($value));
}
}
public function onLoad(Event $event): void {
$entity = $event->getEntity();
foreach ($this->getEncryptedProperties($entity) as $property) {
$value = $entity->getValue($property);
$entity->setValue($property, $this->cryptographer->decrypt($value));
}
}
}
While this isn't that bad, the onSave
and onLoad
are practically the same method; only the content of setValue()
is different. And to be honest, with two tiny functions like these; I might not bother. But it often happens that three or even more methods are the same. Or the functions are way more elaborate; while they still only differ by a tiny thing.
Extracting a callback
helper
A solution to this duplication can be to extract one helper method that contains all the logic once. But at the point where the logic is different we call a callable
which is injected. The original onSave
and onLoad
will then defer to that helper method, and provide the callable
to handle the specific difference.
final class EncryptionSubscriber
{
// ...
public function onSave(Event $event): void
{
$this->processEvent($event, fn($value) => $this->cryptographer->encrypt($value));
}
public function onLoad(Event $event): void
{
$this->processEvent($event, fn($value) => $this->cryptographer->decrypt($value));
}
private function processEvent(Event $event, callable $callback): void
{
$entity = $event->getEntity();
foreach ($this->getEncryptedProperties($entity) as $property) {
$value = $entity->getValue($property);
$entity->setValue($property, $callback($value)); // [tl! highlight]
}
}
}
As you can see, we added a processEvent()
method that also receives a callable $callback
. This callback is executed with the current value instead of the previous encrypt
or decrypt
calls. This makes the entire method a very generic implementation. Only the specific difference is handled by the callback.
If the callback needs more context to do its job, that can be added as additional parameters on the $callback()
call.
While the onSave
and onLoad
still have some necessary duplication; the amount is way less and the difference is easier to spot. And with the help of some shorthand fn
it's very readable.
Thoughts and comments?
Like I said, there is a time and place for things like this. If the duplication is minimal; you might not need to bother with it. But I think it's a nice tool to know. Do you have other nice examples where this would be useful? Please share it with the rest of us. And if you have any other comments, drop them below.
Top comments (0)