When developing an external component which can be used in other projects, we would want to make it extensible so other developers can add their own capabilities.
In this post I would like to share how to drive it when developing a symfony third party bundle. This is a short example and the idea can be extrapolated to other languages or frameworks.
Let's imagine we are developing a bundle which allows us to send notifications by using three channels: Email, Push notification and sms. Our bundle define an interface which looks like this:
interface NotificationInterface {
public function sendNotification(array $data): bool;
public function getName(): string;
}
Each of our notifications handlers will look like this:
class EmailNotificationHandler implements NotificationInterface {
public function sendNotification(array $data): void
{
/* handler email notification. Throw an exception in
case on error */
}
public function getName(): string
{
return 'email';
}
}
The other ones (sms and push) will look as email one but its sendNotification implementation would be different.
Now, It's time to tell our bundle to tag all services which implements NofiticationInterface properly. To do it, we need to register it in our extension class (under DependencyInjection folder)
class MyBundleExtension extends Extension
{
/**
* @throws \Exception
*/
public function load(array $configs, ContainerBuilder $container)
{
// ... load services from file (services.xml, services.yaml or services.php)
$container->registerForAutoconfiguration(NotificationInterface::class)->addTag('my_bundle.notification');
// other loads ....
}
}
Now, in our project, we can use symfony tagged_iterator shortcut to get an iterable holding all notification handlers. We can do it simply by binding a variable in our services.yaml file:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
$notificationHandlers: !tagged_iterator my_bundle.notification
Now, if we inject $notificationHandlers in any of our services we will have access to all notification handlers. If we would create a new notification handler, for instance, TelegramNotificationHandler, it would be tagged as the other ones since it would implement NotificationInterface and it would be available in $notificationHandlers iterable
With this, we follow the open-closed principle since we can add other notification handlers without modifying the existing ones.
Tagged iterators are really useful to create central repositories of services which are easily accesible. In my recently published book, I use them to create an accesible collection of api operations. If you want to know more, you can find the book here