oasis/slimapp

slim 应用框架


README

瘦应用框架(SlimApp)是一个一体化框架,旨在使PHP项目的开发,无论是Web还是控制台项目,都更快、更简单。

安装与设置

该框架包含了一系列有用的PHP组件。这使得在设置新项目时更加方便:您只需使用composer要求项目本身,然后运行项目设置命令。

在新的项目根目录下运行以下命令以安装项目

composer require oasis/slimapp

安装后,您可以通过运行以下命令初始化项目

./vendor/bin/slimapp slimapp:project:init

按照屏幕提示提供必要的信息,您的项目目录结构将自动创建。

以下是项目初始化过程中所需信息的说明列表

在本文档的其余部分中,我们假设项目名称为 test-project,供应商为 minhao。所有其他设置将保持默认值。

目录结构

自动初始化的项目将具有以下目录结构

+ PROJECT_DIR/
    + assets/                            # static assets directory
    + bin/                               # executable directory
        - test-project.php               # auto-generated project CLI entry point (executable)
    + cache/                             # default cache directory
    + config/                            # config file direcotry
        - cli-config.php                 # auto-generated Doctrine CLI config file
        - config.yml                     # auto-generated configuration YAML file
        - routes.yml                     # auto-generated routing YAML file
        - services.yml                   # auto-generated service container (to be parsed by symfony/di)
    + src/                               # default source code (class files) directory
        + Controllers/                   # namespace for controller classes
            - DemoController.php         # as the name tells, a demo controller
        + Database/                      # namespace for db classes
            - TestProjectDatabase.php    # class which provides access to EntityManager and DBAL connection
        - TestProject.php                # base class for the project, extending SlimApp class
        - TestProjectConfiguration.php   # configuration definition class for config/config.yml
    + templates/                         # default Twig template base directory
    + vendor/                            # composer components directory
    + web/                               # web entry directory
        - front.php                      # entry file for HTTP Kernel
    - bootstrap.php                      # bootstrap file
    - composer.json                      # composer config file
    - composer.lock                      # composer lock file

配置

SlimApp的基本配置文件是config.yml,位于config目录下。这是一个YAML文件,可以解释为PHP中的数组。以下是自动生成的配置文件的默认内容

# config.yml

is_debug: true                          # is application in debug mode
dir:                                    # directory settings
    log: /data/logs/test-project        # logging dir
    data: /data/test-project            # data dir
    cache: /project-root/cache          # cache dir
    template: /project-root/templates   # template dir
db:                                     # database settings
    host: localhost                     # db host
    port: 3306                          # db port
    user: test_project                  # db user
    password: test-project              # db user password
    dbname: test_project                # db name
memcached:                              # cache settings
    host: localhost                     # memcached host
    port: 11211                         # memcached port

config.yml文件是严格解析的,这意味着在此文件中定义的所有值都应该符合配置定义。SlimApp利用symfony/config来支持配置定义和解析配置文件。在src/目录下有一个自动生成的配置定义类。

引导

让我们首先看一下位于PROJECT_DIR下的bootstrap.php文件,以开始使用启用SlimApp的应用程序

<?php

use Minhao\TestProject\TestProject;
use Minhao\TestProject\TestProjectConfiguration;

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

define('PROJECT_DIR', __DIR__);

/** @var TestProject $app */
$app = TestProject::app();
$app->init(__DIR__ . "/config", new TestProjectConfiguration(), __DIR__ . "/cache/config");

return $app;

以下是一些需要注意的事项

  • 在这个文件中执行composer组件自动加载
  • 配置定义是即时实例化的,使用自动生成的配置定义类TestProjectConfiguration
  • 引导文件返回一个类型为TestProject的对象,它是SlimApp的扩展

参考配置值

有了TestProject实例(因此是SlimApp的实例),我们可以通过两种略有不同的方式访问配置值

通过 配置键 获取值
<?php

use Oasis\SlimApp\SlimApp;
use Oasis\Mlib\Utils\DataProviderInterface;

/** @var SlimApp $app */
$isDebug           = $app->getMandatoryConfig('is_debug', DataProviderInterface::BOOL_TYPE);
$logDir            = $app->getMandatoryConfig('dir.log', DataProviderInterface::STRING_TYPE);
$nonExistingConfig = $app->getMandatoryConfig('non_existing_config'); // will throw
$port              = $app->getOptionalConfig('db.port', DataProviderInterface::INT_TYPE, 3306);
$nonExistingConfig = $app->getOptionalConfig('non_existing_config'); // will return null

注意:层次配置键可以通过点"."连接

通过 参数键 获取值
<?php

