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.