DEV Community

Doeke Norg
Doeke Norg

Posted on • Originally published at doeken.org

Using callbacks to prevent code duplication

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));
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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]
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)