() translation by (you can also view the original English article)
Inversión
de control, o IoC, es una técnica que permite invertir el control en
comparación con el código de procedimiento clásico. La forma más prominente de IoC es, por supuesto, la Inyección de Dependencia, o DI. El contenedor IoC de Laravel es una de las características más usadas de Laravel, aunque probablemente sea el menos comprendido.
He aquí un ejemplo muy rápido de la utilización de la inyección de dependencia para lograr la inversión de control.
1 |
<?php
|
2 |
|
3 |
class JeepWrangler |
4 |
{
|
5 |
public function __construct(Petrol $fuel) |
6 |
{
|
7 |
$this->fuel = $fuel; |
8 |
}
|
9 |
|
10 |
public function refuel($litres) |
11 |
{
|
12 |
return $litres * $this->fuel->getPrice(); |
13 |
}
|
14 |
}
|
15 |
|
16 |
class Petrol |
17 |
{
|
18 |
public function getPrice() |
19 |
{
|
20 |
return 130.7; |
21 |
}
|
22 |
}
|
23 |
|
24 |
$petrol = new Petrol; |
25 |
$car = new JeepWrangler($petrol); |
26 |
|
27 |
$cost = $car->refuel(60); |
Mediante
el uso de inyección constructor hemos delegado la creación de nuestra
instancia Petrol
de nuevo a la persona que llama, logrando así la
inversión de control. Nuestro JeepWrangler
no necesita saber de dónde proviene Petrol
,
siempre que lo consiga.
Entonces, ¿qué tiene que ver todo esto con
Laravel? Bastante, en realidad. Laravel, si no lo sabías, en realidad es
un contenedor de IoC. Un contenedor es un objeto que, como es de
esperar, contiene cosas. El contenedor IoC de Laravel se usa para
contener muchos, muchos enlaces diferentes. Todo lo que haces en Laravel
tendrá en algún momento una interacción con el contenedor de IoC. Esta
interacción es generalmente en forma de una unión que se resuelve.
Si
abre alguno de los proveedores de servicios existentes de Laravel, lo
más probable es que vea algo como esto en el método de registro register
(el
ejemplo se ha simplificado mucho).
1 |
$this->app['router'] = $this->app->share(function($app) { |
2 |
return new Router; |
3 |
});
|
Es un enlace muy, muy básico. Consiste en el nombre de la encuadernación (router
) y un resolver (el
cierre). Cuando el enlace se resuelve desde el contenedor, obtendremos
una instancia de Router
devuelta.
Laravel normalmente agrupa nombres de enlace similares, comosession
ysession.store
.
Para resolver el
enlace podemos simplemente llamar a un método directamente de él, o
utilizar el método make
en el contenedor.
1 |
$router = $this->app->make('router'); |
Eso es lo que hace el contenedor en su forma más básica. Pero, al igual que la mayoría de las cosas de Laravel, hay mucho más que limitarse a limitar y resolver las clases.
Fijaciones compartidas y no
compartidas
Si
ha examinado varios de los proveedores de servicios de Laravel, notará
que la mayoría de los enlaces se definen como el ejemplo anterior. Aquí
está de nuevo:
1 |
$this->app['router'] = $this->app->share(function($app) { |
2 |
return new Router; |
3 |
});
|
Esta vinculación utiliza el método share
compartido en el contenedor. Laravel
utiliza una variable estática para almacenar un valor resuelto anterior
y simplemente reutilizará ese valor cuando un enlace se resuelva de
nuevo. Esto es básicamente lo que hace el método share
hace.
1 |
$this->app['router'] = function($app) { |
2 |
static $router; |
3 |
|
4 |
if (is_null($router)) { |
5 |
$router = new Router; |
6 |
}
|
7 |
|
8 |
return $router; |
9 |
|
10 |
};
|
Otra forma de escribir esto sería utilizar el método bindShared
.
1 |
$this->app->bindShared('router', function($app) { |
2 |
return new Router; |
3 |
});
|
También puede utilizar los métodos singleton
y instance
para lograr un enlace compartido. Por lo tanto, si todos logran lo mismo, ¿cuál es la diferencia? No
mucho, en realidad. Yo personalmente prefiero usar el método bindShared
.
Enlace condicional
Puede haber ocasiones en las que quiera unir algo al contenedor, pero
sólo cuando no haya sido enlazado antes. Hay algunas maneras que usted
podría ir sobre esto, pero el más fácil es utilizar el método bindIf
.
1 |
$this->app->bindIf('router', function($app) { |
2 |
return new ImprovedRouter; |
3 |
});
|
Esto sólo se enlazará al contenedor si la vinculación del enrutador router
ya no existe. Lo único que hay que tener en cuenta aquí es cómo compartir una
vinculación condicional. Para ello, debe proporcionar un tercer
parámetro al método bindIf
con un valor de true
.
Resolución de
dependencia automática
Una
de las características más utilizadas del contenedor IoC es su
capacidad para resolver automáticamente las dependencias de las clases
que no están enlazadas. ¿Que significa exactamente? En primer lugar, en
realidad no es necesario vincular algo al contenedor para resolver una
instancia. Podemos simplemente make
una instancia de casi cualquier
clase.
1 |
class Petrol |
2 |
{
|
3 |
public function getPrice() |
4 |
{
|
5 |
return 130.7; |
6 |
}
|
7 |
}
|
8 |
|
9 |
// In our service provider...
|
10 |
$petrol = $this->app->make('Petrol'); |
El contenedor instanciará nuestra clase Petrol
para nosotros. La mejor parte de esto es que también resolverá las dependencias del constructor para nosotros.
1 |
class JeepWrangler |
2 |
{
|
3 |
public function __construct(Petrol $fuel) |
4 |
{
|
5 |
$this->fuel = $fuel; |
6 |
}
|
7 |
|
8 |
public function refuel($litres) |
9 |
{
|
10 |
return $litres * $this->fuel->getPrice(); |
11 |
}
|
12 |
|
13 |
}
|
14 |
|
15 |
// In our service provider...
|
16 |
$car = $this->app->make('JeepWrangler'); |
Lo primero que hace el contenedor es inspeccionar las dependencias de la clase JeepWrangler
. A continuación, intentará resolver esas dependencias. Por
lo tanto, debido a que nuestro JeepWrangler
tipo de pistas de la clase
Petrol
, el contenedor automáticamente resolver e inyectar como una
dependencia.
El contenedor no puede inyectar automáticamente dependencias
que no sean de tipo insinuado. Así
que si una de sus dependencias es una matriz, entonces necesitará
instanciarla manualmente o darle al parámetro un valor
predeterminado.
Implementaciones vinculantes
Tener Laravel resolver
automáticamente sus dependencias es grande y simplifica el proceso de
instanciar las clases manualmente. Sin
embargo, hay momentos en los que se desea inyectar una implementación
específica, especialmente cuando se utilizan interfaces. Esto se logra
fácilmente utilizando el nombre completo de la clase como enlace. Para
demostrar esto usaremos una nueva interfaz llamada Fuel
.
1 |
interface Fuel |
2 |
{
|
3 |
public function getPrice(); |
4 |
}
|
Ahora
nuestra clase JeepWrangler
puede teclear la interfaz, y nos
aseguraremos de que nuestra clase Petrol
implemente la interfaz.
1 |
class JeepWrangler |
2 |
{
|
3 |
public function __construct(Fuel $fuel) |
4 |
{
|
5 |
$this->fuel = $fuel; |
6 |
}
|
7 |
|
8 |
public function refuel($litres) |
9 |
{
|
10 |
return $litres * $this->fuel->getPrice(); |
11 |
}
|
12 |
}
|
13 |
|
14 |
class Petrol implements Fuel |
15 |
{
|
16 |
public function getPrice() |
17 |
{
|
18 |
return 130.7; |
19 |
}
|
20 |
}
|
Ahora podemos unir nuestra interfaz de Fuel
al contenedor y hacer que resuelva una nueva instancia de Petrol
.
1 |
$this->app->bind('Fuel', 'Petrol'); |
2 |
|
3 |
// Or, we could instantiate it ourselves.
|
4 |
$this->app->bind('Fuel', function ($app) { |
5 |
return new Petrol; |
6 |
});
|
Ahora
cuando hacemos una nueva instancia de nuestro JeepWrangler
, el
contenedor verá que pide Fuel
, y sabrá inyectar automáticamente
una instancia Petrol
.
Esto también
hace que sea realmente fácil intercambiar una implementación, ya que
simplemente podemos cambiar el enlace en el contenedor. Para
demostrarlo, podríamos empezar a recargar nuestro coche con gasolina
premium, que es un poco más caro.
1 |
class PremiumPetrol implements Fuel |
2 |
{
|
3 |
public function getPrice() |
4 |
{
|
5 |
return 144.3; |
6 |
}
|
7 |
}
|
8 |
|
9 |
// In our service provider...
|
10 |
$this->app->bind('Fuel', 'PremiumPetrol'); |
Vinculaciones contextuales
Tenga en cuenta que los enlaces contextuales sólo están disponibles en
Laravel 5.
Una vinculación contextual le permite enlazar una implementación (como lo hicimos anteriormente) a una clase específica.
1 |
abstract class Car |
2 |
{
|
3 |
public function __construct(Fuel $fuel) |
4 |
{
|
5 |
$this->fuel = $fuel; |
6 |
}
|
7 |
|
8 |
public function refuel($litres) |
9 |
{
|
10 |
return $litres * $this->fuel->getPrice(); |
11 |
}
|
12 |
}
|
A
continuación, crearemos una nueva clase NissanPatrol
que amplíe la
clase abstracta y actualizaremos nuestro JeepWrangler
para extenderlo
también.
1 |
class JeepWrangler extends Car |
2 |
{
|
3 |
//
|
4 |
}
|
5 |
|
6 |
class NissanPatrol extends Car |
7 |
{
|
8 |
//
|
9 |
}
|
Por último crearemos una nueva clase Diesel
que implementa la interfaz Fuel
.
1 |
class Diesel implements Fuel |
2 |
{
|
3 |
public function getPrice() |
4 |
{
|
5 |
return 135.3; |
6 |
}
|
7 |
}
|
Ahora, nuestro Jeep Wrangler se reposta con gasolina y nuestro Nissan Patrol se reposta con diesel. Si tratamos de usar el mismo método que antes vinculando una implementación a la interfaz, entonces ambos coches tendrían el mismo tipo de combustible, que no es lo que queremos.
Así que para asegurarse de que cada coche se rellena con el combustible correcto, podemos informarle al contenedor qué implementación utilizar en cada contexto.
1 |
$this->app->when('JeepWrangler')->needs('Fuel')->give('Petrol'); |
2 |
$this->app->when('NissanPatrol')->needs('Fuel')->give('Diesel'); |
Etiquetado
Tenga en cuenta que el etiquetado sólo está disponible en Laravel 5.
Ser
capaz de resolver enlaces desde el contenedor es muy importante. Normalmente sólo podemos resolver algo si sabemos cómo se ha ligado al
contenedor. Con
Laravel 5, ahora somos capaces de etiquetar nuestros enlaces para que
los desarrolladores puedan resolver fácilmente todos los enlaces que
tengan la misma etiqueta.
Si está
desarrollando una aplicación que permite a otros desarrolladores crear
complementos y desea poder resolver fácilmente todos esos complementos,
las etiquetas serán extremadamente útiles.
1 |
$this->app->tag('awesome.plugin', 'plugin'); |
2 |
|
3 |
// Or an array of tags.
|
4 |
|
5 |
$tags = ['plugin', 'theme']; |
6 |
|
7 |
$this->app->tag('awesome.plugin', $tags); |
Ahora, para resolver todos los enlaces de una etiqueta dada, podemos usar el método etiquetado tagged
.
1 |
$plugins = $this->app->tagged('plugin'); |
2 |
|
3 |
foreach ($plugins as $plugin) { |
4 |
$plugin->doSomethingFunky(); |
5 |
}
|
Rebotes y Rebinding
Cuando se enlaza algo al contenedor con el mismo nombre más de una vez,
se llama rebinding. Laravel se dará cuenta de que algo vinculante de
nuevo y provocará un rebound.
El
mayor beneficio aquí es cuando se está desarrollando un paquete que
permite a otros desarrolladores para ampliarlo por volver a conectar los
componentes en el contenedor. Para usarlo, tendremos que implementar la
inyección de setter Car
en nuestro abstract.
1 |
abstract class Car |
2 |
{
|
3 |
public function __construct(Fuel $fuel) |
4 |
{
|
5 |
$this->fuel = $fuel; |
6 |
}
|
7 |
|
8 |
public function refuel($litres) |
9 |
{
|
10 |
return $litres * $this->fuel->getPrice(); |
11 |
}
|
12 |
|
13 |
public function setFuel(Fuel $fuel) |
14 |
{
|
15 |
$this->fuel = $fuel; |
16 |
}
|
17 |
|
18 |
}
|
Supongamos que estamos vinculando nuestro JeepWrangler
al contenedor como tal.
1 |
$this->app->bindShared('fuel', function ($app) { |
2 |
return new Petrol; |
3 |
});
|
4 |
|
5 |
$this->app->bindShared('car', function ($app) { |
6 |
return new JeepWrangler($app['fuel']); |
7 |
});
|
Esto
es perfectamente bien, pero digamos que otro desarrollador viene a lo
largo y quiere extender esto y utilizar gasolina premium en el coche. Así que utilizan el método setFuel
para inyectar su nuevo combustible en el coche.
1 |
$this->app['car']->setFuel(new PremiumPetrol); |
En la mayoría de los casos, esto podría ser todo lo que se necesita; Sin
embargo, ¿qué pasa si nuestro paquete se vuelve más complicado y el
fuel
vinculante se inyecta en varias otras clases? Esto haría que el otro desarrollador tuviera que establecer su nueva
instancia un montón de veces. Por lo tanto, para resolver esto, podemos
hacer uso de rebinding:
1 |
$this->app->bindShared('car', function ($app) { |
2 |
return new JeepWrangler($app->rebinding('fuel', function ($app, $fuel) { |
3 |
$app['car']->setFuel($fuel); |
4 |
}));
|
5 |
});
|
El
método de rebinding
nos devolverá inmediatamente la instancia ya
enlazada para que podamos usarla en el constructor de nuestro
JeepWrangler
. El cierre dado al método de rebinding
recibe dos parámetros, siendo el
primero el contenedor IoC y el segundo el nuevo binding. A
continuación, podemos utilizar el método setFuel
nosotros mismos para
inyectar el nuevo enlace en nuestra instancia JeepWrangler
.
Todo lo que
queda es que otro desarrollador simplemente vuelva a conectar
combustible fuel
en el contenedor. Su proveedor de servicios podría ser así:
1 |
$this->app->bindShared('fuel', function () { |
2 |
return new PremiumPetrol; |
3 |
});
|
Una vez que la unión se rebote en el contenedor, Laravel activará automáticamente los cierres asociados closures. En nuestro caso, la nueva instancia de PremiumPetrol
se establecerá en
nuestra instancia de JeepWrangler
.
Extensión
Si
desea inyectar una dependencia en uno de los enlaces principales o un
enlace creado por un paquete, el método extend
en el contenedor es una
de las formas más sencillas de hacerlo.
Este método resolverá el enlace
del contenedor y ejecutará un cierre con el contenedor y la instancia
resuelta como parámetros. Esto le permite resolver fácilmente e inyectar
sus propios enlaces, o simplemente instanciar una nueva clase e
inyectarla.
1 |
$this->app->extend('car', function ($app, $car) { |
2 |
$car->setFuel(new PremiumPetrol); |
3 |
});
|
A diferencia de rebinding, esto sólo establecerá la dependencia de un solo enlace.
Uso fuera de Laravel
Al
igual que muchos de los componentes de iluminación que componen el
marco de Laravel, el contenedor se puede utilizar fuera de Laravel en
una aplicación independiente. Para ello, primero debe solicitarlo como una dependencia en su archivo composer.json
.
1 |
{
|
2 |
"require": { |
3 |
"illuminate/container": "4.2.*" |
4 |
}
|
5 |
}
|
Esto instalará la versión 4.2
más reciente del contenedor. Ahora, todo lo que queda por hacer es instanciar un nuevo contenedor.
1 |
require 'vendor/autoload.php'; |
2 |
|
3 |
$app = new Illuminate\Container\Container; |
4 |
|
5 |
$app->bindShared('car', function () { |
6 |
return new JeepWrangler; |
7 |
});
|
De todos los componentes, este es uno de los más fáciles de usar dondequiera que necesite un contenedor IoC flexible y destacado.