Laravel 在其代码库中大量使用了Macroabletrait,但官方文档只是顺便提及。没有解释它的用途,或者你应该(和不应该)使用它的时间。让我们深入挖掘。

Macroable 特性的目的是什么?

该特征的唯一目的Macroable是允许您扩展(某些)内置的 Laravel 类的功能。

我喜欢将“可宏化”类视为支持临时 [traits][php-traits]。也就是说,您可以将“特征”添加到您不拥有的类中,而不必扩展它。

这有两个优点:

  1. 使用宏添加一些功能比扩展和覆盖 Laravel 类要简单得多。
  2. 它保持 Laravel 代码库的清洁,而不限制开发人员的自由。拼命想tail给类加一个方法Collection? 没问题

你应该在你自己的类中使用 Macroable 特性吗?

在您自己的类中使用该Macroable特性的唯一原因是您正在构建它们以供重用。这可以作为一个分布式包,也可以在您的代码库中私下使用。

Macroable 特性如何工作?

当你深入了解时,你会发现这个Macroable特性非常简单。

本质上,它维护了一个“宏”方法的关联数组,其中数组键是宏名称,数组值是一个可调用的

__call该特征使用魔法方法捕获任何未处理的实例和方法调用__callStatic。如果您的类已经实现了__callor__callStatic方法,您需要做一些额外的工作才能使用该Macroable特征。

如果请求的函数名称存在于宏数组中,则Macroabletrait 调用它并返回结果。如果它不存在,则该Macroable特征抛出一个BadMethodCallException.

如何使用 Macroable 特性添加宏?

有两种方法可以向可宏类添加功能:

  1. 使用Macroable::macro方法。
  2. 使用Macroable::mixin方法。

如何使用 Macroable::macro 方法

Macroable::macro方法是向可宏类添加功能的最常用方法。

以文档中的规范示例为例,以下代码capsResponse类添加了一个方法:

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));
    }
}

这是它的工作原理,一步一步:

  1. mixin方法接受一个对象(通常是类实例)并将其分配给$mixin变量。
  2. Laravel使用反射$mixin从对象中检索每个非私有方法。
  3. Laravel 将每个方法设置为“可访问的”。
  4. 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 方法之前调用它。

这是通过将准备工作移至宏来清理代码的绝佳机会。结果将更清晰、更具可读性,而且可测试性也不会降低。

脚注

  1. 大概是因为它与所有数据库引擎都不兼容。

了解 Laravel 的 Macroable 特性
标签: