编程

如何修改 Laravel HTTP 客户端的默认行为

49 2025-05-30 00:24:00

当使用依赖于 Laravel HTTP 客户端的 Laravel 包时,你有时会想改变包与外部服务交互的行为。其中一种方法是使用 Laravel 服务容器带来的强大依赖注入功能。

问题

如果你只想看代码示例,你可以跳过这一节。

在我们的一个项目中,我们不得不使用一个名为 DocuWare 的文档管理软件中的 API。当你必须做这样的事情时,在搜索引擎上搜索现有的软件包总是值得一试的。Laravel 生态系统非常庞大,所以过去可能有人不得不解决同样的问题。
事实上,我们发现了一家名为 CodeBar AG 的公司的 Laravel Docuware 包。经过深入调查,这个包似乎符合我们的需求。所以我们尝试了一下,并将其安装在一个新的 Laravel 安装中。在对其进行修补后,我们发现了一个关于使用此软件包的能力的问题。

我们的应用必须使用的 DocuWare REST API 服务,位于客户局域网的防火墙后面。由于此文档管理系统处理非常敏感的数据,因此将此系统暴露于整个互联网将是一个安全漏洞。因此,只有经过授权的系统才能通过预定义规则的防火墙。客户网络管理员告诉我们,每次向防火墙发送请求时,我们都需要在 HTTP 标头中发送更多信息/凭据。当凭据正确时,防火墙系统会让我们的请求传递到 DocuWare 系统。一切都应该很好。

当然,Laravel DocuWare 包没有在每次请求时发送此类额外标头信息的机制。这是一个非常特殊的用例,在 100 个不同的客户实现中,你可能有 100 个不同解决方案。你无法构建一个包来匹配所有这些情况。所以我们必须找到解决这个问题的办法。我们找到了解决这个问题的不同方法——以下是其中一些。

我们可以放弃使用现有包的想法,构建我们自己的 DocuWare REST API 实现。这可能是最灵活但也是最耗时的方法。

其次,我们可以 fork 现有的包并使其适应我们的需求。这很灵活,也不那么耗时,但这样做时,你要对整个代码库负责。你必须意识到源代码中可能出现的安全问题或未来的迁移,你可能无法完全理解每一点。

第三,你可以尝试用您的特定逻辑扩展一些应用程序核心组件,这样原始包就可以在不进行修改的情况下使用。这就是我们要做的。这里的关键词是 Laravel 强大的依赖注入功能,这是由服务容器带来的。

如何扩展 Laravel 的 HTTP 客户端

让我们编写一些代码!深挖包源码,我们知道该包使用的是 Laravel HTTP 客户端,该客户端是从 Laravel 8.0 开始添加到框架中的。让我们假设我们必须在 HTTP 客户端完成的每个请求上传递一些额外的标头。在我们的案例中,我们使用的是微服务架构的简化方式,我们将要构建的服务可以称为 “DocuWare API 服务”。

与许多其他 Laravel 核心组件一样,HTTP 客户端在底层使用工厂模式。因此,每次必须创建新的 Pending Request 实例时,这项工作都是由一个名为 HttpClientFactory 的类完成的。由于我们希望更改 API 微服务发送的每个请求,因此 HttpClientFactory 是更改所有应用 HTTP 请求的正确位置。那么,让我们创建自己的 HttpClientFactory 实现。

首先,我们创建一个新类 app/Factories/HttpClientFactory.

namespace App\Factories;

use Illuminate\Http\Client\Factory;

class HttpClientFactory extends Factory
{
    /**
     * Execute a method against a new pending request instance.
     *
     * @param string $method
     * @param array $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        if (static::hasMacro($method)) {
            return $this->macroCall($method, $parameters);
        }

        return tap($this->newPendingRequest(), function ($request) {
            $request
            //This line has changed from the base factory class
                ->withBasicAuth(config('services.firewall.login'), config('services.firewall.password'))
                ->stub($this->stubCallbacks);
        })->{$method}(...$parameters);
    }
}

在这个 HttpClientFactory 类中,我们继承了框架的原始 Factory 类。由于我们希望扩展来自 HTTP 客户端的每个请求,因此魔术 __call 方法是实现这一点的正确方式。大部分代码都是从基础 Factory 类复制的。只需一行代码,我们就为每个 HTTP 请求添加了一个 basicAuth Authorization Header。就这么简单。

现在我们已经创建了自己的 HTTP 客户端工厂类,我们必须通知 Laravel 服务容器,我们想对所有 HTTP 客户端实例使用这个工厂,而不是原来的工厂。这是在 AppServiceProvider 的引导(boot)方法中完成的:

/**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->bind(Illuminate\Http\Client\Factory::class, function ($app) {
            return new App\Factories\HttpClientFactory($app->make(Dispatcher::class));
        });
    }

通过这两行代码,我们将新的 Factory 类绑定到服务容器。因此,每次应用从服务容器请求新的 HttpClientFactory 实例时,都将使用我们自己的 Factory 实现。Laravel 在幕后使用了依赖注入模式。

请注意,这个新工厂会更改整个应用中的每一个 HTTP 客户端请求。即使是那些你不想适用的。由于我们目前正在使用微服务,因此可以很容易地做到这一点。在经典的单体架构中,你必须小心这些变化。在单体应用中,你可能希望有条件地绑定这个新工厂。有条件绑定工厂的一个好方法是创建一个只应用于应用某些部分的新中间件。

小结

我知道 Laravel 应用容器或依赖注入对某些人来说就像一种黑魔法。我理解。起初,这似乎很抽象,对于新开发人员来说很难理解。但我可以告诉你,学习如何使用它是值得的。

你可以用这项技术替换 Laravel 框架的几乎所有组件。它非常强大,使用它会让你变得非常灵活。你的行为几乎没有任何限制。这种灵活性是 Laravel 框架如此受欢迎的主要原因之一。