metanull/restful-service

RESTfulService 提供了实现 RESTful 网络服务所需的功能。

v0.2.4 2023-04-20 22:19 UTC

This package is auto-updated.

Last update: 2024-09-21 01:14:14 UTC


README

为了使用此类创建 RESTful Web 服务,我们需要

  • 一个应用程序类
  • 一些请求处理器(或路由)
  • 在应用程序中注册我们的处理器
  • 运行应用程序

以下是一个简单的示例实现。

导入库

  • 如果尚未完成,请安装 composer
  • 为您的项目创建一个目录,在该目录中
  • 安装 MetaNull\RESTfulService 包: composer require metanull/restful-service
  • 创建以下文件

应用程序类

这是应用程序的核心。它负责

  • 初始化系统,通过实例化一个 RESTfulService 对象
  • 注册应用程序的请求处理器
  • 调用 RESTfulServiceServe() 方法
  • 注意: Serve() 查找请求的合适处理器并调用它。
./Application/Application.php
<?php
    namespace Application;

    use MetaNull\RESTfulService\Network\Http\Http;
    use MetaNull\RESTfulService\Network\Http\RESTfulService;
    use MetaNull\RESTfulService\Application\ApplicationInterface;

    /** The RESTfulService application */
    class Application implements ApplicationInterface
    {
        public ?RESTfulService $service = null;
        public function __construct()
        {
            $this->service = new RESTfulService();  // Create the RESTfulService
        }

        public function Run()
        {
            // Add some headers that will be returned with any response
            $this->service->serviceData['ExtraHeaders'][Http::HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = Http::ALLOW_ORIGIN_ANY;
            $this->service->serviceData['ExtraHeaders'][Http::HEADER_VARY] = Http::VARY_ORIGIN;

            // Add request handlers - in order of precedence
            $this->service->Route(Routes\Root::class, '/');                     // Handler for requests to "/"
            $this->service->Route(Routes\Item::class, '%^/item/([\d+])$%');     // Handler fro requests matching the provided perl regexp
            $this->service->Route(Routes\Fallback::class);                      // Handler for anything else

            // Handle the received request
            $this->service->Serve();
        }
    }

请求处理器类

处理 "GET /"

首先我们想要处理任何到达我们 API 根目录的 GET 请求(例如:/)。

为了被识别为 GET 请求的处理器,我们的处理器应实现 Get 接口。

所有基本请求处理器类和接口都实现了实现 RouteHandlerInterface。它定义了一个机制,通过该机制应用程序可以 询问 处理器是否能够处理给定的路由。您可以定义自己的路由处理器类,但系统附带了一些预定义的。

在这种情况下,我们将扩展 StringMatchRouteHandler 类。这是一个将 route 与字符串进行比较的处理器。如果它们严格相等,则处理器可以处理请求。

注意:该字符串的值可以在类本身中定义(使用 SpecializedRouteHandler::SetExpression() 方法),但将所有 '路由' 定义在应用程序初始化函数中更为灵活且易于阅读。

以下是如何在 Application 类的 Run() 方法中完成的

    // Registers our 'Root' handler for requests to `/`.
    $this->service->Route('\Application\Routes\Root', '/');
./Application/Routes/Root.php
<?php
namespace Application\Routes;

use MetaNull\RESTfulService\Network\Http\Get;
use MetaNull\RESTfulService\Network\Http\StringMatchRouteHandler;

use MetaNull\RESTfulService\Network\Http\Request;
use MetaNull\RESTfulService\Network\Http\Response;
use MetaNull\RESTfulService\Network\Http\ResponseFactory;

class Root extends StringMatchRouteHandler implements Get
{

    /**
     * A RequestHandler provides a method to handle a HTTP Request, this processing produces a HTTP Response
     * @param Request $request The received HTTP request
     * @param array $serviceData Data shared by the controller
     * @param string $routeParameters,... May receive parameters that were extracted from the Request's Route)
     * @return Response The resulting HTTP Response
     * @throws HttpException
     */
    public function Handle(Request $request, array &$serviceData, string ...$routeParameters) : Response
    {
        $response = ResponseFactory::Json([
            'message' => 'It works!',
            'handler' => __METHOD__,
        ]);
        return $response;
    }
}

处理 "GET /Item/{itemId}"

与前面的示例类似,这次我们的处理器需要支持一些参数。

它应接收一个项目的 ID,并返回匹配的项目。

我们可以通过扩展 RegexpMatchRouteHandler 类来实现这一点。与字符串比较不同,这个类将使用 PERL 正则表达式对路由进行正则表达式匹配。如果路由匹配,则处理器处理请求。如果正则表达式提取了一些信息,则提取的值将自动通过处理器 Handle() 方法的第三个参数传递给我们的处理器: string ...$routeParameters

Application 类的 Run() 方法中,我们使用以下方式注册了我们的处理器

    // Registers our 'Item' handler for requests to `/item/{itemId}`.
    // itemId is expected to be composed of one or more digits.
    $this->service->Route('\Application\Routes\Item', '%^/item/([\d+])$%');

表达式 %^/item/(\d+)$% 将从路由中提取 exactly one 组: (\d+)注意: \d+ 代表一系列 1 或多个连续的数字。

我们的处理器将接收提取的值作为 $routeParameters[0]

./Application/Routes/Item.php
    // Registers our 'Item' handler for requests to `/item/{itemId}`.
    // itemId is expected to be composed of one or more digits.
    $this->service->Route('\Application\Routes\Item', '%^/item/(\d+)$%');
<?php
namespace Application\Routes;

use MetaNull\RESTfulService\Network\Http\Get;
use MetaNull\RESTfulService\Network\Http\RegexpMatchRouteHandler;
use MetaNull\RESTfulService\Network\Http\Request;
use MetaNull\RESTfulService\Network\Http\Response;
use MetaNull\RESTfulService\Network\Http\ResponseFactory;

class Item extends RegexpMatchRouteHandler implements Get
{
    /**
     * A RequestHandler provides a method to handle a HTTP Request, this processing produces a HTTP Response
     * @param Request $request The received HTTP request
     * @param array $serviceData Data shared by the controller
     * @param string $routeParameters,... May receive parameters that were extracted from the Request's Route)
     * @return Response The resulting HTTP Response
     * @throws HttpException
     */
    public function Handle(Request $request, array &$serviceData, string ...$routeParameters) : Response
    {
        dd($request->route);
        $response = ResponseFactory::Json([
            'message' => 'It works!',
            'item_id' => $routeParameters[0] ?? null;   // The ItemId received from the route.
            'handler' => __METHOD__,
        ]);
        return $response;
    }
}

处理 "任何" 其他 GET 请求

最后,我们希望拦截任何未被我们的处理器处理的请求。这是通过扩展 AnyRouteHandler 类来实现的。

与前面的两个例子不同,AnyRouteHandler 不需要检查是否可以处理该路由。它将简单地处理 所有 请求。正因为如此,定义此路由时,将其放在最后非常重要,因为处理器的评估顺序是它们定义的顺序。

如果这个路由首先被定义,它将 吞没 它看到的任何 GET 请求。

以下是如何通过我们的 Application 类的 Run() 方法注册处理器的

    // Registers our 'Fallback' handler for any other request.
    $this->service->Route('\Application\Routes\Fallback');
./Application/Routes/Fallback.php
<?php
namespace Application\Routes;

use MetaNull\RESTfulService\Network\Http\Get;
use MetaNull\RESTfulService\Network\Http\AnyRouteHandler;
use MetaNull\RESTfulService\Network\Http\Request;
use MetaNull\RESTfulService\Network\Http\Response;
use MetaNull\RESTfulService\Network\Http\ResponseFactory;

class Fallback extends AnyRouteHandler implements Get
{
    /**
     * A RequestHandler provides a method to handle a HTTP Request, this processing produces a HTTP Response
     * @param Request $request The received HTTP request
     * @param array $serviceData Data shared by the controller
     * @param string $routeParameters,... May receive parameters that were extracted from the Request's Route)
     * @return Response The resulting HTTP Response
     * @throws HttpException
     */
    public function Handle(Request $request, array &$serviceData, string ...$routeParameters) : Response
    {
        $response = ResponseFactory::Json([
            'message' => 'It works!',
            'handler' => __METHOD__,
        ]);
        return $response;
    }
}

我们还需要什么?

一个引导文件

引导是应用程序的起点。它负责设置自动加载器,并调用应用程序的主方法。

./bootstrap.php
<?php
    // Your autoloader (see below)
    require_once __DIR__ . '/autoload.php';
    // Composer autoloader
    require_once __DIR__ . '/vendor/autoload.php';

    use Application\Application;

    $app = new Application();
    $app->Run();

为我们的类提供自动加载器

自动加载器能够加载 '你的' 应用程序类(定义在 '/Application' 目录中)。

./autoload.php
<?php
    function autoload($class)
    {
        // Define an array of namespace prefixes and their corresponding base directory paths
        $prefixes = [
            'Application\\' => __DIR__ . '/Application',
        ];

        // Iterate over the prefixes and try to match the class with the corresponding file path
        foreach ($prefixes as $prefix => $baseDir) {
            // Check if the class uses the current namespace prefix
            $len = strlen($prefix);
            if (strncmp($prefix, $class, $len) !== 0) {
                continue;
            }
            
            // Remove the namespace prefix and convert the namespace separators to directory separators
            $relativeClass = substr($class, $len);
            $relativeClass = str_replace('\\', '/', $relativeClass);
            
            // Construct the file path by appending the base directory path and the relative class name with '.php' extension
            $file = $baseDir . '/' . $relativeClass . '.php';
            
            // Require the file if it exists
            if (file_exists($file)) {
                require_once $file;
            }
        }
    }

    // Register the autoloader function with spl_autoload_register()
    spl_autoload_register('autoload');

在 Web 服务器上发布我们的应用程序代码

此文档的范围不包含 Web 服务器的配置。但作为一个例子,以下是如何使用 Apache 2.4 进行配置。

将以下 .htaccess 文件复制到我们的应用程序根目录,将指示 Apache 将所有请求重写(透明地)传递到我们的 bootstrap.php

例如,如果服务器收到对 http://my.domain.com/directory/do/something 的请求,它将重写为 /directory/bootstrap.php?route=/do/something

注意: route 是 RESTfulService 框架内部使用的参数。该服务捕获该参数,并通过 Request 对象将其提供给所有请求处理器。

    public function Handle(Request $request, array &$serviceData, string ...$routeParameters) : Response
    {
        error_log($request->route); // Prints the received route into the php log file
    }
./.htaccess
# If you are using Apache 2.4, and do not have access to the config files, you
# may copy this .htaccess file to the root of your directory.
#
# It is required that administrators of the server permit loading .htaccess
# file from you application's directory. This is typically achieved by adding:
#    AllowOverride FileInfo
# in the Directory configuration.

# Modify /directory/ to match with the path (relative to the root) of your
# server. Note that the trailing slash is important!
RewriteBase /directory/

# Rewrite all requests so that they are "passed" to the bootstrap function
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule ^bootstrap\.php$ "bootstrap.php?route=/" [L,QSA]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ "bootstrap.php?route=/$1" [L,DPI,QSA]
</IfModule>