use Oasis\SlimApp\SlimApp;
use Oasis\Mlib\Utils\DataProviderInterface;

/** @var SlimApp $app */
$isDebug           = $app->getParameter('app.is_debug');
$dbPort            = $app->getParameter('app.db.port');
$nonExistingConfig = $app->getParameter('app.non_existing_config'); // will throw

与第一种方法相比,参数键仅在配置键之前添加"app."前缀。此外,所有参数键都必须存在,否则在访问时将抛出异常。

在实际应用中,我们建议使用参数键,因为它与在容器定义和twig模板中访问参数的方式一致。另一方面,当使用配置键时,自动类型检查能力有时是一个有用的优点。

服务容器

SlimApp大量使用依赖注入设计模式。内部,它使用symfony/dependency-injection组件来实现服务容器。

存在一个位于 config 目录下的集中式服务容器定义文件,格式为 YAML。该文件名为 services.yml,以下是一个示例。

# services.yml
imports:
    - { resource: "external_definition1.yml" }
    - { resource: "external_definition2.yml" }

parameters:
    default.namespace:
        - Oasis\Mlib\
        - Minhao\TestProject\

services:
    app:
        properties:
            logging:
                path: '%app.dir.log%'
                level: debug
            cli:
                name: test-project
                version: '0.1'
            http:
                cache_dir: '%app.dir.cache%'
                routing:
                    path: '%app.dir.config%/routes.yml'
                    namespaces:
                        - Minhao\TestProject\
                        - Minhao\TestProject\Controllers\
                twig:
                    template_dir: '%app.dir.template%'
                    globals:
                        app: '@app'
                injected_args:
                    - '@entity_manager'
    memcached:
        class: Memcached
        calls:
            -
                - addServer
                -
                    - '%app.memcached.host%'
                    - '%app.memcached.port%'
    entity_manager:
        class: Doctrine\ORM\EntityManager
        factory:
            - Minhao\TestProject\Database\TestProjectDatabase
            - getEntityManager

常见的 services.yml 文件包含三个部分

  • 导入:这是导入其他服务定义文件的地方。借助导入功能,我们可以将大型服务定义文件分解成更小、更有意义的部分。
  • 参数:这是一个键值对数组。这里定义的所有参数,以及通过使用它们的相应 参数键config.yml 中定义的参数,都可以使用 "%%" 符号进行解引用,例如 '%app.memcached.host%'。
  • 服务:这是服务的实际定义。服务是一个键,用于引用可注入的变量。服务可以在其他服务中引用,使用 "@" 符号,例如 '%entity_manager'。

服务描述

服务的定义方式值得更详细的解释。描述(在服务键下允许的属性)可以分为 3 个阶段

  • 构建阶段
  • 设置阶段
  • 装饰阶段
构建阶段

首先,任何服务都需要有一个类型,这通过 class 属性来描述。

注意 类名必须是完全限定的,除非在参数 '%default.namespace%' 中可以找到前缀命名空间。

定义了 class 后,我们需要访问对象。有两种方式可以获取服务对象

  • 通过使用构造函数实例化
  • 或者通过使用工厂类(或工厂对象)来获取服务
services:
    object.constructed:
        class: Minhao\TestProject\User
        arguments:
            - "John Smith" # name
            - 25 # age
    object.factory.provided:
        class: Minhao\TestProject\User
        factory: [UserProvider, getUser] # factory class, factory method
        arguments:
            - 250008 # student ID
设置阶段

服务对象定义后,我们还可以修改对象,如下所示

services:
    object.modified:
        class: Minhao\TestProject\User
        arguments:
            - "John Smith" # name
            - 25 # age
        properties:
            tel: "1234567890" # accessed as public property
            age: 40 # accessed as public property, overrides constructor
        calls:
            - [setSupervisor, "@another.user"] # method and arguments

正如你所看到的,我们可以通过访问其属性或调用对象上的方法来进一步修改服务。

注意 与构建阶段相同,设置阶段中定义的所有属性将仅在构建阶段之后应用一次。

装饰阶段

还有更多强大的技术可以描述服务。尽管它们在实践中的应用并不广泛,但你可能仍然会对它们感兴趣。请参阅 Symfony 官方网站上的 指南 以了解更多信息。

定义 "@app" 服务

services.yml 中,必须详细描述一个且只有一个特殊服务,即 "app" 服务。

"app" 服务的构建阶段是自动完成的,需要很少的关注。区分每个应用程序的是 properties 属性

