h4d/leveret

PHP 微框架,用于构建 Web 应用程序和 API

v1.4.0 2016-11-19 11:25 UTC

README

这是一个微框架,允许以简单的方式创建 HTTP 应用程序(类似 slim, silex 等)。

    $app = new Application();
    $app->registerRoute('GET', '/hello/:string')
        ->setAction(
            function ($name) use ($app)
            {
                $app->getResponse()->setBody('Hello '.$name);
            });
    $app->run();

如何安装?

要使用 composer 安装 leveret,您需要执行以下命令

    $ composer require h4d/leveret

注意: 必须包含托管在私有仓库中的依赖项仓库的所有数据。例如,由于 h4d/leveret 依赖于包 h4d/template,因此还需要包含该仓库的数据(与在 Packagist 上发布的包不同,composer 不能自动解析)。

如何使用?

配置文件

为了使应用程序正常工作,需要创建一个配置文件,其路径将作为参数传递给 \H4D\Leveret\Application 的构造函数。如果未向构造函数传递配置文件,则将实例化一个具有默认配置值的应用程序。

以下表格显示了默认配置文件的内容。

    [application]
    ; Application name
    name = NoNamedApp
    ; Application environmnet: production, development.
    environment = production
    ; Application root directory.
    path = ../app
    ; Default content type for responses: text/html, application/json, etc.
    defaultContentType = text/html
    ; Error handler: internal Application static method name.
    errorHandler = errorHandler
    ; Default input filter type (@see https://php.ac.cn/manual/en/filter.filters.sanitize.php)
    ; 516: FILTER_UNSAFE_RAW
    ; 522: FILTER_SANITIZE_FULL_SPECIAL_CHARS
    ; 513: FILTER_SANITIZE_STRING
    defaultInputFilterType = 516
    ; Register routes defined in [routes] section (values: true|false).
    registerRoutesDefinedInConfigFile = true
    
    [views]
    ; View templates directory
    path = ../app/views
    
    [routes]
    ; Example: Call appplication method
    appInfo[pattern] = "/app/info"
    appInfo[method] = "GET"
    appInfo[callback] = "renderAppInfo"
    ; Example: Call controller/action
    ;status[pattern] = "/app/status"
    ;status[method] = "GET"
    ;status[callback] = "Your/Controller/ClassName::controllerMethod"

注意: 如果配置文件中指定的路径是相对路径,则它们将相对于服务器根目录。

实例化应用程序

    /** @var \H4D\Leveret\Application $app */
    $app = new \H4D\Leveret\Application($configFilePath);

注册路由

路由通过 registerRoute($method, $routePattern) 方法进行注册。

第一个参数将指示 HTTP 方法(GET、POST、DELETE 等)。第二个参数是必须处理的路由表示。

路由表示支持“通配符”。通配符字符串的格式如下::(类型)名称,其中 类型 是变量的类型(int、float、string 等),名称 是变量的名称/别名。通配符字符串中指定类型的部分是可选的,如果没有指定类型,则变量将被映射为字符串类型。

目前支持的通配符类型包括

  • word:一个单词 ([\w] )*。
  • string:任何由 a-z、0-9、大写字母、下划线和空格组成的字符串 ([a-zA-Z0-9- _]*)
  • integerint:带有可选符号的整数 ([-+]?[0-9]*)
  • floatnumber:带有可选符号的小数 ([-+]?[0-9]*[.]?[0-9]+)

“通配符”示例

  • :(string)server:表示一个名为 "server" 的字符串类型的变量。
  • :server:与前面的例子等价。省略通配符字符串中定义类型的部分,将变量 "server" 映射到 "string" 类型。
  • :(int)age:表示一个名为 "age" 的整型变量。
  • :(float)price:表示一个名为 "price" 的浮点型变量。

路由表示示例

    /add/:(float)num1/:(float)num2
    /hello/:name

每个路由必须通过 setAction($clousure) 方法分配一个动作,如下所示

    // Action with multiple params.
    $app->registerRoute('GET', '/add/:(float)num1/:(float)num2')
        ->setAction(
            function ($num1, $num2) use ($app)
            {
                $result = $num1 + $num2;
                $app->getLogger()->notice(sprintf('Result: %f', $result), ['num1' => $num1,
                                                                           'num2' => $num2,
                                                                           'result' => $result]);
                $app->getResponse()->setBody($result));
            });

