Laravel 在其代码库中大量使用了Macroable
trait,但官方文档只是顺便提及它。没有解释它的用途,或者你应该(和不应该)使用它的时间。让我们深入挖掘。
Macroable 特性的目的是什么?
该特征的唯一目的Macroable
是允许您扩展(某些)内置的 Laravel 类的功能。
我喜欢将“可宏化”类视为支持临时 [traits][php-traits]。也就是说,您可以将“特征”添加到您不拥有的类中,而不必扩展它。
这有两个优点:
- 使用宏添加一些功能比扩展和覆盖 Laravel 类要简单得多。
- 它保持 Laravel 代码库的清洁,而不限制开发人员的自由。拼命想
tail
给类加一个方法Collection
? 没问题。
你应该在你自己的类中使用 Macroable 特性吗?
在您自己的类中使用该Macroable
特性的唯一原因是您正在构建它们以供重用。这可以作为一个分布式包,也可以在您的代码库中私下使用。
Macroable 特性如何工作?
当你深入了解时,你会发现这个Macroable
特性非常简单。
本质上,它维护了一个“宏”方法的关联数组,其中数组键是宏名称,数组值是一个可调用的。
__call
该特征使用魔法方法捕获任何未处理的实例和方法调用__callStatic
。如果您的类已经实现了__call
or__callStatic
方法,您需要做一些额外的工作才能使用该Macroable
特征。
如果请求的函数名称存在于宏数组中,则Macroable
trait 调用它并返回结果。如果它不存在,则该Macroable
特征抛出一个BadMethodCallException
.
如何使用 Macroable 特性添加宏?
有两种方法可以向可宏类添加功能:
- 使用
Macroable::macro
方法。 - 使用
Macroable::mixin
方法。
如何使用 Macroable::macro 方法
该Macroable::macro
方法是向可宏类添加功能的最常用方法。
以文档中的规范示例为例,以下代码caps
向Response
类添加了一个方法:
Response::macro('caps', function ($value) {
return Response::make(strtoupper($value));
});
如果愿意,您还可以创建基于类的宏。如果您想对其进行单元测试,这将特别方便。
<?php
namespace App\Macros;
use Illuminate\Support\Facades\Response;
class CapsResponse
{
public function handle(string $value)
{
return Response::make(strtoupper($value));
}
}
您注册一个基于类的宏如下:
Response::macro('caps', [\App\Macros\CapsResponse::class, 'handle']);
如何使用 Macroable::mixin 方法
如果你想声明一些相关的方法,你可能更喜欢使用Macroable::mixin
方法。
该mixin
方法可能令人困惑,所以让我们花点时间对其进行分解。
public static function mixin($mixin)
{
$methods = (new ReflectionClass($mixin))->getMethods(
ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
);
foreach ($methods as $method) {
$method->setAccessible(true);
static::macro($method->name, $method->invoke($mixin));
}
}
这是它的工作原理,一步一步:
- 该
mixin
方法接受一个对象(通常是类实例)并将其分配给$mixin
变量。 - Laravel使用反射
$mixin
从对象中检索每个非私有方法。 - Laravel 将每个方法设置为“可访问的”。
- Laravel 调用每个方法,并将其返回值用作已注册的可调用宏。
最后一点是容易使人绊倒的事情。他们想象他们的 mixin 类应该看起来像这样:
// Incorrect example
class ResponseMixin
{
public function caps(string $value)
{
return Response::make(strtoupper($value));
}
}
而实际上,他们的 mixin 类应该是这样的:
// Correct example
class ResponseMixin
{
public function caps(): Closure
{
/**
* This is the function that will run when we call
* Response::caps
*/
return function (string $value) {
return Response::make(strtoupper($value));
}
}
}
宏用法示例
在撰写本文时,Laravel 的核心类中有 30 个是可宏化的。下面是几个示例,说明如何使用此功能清理代码。
API 响应
许多 API 响应非常相似,这可能会导致控制器中出现大量不必要的繁忙工作。宏是解决此问题的绝佳方法。
例如,我们可以使用宏轻松生成对OPTIONS
请求的响应:
Response::macro('options', function (
array $methods,
int $status = 200,
array $headers = []
): JsonResponse {
$methods = array_sort($methods);
$headers = array_merge($headers, [
'allow' => implode(',', $methods),
]);
return response()->json(
['options' => $methods],
$status,
$headers
);
});
现在我们的控制器代码可以简单到:
return response()->options(['GET', 'HEAD', 'OPTIONS']);
数据库迁移
有时您可能希望在尝试删除外键之前检查它是否存在。Laravel 不提供开箱即用的功能1,但对我们来说幸运的是,该类Illuminate\Database\Schema\Blueprint
是可宏化的:
Blueprint::macro('hasForeign', function ($index) {
$indexString = is_array($index) ? $this->createIndexName('foreign', $index) : $index;
$doctrineTable = Schema::getConnection()
->getDoctrineSchemaManager()
->listTableDetails($this->table);
return $doctrineTable->hasIndex($indexString);
});
有了这个,您的迁移将保持干净和可读:
Schema::table('users', function (Blueprint $table) {
if ($table->hasForeign(['roles'])) {
$table->dropForeign(['roles']);
}
});
下一步去哪里
注意任何似乎需要大量“准备”工作的 Laravel 方法调用。一个很好的指标是,如果你有一个单独的“构建数据”方法,你在调用 Laravel 方法之前调用它。
这是通过将准备工作移至宏来清理代码的绝佳机会。结果将更清晰、更具可读性,而且可测试性也不会降低。
脚注
- 大概是因为它与所有数据库引擎都不兼容。↩