Há alguns dias, eu estava corrigindo um teste instável e descobri que precisava de alguns valores exclusivos e válidos em minha fábrica. O Laravel envolve o FakerPHP, que normalmente acessamos através do auxiliar fake()
. O FakerPHP vem com modificadores como valid()
e unique()
, mas você pode usar apenas um de cada vez, então você não pode fazer fake()->unique()->valid()
, que é exatamente o que eu precisava.
Isso me fez pensar: e se quisermos criar nosso próprio modificador? Por exemplo, uniqueAndValid()
ou qualquer outro modificador. Como podemos estender a estrutura?
Estarei abandonando minha linha de pensamento.
Antes de saltar para qualquer solução excessivamente projetada, sempre quero verificar se existe uma opção mais simples e entender com o que estou lidando. Então, vamos dar uma olhada no auxiliar 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); }
Lendo o código, podemos ver que o Laravel vincula um singleton ao contêiner. Porém, se inspecionarmos o abstrato, é uma classe regular que não implementa nenhuma interface, e o objeto é criado por meio de uma fábrica. Isso complica as coisas. Por que?
Porque se fosse uma interface, poderíamos simplesmente criar uma nova classe que estendesse a classe \Faker\Generator
base, adicionar alguns novos recursos e revinculá-la ao contêiner. Mas não temos esse luxo.
Há uma fábrica envolvida. Isso significa que não é uma simples instanciação; há alguma lógica sendo executada. Neste caso, a fábrica adiciona alguns provedores (PhoneNumber, Text, UserAgent, etc.). Portanto, mesmo que tentemos religar, teremos que usar a fábrica, que retornará o \Faker\Generator
original.
Soluções 🤔? Alguém poderia pensar: “O que nos impede de criar nossa própria fábrica que devolva o novo gerador conforme descrito no ponto 1?” Bem, nada, podemos fazer isso, mas não faremos! Usamos uma estrutura por vários motivos, um deles sendo atualizações. O que acontecerá se o FakerPHP adicionar um novo provedor ou fizer uma grande atualização? O Laravel ajustará o código e as pessoas que não fizeram nenhuma alteração não notarão nada. No entanto, ficaríamos de fora e nosso código poderia até quebrar (provavelmente). Então, sim, não queremos ir tão longe.
Agora que exploramos as opções básicas, podemos começar a pensar nas mais avançadas, como padrões de design. Não precisamos de uma implementação exata, apenas de algo familiar ao nosso problema. É por isso que sempre digo que é bom conhecê-los. Neste caso, podemos “decorar” a classe Generator
adicionando novos recursos enquanto mantemos os antigos. Parece bom? Vamos ver como!
Primeiro, vamos criar uma nova classe, 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á o nosso “decorador” (mais ou menos). É uma classe simples que espera o Generator
base como uma dependência e introduz um novo modificador, uniqueAndValid()
. Ele também usa o atributo ForwardsCalls
do Laravel, que permite fazer proxy de chamadas para o objeto base.
Essa característica possui dois métodos:
forwardCallTo
eforwardDecoratedCallTo
. Use o último quando quiser encadear métodos no objeto decorado. No nosso caso, teremos sempre uma única chamada.
Também precisamos implementar UniqueAndValidGenerator
, que é o modificador personalizado, mas esse não é o objetivo do artigo. Se você estiver interessado na implementação, esta classe é basicamente uma mistura do ValidGenerator e UniqueGenerator que vem com o FakerPHP, você pode encontrá-la aqui .
Agora, vamos estender o framework, no 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'); } }
O método extend()
verifica se um resumo correspondente ao nome fornecido foi vinculado ao contêiner. Se sim, ele substitui seu valor pelo resultado do fechamento, veja:
// 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); } } }
É por isso que definimos o método fakerAbstractName()
, que gera o mesmo nome que o auxiliar fake()
se liga no contêiner.
Verifique novamente o código acima se você perdeu, deixei um comentário.
Agora, toda vez que chamarmos fake()
, uma instância de FakerGenerator
será retornada e teremos acesso ao modificador personalizado que introduzimos. Cada vez que invocamos uma chamada que não existe na classe FakerGenerator
, __call()
será acionado e fará proxy para o Generator
base usando o método forwardCallTo()
.
É isso! Finalmente posso fazer fake()->uniqueAndValid()->randomElement()
, e funciona perfeitamente!
Antes de concluirmos, quero ressaltar que este não é um padrão puramente decorador. Contudo, os padrões não são textos sagrados; ajuste-os para atender às suas necessidades e resolver o problema.
Frameworks são incrivelmente úteis e o Laravel vem com muitos recursos integrados. No entanto, eles não podem cobrir todos os casos extremos em seus projetos e, às vezes, você pode chegar a um beco sem saída. Quando isso acontecer, você sempre poderá estender a estrutura. Vimos como é simples e espero que você tenha entendido a ideia principal, que se aplica além deste exemplo do Faker.
Comece sempre de forma simples e procure a solução mais simples para o problema. A complexidade virá quando for necessária, portanto, se a herança básica funcionar, não há necessidade de implementar um decorador ou qualquer outra coisa. Ao ampliar a estrutura, certifique-se de não ir longe demais, onde a perda supera o ganho. Você não quer acabar mantendo uma parte da estrutura sozinho.