Antes que te vayas
Inscríbete en nuestro curso gratuito de Laravel
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:
Empecemos a revisar los fundamentos, y así mismo, ejemplos ?.
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".
Publication
controlaba la instanciación de la clase independiente FacebookService
.Esto último se conoce como inversion of control (IoC).
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:
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:
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:
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.
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}
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.
app/Providers
.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.
Hemos visto que:
app()
o desde $this->app
si estamos desde una clase Service Provider.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 ?.
Comparte este post si te fue de ayuda 🙂.
Cualquier duda y/o sugerencia es bienvenida.
Regístrate
Inicia sesión para acceder a nuestros cursos y llevar un control de tu progreso.
Cursos recomendados
Aprende Laravel desde cero y desarrolla aplicaciones web reales, en tiempo récord, de la mano de Laravel.
Ingresar al cursoActualiza tus proyectos desde cualquier versión hasta la última versión estable de Laravel.
Ingresar al cursoDesarrollemos un Messenger! Aprende sobre Channels, Queues, Vuex, JWT, Sesiones, BootstrapVue y mucho más.
Ingresar al cursoInscríbete en nuestro curso gratuito de Laravel