在 Laravel 中利用 Guzzle 提高代码质量

Improve code quality while utilizing Guzzle in Laravel

我是 laravel 的新手,所以请不要苛刻。

我正在构建一个简单的网络,它通过 Guzzle 连接到外部 API(几个端点),获取一些数据,清理它们并存储它们。

目前 - 从一个端点来看 - 我有以下工作:

public function handle(Client $client)
{
        try {
            $request= $client->request('GET', 'https://api.url./something', [
                'headers' => [
                    'X-RapidAPI-Key'=> env("FOOTBALL_API_KEY"),
                    'Accept'     => 'application/json'
                ]
            ]);
            $request = json_decode($request->getBody()->getContents(), true);
            foreach ($request as $array=>$val) {
                foreach ($val['leagues'] as $id) {
                    League::firstOrCreate(collect($id)->except(['coverage'])->toArray());
                }
            }
        } catch (GuzzleException $e) {
        };
}

因此我想要一些代码建议,如何从设计的角度使我的代码更好。

我的想法是:

a) 将 Guzzle 绑定为 service provider.
b) 使用设计模式来实现对端点的调用。URI builder maybe?

如有任何帮助,我们将不胜感激。

愿原力与你同在

详细反馈

一些特定于提供的代码本身的指针:

  • guzzle 客户端 request return 的响应与您分配给它的参数名称不匹配
  • 调用 json_decode 可能会失败,在这种情况下它们会 return null。在防御性编程方面,检查那些失败案例是很好的
  • 您的案例对响应中的数据做出了一些假设。最好在使用之前检查响应是否是您期望的实际格式。
  • 你抓住了所有 GuzzleExceptions,但在这些情况下什么都不做。我认为您可以通过以下任一方式改善这一点:
    • 记录异常
    • 抛出您将在 class 捕获的另一个异常,调用 handle() 方法
    • 以上两种选择
  • 您可以选择注入 api 密钥,而不是直接通过 env() 方法获取它。这将防止 warning block here
  • 中描述的问题

一般反馈

感觉你的代码在混合职责,这被认为是不好的做法。 handle() 方法现在执行以下操作:

  • 发送API个请求
  • 解码API个请求
  • 验证 API 回复
  • 解析 API 回复
  • 创建模型

您可以考虑将其中的部分或全部移动到单独的 class 中,如下所示:

  • ApiClient 负责发出请求
  • ResponseDecoder 负责将响应转换为 \stdClass
  • ResponseValidator 负责检查响应是否具有预期的数据结构
  • RepsonseParser 负责将响应 \stdClass 转换为集合
  • LeagueFactory 负责将集合转换为 League 模型

有人可能会争辩说前四个 class 应该放在一个名为 ApiClient 的 class 中。这完全取决于您。

所以最后你会想出这样的东西:

<?php


namespace App\Example;

use Psr\Log\LoggerInterface;

class LeagueApiHandler
{
    /**
     * @var ApiClient
     */
    private $apiClient;
    /**
     * @var ResponseDecoder
     */
    private $decoder;
    /**
     * @var ResponseValidator
     */
    private $validator;
    /**
     * @var ResponseParser
     */
    private $parser;
    /**
     * @var LeagueFactory
     */
    private $factory;
    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(
        ApiClient $apiClient,
        ResponseDecoder $decoder,
        ResponseValidator $validator,
        ResponseParser $parser,
        LeagueFactory $factory,
        LoggerInterface $logger
    ) {
        $this->apiClient = $apiClient;
        $this->decoder = $decoder;
        $this->validator = $validator;
        $this->parser = $parser;
        $this->factory = $factory;
        $this->logger = $logger;
    }

    public function handle()
    {
        try {
            $response = $this->apiClient->send();
        } catch (\RuntimeException $e) {
            $this->logger->error('Unable to send api request', $e->getMessage());
            return;
        };

        try {
            $decodedResponse = $this->decoder->decode($response);
        } catch (\RuntimeException $e) {
            $this->logger->error('Unable to decode api response');
            return;
        };

        if (!$this->validator->isValid($decodedResponse)) {
            $this->logger->error('Unable to decode api response');
            return;
        }

        $collections = $this->parser->toCollection($decodedResponse);
        foreach ($collections as $collection) {
            $this->factory->create($collection);
        }
    }
}
namespace App\Example;

use GuzzleHttp\Client;

class ApiClient
{
    /**
     * @var Client
     */
    private $client;
    /**
     * @var string
     */
    private $apiKey;

    public function __construct(Client $client, string $apiKey)
    {
        $this->client = $client;
        $this->apiKey = $apiKey;
    }

    public function send()
    {
        try {
            return $this->client->request('GET', 'https://api.url./something', [
                'headers' => [
                    'X-RapidAPI-Key' => $this->apiKey,
                    'Accept' => 'application/json'
                ]
            ]);
        } catch (GuzzleException $e) {
            throw new \RuntimeException('Unable to send request to api', 0, $e);
        };
    }
}
namespace App\Example;

use Psr\Http\Message\ResponseInterface;

class ResponseDecoder
{
    public function decode(ResponseInterface $response): \stdClass
    {

        $response = json_decode($response->getBody()->getContents(), true);
        if ($response === null) {
            throw new \RuntimeException('Unable to decode api response');
        }

        return $response;
    }
}
namespace App\Example;

class ResponseValidator
{
    public function isValid(\stdClass $response): bool
    {
        if (is_array($response) === false) {
            return false;
        }

        foreach ($response as $array) {
            if (!isset($array['leagues'])) {
                return false;
            }
        }

        return true;
    }
}
namespace App\Example;

use Illuminate\Support\Collection;

class ResponseParser
{
    /**
     * @param \stdClass $response
     * @return Collection[]
     */
    public function toCollection(\stdClass $response): array
    {
        $collections = [];
        foreach ($response as $array => $val) {
            foreach ($val['leagues'] as $id) {
                $collections[] = collect($id)->except(['coverage'])->toArray();
            }
        }
        return $collections;
    }
}
namespace App\Example;

use Illuminate\Support\Collection;

class LeagueFactory
{
    public function create(Collection $collection): void
    {
        League::firstOrCreate($collection);
    }
}