Dependency Injection in Jet framework

Does it seem to you that the Jet framework does not use the Dependency Injection paradigm? But it does! You can't do big projects without it. However, Dependency Injection (DI) as you probably know it uses the part that objectively makes your life easier and, on the contrary, eliminates the part that can make your life more complicated. Let's explain.

What is DI actually good for?

The main advantage of DI is that it completely isolates user classes from the creation and initialization of their services. This is the main and most important advantage. But it will be better to explain it on a specific case.

Suppose we have logging in the system. Logging must be provided by a class. And logging is a service that this class provides - the class is the service provider.

We also have service users. So in our case, anything that needs to log to some log. Again, let's be specific and say it can be a controller (but it can be anything else, of course). In a poorly designed application that completely ignores DI, that controller would log like this:

class Controller extends Controller_Abstract {
    
    public function 
someAction() : void
    
{
        
//... ... ...
        
$logger = new Logger('~/path/to/some/log/dir');
        
$logger->logEvent$event$event_data );
        
//... ... ...
    
}
}

This is absolutely wrong and this is not the way to develop software. Why? For purely objective reasons. And then the day comes when you need to change the logging:

  • Maybe replace that class with a completely different one
  • Or reconfigure the class, change the service parameters
  • Or let some higher authority decide how the application will log.
None of this will come easily to you. You will have to go through the whole project, modify many places in the code (hundreds of them), and of course risk errors and problems. A nightmare for a larger project.

Not to mention the simple fact that the controller in our example simply does what it is not supposed to do. He takes care of initializing the service and that is not his competence and responsibility. This is again a transgression against the rules for creating sustainable projects.

But I would say that the main scare is the idea that for example in two years of project operation you have to go through and edit hundreds of places in the project, even just for a small partial change. That's obviously an objective big problem and we want to avoid that.

This is the main benefit of DI mechanisms. When it is followed, it is ensured that the initialization of the service is not taken care of by the service user.

DI is therefore a necessary thing.

DI as you may know it ... and as you won't find it in Jet

The fact is that there are many methods of using DI - not just one. The main goal is not to initiate services in many places and in places that are not designed for that purpose. But there are more paths to that goal.

You probably know something like this from other frameworks:

class Controller extends Controller_Abstract {
    
    public function 
someActionLogger_Interface $logger ) : void
    
{
        
//... ... ...
        
$logger->logEvent$event$event_data );
        
//... ... ...
    
}
}

That is, a mechanism where service instances are injected, for example, as action parameters or constructor parameters. In these frameworks there are different and often very complex systems and infrastructure for DI. Furthermore, various mega containers and so on.

Yes, it works. It's inherently correct. But that very complexity can sometimes be very counterproductive. The very fact that in such a situation you often have to deal with passing dependencies on yourself, you can potentially have a problem with cyclic dependencies and so on. Not to mention the simple fact that, given the complexity of these frameworks, it all means a lot of code and therefore technical burden.

And the Jet framework has as a resolution to be as simple as possible and more efficient because of it. And that's why the PHP Jet framework has taken a different path.

Service Guarantor

Jet introduces the term Service Guarantor. What is it? Let's start with a concrete example of logging in Jet (although of course this is not just about logging):

namespace JetApplicationModule\Content\Articles\Admin;

use 
Jet\Logger;

use 
Jet\MVC_Controller_Default;

class 
Controller_Main extends MVC_Controller_Default
{
    
    public function 
add_Action(): void
    
{
        
//... ... ...
        
        
Logger::success(
            
event'article_created',
            
context_object_data$article
        
);
        
        
//... ... ...
    
}
}

No, don't be scared! It's not a service locator (for example). And everything makes perfect sense. Read on ;-)

In general, any service must have some defined interface (whether it is the interface it implements directly or an abstract class). Plain and simple, users of a service need to know what interface the service has and how to use it. This is true here as well. The logger in PHP Jet has an interface Jet\Logger_Interface (which in this case is really primitive). In this respect, nothing changes in PHP Jet at all, and it's firmly established that services have their given interface.

In other frameworks, however, what we call the one big pile syndrome works. The services are taken care of by the complex subsystem mentioned above. The services are thus on one heap.

PHP Jet does this by having the system define a service guarantor in addition to the service interface. The service guarantor is not just a static facade that is a bridge to that big service container (as one competing framework has). Yes, the convenience and straightforwardness of using the facade remains the same here. But the service guarantor in Jet is not just a facade, but also a container into which a specific service provider is embedded.

So if we continue with our logging example, you should know that the Jet\Logger facade itself does not perform any logging. It's just a service guarantor - a container having an interface reflecting the service, but a specific service provider had to be injected into that container (here, a logger that already knows how to perform the operation and provide the service). For example:

