Hace unos días, estaba arreglando una prueba inestable y resultó que necesitaba algunos valores únicos y válidos dentro de mi fábrica. Laravel envuelve FakerPHP, al que normalmente accedemos a través del asistente fake()
. FakerPHP viene con modificadores como valid()
y unique()
, pero solo puedes usar uno a la vez, por lo que no puedes hacer fake()->unique()->valid()
, que es exactamente lo que necesitaba.
Esto me hizo pensar, ¿y si queremos crear nuestro propio modificador? Por ejemplo, uniqueAndValid()
o cualquier otro modificador. ¿Cómo podemos ampliar el marco?
Estaré abandonando mi línea de pensamiento.
Antes de lanzarme a cualquier solución excesivamente diseñada, siempre quiero comprobar si existe una opción más sencilla y entender a qué me estoy enfrentando. Entonces, echemos un vistazo al asistente fake()
:
function fake($locale = null) { if (app()->bound('config')) { $locale ??= app('config')->get('app.faker_locale'); } $locale ??= 'en_US'; $abstract = \Faker\Generator::class.':'.$locale; if (! app()->bound($abstract)) { app()->singleton($abstract, fn () => \Faker\Factory::create($locale)); } return app()->make($abstract); }
Al leer el código, podemos ver que Laravel vincula un singleton al contenedor. Sin embargo, si inspeccionamos el resumen, es una clase normal que no implementa ninguna interfaz y el objeto se crea a través de una fábrica. Esto complica las cosas. ¿Por qué?
Porque si fuera una interfaz, podríamos simplemente crear una nueva clase que extienda la clase base \Faker\Generator
, agregar algunas características nuevas y volver a vincularla al contenedor. Pero no tenemos este lujo.
Hay una fábrica involucrada. Esto significa que no es una simple creación de instancias; se está ejecutando cierta lógica. En este caso, la fábrica agrega algunos proveedores (PhoneNumber, Text, UserAgent, etc.). Entonces, incluso si intentamos volver a vincular, tendremos que usar la fábrica, que devolverá el \Faker\Generator
original.
¿Soluciones 🤔? Uno podría pensar: "¿Qué nos impide crear nuestra propia fábrica que devuelva el nuevo generador como se describe en el punto 1?" Bueno, nada, podemos hacer eso, ¡pero no lo haremos! Usamos un marco por varias razones, una de ellas son las actualizaciones. ¿Qué pasará si FakerPHP agrega un nuevo proveedor o realiza una actualización importante? Laravel ajustará el código y las personas que no hayan realizado ningún cambio no notarán nada. Sin embargo, quedaríamos excluidos y nuestro código podría incluso fallar (lo más probable). Entonces sí, no queremos llegar tan lejos.
Ahora que hemos explorado las opciones básicas, podemos empezar a pensar en opciones más avanzadas, como patrones de diseño. No necesitamos una implementación exacta, solo algo familiar para nuestro problema. Por eso siempre digo que es bueno conocerlos. En este caso, podemos "decorar" la clase Generator
agregando nuevas funciones manteniendo las antiguas. ¿Suena bien? ¡Veamos cómo!
Primero, creemos una nueva clase, FakerGenerator
:
<?php namespace App\Support; use Closure; use Faker\Generator; use Illuminate\Support\Traits\ForwardsCalls; class FakerGenerator { use ForwardsCalls; public function __construct(private readonly Generator $generator) { } public function uniqueAndValid(Closure $validator = null): UniqueAndValidGenerator { return new UniqueAndValidGenerator($this->generator, $validator); } public function __call($method, $parameters): mixed { return $this->forwardCallTo($this->generator, $method, $parameters); } }
Este será nuestro "decorador" (más o menos). Es una clase simple que espera el Generator
base como una dependencia e introduce un nuevo modificador, uniqueAndValid()
. También utiliza el rasgo ForwardsCalls
de Laravel, que le permite enviar llamadas al objeto base.
Este rasgo tiene dos métodos:
forwardCallTo
yforwardDecoratedCallTo
. Utilice este último cuando desee encadenar métodos en el objeto decorado. En nuestro caso siempre tendremos una única llamada.
También necesitamos implementar UniqueAndValidGenerator
, que es el modificador personalizado, pero ese no es el objetivo del artículo. Si está interesado en la implementación, esta clase es básicamente una mezcla de ValidGenerator y UniqueGenerator que se incluyen con FakerPHP, puede encontrarla aquí .
Ahora, ampliemos el marco, en AppServiceProvider
:
<?php namespace App\Providers; use Closure; use Faker\Generator; use App\Support\FakerGenerator; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->extend( $this->fakerAbstractName(), fn (Generator $base) => new FakerGenerator($base) ); } private function fakerAbstractName(): string { // This is important, it matches the name bound by the fake() helper return Generator::class . ':' . app('config')->get('app.faker_locale'); } }
El método extend()
comprueba si se ha vinculado al contenedor un resumen que coincide con el nombre dado. Si es así, anula su valor con el resultado del cierre, eche un vistazo:
// Laravel: src/Illuminate/Container/Container.php public function extend($abstract, Closure $closure) { $abstract = $this->getAlias($abstract); if (isset($this->instances[$abstract])) { // You are interested here $this->instances[$abstract] = $closure($this->instances[$abstract], $this); $this->rebound($abstract); } else { $this->extenders[$abstract][] = $closure; if ($this->resolved($abstract)) { $this->rebound($abstract); } } }
Es por eso que definimos el método fakerAbstractName()
, que genera el mismo nombre que el asistente fake()
en el contenedor.
Vuelve a verificar el código de arriba si te lo perdiste, dejé un comentario.
Ahora, cada vez que llamemos a fake()
, se devolverá una instancia de FakerGenerator
y tendremos acceso al modificador personalizado que introdujimos. Cada vez que invocamos una llamada que no existe en la clase FakerGenerator
, se activará __call()
y la enviará al Generator
base utilizando el método forwardCallTo()
.
¡Eso es todo! Finalmente puedo hacer fake()->uniqueAndValid()->randomElement()
, ¡y funciona de maravilla!
Antes de concluir, quiero señalar que este no es un patrón decorativo puro. Sin embargo, los patrones no son textos sagrados; modifíquelos para que se ajusten a sus necesidades y resuelva el problema.
Los marcos son increíblemente útiles y Laravel viene con muchas funciones integradas. Sin embargo, no pueden cubrir todos los casos extremos de sus proyectos y, en ocasiones, es posible que llegue a un callejón sin salida. Cuando eso sucede, siempre puedes ampliar el marco. Hemos visto lo simple que es y espero que hayas entendido la idea principal, que se aplica más allá de este ejemplo de Faker.
Empiece siempre de forma sencilla y busque la solución más sencilla al problema. La complejidad llegará cuando sea necesaria, por lo que si la herencia básica funciona, no hay necesidad de implementar un decorador ni nada más. Cuando amplíe el marco, asegúrese de no ir demasiado lejos, donde la pérdida supere la ganancia. No querrás terminar manteniendo una parte del marco por tu cuenta.