一个无需构建步骤的 Hack 和 HHVM 请求路由器。

v1.0.0 2023-11-12 00:45 UTC

This package is auto-updated.

Last update: 2024-09-26 18:47:37 UTC


README

一个无需构建步骤的 Hack 和 HHVM 请求路由器。

L.E.C.O.F. 路由器

  • L. 懒加载
  • E. 评估
  • C. 组合
  • O. 的
  • F. 过滤器

发音为 "Lack of router"

为什么选择 Lecof 路由器?

这个路由器通过使用懒加载来避免性能原语构建步骤的需要。对于具有许多端点的复杂应用程序,构建完整路由器可能比实际路由本身需要更长的时间。Codegen 通过最小化每个请求需要进行的发现量来解决这个问题。这是以服务器上的内存和 CPU 为代价换取良好的开发者体验。我迫切希望重新获得添加路由而无需延迟或需要记住运行 codegen 的开发者体验。这个小巧的包只包含足够的基本元素,允许您通过嵌套函数调用的树来表达您的路由需求,这些调用在每次请求时执行,同时给您提供使其快速的手段。任何子树都可以通过调用 Lecof\lazy(function_returning_my_subtree<>) 来替换,它将子树的构建推迟到需要的时候。如果它从未被需要,就不会被构建。

使用和扩展 Lecof 路由器的准备

为了开始使用 lecof-router,您需要提供自己的类并实现一个接口。

一个 RequestInfo 实现是绝对必需的。这个类知道如何在您的当前 HHVM 版本中获取请求信息。这个类有意不提供,因为这可能会在不久的将来发生变化。这也是主要的自定义点。如果您需要根据 IP、cookie、URL 参数或其他内容进行路由,您可以将它们添加到您的 RequestInfo,并通过 ->getByType<T>() API 访问这些信息。

此点之后的一切都是严格可选的。您的基本使用可跳转到 使用

如果您希望将路由变量注入到您的 RouteResult 中,您必须提供 ParsedVariable 的实现。您可以使用 reify 只使用一个类,或者使用多个类,每个类针对不同的类型进行专门化。

如果您希望从您的 RouteResult 中解析路由变量,您必须创建一个 VariableParser 的实现,该实现知道如何将原始文本转换为您的首选 ParsedVariable

最后但同样重要的是,这个库旨在扩展。如果内置的 Filter 类不能满足您的需求,您可以用几行代码编写自己的。也许您有很多端点应该被路由到而没有文件扩展名,如 /about,但它们需要与扩展一起工作以保持向后兼容性。您可以实现自己的

function literal_with_optional_extension<T as nonnull>(
  string $literal,
  string $extension,
  LecofInterfaces\Filter<T> $next,
)[]: LecofInterfaces\Filter<T> {
  return new LiteralWithOptionalExtension($literal, $extension, $next);
}

final class LiteralWithOptionalExtension<T as nonnull>
  implements LecofInterfaces\Filter<T> {

  public function __construct(
    private string $literal,
    private string $extension,
    private LecofInterfaces\Filter<T> $next,
  )[] {}

  public function filter(
    LecofInterfaces\RequestInfo $request_info,
    int $index,
  )[]: ?LecofInterfaces\RouteResult<T> {
    $segment = $request_info->getPathSegment($index);
    if (
      $segment !== $this->literal &&
      $segment !== $this->literal.$this->extension
    ) {
      return null;
    }

    return $this->next->filter($request_info, $index + 1);
  }
}

使用

此示例展示了 Lecof 路由器可以做什么。入口点可能是最接近最小启动器的。您可能决定更改 MyEntryPointType 的签名以更好地满足您的需求。在从未路由的 __EntryPoint 应用程序迁移时,使用 (function(): Awaitable<void>) 并将其作为路由目标和 __EntryPoint 目标双重用途可能是有益的。您必须将解析信息放入静态变量中(就像使用 HH\\global_get() 一样)。

type RequestVariables = dict<string, LecofInterfaces\ParsedVariable<mixed>>;
type MyEntryPointType = (function(RequestVariables): Awaitable<void>);
type MyFilterType = LecofInterfaces\Filter<MyEntryPointType>;

<<__EntryPoint>>
async function my_web_entry_point_async(): Awaitable<void> {
  $request_info = get_my_request_info_from_globals();

  $router = Lecof\merge(
    // Index is very likely, so let's match it first.
    Lecof\done(web_index_async<>),
    // Only construct the /api subtree if we need it.
    Lecof\literal('api', Lecof\lazy(api_routes<>)),
    // Static resource not found, we can short circuit here.
    Lecof\literal('static', Lecof\ignore_trailing_path(Lecof\done(
      four_oh_four_async<>,
    ))),
    Lecof\lazy(web_routes<>),
  );

  $default = tuple(four_oh_four_async<>, vec[]);
  list($route, $variables) = $router->filter($request_info, 0) ?? $default;
  await $route(Dict\from_values($variables, $v ==> $v->getName()));
}

// Some other file

function api_routes()[]: MyFilterType {
  return Lecof\literals(dict[
    'user' => Lecof\merge(
      Lecof\literal('me', Lecof\done(api_response_current_user_async<>)),
      Lecof\parse_variable(
        new IntegerParser('user_id'),
        Lecof\done(api_response_user_by_id_async<>),
      ),
    ),
    'users' => Lecof\merge(
      Lecof\inject_variable(
        new GenericParsedVariable<int>('page_number', '1', 1),
        Lecof\done(api_response_users_on_page_async<>),
      ),
      Lecof\parse_variable(
        new IntegerParser('page_number'),
        Lecof\done(api_response_users_on_page_async<>),
      ),
    ),
    // ...
  ]);
}

function web_routes()[]: MyFilterType {
  return Lecof\merge(
    Lecof\slashed_literals(dict[
      'about' => Lecof\done(web_about_async<>),
      'legal/privacy' => Lecof\done(web_privacy_async<>),
    ]),
    // ...
  );
}

// Another file

final class IntegerParser implements LecofInterfaces\VariableParser<int> {
  public function __construct(private string $name)[] {}
  public function canParse(string $raw)[]: bool {
    return Str\to_int($raw) is nonnull;
  }
  public function parse(string $raw)[]: LecofInterfaces\ParsedVariable<int> {
    return new GenericParsedVariable<int>(
      $this->name,
      $raw,
      Str\to_int($raw) as nonnull,
    );
  }
}

final class GenericParsedVariable<reify T>
  implements LecofInterfaces\ParsedVariable<T> {
  public function __construct(
    private string $name,
    private string $raw,
    private T $value,
  )[] {}
  public function getName()[]: string {
    return $this->name;
  }
  public function getRawValue()[]: string {
    return $this->raw;
  }
  public function getValue()[]: T {
    return $this->value;
  }
}