如果想要向闭包传递其他变量,甚至整个应用程序,可以通过使用 use 来实现。

另一种将动作分配给路由的方法是使用控制器。可以通过 useController($controllerClassName, $controllerClassMethod) 方法设置控制器/动作对,其中第一个参数是控制器类的名称,第二个参数是在调度期间要执行的方法的名称。

以下是一个将控制器/动作对分配给路由的示例。

    $app->registerRoute('GET', '/add/:(float)num1/:(float)num2')->useController('MathController', 'add');

对于每个路由,可以选择性地关联多个预分发和后分发的动作。为此,提供了addPreDispatchAction($callable)addPostDispatchAction($callable)方法。默认情况下,将路由和相关应用作为参数传递给“callable”,这样就可以访问应用中的任何子组件(记录器、请求、响应等)。

在下面的示例中,添加了一个预分发动作,该动作修改请求参数,将所有字符串类型的参数转换为大写。

    $app->registerRoute('GET', '/hello/:name')
        ->setAction(
            function ($name) use ($app)
            {
                $app->getView()->addVar('name', $name);
                $app->render('hello.phtml');
            })
        ->addPreDispatchAction(
            function ($route, $app)
            {
                $newParams = array();
                /** @var \H4D\Leveret\Application\Route $route */
                foreach($route->getParams() as $key => $value)
                {
                    $newParams[$key] = is_string($value) ? strtoupper($value) : $value;
                }
                $route->setParams($newParams);
            })

在配置文件中注册路由

可以从配置文件中注册简单路由。

要激活在配置文件中定义的路由的注册,需要将配置值registerRoutesDefinedInConfigFile定义为true

    registerRoutesDefinedInConfigFile = true

路由配置在[routes]部分,可以是两种类型之一

  • 指向Controller/Action的路由。
  • 指向应用类方法的路由。

指向Controller/Action的路由定义

    [routes]
    ; Example: Define a route named "status" dispatched by a controller/action
    status[pattern] = "/example/route"
    status[method] = "GET"
    status[callback] = "Your/Controller/ClassName::controllerMethod"

指向应用类方法的路由定义

    [routes]
    ; Example: Define a route named "info" dispatched by an app's method
    info[pattern] = "/example/route"
    info[method] = "GET"
    info[callback] = "anAppMethod" ;; Method's name of your app class

请求验证

当在应用中注册路由时,可以为预期接收的每个参数添加必要的验证器。可用于此目的的方法是addRequestConstraints($paramName, [Constraints]),它接受一个字符串作为第一个参数,用于标识要验证的参数名称,以及一个规则(Constraint)或规则集(Constraints)数组,该参数必须遵守。

在下面的示例中,为usernamealias参数添加了多个验证规则。

     $this->registerRoute('POST', '/admin/deploy-request')
          ->addRequestConstraints('username', [new Required(), new NotBlank(), new Email()])
          ->addRequestConstraints('alias', [new Required(), new NotBlank(),
                                            new Length(array('min'=>3, 'max'=>100))])
          ->useController('AdminController', 'deployRequest');

验证规则必须是符合H4D\Leveret\Validation\ConstraintInterface接口的类的实例。如果需要不符合该接口的验证规则,可以创建适配器,例如H4D\Leveret\Validation\Adapters\H4DConstraintAdapter,它允许使用项目h4d/validator中定义的验证规则,或者H4D\Leveret\Validation\Adapters\SymfonyConstraintAdapter,它允许使用项目https://symfony.com.cn/doc/current/reference/constraints.html中的验证规则。

