anax/controller

Anax 控制器模块,包含示例启动控制器。

v2.0.2 2020-05-15 20:58 UTC

README

Latest Stable Version Join the chat at https://gitter.im/canax/controller

Build Status CircleCI

Build Status Scrutinizer Code Quality Code Coverage

Maintainability Codacy Badge

Anax 控制器是一个示例实用控制器集合,创建自己的控制器时,可以用它们作为脚手架(复制并修改)。

在构建网站时,控制器可以作为普通路由回调的替代品,具有良好的代码结构。

目录

类、接口、特质

以下类、接口和特质存在。

以下内容直接使用。

以下为示例控制器,用于复制粘贴,在构建自己的控制器时。

异常

没有特定于模块的异常。

当路由回调由于某些原因失败时,通常使用路由器异常。以下是一些创建适当HTTP响应的路由器异常。

应用风格或DI风格

控制器类可能需要访问框架服务容器,该容器在 $app$di 中实现。根据控制器类是否实现了已知接口,路由器会自动注入服务容器。

如果控制器类实现了 Anax\Commons\AppInjectableInterface,则路由器会注入 $app

如果控制器类实现了 Anax\Commons\ContainerInjectableInterface,则路由器会注入 $di

要实现此行为,控制器类应使用模块 anax/commons 提供的实用工具,并创建一个如下的控制器类。

这是一个应用风格注入控制器。

namespace Anax\Controller;

use Anax\Commons\AppInjectableInterface;
use Anax\Commons\AppInjectableTrait;

/**
 * A sample controller to show how a controller class can be implemented.
 * The controller will be injected with $app if implementing the interface
 * AppInjectableInterface, like this sample class does.
 * The controller is mounted on a particular route and can then handle all
 * requests for that mount point.
 *
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
 */
class SampleAppController implements AppInjectableInterface
{
    use AppInjectableTrait;
}

这是一个DI风格注入控制器。

namespace Anax\Controller;

use Anax\Commons\ContainerInjectableInterface;
use Anax\Commons\ContainerInjectableTrait;

/**
 * A sample controller to show how a controller class can be implemented.
 * The controller will be injected with $di if implementing the interface
 * ContainerInjectableInterface, like this sample class does.
 * The controller is mounted on a particular route and can then handle all
 * requests for that mount point.
 *
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
 */
class SampleController implements ContainerInjectableInterface
{
    use ContainerInjectableTrait;
}

根据所使用的接口,服务容器将作为 $this->app$this->di 可用。

在路由器上挂载控制器类

控制器类挂载在路由路径上,称为挂载点,在路由器中。这是通过向路由器添加配置文件来完成的。

以下是如何将 DevelopmentController 挂载在挂载点 dev/ 上的示例。该文件存储在 config/router/ 目录中,文件名为 600_development.php

/**
 * Routes to ease development and debugging.
 */
return [
    "routes" => [
        [
            "info" => "Development and debugging information.",
            "mount" => "dev",
            "handler" => "\Anax\Controller\DevelopmentController",
        ],
    ]
];

配置文件指定挂载点为 dev,控制器类为 \Anax\Controller\DevelopmentController

您可以通过查看 config/router 目录的内容来查看更多关于如何将控制器类挂载到路由器挂载点的示例。

路由路径如何映射到控制器/操作

所有低于挂载点的请求都将转发到挂载的控制器进行处理。当控制器无法处理请求时,路由器会将该路径视为 NotFoundException。

控制器中的每个方法都可以映射一个路由路径,即控制器操作。以下是一些如何通过路由路径引用控制器方法(操作)的示例。

假设控制器 SampleAppController 挂载在挂载点 app 上,那么以下内容将是正确的。

需要Action部分,它告诉路由器这个控制器方法应该被处理为可调用的控制器操作,并将其映射到路由路径。

方法名称被映射到路由路径,除了移除的Action部分。方法infoAction()被映射到路由路径info,方法dumpSomeDataAction()被映射到路由路径dump-some-data

指定要支持的HTTP方法

您可以将HTTP方法添加到方法名称的末尾,然后HTTP请求方法也必须映射,在控制器方法被路由器匹配之前。

定义控制器方法(操作)

