Menu

Introducing Mangan Aspects

Isolating scope with self executing anonymous function in PHP

Dev Blog

When using our favorite IDE, we usually have option to find all implementations of interface. This gives us overview of available classes that can do certain operations or have certain purpose. When developing modular application the new types might appear when we install additional dependencies. As well some implementations - through less common - could possibly disappear on dependency upgrade. We have developers tools to to keep our applications up to date to our or external vendor dependencies. But, hey we are developers who do not like to track manually all changes, as it's error prone and tedious. For instance if we have some notification interface and bunch of notifiers. At some point we need to collect those notifiers and notify with them about our application action. The other example might product interface indicating that class represents some kind of product in our system. Then when creating new product, we've had to choose which one from the list of all implementations of product interface. To avoid hardcoding such list of available implementations we could use configuration file to set up all available options. But that's in fact hardcoding configuration, as it would likely be committed with code.

As in real life, often we need to collect some kind of things. Things that have certain purpose.

Collecting Options

How about if we could collect such classes automatically in our application? So that we could have literally array of objects implementing particular interface. The PHP does not facilitate such feature. First thought might be to use the built-in get_declared_classes PHP function. But it will return only classes that are already included. With currently widely used class auto-loaders the get_declared_class will fail in our use case.

The brute-force method could be used to obtain list of interfaces - just scan all files and check for implementations - then cache it. That's the way to go, however with many pitfalls. Often it is not even possible to autoload some classes or even include as it will result in fatal error. Such list could be useful for smaller projects.

Using Signals

TLDR/Get repository with example:

git clone https://github.com/MaslosoftGuides/signals.collecting-interfaces.git
cd signals.collecting-interfaces
composer install
php signals.php

The signals project is designed to make free communication with application components, somewhat like connecting devices with WI-FI versus connecting by wire. So that once configured and in range, components will communicate. In other words, once components are added and signals definition generated the components can communicate. At very first implementations signals could be attached to classes only, however newer versions support adding signals and slots also to traits and interfaces. Particularly interested side feature appeared for interface declared signals. Adding @SignalFor annotation on top of interface declaration will actually instruct gather to collect all classes implementing this interface! Then with simple method call we can obtain object instances implementing such interface. The implementing class might or might not be aware of being gathered, as the annotation is on class definition itself, so that it does not require any method or property. In fact even empty interface can be used. 

Before staring example, please add signals to our project with composer require maslosoft/signals command.

For example let's create our NotifierInterface:

<?php

namespace Maslosoft\Guides\Signals\Interfaces;

use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
use Maslosoft\Guides\Signals\Slots\NotifierSlot;

/**
 * @SignalFor(NotifierSlot)
 * @see NotifierSlot
 */
interface NotifierInterface extends AnnotatedInterface
{
        public function notify($message);
}

Notice the @see PHP doc block instructing IDE to keep use statement for NotifierSlot.

Notice that to have class processed by annotations engine, it must implement AnnotatedInterface. Either extend NotifierInterface from AnnotatedInterface or let implementing classes implement this.

Having class used as signal, or more like beacon we need also receiver. The receiver is used to trigger gathering as parameter to gather method. The receiver must implement SlotInterface which will receive signal by setSignal method when gathering beacon signals. In our notifier example, the $signal value will be instance of concrete receiver. The receiver can also do something with received result before returning it back.

Example receiver for our NotifierInterface beacon signal, this will be used to gather notifiers:

<?php

namespace Maslosoft\Guides\Signals\Slots;

use Maslosoft\Signals\Interfaces\SlotInterface;
use Maslosoft\Signals\ISignal;

class NotifierSlot implements SlotInterface
{
        private $notifier = null;

        public function setSignal(ISignal $signal)
        {
                $this->notifier = $signal;
        }

        public function result()
        {
                return $this->notifier;
        }
}

The next thing to do is to create some notifiers, by creating classes implementing NotifierInterface with one method notify. In our example this will only echo provided message suffixed with class name.

To make signals scanning faster, the signals.yml file can be added, specifying which paths to scan:

paths:
- src

Now finally call signals scanning command can be issued:

vendor/bin/signals build

Bear in mind that if You use composer autoloading and just added autoload config part to composer.json, run composer dumpautoload first. The signals scanner will show errors indicating any problems encountered, however it will try to continue scanning. If autoloading is not properly configured in our example it will display error message like following:

Error: Could not find class NotifierSlot, 
when processing annotations on Maslosoft\Guides\Signals\Interfaces\NotifierInterface, near NotifierSlot)
 * @see NotifierSlot
 */ while scanning file src/Interfaces/NotifierInterface.php

When build is complete without errors, there will be - linux style - no output. To get some verbose feedback add -v up to -vvv parameter (verbose, very verbose, very very verbose):

vendor/bin/signals build -vvv

This command will create runtime and generated folders. The runtime content should not be added to repository, while generated should be committed along with code. The generated folder should contain signals-definition.php file containing relations between signals and slots.

To see the results, we've add an application entry file in project root folder, let's name it signals.php and trigger our notifiers in it. To make it work we need to include composer autoload file too:

<?php

use Maslosoft\Guides\Signals\Interfaces\NotifierInterface;
use Maslosoft\Guides\Signals\Slots\NotifierSlot;
use Maslosoft\Signals\Signal;

require 'vendor/autoload.php';

$results = (new Signal)->gather(new NotifierSlot);
/* @var $results NotifierInterface[] */
foreach($results as $notifier)
{
        $notifier->notify('Hello Signals!');
}

When using repository project, to see the results, run php signals.php, You should have output like this:

Email   : Hello Signals!
Facebook: Hello Signals!
Twitter : Hello Signals!

Summary

While the setup might seem to be complicated, the benefits are that adding new notifiers will automatically gather them by signals mechanism. This includes those in current project as well as additionally added as external libraries. No matter on which namespace class is created. The only thing which need to be made after adding new components is to run vendor/bin/signals build command. Removing components is even easier, as signals gathering will skip not found classes. In the large projects, using multiple libraries using signals is inevitable, as it helps keeping communication tight and robust.

You might have noticed, that gather mechanism will create new instances of notifiers. In this example those do not have any configurable properties. But in many projects, objects do require some parameters. The best way to apply configuration is to use dependency injection, or even better embedded dependency injection.

The Signals do not require database and when signals definition is committed along with the code, application is ready to use right away. Signals definitions can be even combined from different sources, but that's a topic for another article.