以下示例展示了如何使用适配器来使用H4D验证规则。

    $app = new Application();
    $app->registerRoute('GET', '/hello/:(string)name')
        ->addRequestConstraints('name', new H4DConstraintAdapter((new Enum())->setOptions(['paco', 'maria'])))
        ->setAction(
            function ($name) use ($app)
            {
                $isValid = $app->isValidRequest();
                if (!$isValid)
                {
                    throw new \Exception($app->getRequestConstraintsViolationMessagesAsString());
                }
                $app->getResponse()->setBody('Hello '.$name);
            });
    $app->run();

以下示例展示了如何使用Symfony验证规则。

    $app = new Application();
    $app->registerRoute('GET', '/hello/:(string)name')
        ->setRequiredParam('name')
        ->addRequestConstraints('name', [new SymfonyConstraintAdapter(new Required()),
                                         new SymfonyConstraintAdapter(new NotBlank())])
        ->setAction(
            function ($name) use ($app)
            {
                $isValid = $app->isValidRequest();
                if (!$isValid)
                {
                    throw new \Exception($app->getRequestConstraintsViolationMessagesAsString());
                }
                $app->getResponse()->setBody('Hello '.$name);
            });
    $app->run();

从Leveret的Application对象中,可以通过以下方法获取请求验证结果的信息

  • isValidRequest($request, $constraints):返回布尔值(true:一切正常,false:有验证错误)
  • getRequestConstraintsViolations():返回一个关联数组,包含ConstraintViolationList中的验证错误信息。
  • getRequestConstraintsViolationMessages():返回一个关联数组,包含所有错误信息的对象。
  • getRequestConstraintsViolationMessagesAsString($separator):返回一个包含错误信息的字符串。

有关这些方法的更多信息,请参阅它们的签名。

必需参数

要定义特定路由的哪些参数是必需的,提供了两种方法

  • setRequiredParam(string $paramName):允许独立设置一个参数为必需。
  • setRequiredParams(array $paramsNames):允许一次性设置多个参数为必需。

使用示例

    $this->registerRoute('POST', '/admin/deploy-request')
        ->setRequiredParam('username')
        ->addRequestConstraints('username', [new Required(), new NotBlank(), new Email()]);

请求自动验证

Levaret 允许配置参数验证是否以自动方式(在路由后程序之后)或手动方式进行。在自动模式下,可以定义何时进行请求验证,是在认证之后(如果需要)还是认证之前(如果需要)。

可以通过方法 setAutoRequestValidationMode($mode) 配置这三种模式。$mode 的可能值有:

  • NO_VALIDATION: 不进行自动验证。
  • VALIDATION_BEFORE_AUTH: 在认证程序之前自动进行验证。
  • VALIDATION_AFTER_AUTH: 在认证程序之后自动进行验证。

对于每种模式,在 Application 类中都有定义的常量。

    const AUTO_REQUEST_VALIDATION_MODE_NO_REQUEST_VALIDATION          = 'NO_VALIDATION';
    const AUTO_REQUEST_VALIDATION_MODE_REQUEST_VALIDATION_BEFORE_AUTH = 'VALIDATION_BEFORE_AUTH';
    const AUTO_REQUEST_VALIDATION_MODE_REQUEST_VALIDATION_AFTER_AUTH  = 'VALIDATION_AFTER_AUTH';

使用示例

    $app = new Application(APP_CONFIG_DIR.'/config.ini');
    $app->setAutoRequestValidationMode(Application::AUTO_REQUEST_VALIDATION_MODE_REQUEST_VALIDATION_BEFORE_AUTH);

POST|PUT|PATH|DELETE 参数过滤、查询参数和 URL