services:
    app:
        properties:
            logging:
                path: %app.dir.log%
                level: debug
                handlers: # array extra monolog hanlders
                    - "@log.handler.email"
            cli:
                name: Slim App Console
                version: 1.1
                commands: # array of command object
                    - '@cli.command.dummy'
            http: # http bootstrap config

日志记录

SlimApp 使用 oasis/logging 作为日志工具。 oasis/logging 提供了一个即插即用的 PSR-3 兼容的日志解决方案,该解决方案建立在 monolog 之上。

默认情况下,SlimApp 会安装两个日志处理器

  • 本地文件日志处理器,根据配置的日志级别将日志流到本地文件系统
  • 本地文件错误处理器,仅在发生错误时(即触发 error 或更高级别的日志)将日志保存到本地文件系统

此外,对于在命令行上运行的应用程序,还会安装一个额外的控制台日志处理器。控制台日志处理器将直接写入 STDERR,并启用 ANSI 颜色 装饰。

在定义 "@app" 服务时,可以使用以下属性来设置 logging 属性:

HTTP 内核

记得我们声称 SlimApp 是一个适用于 web 和控制台开发的微框架吗?现在是时候了解 SlimApp 如何以简单而强大的方式提供构建 web 应用程序的能力了。

SlimApp 使用 oasis/http 作为其 HTTP 内核实现。 oasis/http 是广泛使用的 Silex 框架的扩展,并提供了一个严格实现 Symfony\Component\HttpKernel\HttpKernelInterface 的内核定义。

oasis/http 中已经很好地记录了如何引导 HTTP 内核。所以这里我们要介绍的是更简单的,即如何在服务定义中注入引导配置。

对于 "@app" 服务有一个属性,http,它将被传递到 oasis/http 作为引导配置。http 的值必须是一个符合 oasis/http 标准 的数组。以下是一个示例配置:

services:
    app:
        properties:
            http:
                routing:
                    path: %app.dir.config%/routes.yml
                    namespaces:
                        - Oasis\SlimApp\Ut\
                error_handlers: '@http.error_handler'
                view_handlers: '@http.view_handler'
                cors:
                    -
                        path:       /cors/*
                        origins:    "*"

配置无误后,我们可以像在自动生成的 front.php 文件中展示的那样使用 HTTP 内核。

<?php

use Minhao\TestProject\TestProject;

/** @var TestProject $app */
$app = require_once __DIR__ . "/../bootstrap.php";

$app->getHttpKernel()->run();

命令行界面

正如讨论的那样,SlimApp 不仅适用于 web 应用程序。它还提供了一个功能丰富的 CLI 框架。其底层实现大量使用了 symfony/console 组件。尽管使用 SlimApp 的基本 CLI 功能不需要对 symfony/console 有全面的知识,但如果您想扩展框架或使用一些高级功能,建议阅读 symfony 文档。

首先,让我们看看 "@app" 服务的 cli 属性。

ervices:
    app:
        properties:
            cli:
                name: Slim App Console
                version: 1.1
                commands: # array of command object
                    - '@cli.command.dummy'

可以设置 3 个属性:

  • name:控制台应用程序的名称,在请求信息时使用(如 --help)
  • version:控制台应用程序的版本,与 name 属性一起使用
  • commands:应用程序特定命令的数组。命令对象也必须是类型为 Symfony\Component\Console\Command\Command 的定义服务。

控制台应用程序可以从自动生成的入口脚本 PROJECT_DIR/bin/.php 启动。

./bin/test-project.php <command>

有一个必填的 command 参数,用于确定要调用哪个 Command 对象。在 command 参数之后的所有内容都将被解释为输入参数和选项。

注意:还可以通过在控制台入口脚本中调用 addCommands() 方法将支持的命令注入 CLI,这可能更方便(尤其是在具有自动完成功能的 IDE 中)。

<?php
use Minhao\TestProject\TestProject;

/** @var TestProject $app */
$app = require_once __DIR__ . "/../bootstrap.php";

$console = $app->getConsoleApplication();
$console->addCommands(
    [
        new MyCustomCommandOne(),
        new MyCustomCommandTwo(),
    ]
);

$console->run();
编写自己的命令

要创建自己的命令,您可以从扩展 Symfony\Component\Console\Command\Command 类并重写至少 configure() 方法和 execute() 方法开始。

<?php

namespace Minhao\TestProject\Console\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class MyCustomCommandOne extends Command
{
    protected function configure()
    {
        parent::configure();

        $this->setName('custom:command:one')
            ->setDescription("Say hello world!");
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln("Hello World!");
    }
}

注意:命令被放置在项目根命名空间下的 Console\Commands\ 命名空间中。这是 SlimApp 的一个约定,建议遵循。

