Cómo Inyectar Servicios en LiveWire

Spread the love

Estoy seguro que has escuchado la frase:

No es lo mismo la teoría que la practica.

Esto aplica muy bien a los componentes de Laravel Livewire; cuando quieres usar la inyección de dependencias.

Los componentes de Livewire utilizan el método mount() en lugar del constructor de clase __construct() como suele hacerse con cualquier clase.

Y al igual que un controlador, puedes inyectar dependencias añadiendo parámetros tipados antes de los que recibirá el componente.

Así que en teoría lo siguiente funciona:

use \Illuminate\Session\SessionManager;

class ShowPost extends Component
{
    public $title;
    public $content;

    public function mount(SessionManager $session, $post)
    {
        $session->put("post.{$post->id}.last_viewed", now());

        $this->title = $post->title;
        $this->content = $post->content;
    }

    ...
}

Este ejemplo funciona muy bien, pero cuando intentas inyectar una clase que quieres que este disponible para todo el componente, pronto te das cuenta que las cosas no funciona como esperas.

para explicar este punto; vamos a suponer que tienes el siguiente componente que se encarga de crear tareas:

<?php

namespace App\Http\Livewire\Todo;

use App\Services\TaskService;
use Livewire\Component;

class TaskForm extends Component
{
    public $name;
    protected $service;

    protected $rules = [
        'name' => 'required|string|min:6',
    ];

    public function mount(TaskService $taskService) //👈🏼 Inyectas el servicio
    {
        $this->service = $taskService;
    }

    public function render()
    {
        return view('livewire.todo.task-form');
    }

    public function store()
    {
        //👇🏼 Esto no funciona como esperas
        $this->service->set([
            'user' => auth()->user(),
            'validated' => $this->validate(),
        ])->create();

        $this->reset('name');
        $this->emit('reloadTaskList');
        $this->emit('alert.success','The Task was successfully created');
    }
}

Este componente recibe el servicio TaskService en el método mount como dice la documentación. y podrías pensar que el servicio va a crear un nueva tarea cuando se invoque el método store().

Pero esto no es del todo cierto en la practica…

Cuando se ejecute este componente se va a cargar con normalidad en la vista, pero cuando se haga una llamada al método store() mediante alguna acción desde el front,

Vas obtener el siguiente error.

Error

Call to a member function set() on null

Este error sucede porque mount() se ejecuta solamente en el primer request, las peticiones posteriores que se hacen desde el Front no vuelven a ejecutar mount()!.

Por lo cual la llamada a $this->service contiene el valor de null.

¿Entonces como puedo inyectar una dependencia que pueda usar en todo el componente?

La ventaja es que los componente de Livewire usan el contenedor de Laravel, así que podemos inyectar las dependencias directamente en el método que las necesita.

Inyecta el servicio en el método que lo requiere

Para lograr esto solo quita el método mount() del componente e inyecta el servicio directamente en el método store() como se muestra en el ejemplo:

<?php

namespace App\Http\Livewire\Todo;

use App\Services\TaskService;
use Livewire\Component;

class TaskForm extends Component
{
    public $name;

    protected $rules = [
        'name' => 'required|string|min:6',
    ];

    public function render()
    {
        return view('livewire.todo.task-form');
    }

    public function store(TaskService $taskService) //👈🏼 Inyección del servicio
    {
        $taskService->set([
            'user' => auth()->user(),
            'validated' => $this->validate(),
        ])->create();

        $this->reset('name');
        $this->emit('reloadTaskList');
        $this->emit('alert.success','The Task was successfully created');
    }
}

Con este cambio el método store() va a trabajar de la forma que esperamos.

Pero ahora tienes una desventaja, ¿Pero como es eso?

Esto de inyectar directamente la dependencia en el método esta muy bien cuando tenemos un método o dos, pero si vas a tener mas de una acción; tendrás que declarar de forma explicita el servicio en todos los métodos que lo necesitan.

y eso como que no es muy cómodo, pero no te angusties, existen otras dos alternativas para solucionar el problema en esta situación.

Inyecta el servicio usando una propiedad computada.

Livewire ofrece una solución para acceder a propiedades dinámicas, es decir, propiedades que no están declaradas en el componente y que se pueden resolver en el momento que se solicitan.

Digamos que son similares a los query scope de Laravel o los mutators.

Veamos un ejemplo antes de continuar.

class ShowPost extends Component
{
    // Computed Property
    public function getPostProperty()
    {
        return Post::find($this->postId);
    }

    public function deletePost()
    {
        //👇🏼 cuando se llame a post se ejecutara getPostProperty
        $this->post->delete(); 
    }
}

En el ejemplo cuando se ejecute el método deletePost(), este va a llamar a la propiedad $this->post que realmente hace una llamada al método getPostProperty().

Como puedes ver usar propiedades computadas puede ser útil, para crear propiedades que obtienen sus datos de una base de datos o de otro medio persistente como una caché.

Y nada te impide usar estas propiedades para “inyectar” el servicio TaskService de nuestro ejemplo.

Así que solo tienes que hacer algunos cambios para resolver el problema como sigue:

<?php

namespace App\Http\Livewire\Todo;

use App\Services\TaskService;
use Livewire\Component;

class TaskForm extends Component
{
    public $name;

    protected $rules = [
        'name' => 'required|string|min:6',
    ];

    public function getServiceProperty() : TaskService
    {
        return resolve(TaskService::class);
    }

    public function render()
    {
        return view('livewire.todo.task-form');
    }

    public function store()
    {
        //👇🏼 nota que ahora llamamos a la propiedad service
        $this->service->set([
            'user' => auth()->user(),
            'validated' => $this->validate(),
        ])->create();

        $this->reset('name');
        $this->emit('reloadTaskList');
        $this->emit('alert.success','The Task was successfully created');
    }
}

Ahora ya es posible llamar al servicio en cualquier método que lo requiera, sin tener que hacer cambios adicionales.

El Confiable Facade

Si por alguna razón las dos opciones anteriores no te gustan, siempre queda la confiable y bien conocida opción de crear un Facade para tu servicio, y ahorrarte el asunto de ver donde tienes que inyectar tu servicio.

<?php

namespace App\Http\Livewire\Todo;

use App\Services\TaskService;
use Livewire\Component;

class TaskForm extends Component
{
    public $name;

    protected $rules = [
        'name' => 'required|string|min:6',
    ];

    public function render()
    {
        return view('livewire.todo.task-form');
    }

    public function store()
    {
        TaskService::set([
            'user' => auth()->user(),
            'validated' => $this->validate(),
        ])->create();

        $this->reset('name');
        $this->emit('reloadTaskList');
        $this->emit('alert.success','The Task was successfully created');
    }
}

Y eso es todo, ya estas listo para dominar la inyección de dependencias en LiveWire.

Por ultimo recuerda que ninguna de estas opciones es la “mejor” opción, así que la opción correcta para tí siempre estará en función de tus necesidades.