默认情况下,会对通过 POST、PUT、PATH、DELETE、查询参数或 URL 到达应用程序的所有参数应用过滤。默认应用的过滤器可以在应用程序的配置文件中通过字段 defaultInputFilterType 指定。该字段的值是一个整数,相当于 PHP 的某些标准过滤器([查看 PHP 文档] (https://php.ac.cn/manual/en/filter.filters.sanitize.php))。以下列出了 defaultInputFilterType 的常见值:

  • 516 (FILTER_UNSAFE_RAW): 不过滤参数。
  • 522 (FILTER_SANITIZE_FULL_SPECIAL_CHARS): 等同于 htmlspecialchars()。
  • 513 (FILTER_SANITIZE_STRING): 过滤字符串中的标签。

可以使用方法 addRequestFilters($paramName, $filters) 对特定参数应用过滤器,其中 $paramName 是要过滤的参数的名称,$filters 是一个数组,包含符合接口 H4D\Leveret\Filter\FilterInterface(或接受一个值并返回过滤值的闭包)的对象。

示例

    $this->registerRoute('POST', '/create/alias')
            ->addRequestFilters('alias', [new Filter1(), new Filter2(), 
                                         function($alias){return strtolower($alias)}]);

应用程序执行

方法 run() 负责路由请求并向客户端提供 HTTP 响应。

    $app->run();

控制器

应用程序的控制器必须继承自 H4D\Leveret\Application\Controller 才能使用。

从控制器的任何方法中,都可以通过内部变量 $app 访问应用程序,并通过该对象访问 request、response、view 等。

基本控制器的示例

    use H4D\Leveret\Application;
    use H4D\Leveret\Application\Controller;
    use H4D\Leveret\Http\Response\Headers;
    
    class MathController extends Controller
    {
       /**
        * Sobreescribo método init
        */
        public function init()
        {
            // Especifico el layout que se utilizará para todas las acciones de este controller
            $this->useLayout('layouts/main.phtml');
        }
        
        /**
         * @param float $a
         * @param float $b
         */
        public function add($a, $b)
        {
            // Obtengo la vista y paso las variables necesarias.
            $this->getView()
                ->addVar('title', sprintf('%s + %s', $a, $b))
                ->addVar('num1', $a)
                ->addVar('num2', $b)
                ->addVar('result', $a+$b);
            // Especifico la pantilla que se va a emplear.    
            $this->render('add.phtml');
        }
    
        public function info()
        {
            // No uso vista, seteo directamente el cuerpo de la respuesta.
            $this->getResponse()->setBody(phpinfo());
        }
    }

方法 init()

在控制器中,我们可以实现方法 init(),该方法将在实现它的控制器实例化时被调用。

  • init(): 当实例化控制器时执行。

方法 preDispatch()postDispatch()

在控制器中,我们可以实现具有特殊行为的方法 preDispatch()postDispatch()

  • preDispatch(): 如果控制器中存在 preDispatch() 方法,则该方法将在路由注册表中的指定动作之前执行。
  • postDispatch(): 如果控制器中存在 postDispatch() 方法,则该方法将在路由注册表中的指定动作之后执行。

其他有趣的方法

控制器有多个有用的方法,其中一些是

  • getApp(): 返回 \H4D\Leveret\Application 的实例对象。
  • getLogger(): 返回应用程序中注册的 Logger (LoggerInterface)。
  • getRequest(): 返回正在处理的 Request 对象 (\H4D\Leveret\Http\Request)。
  • getRoute(): 返回正在处理的 Route 对象 (\H4D\Leveret\Application\Route)。
  • getResponse(): 返回 Response 对象 (\H4D\Leveret\Http\Response)。
  • getView(): 返回 View 对象 (\H4D\Leveret\Application\View)。
  • getLayout(): 返回用作主布局的 View 对象 (\H4D\Leveret\Application\View)。
  • setView(View $view):设置应用程序的View对象。
  • setLayout(View $view):设置用作应用程序布局的View对象。
  • setResponse(Response $response):设置应用程序的Response对象。
  • render(string $template):应用程序render(string $template)方法的快捷方式。
  • isValidRequest():应用程序isValidRequest()方法的快捷方式。
  • getRequestValidationErrorMessages():应用程序getRequestConstraintsViolationMessages()方法的快捷方式。
  • getRequestValidationErrorMessagesAsString($separator):应用程序getRequestConstraintsViolationMessagesAsString()方法的快捷方式。

视图

当需要使用模板时,可以利用与应用程序关联的默认视图,通过相应的方法传递将被模板替换的变量(请参阅H4D\Leveret\Application\View.php)。

要指定要渲染的模板,将使用应用程序对象的render($template)方法。它接受一个参数,即模板文件的相对路径,该路径相对于视图的基本路径(在应用程序的配置文件中定义,在views/path部分)。

    $app->render('add.phtml');

模板通常是phtml文件(尽管可以是任何其他类型)。

模板示例

    <html>
    <head>
        <title><?php echo $title?></title>
    </head>
    <body>
    <div style="text-align: center; font-size: 40pt; margin: 40px;">
        <?php echo $num1; ?> + <?php echo $num2; ?> = <?php echo $result; ?>
    </div>
    </body>
    </html>

部分视图

可以在其他视图中使用部分视图,方法如下

    <div>
        <?php echo $this->partial(APP_VIEWS_DIR.'/partials/test/test.phtml', ['nombre'=>'Pakito']);?>
    </div>

部分视图“继承”了容器视图的所有方法和变量。

允许在部分视图内部使用部分视图。例如

在主视图

    <div>
        <?php echo $this->partial(APP_VIEWS_DIR.'/partials/test/partial.phtml', ['nombre'=>'Pakito']);?>
    </div>

在 APP_VIEWS_DIR.'/partials/test/partial.phtml'

    <h1>Partial</h1>
    <p>
        <?php echo $this->translate('Hola %s! Esto es un partial.', $nombre);?>
    </p>
    
    <?php echo $this->partial(APP_VIEWS_DIR.'/partials/test/internal.phtml');?>

在 APP_VIEWS_DIR.'/partials/test/internal.phtml'

    <h2>Partial interno</h2>
    <?php echo $this->translate('Hola %s! Soy un partial dentro de otro partial', $nombre);?>

如何在部分视图中解决变量名的问题?

  1. 如果变量在部分视图中定义,则使用该变量。
  2. 如果变量未在部分视图中定义,但在容器视图中定义,则使用容器视图中的变量。
  3. 如果变量既未在部分视图中定义,也未在容器视图中定义,则抛出渲染异常。

注意!内部部分视图不“继承”容器部分视图的变量,只继承容器视图的变量。

布局

布局是可以用作其他视图容器的视图。在所有布局中,都存在一个默认变量,名为$contents,在应用程序的渲染过程中,它将被其他视图的内容所替换。

要使用特定的布局,必须使用应用程序的useLayout($template)方法,其中$template是布局模板文件的相对路径,相对于应用程序视图的基本路径。

    $app->useLayout('layouts/main.phtml');
    $app->render('add.phtml');

事件

无论是应用程序还是Leveret的控制器,都实现了publisher模式,因此可以通过publish(Event $event)方法发布事件。

    $app->publish($myEvent);

可以订阅尽可能多的应用程序事件,使用attachSubscriber(SubscriberInterface $subscriber)方法。

    $app->attachSubscriber($mySubscriberOne);
    $app->attachSubscriber($mySubscriberTwo);

如果需要取消订阅,请使用detachSubscriber(SubscriberInterface $subscriber)方法。

    $app->dettachSubscriber($mySubscriberTwo);

控制器具有添加或删除监听器的方法。从控制器发布的所有事件都会传播到应用程序。

依赖注入/服务容器

Leveret包含一个非常简单的服务容器,支持以下类型

  • 实例
  • 可调用
  • 键值对
  • 资源

要在应用程序中注册服务,可以使用应用程序对象的registerService( string $serviceName, mixed $value, $singleton = false)方法。其中

  • $serviceName:是要分配给服务的字符串名称。
  • $value:是服务本身,可以是可调用、实例、资源或键值对。
  • $singleton:仅当 $value 是可调用的并且只执行一次调用,后续调用返回相同返回值时使用。

为了获取已注册的服务,应用程序对象提供了 getService( string $serviceName) 方法,其中

  • $serviceName:是我们先前已注册的服务名称。

与其他服务相关的有用方法有

  • bool isServiceRegistered(string $serviceName):如果名为 $serviceName 的服务已注册,则返回 true 或 false。
  • ServiceContainerInterface getServiceContainer():返回应用程序的服务容器。
  • Application setServiceContainer(ServiceContainerInterface $serviceContainer):允许为应用程序设置服务容器。

Leveret 应用程序有一个注册服务的位置,那就是我们应用程序类中的 initServices() 方法。

将实例作为服务注册

示例:MyService 类的实例注册为应用程序的服务。

        $app->registerService('ServiceName', new MyService());

将可调用项作为服务注册

示例:注册服务 'ServiceName'。当第一次调用 $app->getService('ServiceName') 时,将创建一个 MyService 的实例并返回。在后续调用中,将返回相同的 MyService 实例,不会再次执行可调用项的代码。

        $app->registerService('ServiceName', function ()
        {
            $configFile = IniFile::load(APP_CONFIG_DIR . '/sample.ini');
            $myService = new MyService($configFile);

            return $myService;
        }, true);

如果希望在每次调用 $app->getService('ServiceName') 时都创建不同的 MyService 实例,只需将 $singleton 参数值更改为 false 即可。

示例:注册服务 'ServiceName'。每次调用 $app->getService('ServiceName') 时,都将实例化一个新的 MyService 对象并返回。

        $app->registerService('ServiceName', function ()
        {
            $configFile = IniFile::load(APP_CONFIG_DIR . '/sample.ini');
            $myService = new MyService($configFile);

            return $myService;
        }, false);

注意:与注册实例相比,注册可调用项有一个重要的优势:如果未调用 $app->getService('ServiceName'),则不会执行对象实例化代码。因此,将服务作为可调用项注册可以减少应用程序的“启动”时间和内存消耗,因为只有在使用服务时才会创建新的实例,并且实例化将在运行时而不是在应用程序加载时进行。

将键值对作为服务注册

Leveret 允许以下方式注册键值对作为服务

     $app->registerService('MyKey', 'MyValue');

将资源作为服务注册

与上述所有情况一样,为了在我们的应用程序中将资源(resource)注册为服务,我们将使用 $app->registerService( string $serviceName, resource $resuorce) 方法。

     $app->registerService('MyResource', $myResource);

ACLs

Leveret 支持使用基本的 ACL(访问控制列表)。通过它们,我们可以根据我们定义的规则(必须符合 H4D\Leveret\Application\AclInterface 接口)限制对我们应用程序中某些组件的访问。

ACLs 的注册位置是我们应用程序类中的 initAcls() 方法。

可以注册针对路由和控制器/操作的 ACLs,为此提供了以下方法:

  • registerAclForRoute(AclInterface $acl, string $routeName)
  • registerAclForController(AclInterface $acl, string $controllerName, array $applyToActions = ['*'], array $excludedActions = [])

为路由注册 ACLs

为了为特定路由注册一个或多个 ACLs,我们必须使用应用程序的方法:registerAclForRoute(AclInterface $acl, string $routeName),其中

  • $acl: 是一个必须遵守 AclInterface 接口的类实例。
  • $routeName: 是我们分配给想要应用 ACL 的路由的名称。

ACLs 的控制器注册

我们可以使用以下应用程序方法注册应用于控制器或控制器中特定 action 的 ACLs: registerAclForController(AclInterface $acl, string $controllerName, array $applyToActions = ['*'], array $excludedActions = []),其中

  • $acl: 是一个必须遵守 AclInterface 接口的类实例。
  • $controllerName: 是控制器的完整名称(命名空间 + 类名)。
  • $applyToActions: 是一个字符串数组,可以用来指定将 ACL 应用到哪个控制器中的 action 列表。默认情况下,此数组的值为 ['*'],表示 ACL 将应用于控制器中的所有 action。
  • $excludedActions: 是一个字符串数组,用来指定不希望应用 ACL 的 action(相当于 action 的白名单)。

示例:将注册为服务的 ACL AdminLoggedInRequired 应用于控制器 MyAppp\Controller\AdminController

$this->registerAclForController($this->getService(AdminLoggedInRequired::class),
                                'MyAppp\Controller\AdminController');

完整示例

    <?php
    
    use H4D\Leveret\Application;
    use H4D\Leveret\Http\Response;
    use H4D\Leveret\Http\Response\Headers;
    use H4D\Logger;
    
    require_once('../app/bootstrap.php');
    
    /** @var Application $app */
    $app = new Application(APP_CONFIG_DIR.'/config.ini');
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // INI: Register routes and actions ////////////////////////////////////////////////////////////////
    
    // Simple action without params returning html contents using a template.
    $app->registerRoute('GET', '/')
        ->setAction(
            function () use ($app)
            {
                $app->getLogger()->notice('It works!');
                $app->render('default.phtml');
            });
    
    // Action with one param, multiple predispatch actions and one postdispatch action returning html
    // contents using a template.
    $app->registerRoute('GET', '/hello/:name')
        ->setAction(
            function ($name) use ($app)
            {
                $app->getLogger()->notice('Hello', array('name' => $name));
                $app->getView()->addVar('name', $name);
                $app->render('hello.phtml');
            })
        ->addPreDispatchAction(
            function ($route, $app)
            {
                $newParams = array();
                /** @var \H4D\Leveret\Application\Route $route */
                foreach($route->getParams() as $key => $value)
                {
                    $newParams[$key] = is_string($value) ? strtoupper($value) : $value;
                }
                $route->setParams($newParams);
            })
        ->addPreDispatchAction(
            function ($route, $app)
            {
                $newParams = array();
                /** @var \H4D\Leveret\Application\Route $route */
                foreach($route->getParams() as $key => $value)
                {
                    $newParams[$key] = is_string($value) ?
                        '"' . $value . '"' : $value;
                }
                $route->setParams($newParams);
            })
        ->addPostDispatchAction(
            function ($route, $app)
            {
                /** @var Application $app */
                $app->getResponse()->setStatusCode('404');
            }
        );
    
    // Action with multiple params returning JSON content type.
    $app->registerRoute('GET', '/add/:(float)num1/:(float)num2')
        ->setAction(
            function ($num1, $num2) use ($app)
            {
                $result = $num1 + $num2;
                $app->getLogger()->notice(sprintf('Result: %f', $result), array('num1' => $num1,
                                                                                'num2' => $num2,
                                                                                'result' => $result));
                // Change response headers.
                $app->getResponse()->getHeaders()->setContentType(Headers::CONTENT_TYPE_JSON);
                $app->getResponse()->setBody(json_encode(array('num1' => $num1,
                                                               'num2' => $num2,
                                                               'result' => $result)));
            })
        ->addPreDispatchAction(function ($route, $app)
        {
            /** @var Application $app */
            if ($app->getRequest()->hasAuth())
            {
                $user = $app->getRequest()->getAuthUser();
                $pass = $app->getRequest()->getAuthPassword();
                $app->getLogger()->debug(sprintf('User: %s, Pass: %s', $user, $pass));
            }
    
        });
    
    // END: Register routes and actions ////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    // Run the application!
    $app->run();