几天前,我正在修复一个不稳定的测试,结果发现我需要工厂中的一些唯一且有效的值。 Laravel 包装了 FakerPHP,我们通常通过fake()
帮助程序访问它。 FakerPHP 附带了valid()
和unique()
等修饰符,但您一次只能使用一个,因此您不能执行fake()->unique()->valid()
,而这正是我所需要的。
这让我想到,如果我们想创建自己的修饰符怎么办?例如, uniqueAndValid()
或任何其他修饰符。我们如何扩展框架?
我将抛弃我的思路。
在采用任何过度设计的解决方案之前,我总是想检查是否有更简单的选项,并了解我正在处理的问题。所以,让我们看一下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); }
阅读代码后,我们可以看到 Laravel 将单例绑定到容器。但是,如果我们检查抽象,它是一个不实现任何接口的常规类,并且对象是通过工厂创建的。这使事情变得复杂。为什么?
因为如果它是个接口,我们只需创建一个新类来扩展基类\Faker\Generator
,添加一些新功能,然后将其重新绑定到容器即可。但我们没有这种奢侈。
这里涉及到一个工厂。这意味着它不是一个简单的实例化;有一些逻辑正在运行。在这种情况下,工厂会添加一些提供程序(PhoneNumber、Text、UserAgent 等)。因此,即使我们尝试重新绑定,我们也必须使用工厂,它将返回原始的\Faker\Generator
。
解决方案🤔?有人可能会想,“是什么阻止我们创建自己的工厂来返回第 1 点中概述的新生成器?”好吧,没什么,我们可以这样做,但我们不会!我们使用框架有几个原因,其中之一是更新。如果 FakerPHP 添加了新的提供商或进行了重大升级,会发生什么?Laravel 将调整代码,没有进行任何更改的人不会注意到任何事情。但是,我们会被排除在外,我们的代码甚至可能会中断(最有可能)。所以,是的,我们不想走那么远。
现在我们已经探索了基本选项,我们可以开始考虑更高级的选项,比如设计模式。我们不需要确切的实现,只需要一些熟悉我们问题的东西。这就是为什么我总是说了解它们是件好事。在这种情况下,我们可以通过添加新功能同时保留旧功能来“装饰” Generator
类。听起来不错?让我们看看怎么做!
首先,让我们创建一个新类, 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); } }
这将是我们的“装饰器”(有点)。它是一个简单的类,它需要基础Generator
作为依赖项,并引入了一个新的修饰符uniqueAndValid()
。它还使用 Laravel 的ForwardsCalls
特性,这允许它代理对基础对象的调用。
此特征有两种方法:
forwardCallTo
和forwardDecoratedCallTo
。当您想要在装饰对象上链接方法时,请使用后者。在我们的例子中,我们将始终进行一次调用。
我们还需要实现UniqueAndValidGenerator
,这是自定义修饰符,但这不是本文的重点。如果你对实现感兴趣,这个类基本上是 FakerPHP 附带的ValidGenerator和UniqueGenerator的混合体,你可以在这里找到它。
现在,让我们在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'); } }
extend()
方法检查与给定名称匹配的抽象是否已绑定到容器。如果是,它将使用闭包的结果覆盖其值,请看一下:
// 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); } } }
这就是我们定义fakerAbstractName()
方法的原因,该方法生成与容器中fake()
辅助程序绑定相同的名称。
如果您错过了,请重新检查上面的代码,我留下了评论。
现在,每次我们调用fake()
时,都会返回一个FakerGenerator
实例,并且我们将可以访问我们引入的自定义修饰符。每次我们调用FakerGenerator
类中不存在的调用时,都会触发__call()
,并且它会使用forwardCallTo()
方法将其代理到基本Generator
。
就是这样!我终于可以做fake()->uniqueAndValid()->randomElement()
,而且效果非常好!
在结束之前,我想指出这不是一个纯粹的装饰器模式。然而,模式不是神圣的文本;调整它们以满足您的需求并解决问题。
框架非常有用,Laravel 具有许多内置功能。但是,它们无法覆盖项目中的所有边缘情况,有时,您可能会陷入困境。当这种情况发生时,您可以随时扩展框架。我们已经看到了它是多么简单,我希望您理解了主要思想,这不仅适用于这个 Faker 示例。
总是从简单开始,寻找问题最简单的解决方案。复杂性会在必要时出现,因此如果基本继承可以解决问题,则无需实现装饰器或其他任何东西。当您扩展框架时,请确保不要走得太远,否则得不偿失。您不想最终独自维护框架的一部分。