Reutilizando patrones de Laravel para formularios de Livewire

Siempre me gustó mucho Livewire, pero desde el inicio algo no me cuadró: su manejo de formularios

Aunque Livewire tiene su propia solución para manejar formularios con AJAX, se siente muy desconectado del ecosistema de Laravel, porque claro, Livewire no procesa el formulario con una solicitud HTTP como un Controlador, pero siempre me pareció extraño duplicar reglas de validación en el Componente cuando ya tenía escrita esa lógica en un custom Request

En Laravel comúnmente usamos un patrón de separación de responsabilidades para manejo de formularios, compuesto por

// Request ➜ Controller ➜ Action

Por ejemplo ProductController@store como hub central de la lógica de negocio, pero el proceso de validación del formulario aparte en ProductStoreRequest y las operaciónes relacionadas a la base de datos en ProductStoreAction

// app/Http/Requests/ProductStoreRequest.php
class ProductStoreRequest extends FormRequest
{
    public function rules()
    {
        return [
            'title' => 'required',
            'price' => 'required',
            'image' => 'required|file|max:10240',
        ];
    }
}
// app/Http/Controllers/ProductController.php
class ProductController extends Controller
{
    public function store(ProductRequest $request, ProductAction $action)
    {   
        $product = $action->handle($request);
        ...
    }
}
// app/Actions/ProductStoreAction.php
class ProductStoreAction
{
    public function handle($request)
    {
        $product = new Product();
        $product->fill($request->toArray());
        $product->save();
    }
}

Para no perder éste patrón de separación de responsabiliades tan útil, me puse a experimentar con maneras de replicar éste patron en el Componente de Livewire, tratándolo mas o menos como un equivalente de un Controlador

Normalmente un Componente Livewire con un formulario se ve así

// app/Livewire/ProductForm.php
class ProductForm extends Component
{
    public $form;

    protected function rules()
    {
        return [
            'form.title' => 'required',
            'form.price' => 'required',
            'form.image' => 'required|file|max:10240',
        ];
    }

    public function submit()
    {
        $this->validate();
 
        $product = new Product();
        $product->fill($this->form);
        $product->save();

        $this->reset();
    }
}

En donde cómo se puede ver, se repiten los procesos de establecer reglas (que ya tenemos en ProductStoreRequest) y almacenar los datos (que ya tenemos en ProductStoreAction)

Bueno, mi solución se ve así

// app/Livewire/ProductForm.php
use App\Actions\ProductStoreAction;
use App\Http\Requests\ProductStoreRequest;

class ProductForm extends Component
{
    public $form;

    public function submit()
    {
        // Construyo manualmente el Request ========== //
        $request = new ProductStoreRequest();
        $request->merge($this->form);
        if ($this->form['image']) {
            $request->files->set('image', UploadedFile::createFromBase($this->form['image']));
        }

        // Valido manualmente el Request ============= //
        $validator = Validator::make($request->all() + $request->files->all(), $request->rules());
        if ($validator->fails()) {
            // Si hay errores seteo el error bag
            foreach ($validator->errors()->messages() as $key => $messages) {
                $errors["form.$key"] = $messages[0];
            }
            $this->setErrorBag($errors);
            return false;
        }

        // Invoco el Action ========================== //
        $action = new ProductStoreAction();
        $action->handle($request);
    }
}

// You may not like it, but this is what peak performance looks like

La solicitud HTTP nunca se realiza realmente, pero utilizo el blueprint de un Request para crear un objeto compatible e imitar el flujo de un Controlador

Porqué no simplemente repetir el código? Existen dos respuestas correctas, la primera es más emocional: Repetir código es un pecado, la segunda es más sabia: Así hay menos código que mantener. Cuando se trabaja en aplicaciones grandes, los patrones de separación de responsabilidades son invaluables