小型框架,大梦想

0.6 2024-02-28 07:59 UTC

This package is auto-updated.

Last update: 2024-09-14 20:40:50 UTC


README

Mono 是一个单文件的最小化现代 PHP 框架。

它不包含自定义实现,而是作为以下优秀项目的包装器

  1. 路由(使用 nikic/FastRoute
  2. 依赖注入(使用 php-di/php-di
  3. 中间件(使用 relay/relay
  4. 模板(使用 twigphp/wig
  5. 数据到对象映射(使用 cuyz/valinor

这是 Mono 类的接口

interface MonoInterface
{
    // Render a Twig template
    public function render(string $template, array $data): string;
    
    // Add a middleware to the PSR-15 stack
    public function addMiddleware(MiddlewareInterface|callable $middleware): void;
    
    // Add a route to the FastRoute router
    public function addRoute(string|array $method, string $path, callable $handler): void;
        
    // Run the app
    public function run(): void;
}

仅使用这些方法,我们就拥有了构建应用程序所需的所有工具

<?php

$mono = new Mono(__DIR__ . '/templates');

$mono->addRoute('GET', '/example', function() {
    $result = $mono->get(SomeDependency::class)->doSomething();
    
    return new HtmlResponse($mono->render('example.twig', [
        'result' => $result
    ]));
});

$mono->run();

Mono 不打算作为一个完整的框架,而是作为一个小型 PHP 应用程序的原型。目标是展示通过结合知名库和 PSR 实现,你可以走多远。

源代码 有注释,易于阅读。

1. 路由

您使用 $mono->addRoute() 添加所有路由。与底层 FastRoute 方法的签名相同。默认情况下,路由处理程序是闭包,因为此框架主要用于小型应用程序,但您也可以使用可调用的控制器。

有关路由模式的更多信息,请参阅 FastRoute 文档。输入的路径直接传递给 FastRoute。

闭包的第一个参数是始终当前请求,它是一个 PSR-7 ServerRequestInterface 对象。之后,下一个参数是路由参数。

当调用 $mono->run() 时,当前请求与您添加的路由进行匹配,调用闭包并发出响应。

1.1 使用闭包的示例

<?php

$mono = new Mono();

$mono->addRoute('GET', '/books/{book}', function(ServerRequestInterface $request, string $book) {
    return new TextResponse(200, 'Book: ' . $book);
});

$mono->run();

1.2 使用控制器的示例

<?php

class BookController
{
    public function __construct(
        private readonly Mono $mono
    ) {
    }

    public function __invoke(ServerRequestInterface $request, string $book): ResponseInterface
    {
        return new TextResponse('Book: ' . $book');
    }
}
<?php

$mono = new Mono();

// By fetching the controller from the container, it will autowire all constructor parameters.
$mono->addRoute('GET', '/books/{book}', $mono->get(BookController::class));

$mono->run();

在生产中缓存您的路由是微不足道的。将您的缓存文件的有效、可写路径作为 routeCacheFile 参数传递给您的 Mono 对象。

$mono = new Mono(routeCacheFile: sys_get_temp_dir() . '/mono-route.cache');

2. 依赖注入

当创建 Mono 对象时,它会使用默认配置构建一个基本的 PHP-DI 容器。这意味着任何加载的类(例如通过 PSR-4)都可以自动装配或手动从容器中获取。

您可以使用 Mono 对象上的 get() 方法从容器中检索实例。

<?php

$mono = new Mono();

$mono->addRoute('GET', '/example', function() use ($mono) {
    $result = $mono->get(SomeDependency::class)->doSomething();
    
    return new JsonResponse($result);
});

$mono->run();

自定义容器

如果您需要定义自定义定义,您可以将自定义容器传递给 Mono 构造函数。有关更多信息,请参阅 PHP-DI 文档

<?php

// Custom container
$builder = new DI\ContainerBuilder();
$builder->... // Add some custom definitions
$container = $builder->build();

$mono = new Mono(container: $container);

$mono->addRoute('GET', '/example', function() use ($mono) {
    $result = $mono->get(SomeDependency::class)->doSomething();
    
    return new JsonResponse($result);
});

$mono->run();

3. 中间件

Mono 构建为一个中间件堆栈应用程序。默认流程是

  • 错误处理
  • 路由(路由与处理程序匹配)
  • 您的自定义中间件
  • 请求处理(调用路由处理程序)

您可以使用 addMiddleware() 方法向堆栈添加中间件。中间件可以是可调用的或实现 MiddlewareInterface 接口的类。中间件的执行顺序与添加顺序相同。

<?php

$mono = new Mono();

$mono->addMiddleware(function (ServerRequestInterface $request, callable $next) {
    // Do something before the request is handled
    if ($request->getUri()->getPath() === '/example') {
        return new TextResponse('Forbidden', 403);
    }
    
    return $next($request);
});

$mono->addMiddleware(function (ServerRequestInterface $request, callable $next) {
    $response = $next($request);

    // Do something after the request is handled
    return $response->withHeader('X-Test', 'Hello, world!');
});

您可以在 middlewares/psr15-middlewares 项目中找到许多已经编写好的、兼容PSR-15的中间件。这些中间件可以直接插入到Mono中使用。

4. 模板化

如果您想使用Twig,需要在Mono构造函数中传入您的模板文件夹路径。

之后,您可以在您的Mono对象上使用 render() 方法来渲染该文件夹中的Twig模板。

<?php

$mono = new Mono(__DIR__ . '/templates');

$mono->addRoute('GET', '/example', function() use ($mono) {
    $result = $mono->get(SomeDependency::class)->doSomething();
    
    return new HtmlResponse($mono->render('example.twig', [
        'result' => $result
    ]));
});

$mono->run();

5. 数据到对象的映射

这允许您将数据(例如,来自请求的POST体)映射到对象(例如,DTO)。

<?php

class BookDataTransferObject
{
    public function __construct(
        public string $title,
        public ?int $rating,
    ) {
    }
}

$_POST['title'] = 'Moby dick';
$_POST['rating'] = 10;

$mono = new Mono();

$mono->addRoute('POST', '/book', function (
    ServerRequestInterface $request
) use ($mono) {
    /*
     * $bookDataTransferObject now holds
     * all the data from the request body,
     * mapped to the properties of the class.
     */
     $bookDataTransferObject = $mono->get(TreeMapper::class)->map(
        BookDataTransferObject::class,
        $request->getParsedBody()
    );
});

$mono->run();

如果您想覆盖默认的映射行为,定义一个自定义的 Treemapper 实现,并将其设置在传递给 Mono 构造函数的容器中。

自定义映射器配置示例

$customMapper = (new MapperBuilder())
    ->enableFlexibleCasting()
    ->allowPermissiveTypes()
    ->mapper();
    
$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(true);
$containerBuilder->addDefinitions([
    TreeMapper::class => $customMapper
]);

$mono = new Mono(
    container: $containerBuilder->build()
);

其他

调试模式

Mono有一个默认会捕获所有错误并显示通用500响应的调试模式。

在开发过程中,您可以通过将 false 作为Mono构造函数的第二个参数传入来禁用此模式。这将显示实际的错误消息,并允许您在Twig模板中使用 dump

文件夹结构 & 项目设置

开始一个新项目很快。按照以下步骤操作

  1. 为您的项目创建一个新的文件夹。
  2. 运行 composer require sanderdlm/mono
  3. 在项目根目录下创建一个 public 文件夹。添加一个 index.php 文件。下面有一个“Hello world”示例。
  4. 可选地,在项目根目录下创建一个 templates 文件夹。添加一个 home.twig 文件。下面有一个示例。
  5. 运行 php -S localhost:8000 -t public 以启动内置的PHP服务器。
  6. 开始开发您的想法!

public/index.php:

<?php

declare(strict_types=1);

use Mono\Mono;

require_once __DIR__ . '/../vendor/autoload.php';

$mono = new Mono(__DIR__.'/../templates');

$mono->addRoute('GET', '/', function() {
    return new HtmlResponse($mono->render('home.twig', [
        'message' => 'Hello world!',
    ]));
});

$mono->run();

templates/home.twig:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Home</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    {{ message }}
</body>
</html>

如果您想保持简单,可以直接在index.php中工作。如果您需要定义多个文件/类,可以在项目中添加一个 src 文件夹,并将以下PSR-4自动加载片段添加到您的 composer.json 文件中

"autoload": {
    "psr-4": {
        "App\\": "src/"
    }
},

您现在可以从DI容器中访问 src 文件夹中的所有类(并自动注入它们)。

其他包

以下包与Mono配合得非常好。其中大部分可以通过Composer快速安装,然后通过向容器中添加定义来配置。

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
// Custom container
$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->useAutowiring(true);
$containerBuilder->addDefinitions([
    // Build & pass a validator
    ValidatorInterface::class => Validation::createValidatorBuilder()
        ->enableAttributeMapping()
        ->getValidator(),
]);

$mono = new Mono(
    container: $containerBuilder->build()
);

// Generic sign-up route
$mono->addRoute('POST', '/sign-up', function(
    ServerRequestInterface $request
) use ($mono) {
    // Build the DTO
    $personDataTransferObject = $mono->get(TreeMapper::class)->map(
        PersonDataTransferObject::class,
        $request->getParsedBody()
    );

    // Validate the DTO
    $errors = $mono->get(ValidatorInterface::class)->validate($personDataTransferObject);
    
    // Do something with the errors
    if ($errors->count() > 0) {
    }
});
// Create a new translator, with whatever config you like.
$translator = new Translator('en');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', [
    'hello_world' => 'Hello world!',
], 'en');

// Create a custom container & add your translator to it
$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->useAutowiring(true);
$containerBuilder->addDefinitions([
    TranslatorInterface::class => $translator,
]);

$mono = new Mono(
    container: $containerBuilder->build()
);

/*
 * Use the |trans filter in your Twig templates,
 * or get the TranslatorInterface from the container
 * and use it directly in your route handlers.
 */