使用输入参数

在大多数实际场景中,没有参数的命令是没有用的。我们可以让命令接受输入参数。

注意:请注意命令行参数和命令输入参数之间的区别。当我们用shell执行一个命令时,shell会将整个输入行分割成由空格分隔的命令行参数。当我们执行CLI命令时,第一个命令行参数($arg[0])是入口脚本名,第二个($arg[1])必须是命令名。之后,任何不以连字符('-')开头的额外命令行参数都被认为是输入参数(除了选项值,它跟在需要值的输入选项后面)。

要使您的命令能够接受输入参数,您可以使用addArgument()方法声明您期望的参数,您可以使用传递给execut()$input上的getArgument()方法来读取参数

<?php

// namespace imports omitted ...

class MyCustomCommandOne extends Command
{
    protected function configure()
    {
        parent::configure();

        $this->setName('custom:command:one')
             ->setDescription("Say hello world!");

        $this->addArgument(
            'name',
            InputArgument::REQUIRED,
            "give your name here"
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        $output->writeln('Hello, ' . $name . '!');
    }
}

运行测试命令将输出

$ ./bin/test-project.php custom:command:one John
Hello, John!

注意:您可能期望有多个参数。但是,您只能将可选参数放在列表的末尾。为了更清晰,在可选参数后面不能有任何必需参数。

使用输入选项

输入选项是以一个或两个连字符开头的命令行参数。正如“选项”这个名字所暗示的,输入选项总是可选的。输入选项名有两种不同的形式:长选项名和短选项名。长选项名以两个连字符("--”)开头,每个选项都是强制性的。短选项名以单个连字符("-”)开头,是可选的。

此外,还有一些输入选项需要附加值。这些选项值的设置方式有两种

  • 直接跟在选项后面的命令行参数
$ ./bin/test-project.php custom:command:one John -m question
  • 在长选项名后面使用等号
$ ./bin/test-project.php custom:command:one John --mood=question

要声明和使用输入选项,请阅读下面的示例代码

<?php

// namespace imports omitted ...

class MyCustomCommandOne extends Command
{
    protected function configure()
    {
        parent::configure();

        $this->setName('custom:command:one')
             ->setDescription("Say hello world!");

        $this->addArgument(
            'name',
            InputArgument::REQUIRED,
            "give your name here"
        );

        $this->addOption(
            'mood',
            'm',
            InputOption::VALUE_REQUIRED
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        $mood = $input->getOption('mood');
        $sign = ($mood == "question") ? "?" : "!";
        $output->writeln('Hello, ' . $name . '!');
    }
}

执行命令的输出如下

$ ./bin/test-project.php custom:command:one John --mood=question
Hello, John?

守护进程哨兵

在实际应用中,一个项目可以有一个预定的执行计划来执行多个命令。一些命令需要在给定的时间间隔内执行,一些需要并行运行多个实例,一些需要在执行失败时自动发送警报,等等。SlimApp提供了一个非常有用的功能,称为守护进程哨兵,专门用来解决这些问题。

守护进程哨兵本身也是一个命令。您的应用程序应该扩展Oasis\SlimApp\SentinelCommand\AbstractDaemonSentinelCommand以拥有您的命令类

<?php

namespace Minhao\TestProject\Console\Commands;

use Oasis\SlimApp\SentinelCommand\AbstractDaemonSentinelCommand;

class TestSentinelCommand extends AbstractDaemonSentinelCommand
{
    protected function configure()
    {
        parent::configure();
        $this->setName('test:daemon');
    }
}

当执行时,命令期望一个配置文件作为其第一个参数。文件的格式应该是YAML,例如以下内容

commands:

    dummy: # name of the daemon, informative only, use any name meaningful

        # command name
        name: dummy:job

        # command line arguments to pass to the command
        args:
            a: %app.name%
            --tt: true
            --idx: $PARALLEL_INDEX
            -vvv:

        # whether to run in parallel, and if yes, how many
        parallel: 3

        # run only once? if not, command will restart upon previous execution ends
        once: false

        # alert on abnormal exit (exit != 0)
        alert: false

        # interval: minimum number of seconds between last end and next start
        interval: 2

        # frequency: minimum seconds between two start
        frequency: 5
        
        # frequency_fixed: default to false, if enabled, commands will start at fixed frequency, no matter if the previous run has finished or not
        frequency_fixed: false

注意:所有配置值都可以在命令的args设置中使用"%key-to-config-value%"的格式

注意:$PARALLEL_INDEX是args中的一个特殊变量,表示在并行执行时的命令索引。索引从0开始。