author: Michal Kurzeja, PHP Developer at Polcode
mail: michal.kurzeja@polcode.net Github: https://github.com/michalkurzeja/ContextAwareContainerDemo
On one occasion, as part of a project for a client, I was tasked with building an API for our system from the ground up. A major part of it were methods that exported data to partner services – letting some of our information outside as a result. As is usually the case with APIs – whatever is released, it’s not to be touched. And as the system grows, a need to modify its API and publish one version after another remains (and will remain!). Out of other options, I had to find a way to organize the code so that the whole procedure is as simple and painless as possible – without the need to copy tons of code.
You can read more about services and context here: https://www.polcode.com/en/service-and-context/
2. A particularly challenging project we developed included building a
brandnewAPIfeaturingmethodstoexportdatatopartnerservices.
That meant:
whatever was released could no longer be modified to maintain
the integrity of the system
a need to develop an easy way to add new features to the API
doing it all without copying tons of code and in line with the logic
of the applications
The answer I eventually came up with was...
3. ...to make the services dependent on the context
Any number- or string-based value could serve as such:
API version
session parameter
user setting
This approach produces code as follows:
4. With an approach like this:
an object is produced for each class defined for a given
context
there is no limit to the number of contexts we can create
for each service
we can define dependencies for each context
5. With service tagging and container compiling, we can use
tags to make every service we wish context-dependent.
For example:
6. Explanation:
context.sensitive is the tag name
it accepts two arguments: context
and alias
alias groups services that perform
the same task
we pick a service based on the value
of the context parameter (e.g. for
the processor service retrieved
in the a context, the ProcessorA
object is created, and for the
b context – ProcessorB)
the processor.universal alias returns
ProcessorAB for both a and b con-
text
7. We need a class that delivers each service when needed.
Let’s call it ContextAwareContainer and start with defining
its interface:
8. In the next few slides, we will go through an example of a
class that implements the interface
9. Explanation:
ContextAwareContainer is a service
that holds a reference to the con-
tainer
the inner array $services contains
the IDs of services we reference
through context and alias
the set(...) method shown in the pre-
vious slide builds the array
the get(...) method shown in the pre-
vious slide returns a service based
on context and alias passed as
arguments
12. The last step is to create an object that implements the
CompilerPassInterface interface. It finds the services we
tagged and saves their data in the ContextAwareContainer.
13. Explanation:
the findTaggedServiceIds(…)
method accepts an array
the indices of the array are the IDs
of services that have the context.
sensitive tag
the values of the array are the attri-
butes of those tags
all the data is used as arguments of
the set(...) method
14. At this point we can already use the ContextAwareContainer
to select services based on context.
Let’s further this concept by considering another scenario:
15. What if we had no choice but to make our service depen-
dent on another service and the class of the dependency
changes with the context? For example:
To achieve this, we have to find a way to pass the alias of
the dependency to the definition of the service. Let’s see
an example.
Service
Context::A Context::B
A
Dependency
B
Dependency
16. Explanation:
the properties attribute is a multi-
dimensional array that accepts
any type of value
the dynamic_arguments is an array.
Its keys are contexts and its valu-
es are an array of aliases
the new setup is as follows: when
we retrieve the processor.depen-
dant service with the a context,
it passes a reference to a servi-
ce with the processor alias and
the a context (that is an object of
the ProcessorA class) as its de-
pendency. However, if the con-
text equals b, a service of the
ProcessorB class is injected
into an object of the
ProcessorABDependant class.
17. We now have to modify the ContextAwareInterface so that
it stores the information on the dynamic dependencies.
18. Explanation:
we expand the set(…) function so
that it accepts the $arguments
array that contains our context-
related dependencies
we save the aliases in the inner
$arguments array as well. now
on they can be referenced with
the ID of the parent service and
context
19. Let’s have the service accept the dependencies we defined.
To do so, we have to define an interface. We can call it the
DynamicArgumentsInterface.
20. The following is an implementation of the definition for the
ProcessorABDependant class:
Now we can modify the get(…) function in the ContextA-
wareContainer, so that it can pass the dependencies we
defined to a service we want to retrieve:
21. Explanation:
the beginning is the same – we start
with retrieving our service of
choice
if the DynamicArgumentsInterface
is implemented, we get all of our
dependencies and pass them as
an array to the
setDynamicsArguments(...) function
if the DynamicArgumentsInterface
is not implemented, the logic
remains the same
22. The final step is to have the container compiler read
all of the dynamic arguments and pass them to the
ContextAwareContainer.
Let’s expand the ContextSensitiveServicesCompilerPass to
reflect this.
23. Explanation:
to register the calling of the set(…)
method, we retrieve the proper-
ties option of the service that is
currently being processed
next, we read its dynamic_arguments
parameter
we finish by adding it as the last
argument of the set(...) function
24. This is how we go about defining dependencies between
services that automatically adjust to a given context.
Just a bit of effort and a few simple classes allowed us to
create a functionality that expands on what can be achieved
with the Dependency Injection component. The result is
convenient to use and achieves a high level of abstraction.
25. Find the code from this article in my repo:
I hope you enjoyed the presentation. If you have
any feedback, or you believe that there is a better
way to solve this issue, please contact me at:
michal.kurzeja@polcode.net