Key Takeaways
- Dependency Injection (DI) enhances code reusability by separating object creation from usage, allowing for flexible dependency management.
- DI containers simplify the management of object dependencies, particularly when the number of dependencies is large, by automating object creation and configuration.
- Disco, an annotation-based DI container, introduces ease of configuration through annotations like @Bean and @Configuration, streamlining the setup of services.
- Disco supports advanced features like singleton instantiation, lazy loading, and session/request scope management for services, optimizing resource utilization and service lifecycle.
- The integration of Disco with Symfony Components facilitates the creation of a basic HTTP-based framework, demonstrating Disco’s compatibility and utility in modern web application development.
- The article showcases practical implementations of DI patterns using Disco, providing a foundation for developers to build efficient, scalable, and maintainable web applications.
Dependency Injection is all about code reusability. It’s a design pattern aiming to make high-level code reusable, by separating the object creation / configuration from usage.
Consider the following code:
<?php
class Test {
protected $dbh;
public function __construct(\PDO $dbh)
{
$this->dbh = $dbh;
}
}
$dbh = new PDO('mysql:host=localhost;dbname=test', 'username', 'password');
$test = new Test($dbh)
As you can see, instead of creating the PDO object inside the class, we create it outside of the class and pass it in as a dependency – via the constructor method. This way, we can use the driver of our choice, instead of having to to use the driver defined inside the class.
Our very own Alejandro Gervasio has explained the DI concept fantastically, and Fabien Potencier also covered it in a series.
There’s one drawback to this pattern, though: when the number of dependencies grows, many objects need to be created/configured before being passed into the dependent objects. We can end up with a pile of boilerplate code, and a long queue of parameters in our constructor methods. Enter Dependency Injection containers!
A Dependency Injection container – or simply a DI container – is an object which knows exactly how to create a service and handle its dependencies.
In this article, we’ll demonstrate the concept further with a newcomer in this field: Disco.
For more information on dependency injection containers, see our other posts on the topic here.
As frameworks are great examples of deploying DI containers, we will finish the article by creating a basic HTTP-based framework with the help of Disco and some Symfony Components.
Installation
To install Disco, we use Composer as usual:
composer require bitexpert/disco
To test the code, we’ll use PHP’s built-in web server:
php -S localhost:8000 -t web
As a result, the application will be accessible under http://localhost:8000
from the browser. The last parameter -t
option defines the document root – where the index.php
file resides.
Getting Started
Disco is a container_interop compatible DI container. Somewhat controversially, Disco is an annotation-based DI container.
Note that the package container_interop
consists of a set of interfaces to standardize features of container objects. To learn more about how that works, see the tutorial in which we build our own, SitePoint Dependency Injection Container, also based on container-interop.
To add services to the container, we need to create a configuration class. This class should be marked with the @Configuration
annotation:
<?php
/**
* @Configuration
*/
class Services {
// ...
}
Each container service should be defined as a public or protected method inside the configuration class. Disco calls each service a Bean, which originates from the Java culture.
Inside each method, we define how a service should be created. Each method must be marked with @Bean
which implies that this a service, and @return
annotations which notes the type of the returned object.
This is a simple example of a Disco configuration class with one “Bean”:
<?php
/**
* @Configuration
*/
class Configuration {
/**
* @Bean
* @return SampleService
*/
public function getSampleService()
{
// Instantiation
$service = new SampleService();
// Configuration
$service->setParameter('key', 'value');
return $service;
}
}
The @Bean
annotation accepts a few configuration parameters to specify the nature of a service. Whether it should be a singleton object, lazy loaded (if the object is resource-hungry), or even its state persisted during the session’s lifetime is specified by these parameters.
By default, all the services are defined as singleton services.
For example, the following Bean creates a singleton lazy-loaded service:
<?php
// ...
/**
* @Bean({"singleton"=true, "lazy"=true})
* @return \Acme\SampleService
*/
public function getSampleService()
{
return new SampleService();
}
// ...
Disco uses ProxyManager to do the lazy-loading of the services. It also uses it to inject additional behaviors (defined by the annotations) into the methods of the configuration class.
After we create the configuration class, we need to create an instance of AnnotationBeanFactory
, passing the configuration class to it. This will be our container.
Finally, we register the container with BeanFactoryRegistry
:
<?php
// ...
use \bitExpert\Disco\AnnotationBeanFactory;
use \bitExpert\Disco\BeanFactoryRegistry;
// ...
// Setting up the container
$container = new AnnotationBeanFactory(Services::class, $config);
BeanFactoryRegistry::register($container);
How to Get a Service from the Container
Since Disco is container/interop
compatible, we can use get()
and has()
methods on the container object:
// ...
$sampleService = $container->get('sampleService');
$sampleService->callSomeMethod();
Service Scope
HTTP is a stateless protocol, meaning on each request the whole application is bootstrapped and all objects are recreated. We can, however, influence the lifetime of a service by passing the proper parameters to the @Bean
annotation. One of these parameters is scope
. The scope can be either request
or session
.
If the scope is session
, the service state will persist during the session lifetime. In other words, on subsequent HTTP requests, the last state of the object is retrieved from the session.
Let’s clarify this with an example. Consider the following class:
<?php
class sample {
public $counter = 0;
public function add()
{
$this->counter++;
return $this;
}
}
In the above class, the value of $counter
is incremented each time the add()
method is called; now, let’s add this to the container, with scope set to session
:
// ...
/**
* @Bean({"scope"="session"})
* @return Sample
*/
public function getSample()
{
return new Sample();
}
// ...
And if we use it like this:
// ...
$sample = $container->get('getSample');
$sample->add()
->add()
->add();
echo $sample->counter; // output: 3
// ...
In the first run, the output will be three. If we run the script again (to make another request), the value will be six (instead of three). This is how object state is persisted across requests.
If the scope is set to request
, the value will be always three in subsequent HTTP requests.
Container Parameters
Containers usually accept parameters from the outside world. With Disco, we can pass the parameters into the container as an associative array like this:
// ...
$parameters = [
// Database configuration
'database' => [
'dbms' => 'mysql',
'host' => 'localhost',
'user' => 'username',
'pass' => 'password',
],
];
// Setting up the container
$container = new AnnotationBeanFactory(Services::class, $parameters);
BeanFactoryRegistry::register($container);
To use these values inside each method of the configuration class, we use @Parameters
and @parameter
annotations:
<?php
// ...
/**
* @Bean
* @Parameters({
* @parameter({"name"= "database"})
* })
*
*/
public function sampleService($database = null)
{
// ...
}
Disco in Action
In this section, we’re going to create a basic HTTP-based framework. The framework will create a response based on the information received from the request.
To build our framework’s core, we’ll use some Symfony Components.
HTTP Kernel
The heart of our framework. Provides the request / response basics.
Http Foundation
A nice object-oriented layer around PHP’s HTTP super globals.
Router
According to the official website: maps an HTTP request to a set of configuration variables – more on this below.
Event Dispatcher
This library provides a way to hook into different phases of a request / response lifecycle, using listeners and subscribers.
To install all these components:
composer require symfony/http-foundation symfony/routing symfony/http-kernel symfony/event-dispatcher
As a convention, we’ll keep the framework’s code under the Framework
namespace.
Let’s also register a PSR-4 autoloader. To do this, we add the following namespace-to-path mapping under the psr-4
key in composer.json
:
// ...
"autoload": {
"psr-4": {
"": "src/"
}
}
// ...
As a result, all namespaces will be looked for within the src/
directory. Now, we run composer dump-autoload
for this change to take effect.
Throughout the rest of the article, we’ll write our framework’s code along with code snippets to make some concepts clear.
The Kernel
The foundation of any framework is its kernel. This is where a request is processed into a response.
We’re not going to create a Kernel from scratch here. Instead, we’ll extend the Kernel
class of the HttpKernel component we just installed.
<?php
// src/Framework/Kernel.php
namespace Framework;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class Kernel extends HttpKernel implements HttpKernelInterface {
}
Since the base implementation works just fine for us, we won’t reimplement any methods, and will instead just rely on the inherited implementation.
Routing
A Route
object contains a path and a callback, which is called (by the Controller Resolver) every time the route is matched (by the URL Matcher).
The URL matcher is a class which accepts a collection of routes (we’ll discuss this shortly) and an instance of RequestContext to find the active route.
A request context object contains information about the current request.
Here’s how routes are defined by using the Routing component:
<?php
// ...
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$routes = new RouteCollection();
$routes->add('route_alias', new Route('path/to/match', ['_controller' => function(){
// Do something here...
}]
));
To create routes, we need to create an instance of RouteCollection
(which is also a part of the Routing
component), then add our routes to it.
To make the routing syntax more expressive, we’ll create a route builder class around RouteCollection
.
<?php
// src/Framework/RouteBuilder.php
namespace Framework;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
class RouteBuilder {
protected $routes;
public function __construct(RouteCollection $routes)
{
$this->routes = $routes;
}
public function get($name, $path, $controller)
{
return $this->add($name, $path, $controller, 'GET');
}
public function post($name, $path, $controller)
{
return $this->add($name, $path, $controller, 'POST');
}
public function put($name, $path, $controller)
{
return $this->add($name, $path, $controller, 'PUT');
}
public function delete($name, $path, $controller)
{
return $this->add($name, $path, $controller, 'DELETE');
}
protected function add($name, $path, $controller, $method)
{
$this->routes->add($name, new Route($path, ['_controller' => $controller], ['_method' => $method]));
return $this;
}
}
This class holds an instance of RouteCollection
. In RouteBuilder
, for each HTTP verb, there is a method which calls add()
. We’ll keep our route definitions in the src/routes.php
file:
<?php
// src/routes.php
use Symfony\Component\Routing\RouteCollection;
use Framework\RouteBuilder;
$routeBuilder = new RouteBuilder(new RouteCollection());
$routeBuilder
->get('home', '/', function() {
return new Response('It Works!');
})
->get('welcome', '/welcome', function() {
return new Response('Welcome!');
});
The Front Controller
The entry point of any modern web application is its front controller. It is a PHP file, usually named index.php
. This is where the class autoloader is included, and the application is bootstrapped.
All the requests go through this file, and are from here dispatched to the proper controllers. Since this is the only file we’re going to expose to the public, we put it inside our web root directory, keeping the rest of the code outside.
<?php
//web/index.php
require_once __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
// Create a request object from PHP's global variables
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/routes.php';
$UrlMatcher = new Routing\Matcher\UrlMatcher($routes, new Routing\RequestContext());
// Event dispatcher & subscribers
$dispatcher = new EventDispatcher();
// Add a subscriber for matching the correct route. We pass UrlMatcher to this class
$dispatcher->addSubscriber(new RouterListener($UrlMatcher, new RequestStack()));
$kernel = new Framework\Kernel($dispatcher, new ControllerResolver());
$response = $kernel->handle($request);
// Sending the response
$response->send();
In the above code, we instantiate a Request
object based on PHP’s global variables.
<?php
// ...
$request = Request::createFromGlobals();
// ...
Next, we load the routes.php
file into $routes
. Detecting the right route is the responsibility of the UrlMatcher
class, so we create it, passing the route collection along with a RequestContext
object.
<?php
// ...
$routes = include __DIR__.'/../src/routes.php';
$UrlMatcher = new Routing\Matcher\UrlMatcher($routes, new Routing\RequestContext());
// ...
To use the UrlMatcher
instance, we pass it to the RouteListener
event subscriber.
<?php
// ...
// Event dispatcher & subscribers
$dispatcher = new EventDispatcher();
// Add a subscriber for matching the correct route. We pass UrlMatcher to this class
$dispatcher->addSubscriber(new RouterListener($UrlMatcher, new RequestStack()));
// ...
Any time a request hits the application, the event is triggered and the respective listener is called, which in turn detects the proper route by using the UrlMatcher
passed to it.
Finally, we instantiate the kernel, passing in the Dispatcher and an instance of Controller Resolver – via its constructor:
<?php
// ...
$kernel = new Framework\Kernel($dispatcher, new ControllerResolver());
$response = $kernel->handle($request);
// Sending the response
$response->send();
// ...
Disco Time
So far we had to do plenty of instantiations (and configurations) in the front controller, from creating the request context object, the URL matcher, the event dispatcher and its subscribers, and of course the kernel itself.
It is now time to let Disco wire all these pieces together for us.
As before, we install it using Composer:
composer require bitexpert/Disco;
Then, we create the configuration class, and define the services we’ll need in the front controller:
<?php
// src/Framework/Services.php
use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
use bitExpert\Disco\Annotations\Parameters;
use bitExpert\Disco\Annotations\Parameter;
/**
* @Configuration
*/
class Services {
/**
* @Bean
* @return \Symfony\Component\Routing\RequestContext
*/
public function context()
{
return new \Symfony\Component\Routing\RequestContext();
}
/**
* @Bean
*
* @return \Symfony\Component\Routing\Matcher\UrlMatcher
*/
public function matcher()
{
return new \Symfony\Component\Routing\Matcher\UrlMatcher($this->routeCollection(), $this->context());
}
/**
* @Bean
* @return \Symfony\Component\HttpFoundation\RequestStack
*/
public function requestStack()
{
return new \Symfony\Component\HttpFoundation\RequestStack();
}
/**
* @Bean
* @return \Symfony\Component\Routing\RouteCollection
*/
public function routeCollection()
{
return new \Symfony\Component\Routing\RouteCollection();
}
/**
* @Bean
* @return \Framework\RouteBuilder
*/
public function routeBuilder()
{
return new \Framework\RouteBuilder($this->routeCollection());
}
/**
* @Bean
* @return \Symfony\Component\HttpKernel\Controller\ControllerResolver
*/
public function resolver()
{
return new \Symfony\Component\HttpKernel\Controller\ControllerResolver();
}
/**
* @Bean
* @return \Symfony\Component\HttpKernel\EventListener\RouterListener
*/
protected function listenerRouter()
{
return new \Symfony\Component\HttpKernel\EventListener\RouterListener(
$this->matcher(),
$this->requestStack()
);
}
/**
* @Bean
* @return \Symfony\Component\EventDispatcher\EventDispatcher
*/
public function dispatcher()
{
$dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();
$dispatcher->addSubscriber($this->listenerRouter());
return $dispatcher;
}
/**
* @Bean
* @return Kernel
*/
public function framework()
{
return new Kernel($this->dispatcher(), $this->resolver());
}
}
Seems like a lot of code; but in fact, it’s the same code that resided in the front controller previously.
Before using the class, we need to make sure it has been autoloaded by adding it under the files
key in our composer.json
file:
// ...
"autoload": {
"psr-4": {
"": "src/"
},
"files": [
"src/Services.php"
]
}
// ...
And now onto our front controller.
<?php
//web/index.php
require_once __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
$container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class);
\bitExpert\Disco\BeanFactoryRegistry::register($container);
$routes = include __DIR__.'/../src/routes.php';
$kernel = $container->get('framework')
$response = $kernel->handle($request);
$response->send();
Now our front controller can actually breathe! All the instantiations are done by Disco when we request a service.
But How About the Configuration?
As explained earlier, we can pass in parameters as an associative array to the AnnotationBeanFactory
class.
To manage configuration in our framework, we create two configuration files, one for development and one for the production environment.
Each file returns an associative array, which we can be loaded into a variable.
Let’s keep them inside Config
directory:
// Config/dev.php
return [
'debug' => true;
];
And for production:
// Config/prod.php
return [
'debug' => false;
];
To detect the environment, we’ll specify the environment in a special plain-text file, just like we define an environment variable:
ENV=dev
To parse the file, we use PHP dotenv, a package which loads environment variables from a file (by default the filename is .env
) into PHP’s $_ENV
super global. This means we can get the values by using PHP’s getenv() function.
To install the package:
composer require vlucas/phpdotenv
Next, we create our .env
file inside the Config/
directory.
Config/.env
ENV=dev
In the front controller, we load the environment variables using PHP dotenv:
<?php
//web/index.php
// ...
// Loading environment variables stored .env into $_ENV
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../Config');
$dotenv->load();
// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';
$container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class, $parameters); \bitExpert\Disco\BeanFactoryRegistry::register($container);
// ...
In the preceding code, we first specify the directory in which our .env
file resides, then we call load()
to load the environment variables into $_ENV
. Finally, we use getenv()
to get the proper configuration filename.
Creating a Container Builder
There’s still one problem with the code in its current state: whenever we want to create a new application we have to instantiate AnnotationBeanFactory
in our front controller (index.php
). As a solution, we can create a factory which creates the container, whenever needed.
<?php
// src/Factory.php
namespace Framework;
class Factory {
/**
* Create an instance of Disco container
*
* @param array $parameters
* @return \bitExpert\Disco\AnnotationBeanFactory
*/
public static function buildContainer($parameters = [])
{
$container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class, $parameters);
\bitExpert\Disco\BeanFactoryRegistry::register($container);
return $container;
}
}
This factory has a static method named buildContainer()
, which creates and registers a Disco container.
This is how it improves our front controller:
<?php
//web/index.php
require_once __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
// Getting the environment
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../config');
$dotenv->load();
// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';
$request = Request::createFromGlobals();
$container = Framework\Factory::buildContainer($parameters);
$routes = include __DIR__.'/../src/routes.php';
$kernel = $container->get('framework')
$response = $kernel->handle($request);
$response->send();
It looks much neater now, doesn’t it?
Application Class
We can take things one step further in terms of usability, and abstract the remaining operations (in the front controller) into another class. Let’s call this class Application
:
<?php
namespace Framework;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;
class Application {
protected $kernel;
public function __construct(HttpKernelInterface $kernel)
{
$this->kernel = $kernel;
}
public function run()
{
$request = Request::createFromGlobals();
$response = $this->kernel->handle($request);
$response->send();
}
}
Application
is dependent on the kernel, and works as a wrapper around it. We create a method named run()
, which populates the request object, and passes it to the kernel to get the response.
To make it even cooler, let’s add this class to the container as well:
<?php
// src/Framework/Services.php
// ...
/**
* @Bean
* @return \Framework\Application
*/
public function application()
{
return new \Framework\Application($this->kernel());
}
// ...
And this is the new look of our front controller:
<?php
require_once __DIR__ . '/../vendor/autoload.php';
// Getting the environment
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../config');
$dotenv->load();
// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';
// Build a Disco container using the Factory class
$container = Framework\Factory::buildContainer($parameters);
// Including the routes
require __DIR__ . '/../src/routes.php';
// Running the application to handle the response
$app = $container->get('application')
->run();
Creating a Response Listener
We can use the framework now, but there is still room for improvement. Currently, we have to return an instance of Response
in each controller, otherwise, an exception is thrown by the Kernel:
<?php
// ...
$routeBuilder
->get('home', '/', function() {
return new Response('It Works!');
});
->get('welcome', '/welcome', function() {
return new Response('Welcome!');
});
// ...
However, we can make it optional and allow for sending back pure strings, too. To do this, we create a special subscriber class, which automatically creates a Response
object if the returned value is a string.
Subscribers must implement the Symfony\Component\EventDispatcher\EventSubscriberInterface
interface. They should implement the getSubscribedMethods()
method in which we define the events we’re interested in subscribing to, and their event listeners.
In our case, we’re interested in the KernelEvents::VIEW
event. The event happens when a response is to be returned.
Here’s our subscriber class:
<?php
// src/Framework/StringResponseListener
namespace Framework;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class StringResponseListener implements EventSubscriberInterface
{
public function onView(GetResponseForControllerResultEvent $event)
{
$response = $event->getControllerResult();
if (is_string($response)) {
$event->setResponse(new Response($response));
}
}
public static function getSubscribedEvents()
{
return array(KernelEvents::VIEW => 'onView');
}
}
Inside the listener method onView
, we first check if the response is a string (and not already a Response
object), then create a response object if required.
To use the subscriber, we need to add it to the container as a protected service:
<?php
// ...
/**
* @Bean
* @return \Framework\StringResponseListener
*/
protected function ListenerStringResponse()
{
return new \Framework\StringResponseListener();
}
// ...
Then, we add it to the dispatcher service:
<?php
// ...
/**
* @Bean
* @return \Symfony\Component\EventDispatcher\EventDispatcher
*/
public function dispatcher()
{
$dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();
$dispatcher->addSubscriber($this->listenerRouter());
$dispatcher->addSubscriber($this->ListenerStringResponse());
return $dispatcher;
}
// ...
From now on, we can simply return a string in our controller functions:
<?php
// ...
$routeBuilder
->get('home', '/', function() {
return 'It Works!';
})
->get('welcome', '/welcome', function() {
return 'Welcome!';
});
// ...
The framework is ready now.
Conclusion
We created a basic HTTP-based framework with the help of Symfony Components and Disco. This is just a basic Request/Response framework, lacking any other MVC concepts like models and views, but allows for the implementation of any additional architectural patterns we may desire.
The full code is available on Github.
Disco is a newcomer to the DI-container game and if compared to the older ones, it lacks a comprehensive documentation. This article was an attempt at providing a smooth start for those who might find this new kind of DI container interesting.
Do you glue together your app’s components with DI containers? If so, which ones? Have you given Disco a try? Let us know!
Frequently Asked Questions on Dependency Injection and Lazy Services
What is the main advantage of using lazy services in dependency injection?
The primary advantage of using lazy services in dependency injection is the optimization of resources. Lazy services are not instantiated until they are actually needed. This means that the system resources are not unnecessarily consumed by services that might not be used during a particular request. This can significantly improve the performance of your application, especially when dealing with heavy services that consume a lot of memory or take a long time to instantiate.
How does a lazy service work in Symfony?
In Symfony, a lazy service works by creating a proxy instance of the service when the service is defined. This proxy instance does not do much except wait for a method to be called on it. When a method is called, the proxy instance then instantiates the actual service and delegates the method call to it. This way, the actual service is only created when it is really needed.
When should I use lazy services?
Lazy services should be used when the instantiation of a service is expensive in terms of resources and the service is not always used during a request. By making such a service lazy, you can avoid the cost of its instantiation when it is not needed. However, if a service is lightweight and used frequently, making it lazy might not bring significant benefits and could even add a slight overhead due to the use of a proxy.
How can I define a lazy service in Symfony?
In Symfony, you can define a lazy service by adding the “lazy” tag to the service definition in your configuration file. Here is an example:services:
app.my_lazy_service:
class: AppBundle\MyLazyService
lazy: true
Can I use lazy services with interfaces?
Yes, you can use lazy services with interfaces. In fact, it is a good practice to depend on interfaces instead of concrete classes. This way, you can easily substitute the actual service with a different implementation if needed. When a lazy service is defined with an interface, Symfony will generate a proxy class that implements the same interface.
What is the role of a service container in dependency injection?
A service container, also known as a dependency injection container, is a tool that manages the instantiation and configuration of services in a dependency injection system. It allows you to centralize the configuration of your services and to manage their dependencies in a clean and efficient way.
What is a proxy in the context of a lazy service?
A proxy is a stand-in object that is used to control access to another object, in this case, a service. In the context of a lazy service, the proxy is an instance of the service that is created when the service is defined. The proxy does not do much except wait for a method to be called on it. When a method is called, the proxy instantiates the actual service and delegates the method call to it.
Can I make all my services lazy?
While it is technically possible to make all your services lazy, it is not always beneficial. Making a service lazy adds a slight overhead because a proxy needs to be used. If a service is lightweight and used frequently, making it lazy might not bring significant benefits and could even decrease the performance of your application.
How can I check if a service is being instantiated lazily?
You can check if a service is being instantiated lazily by using the Symfony profiler. The profiler provides detailed information about the services that are used during a request, including whether they are lazy or not.
Can I use lazy services outside of Symfony?
Yes, the concept of lazy services is not specific to Symfony and can be used in any application that uses dependency injection. However, the way to define and use lazy services might differ depending on the dependency injection container you are using.
A web developer with a solid background in front-end and back-end development, which is what he's been doing for over ten years. He follows two major principles in his everyday work: beauty and simplicity. He believes everyone should learn something new every day.