以下是如何创建控制器方法的示例。

此方法映射到任何HTTP请求方法上的路由路径appapp/index

/**
 * This is the index method action, it handles:
 * ANY METHOD mountpoint
 * ANY METHOD mountpoint/
 * ANY METHOD mountpoint/index
 *
 * @return string
 */
public function indexAction() : string
{
    // Deal with the action and return a response.
    return "Some kind of response";
}

此方法映射到GET HTTP请求方法上的路由路径app/info

/**
 * Add the request method to the method name to limit what request methods
 * the handler supports.
 * GET mountpoint/info
 *
 * @return string
 */
public function infoActionGet() : string
{
    // Deal with the action and return a response.
    return "Some kind of response";
}

以下两个方法映射到相同的路由路径app/create,第一个只映射GET请求,第二个只映射POST请求。

/**
 * This sample method action it the handler for route:
 * GET mountpoint/create
 *
 * @return string
 */
public function createActionGet() : string
{
    // Deal with the action and return a response.
    return "Some kind of response";
}

/**
 * This sample method action it the handler for route:
 * POST mountpoint/create
 *
 * @return string
 */
public function createActionPost() : string
{
    // Deal with the action and return a response.
    return "Some kind of response";
}

从控制器方法返回值

每个控制器方法都可以返回一个响应。

如果没有返回任何内容,路由器将此视为无操作并继续寻找可以处理请求的下一个处理器。

如果返回任何值,控制器将此视为成功处理请求,并立即将响应返回给调用者。

返回的值被转换为Anax响应类的一个实例。

当控制器方法返回一个字符串时,该字符串的内容将是响应体。以下示例是一个包含正文为Some kind of response的页面。

public function infoActionGet() : string
{
    // Deal with the action and return a response.
    return "Some kind of response";
}

控制器方法可以通过将数组包装在数组中来返回JSON响应。这将通过Anax响应类转换为JSON响应。

public function indexActionGet() : array
{
    // Deal with the action and return a response.
    $json = [
        "message" => "Some kind of response",
    ];
    return [$json];
}

请注意,现在返回类型提示的类型是: array而不是: string

您可以可选地发送响应状态码。

public function indexActionGet() : array
{
    // Deal with the action and return a response.
    $json = [
        "message" => "Some kind of response",
    ];
    return [$json, 200];
}

从控制器方法返回页面

控制器方法可以添加视图到页面并渲染它。渲染阶段产生一个响应对象,然后返回。以下是一个创建并返回渲染页面的示例控制器方法(app风格)。

/**
 * Display the stylechooser with details on current selected style.
 *
 * @return object
 */
public function indexAction() : object
{
    $title = "Stylechooser";

    $page = $this->app->page;
    $session = $this->app->session;

    $page->add("anax/v2/stylechooser/default", [
        "activeStyle" => $session->get("active-style", null),
    ]);

    return $page->render([
        "title" => $title,
    ]);
}

请注意,现在返回类型提示的类型是: object而不是: string: array

向控制器方法发送参数

控制器方法可以接受参数,这些参数是从路由路径映射过来的,如下所示。

  • mountpoint/action/argument1/argument2/argument3

因此,如果我们想创建一个控制器方法来响应类似于app/view/<id>的路由路径,例如app/view/1app/view/42,那么我们创建一个接受一个参数的方法,如下所示。

/**
 * This sample method action takes one argument:
 * GET mountpoint/view/<value>
 *
 * @param mixed $id
 *
 * @return string
 */
public function viewActionGet($id) : string
{
    // Deal with the action and return a response.
    return "You are now viewing id: '$id'";
}

方法可以接受您需要的任意数量的参数。

初始化方法

有时您需要引导控制器,您有一些在执行实际操作回调之前需要执行的公共启动代码。

该代码不应该在构造函数中创建。避免使用控制器类的构造函数。原因是控制器类是如何被路由器使用和检查的。路由器必须在实际上知道控制器应该被调用之前创建控制器的一个对象。因此,您应该避免使用构造函数,而是使用initialize()方法。

如果控制器类定义了initialize()方法,路由器将始终调用控制器方法。

该方法可以定义如下。

