Service Container en Laravel 📚 Qué es y por qué es importante
Tiempo de lectura: 6.60 minutos
Introducción
El service container es una de las piezas más importantes de Laravel como framework. Sin embargo muchas veces no recibe la atención que merece.
En muchas entrevistas, cuando se pregunta por este concepto, generalmente los desarrolladores conocen el término pero no están seguros de su importancia o qué es exactamente.
Esto ocurre principalmente por 2 motivos:
- Muchas veces consideran la inyección de dependencias (dependency inyection) como un concepto muy avanzado.
- No están seguros de si realmente necesitan conocer los conceptos de IoC y IoC container.
Empecemos a revisar los fundamentos, y así mismo, ejemplos ?.
Dependency Injection and IoC
Como su mismo nombre lo indica, este concepto consiste en "inyectar dependencias".
Decimos que se "inyecta una dependencia" cuando se pasa como argumento la instancia de una clase sobre otra, a través de uno de sus métodos.
Ejemplo Vamos a suponer que estás desarrollando un blog, que permite publicar posts y también compartirlos por Facebook.
Para esto defines una clase Publication
.
Veamos cómo se vería la clase sin usar inyección de dependencias:
<?php
namespace App;
use App\Models\Post;
use App\Services\FacebookService;
class Publication
{
public function __construct()
{
// La dependencia es instanciada dentro de la clase
$this->fbService = new FacebookService();
}
public function publish(Post $post)
{
$post->publish();
$this->fbService->share($post);
}
}
Si quieres probar este código, puedes definir una clase FacebookService
en tu carpeta app\Services
:
<?php
namespace App\Services;
use App\Models\Post;
class FacebookService
{
public function share(Post $post)
{
dd('Tu post ha sido compartido en Facebook');
}
}
En este primer ejemplo sin inyección de dependencias, la instancia de FacebookService
se ha creado dentro de la clase Publication
.
En vez de instanciar el servicio dentro de la clase, puedes inyectar una instancia desde fuera a través de un argumento.
El argumento puede ser pasado a cualquier método de la clase. Usualmente usamos el constructor.
<?php
namespace App;
use App\Models\Post;
use App\Services\FacebookService;
class Publication
{
public function __construct(FacebookService $facebookService)
{
$this->fbService = $facebookService;
}
public function publish(Post $post)
{
$post->publish();
$this->fbService->share($post);
}
}
Si quieres probar este código rápidamente, puedes definir una ruta en tu proyecto:
Route::get('/', function () {
$post = new Post();
// Inyección de dependencias
$publication = new Publication(new FacebookService());
$publication->publish($post);
});
Este último ejemplo básico resume lo que es la inyección de dependencias.
Aplicar dependency injection sobre una clase causa "inversión de control".
- En un inicio, la clase dependiente
Publication
controlaba la instanciación de la clase independienteFacebookService
. - Luego, el control ha sido desplazado al framework.
Esto último se conoce como inversion of control (IoC).
IoC Container
Tal como acabamos de ver, la inyección de dependencias le permite a una clase ceder el control de creación de instancias al framework.
Entonces decimos que:
Un IoC container puede hacer el proceso de inyección de dependencias más eficiente.
Se trata de una "simple" clase, capaz de:
- Registrar datos
- Y devolverlos cuando se solicitan
Laravel como framework define un container por nosotros, así que no tenemos que preocuparnos.
Pero si tienes curiosidad de cómo sería una implementación simplificada de un IoC container, aquí las tienes:
<?php
namespace App;
class Container {
// array para almacenar los container bindings
protected $bindings = [];
// enlazar nueva data al container
public function bind($key, $value)
{
// asociar el valor con la key indicada
$this->bindings[$key] = $value;
}
// devolver la data vinculada desde el container
public function make($key)
{
if (isset($this->bindings[$key])) {
// verificar si la data asociada es un callback
if (is_callable($this->bindings[$key])) {
// de ser así, llamar al callback y devolver el resultado
return call_user_func($this->bindings[$key]);
} else {
// de caso contrario, devolver el valor tal cual es
return $this->bindings[$key];
}
}
}
}
Puedes asociar cualquier data al container.
Para esto sólo debes llamar al método bind
, que en español significa enlazar, vincular:
<?php
// routes/web.php
use App\Container;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
$container = new Container();
$container->bind('name', 'Programación y más');
dd($container->make('name'));
});
Este ejemplo usa el container para guardar el nombre de la página Programación y más, y posteriormente imprimir este valor.
Incluso con la versión simplificada que hemos definido, somos capaces de vincular clases al contenedor, a través de callbacks:
<?php
// routes/web.php
use App\Container;
use App\Service\FacebookService;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
$container = new Container;
$container->bind(FacebookService::class, function() {
return new App\Services\FacebookService;
});
ddd($container->make(FacebookService::class));
// App\Services\TwitterService {#269}
});
¿Por qué es útil instanciar a través del container?
Digamos que tu clase FacebookService necesita de unas credenciales para su uso.
En tal caso, puedes usar tu API key de la siguiente manera:
$container = new Container;
$container->bind(FacebookService::class, function() use ($container) {
return new App\Services\FacebookService('tu-api-key');
});
ddd($container->make(FacebookService::class));
// App\Services\FacebookService{#269 ▼
// #apiKey: "tu-api-key"
// }
De esta manera, cada vez que necesites usar este servicio, sólo tienes que pedirlo al container.
Él se encargará de resolver la dependencia.
Si en el futuro necesitas cambiar la forma en que creas esta instancia basta con actualizar el closure usado como 2do parámetro, que usamos al llamar a bind
.
¿Qué ocurre si queremos reemplazar nuestro servicio por otro?
Si de pronto quieres cambiar Facebook por Twitter o por LinkedIn, con la implementación actual, tendrías que:
- Crear una nueva clase LinkedInService.
- Asociar esta clase al container.
- Reemplazar todas las referencias de la clase service anterior.
Pero no tiene por qué ser así. El proceso puede ser mucho mejor si usamos interfaces.
Empecemos definiendo una interfaz llamada SocialMediaServiceInterface
.
<?php
namespace App\Interfaces;
use App\Models\Post;
interface SocialMediaServiceInterface
{
public function share(Post $post);
}
Puedes actualizar tu servicio para que implemente esta interfaz:
<?php
namespace App\Services;
use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;
class FacebookService
{
protected $apiKey;
public function __construct($apiKey)
{
$this->apiKey = $apiKey;
}
public function share(Post $post)
{
dd('Tu post ha sido compartido en Facebook');
}
}
Entonces, en vez de enlazar una clase concreta al contenedor, enlazamos la interfaz.
Y en el callback, devolvemos una instancia de FacebookService
tal como hicimos antes.
$container = new Container;
$container->bind('fb-api-key', 'tu-api-key');
$container->bind(SocialMediaServiceInterface::class, function() use ($container) {
return new App\Services\FacebookService($container->make('fb-api-key'));
});
ddd($container->make(SocialMediaServiceInterface::class));
// App\Services\FacebookService {#269 ▼
// #apiKey: "tu-api-key"
// }
¿Qué sucede ahora si queremos usar LinkedIn en vez de Facebook?
Como has enlazado una interfaz, sólo necesitas seguir 2 simples pasos.
Primero creas una clase LinkedInService
implementando la interfaz:
<?php
namespace App\Services;
use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;
class LinkedInService implements SocialMediaServiceInterface
{
public function share(Post $post)
{
dd('Post compartido en LinkedIn');
}
}
Y por último, simplemente actualizas el bind
de la interfaz para que devuelva una instancia del nuevo servicio:
$container->bind(SocialMediaServiceInterface::class, function() {
return new App\Services\LinkedInService();
});
De esta manera:
- El container resuelve la interfaz usando la nueva clase service.
- Y no necesitas hacer más, ya que las secciones donde se solicita esta dependencia no requieren cambios.
Sólo has tenido que actualizar el 2do argumento que envías al método bind
, gracias a que la dependencia implementa la interfaz SocialMediaServiceInterface
tanto antes como después del cambio, y por tanto se trata de un servicio válido para compartir en redes sociales.
Laravel Service Container
Los ejemplos que hemos visto antes están basados en una clase Container simplificada.
Laravel viene con un IoC container muy potente, conocido como el Service Container de Laravel.
Es decir, Laravel implementa un contenedor por nosotros, y lo pone a nuestra disposición a través del helper app()
.
Al igual que el ejemplo simplificado que vimos antes, el Service Container de Laravel presenta un método bind()
y un método make()
, que son usados para vincular servicios y acceder a ellos, a través del container.
Adicionalmente tenemos acceso a un método singleton()
. Cuando hacemos "bind" usando este método singleton
, el contenedor creará la instancia una única vez y nos devolverá la misma cada vez que la solicitemos.
Entonces nuestro ejemplo anterior queda de la siguiente manera si usamos el Service Container de Laravel:
app()->bind(SocialMediaServiceInterface::class, function() {
return new App\Services\LinkedInService();
});
ddd(app()->make(SocialMediaServiceInterface::class));
// App\Services\LinkedInService {#262}
Service Providers
Ahora que hemos visto cómo funcionan los métodos bind()
, singleton()
, y make()
, lo siguiente es aprender dónde llamar a estos métodos.
No es adecuado ubicarlos en nuestros controladores ni modelos.
El lugar correcto para situar nuestros bindings son los service providers.
- Los service providers son clases que se ubican en la carpeta
app/Providers
. - Constituyen una parte importante del framework, ya que son los responsables de iniciar la mayoría de los servicios.
Todo proyecto de Laravel nuevo, viene con 5 service providers por defecto.
Entre ellos nos encontramos con la clase AppServiceProvider
, que presenta por defecto 2 métodos vacíos: register()
y boot()
.
El método register()
es usado para registrar nuevos servicios sobre la aplicación.
Aquí es donde debemos ubicar nuestras llamadas a los métodos bind()
y singleton()
.
class AppServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->bind(SocialMediaService::class, function() {
return new \App\Services\LinkedInService;
});
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
}
Dentro de providers, tienes acceso al contenedor usando $this->app. Puedes llamar también al helper app()
, pero es recomendable lo primero.
El método boot()
es usado iniciar cualquier lógica que requieran los servicios registrados.
Un buen ejemplo es la clase BroadcastingServiceProvider
:
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Broadcast::routes();
require base_path('routes/channels.php');
}
}
Como puedes ver, este service provider llama al método Broadcast::routes()
y requiere el archivo routes/channels.php
, activando de esta manera las rutas de broadcasting para nuestro proyecto.
Recuerda que también puedes crear tus propios Service Providers, para organizar los bindings que definas para tus propios servicios.
Por ejemplo, el comando php artisan make:provider <name>
te permite generar una clase Service Provider.
Conclusión
Hemos visto que:
- La inyección de dependencias es importante porque desplaza la responsabilidad de instanciar las dependencias al framework.
- Así no tenemos que preocuparnos por instanciar e inicializar nuestros servicios.
- IoC significa "inversion of control" y significa justamente que el control de la instanciar e inicializar dependencias lo tiene ahora el framework en vez de cada clase específica que necesita tales dependencias.
- Toda esta gestión se ve facilitada gracias a la existencia de un IoC Container.
- Laravel como framework nos provee de una implementación de IoC Container, que es el Service Container de Laravel.
- El service container es accesible desde el helper
app()
o desde$this->app
si estamos desde una clase Service Provider. - A través del service container registramos bindings, ya sea registrando clases concretas o interfaces.
- Laravel por defecto crea algunos Service Providers, pero también podemos crear nuestros propios providers y organizar allí nuestros bindings y cualquier inicialización que necesitemos para nuestros servicios.
Al final, todo se relaciona, y hemos comprendido los conceptos en armonía! ?
De igual forma, si tienes alguna duda, te invito a seguir mis cursos (los encuentras aquí debajo), y/o a dejar un comentario con tus inquietudes ?.