Logger::setLogger( new Logger_Admin() );

Thus, when the application was initialized, some higher authority (here base initializer, but it could be anything else) could have decided what specific service to use for logging.

So here you can have different logging for admin panel, different for REST API, different for web and so on. But the rest of the application doesn't care and just logs - just as it should under DI rules. At the same time, there is no dependency hell, no problems. And using everything is perfectly simple and straightforward.

Less code, less complexity, less problems. And at the same time, much more flexibility and better project sustainability.

It is important to mention that in addition to the DI mechanism implemented by service guarantors, Jet also has a factory system. This factory system is mainly used for instantiating system entities.

Overview of service providers in Jet

To illustrate, we have shown logging as a very simple service. Of course, there are many more services and providers in Jet. Here is a list of them.

And of course there is nothing to stop you from creating your own services using a similar mechanism and philosophy. And you don't need a complex framework to do it ;-)

Service / subsystem Service guarantor Method for injecting a service provider
Authentication and authorization Jet\Auth Jet\Auth::setController(
Jet\Auth_Controller_Interface $controller
)
The service provider is the so-called Auth Controller implementing Jet\Auth_Controller_Interface. The role of this controller is to manage the logic of working with users, more specifically the logic of authentication and authorization.

There is no default Auth Controller. Similarly, there is no Auth Controller implementation included in the framework. These controllers are always part of the application space - the application. So it is entirely up to the developer how he implements the logic and nothing prevents any modifications. There are three controllers included in the sample application.

When using Jet MVC, the controller instance is injected into the service guarantor within the base initializer.
Autoloader Jet\Autoloader Jet\Autoloader::register(
Jet\Autoloader_Loader $loader
): void
The system for automatic class loading is actually made up of individual modules that must be injected when the application is initialized. Of course, there can be (and in practice are) more than one of these modules.

In the default configuration, initialization can be found in the script ~/application/Init/Autoloader.php
Autoloaderu Cahce Jet\Autoloader_Cache Jet\Autoloader_Cache::init(
Jet\Autoloader_Cache_Backend $backend
)
The backend cache of the automatic class loading system must be injected.

In the default configuration, the initialization can be found in the script ~/application/Init/Cache/Autoloader.php
Application Module Manager Jet\Application_Modules Jet\Application_Modules::setHandler(
Jet\Application_Modules_Handler $handler
) : void
The application module manager does not need to be injected. The system has a default implementation that is used if no other manager was injected during initialization.

Thus, it is possible to inject an alternative module manager, but in this case it is not necessary to inject it.

The application module manager is also uses the factory system.
Logger Jet\Logger Jet\Logger::setLogger(
Jet\Logger_Interface $logger
): void
System providing general logging. The implementation of the logger is always part of the application and not the framework. The sample application contains prebuilt loggers.

When using Jet MVC, the logger instance is injected into the service guarantor within the base initializer.
Sending emails Jet\Mailing Jet\Mailing::setBackend(
Mailing_Backend_Abstract $backend
): void
The email sending system needs a backend that ensures the actual sending of the email. The default backend will ensure sending before the standard means of PHP itself, and this default backend does not need to be injected. The default backend is used automatically if another backend is not injected into the service guarantor.

Again, it is possible to inject your own backend, but if you are happy with the default backend, then injecting it is not necessary.
Main MVC router Jet\MVC Jet\MVC::setRouter(
MVC_Router_Interface $router
): void
Also the main router Jet MVC does not need to be injected, but has its default implementation, but it is also possible to create your own router implementation and inject it. Interestingly, the Jet MVC router also uses factories.
Keš MVC Jet\MVC_Cache Jet\MVC_Cache::init(
MVC_Cache_Backend $backend
): void
The Jet MVC cache needs to be initialized and the backend inserted into the container/service guarantor at the designated location. That is, in the script:

~/application/Init/Cache/MVC.php

There are two ready-made backends using either files or Redis for storage. And of course there is nothing preventing you from implementing any other backend and injecting it into the system.
REST API server Jet\RESTServer Jet\RESTServer::setBackend(
RESTServer_Backend $backend
) : void
The backend REST API of the server is responsible for the specific implementation of HTTP requests read, in turn creating specific HTTP responses. So if you want to change the default behavior of the REST API server, it is of course not a problem.

Here again, there is a default implementation that is initialized automatically, so there is no need to inject the backend in the default situation.
Translator - dictionary storage Jet\Translator Jet\Translator::setBackend(
Translator_Backend $backend
) : void
The role of the translator backend is to load and store translation dictionaries.

Again, there is a default implementation that operates on files and this default backend is used automatically unless another one is injected.
Previous chapter
Attributes - Jet\Attributes
Next chapter
Factories