/**
 * The initialize method is optional and will always be called before the
 * target method/action. This is a convienient method where you could
 * setup internal properties that are commonly used by several methods.
 *
 * @return void
 */
public function initialize() : void
{
    // Use to initialise member variables.
    $this->db = "active";
    // Use $this->app or $this->di to access the framework services.
}

使用该方法将状态存储在类属性中,例如创建数据库连接或创建和初始化一组对象。

全局捕获方法

当路由器找不到匹配的动作时,它将寻找一个名为 catchAll() 的方法,如果它被定义,则调用它。这个方法可以用来处理错误情况,或者可以用来创建一个控制器,该控制器只有一个方法,可以处理所有传入的路由路径,而不考虑实际的路径。

以下是创建自己的 catchAll() 时可用的模板。

/**
 * Adding an optional catchAll() method will catch all actions sent to the
 * router. You can then reply with an actual response or return void to
 * allow for the router to move on to next handler.
 * A catchAll() handles the following, if a specific action method is not
 * created:
 * ANY METHOD mountpoint/**
 *
 * @param array $args as a variadic parameter.
 *
 * @return mixed
 *
 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
 */
public function catchAll(...$args)
{
    // Deal with the request and send an actual response, or not.
    return;
}

catchAll() 可以接受作为可变参数的变量 ...$args,这意味着它可以接受任何数量的参数,并将它们都存储在数组 $args 中。

以下是一些在控制器中使用 catchAll() 的实际例子。

anax/content 中还有一个更强大的平面文件内容模块,您可以在其中查看其控制器。

额外的控制器类成员

您还可以在控制器类中创建任何公共、受保护或私有方法或属性,除了提到的特殊控制器方法之外。

这使得在类方法中添加在多个控制器动作中使用的公共代码成为可能。因此,控制器可以实现一些代码重用,并允许有良好的代码结构。

瘦或胖控制器

一般想法是有一个瘦控制器,也就是说,一小段代码将框架与模型类粘合在一起。

当您的控制器变大时,花些时间思考代码是否可以分解为其他(模型)类,这些类被控制器使用。

胖控制器是瘦控制器的对立面。它包含大量的代码和逻辑。

避免胖控制器,将代码分解为小的、可用的类,每个类都有明显和明确的职责。

如何对控制器进行单元测试

控制器类可以像任何其他类一样进行单元测试。然而,由于控制器类是框架和您的应用程序(模型)类之间的粘合剂,可能需要进行一些准备,甚至设置一些固定值,以使其更容易。

不使用框架服务容器的控制器方法非常容易测试。以下是一个这样的单元测试示例(phpunit)。

/**
 * Call the controller index action.
 */
public function testIndexAction()
{
    // Create and initiate the controller
    $this->controller = new SampleAppController();
    $this->controller->setApp($app);
    $this->controller->initialize();

    // Carry out the test
    $res = $this->controller->indexAction();
    $this->assertIsString($res);
    $this->assertStringEndsWith("active", $res);
}

当您有一个需要服务容器的控制器时,您可能想要使用一个 setUp() 来设置控制器,就像路由器设置的控制器一样。

/**
 * Setup the controller, before each testcase, just like the router
 * would set it up.
 */
protected function setUp(): void
{
    // Init service container $di to contain $app as a service
    $di = new DIMagic();
    $app = $di;
    $di->set("app", $app);

    // Create and initiate the controller
    $this->controller = new SampleAppController();
    $this->controller->setApp($app);
    $this->controller->initialize();
}



/**
 * Call the controller index action.
 */
public function testIndexAction()
{
    $res = $this->controller->indexAction();
    $this->assertIsString($res);
    $this->assertStringEndsWith("active", $res);
}

然后您可以为每个控制器动作添加更多测试用例,如果您有一些内部控制器方法或想要添加单元测试的失败测试。

例如,您可能想要查看测试类 SampleAppControllerTest,它测试控制器类 SampleAppController。它包含在创建自己的控制器测试类时可以使用的示例。

许可

本软件遵循MIT许可证。有关详细信息,请参阅 LICENSE.txt

 .  
..:  Copyright (c) 2013 - 2019 Mikael Roos, mos@dbwebb.se