rloris/layer

Layer 是一个使用注解的面向对象的 MVC PHP 框架

dev-master 2020-09-08 10:49 UTC

This package is auto-updated.

Last update: 2024-09-08 19:36:51 UTC


README

Layer 是一个面向对象的 MVC PHP 框架

此项目目前处于 alpha 阶段,您仍然可以尝试它并给我反馈

MVC 代表模型-视图-控制器

模型表示要操作的数据,视图表示数据的显示方式,控制器处理应用程序的业务逻辑。

  • 模型通过简单的类定义

  • 视图通过包含 HTML 内容的 .php 文件定义

  • 控制器通过类定义

  • 操作通过控制器内部的方法定义

您可以使用 Layer 轻松创建 API 或网站

  • 网站使用视图来显示内容并允许用户与之交互(MVC)

  • API 不使用视图来显示数据,它以特定格式(如 JSON、XML、文本等)返回数据

  • 您可以定义自定义错误控制器以用于您的网站或 API

入门

Composer

在项目初始化后,运行此命令以从 master 获取最新版本

composer require rloris/layer:dev-master

如果您更新了 layer,请删除生成的文件(map.json、routes.json、hash.json),以获取最新更改

配置

首先,我们需要设置 configuration.json 文件,它应该看起来像这样,这是最小设置

{
    "locations": {
        "controllers": "{path_to_the_folder_containing_your_controllers}",
        "shared": "{path_to_the_folder_containing_your_shared_components}",
        "build": "{path_to_the_folder_for_layer_output_build_files}",
        "log": "{path_to_the_folder_for_layer_output_log_files}"
    },
    "environment": {
        "current": "dev",
        "dev": {
            "routeTemplate" : "",
            "apiRouteTemplate" : "api",
            "log": false,
            "logTemplate" : "[{request_datetime}][{environment}][{request_method} {request_resource}]:{message}",
            "build": true
        },
        "prod": {
            "routeTemplate" : "",
            "apiRouteTemplate" : "api",
            "log": true,
            "logTemplate" : "[{request_datetime}][{environment}][{client_ip} {client_browser} {client_os}][{request_method} {request_resource}]:{message}",
            "build": false
        },
        "{your_other_env_name}": {
            "routeTemplate" : "",
            "apiRouteTemplate" : "api",
            "log": false,
            "logTemplate" : "[{request_datetime}][{environment}][{client_ip} {client_browser} {client_os}][{request_method} {request_resource}]:{message}",
            "build": false
        }
    },
    "layouts" : {
        "{your_layout_name}": {
          "pre": ["header","navbar","breadcrumbs"],
          "post": ["footer"]
        },
        "{your_other_layout_name}": {
          "pre": ["header", "navbar"],
          "post": ["footer"]
        }
    },
    "globals": {
        "{name_of_the_constant}": "{value_of_this_constant}",
        "{name_of_another_constant}": "{value_of_this_constant}",
    }
}

locations

  • controllers 指向包含您应用程序所有控制器的目录
  • shared 指向包含您应用程序所有共享元素(视图、过滤器)的目录
  • build 指定 layer 将生成用于框架的文件的目录
  • log 指向存储日志文件的目录

environment

  • routeTemplate 是将添加到所有控制器路由的前缀
  • ApiRouteTemplate 是将添加到所有 ApiController 路由的前缀(例如:对于将 API 错误显示为 JSON 而不是 HTML 视图很有用)
  • log 启用/禁用 Logger
  • logTemplate 告诉 Logger 使用每个日志条目的结构
  • build 启用/禁用文件夹(控制器、共享)中的更改扫描以生成新的路由映射(建议:在开发环境中设置为 true,在生产环境中设置为 false)

layouts

  • 布局是附加在一起的共享视图,它们仅用于网站,不用于 API,如果您正在构建 API,请将其留空
  • 键是您布局的名称,您可以在控制器中使用布局来指定要使用的布局
  • 布局由 pre 视图(将在主内容之前渲染)和 post 视图(将在主内容之后渲染)组成
  • 您可以通过控制器中可用的 viewManager 在运行时更改布局配置

globals

  • 您可以在 globals 部分存储任何您想要的内容
  • 所有键都将转换为大写常量,例如,my_key 的值将在整个应用程序中可用为 MY_KEY

目录结构

结构组织如下,您有以控制器命名的文件夹,其中包含控制器 php 文件,如果它是网站控制器,您还可以找到包含此控制器使用的视图的视图文件夹。

注意:特定的控制器只能使用它自己的视图文件夹或共享视图,不能使用来自另一个控制器视图文件夹的视图。如果您想为不同的控制器重用视图,请创建一个共享视图!

/mycontrollers
    /home
        HomeController.php
        /views
            index.php
            contact.php
            about.php
    /blog
        BlogController.php
        /views
            list-post.php
            edit-post.php
    /auth  
        AuthController.php
        /views
            login.php
            signin.php
    /api
        /user
            UserApiController.php
        /blog
            BlogApiController.php
/shared
    /filters
        AuthFilter.php
        LogFilter.php
        GlobalFilter.php
    /views
        header.php
        modal.php
        footer.php
        /alerts
            success.php
            warning.php
            failure.php

创建一个index.php

这将是您应用程序的主要入口点,每个请求都会触发此脚本,您不应在其中显示任何内容,但您可以在其中添加代码以进行其他原因。

// require autoloader
require_once "./vendor/autoload.php";

// init app with configuration file path
$app = rloris\layer\App::getInstance("./configuration.json");

// execute app
if($code = $app->execute()) 
{
    // request or error handled successfully
    rloris\layer\utils\Logger::write("Serving content successfully with status code: $code");
} 
else 
{
    // error could not be handled
    rloris\layer\utils\Logger::write("Error occurred");
}

为Apache服务器设置 .htaccess

所有请求都应转发到您的index.php入口文件

<IfModule mod_rewrite.c>    
    Options +FollowSymLinks
    RewriteEngine On
    RewriteCond %{REQUEST_URI} !-d
    RewriteCond %{REQUEST_URI} !-f
    RewriteCond %{REQUEST_URI} !-l
    RewriteRule ^(public)($|/) - [L]
    RewriteRule ^(.*)$ index.php?url=$1 [L,QSA]
</IfModule>

这会将所有请求重定向到index.php文件或公共文件夹,其中包含您的css、图片、js等

为nginx服务器设置

location  -d {}
location  -f {}
location  -l {}
location ~ ^/(public)($|/) { }
location / {
  rewrite ^(.*)$ /index.php?url=$1 break;
}

所有请求都应转发到您的index.php入口文件或公共文件夹,其中包含您的css、js、图片等

设置完成

一旦设置完成,您就可以根据下一章中的说明创建控制器类、视图文件和过滤器类。Layer通过在类和方法上使用注解来自动构建路由映射,无需告诉路由器添加路由。Layer通过使用哈希值检测文件是否已更新,只重建更新的部分。

控制器

您不应在控制器中显示任何内容,但想要显示内容时,应将其传递给视图。为什么?因为Layer处理并发送头信息,在控制器或过滤器中显示内容会破坏此系统,并且您的自定义Layer头信息将不会被发送。

Layer通过在配置文件中指定的控制器文件夹中检测控制器,并且控制器的类名包含{Your_Name}Controller.php来检测控制器,一旦您的类创建完成,只需扩展它以适应您的特定需求

  • 为网站控制器设置BaseController
  • 为API控制器设置ApiBaseController
  • 为网站错误控制器设置ErrorBaseController
  • 为API错误控制器设置ApiErrorBaseController

然后添加一个注解来告诉Layer如何处理此控制器的路由

注解

API控制器的注解

将这些放在ApiBaseController类的顶部 @ApiController

/**
 * @ApiController(routeTemplate='users', defaultAction='getUsers')
 */
class UserApiController extends ApiBaseController { ... }

这意味着当您访问/api/users/时,默认操作是getUsers()

您不需要添加/api,因为它已经通过读取配置文件中的“apiRouteTemplate”键完成,位于environment/{current_env}/apiRouteTemplate

如果您将配置文件中的此键更改为myApi,则所有API路由都可通过访问/myApi/{route}来访问

然后,对于此类中您想要访问的每个公共方法,添加此注解 @ApiAction

    // inside UserApiController class
    /**
     * @ApiAction(routeTemplate='/',methods={'get'})
     */
    public function getUsers() { ... }

    /**
     * @ApiAction(routeTemplate='/',methods={'options'})
     */
    public function getOptions() { /* Handle CORS */ }

getUsers()操作将在您使用GET方法访问/api/users/时被触发

网站控制器的注解

将这些放在BaseController类的顶部 @DefaultController@Controller。如果它是没有指定路由时要触发的控制器(默认路由),则放在@DefaultController

/**
 * @DefaultController(routeTemplate='home', layoutName='basic')
 */
class HomepageController extends BaseController { ... }

如果访问/,我的请求将内部转发到HomepageController,我也可以访问/home来访问此控制器,您的项目中应只有一个@DefaultController,其他应该是@Controller

@Controller@DefaultController的默认操作是index(),您当然可以通过指定其他一个在这些注解中来自定义它

然后,对于此类中您想要访问的每个公共方法,添加此注解 @Action

    // inside HomepageController class
    /**
     * @Action(methods={"get"})
     */
    public function index() { ... }

    /**
     * @Action(methods={"get"})
     */
    public function about_us() { ... }

    /**
     * @Action(methods={"get"})
     */
     public function contact() { ... }

如果您在注解中未指定routeTemplate,则将使用方法名,因此通过访问/home/about_us/home/contact将触发about_us()或contact()操作

用于处理网站错误的错误控制器

将这些放在ErrorBaseController类的顶部 @ErrorController

/**
 * @ErrorController(layoutName='basic')
 */
class ErrorsController extends ErrorBaseController { ... }

这告诉层使用此类来处理网站抛出的所有错误

然后在控制器内部,将此注解放在处理特定错误的方法上方

    // inside ErrorsController class
    /**
     * @ErrorAction(errorCodes={"5\d\d"}, viewName='index')
     */
    public function serverError() { ... }

    /**
     * @ErrorAction(errorCodes={"404", "400"}, viewName='index')
     */
    public function notFoundError() { ... }

    /**
     * @ErrorAction(viewName='index')
     */
    public function clientError() { ... }

在这种情况下,所有带有HTTP状态码5xx的错误将触发serverError(),所有带有HTTP状态码404400的错误将触发notFoundError(),其余的错误将触发clientError()

API错误控制器用于处理API错误

将这些注解放在ApiErrorBaseController类上方 @ApiErrorController

/**
 * @ApiErrorController
 */
class ApiErrorsController extends ApiErrorBaseController { ... }

这告诉层处理所有API错误并将它们转发到该控制器

然后只需通过指定要触发的操作来简单地告诉层如何处理特定错误,将此注解放在控制器内的方法上 @ApiErrorAction

    // inside ApiErrorsController class

    /**
     * @ApiErrorAction(errorCodes={"4\d\d"})
     */
    public function clientError() { ... }

    /**
     * @ApiErrorAction(errorCodes={"5\d\d"})
     */
    public function serverError() { ... }

在这种情况下,所有带有HTTP状态码5xx的错误将触发serverError()操作,所有带有HTTP状态码4xx的错误将触发clientError()操作

路由参数

Layer通过名称处理路由参数,您可以在routeTemplate (控制器和操作)中定义参数,如下所示

    /**
     * @Controller(routeTemplate='auth/{#identifier}', layoutName='basic', filters={'time'})
     */
    class AuthController extends BaseController { ... }

在这里,我在AuthController中声明了一个必须的数字类型参数,这意味着所有操作都需要此参数才能被触发,当然,它们将能够获取它

如果您的参数是必须的并且可以是任何东西,声明它的语法是,{param}

如果您的参数是必须的并且是数字,声明它的语法是,{#param}

如果您的参数不是必须的,只需在末尾添加?即可,无论是数字还是其他,{param?}

然后要获取此参数,只需将其名称作为方法参数使用即可,如下所示

    // inside AuthController class

    /**
     * @ApiAction(methods={"get"})
     */
    public function connect($identifier) { ... }

如您所注意到的,我设置了控制器的routeTemplate,因此我可以在此控制器内的所有方法中使用$identifier,如果您只想对操作获得相同的结果,可以这样做

/**
* @Controller(routeTemplate='auth', layoutName='basic', filters={'time'})
*/
class AuthController extends BaseController { 
    /**
     * @ApiAction(routeTemplate='{#identifier}', methods={"get"})
     */
    public function connect($identifier) { ... }
}

这里的区别在于,只有connect方法将能够使用$identifier,因为我没有为控制器声明它

过滤器(中间件)

过滤器是在主操作之前和/或之后执行的操作,过滤器是类,它们用于在访问特定资源之前测试用户是否已连接,或者简单地记录某些内容

您可以在控制器或apiController上应用过滤器(这意味着它将应用于控制器内的所有操作)或仅针对指定的一些操作

Layer在配置.json文件中指定的共享文件夹中检测过滤器,并且过滤器的类名包含{Your_Name}Filter.php,一旦创建类,只需使用BaseFilter类扩展它即可。

然后,添加此注解: @Filter

eg: 
    /**
    * @Filter
    */
    class AuthFilter extends BaseFilter
    {
    
        public function in()
        {
            /* input stuff here */
        }
    
        public function out()
        {
            /* output stuff here */
        }
    }

您可以在多个操作或控制器上使用过滤器,顺序定义了哪个过滤器将首先被调用,如下所示

  • 要将过滤器应用于控制器(因此应用于此控制器中的每个操作)
    /**
    * @DefaultController(filters={'time', 'log'})
    */
    class HomepageController extends BaseController { ... }

在这种情况下,输入时,首先将调用名为time的过滤器,然后调用log,输出时,首先调用log,然后调用time。

  • 要将过滤器应用于操作
    /**
    * @Action(methods={"post"}, filters={"auth"})
    */
    public function upload()
    {
        self::$data = ["content" => "File uploaded with success", "title" => 'Upload file'];
    }

如果upload在HomepageController中,那么过滤器将以以下顺序应用,在输入时

time > log > auth

然后调用upload()方法,在输出时

auth > log > time

您可以通过将注解更改为@GlobalFilter来定义全局过滤器,它们将在每次都应用

    /**
     * @GlobalFilter
     */
    class MyGlobalFilter extends BaseFilter
    {
    
        public function in()
        {
            // CORS allows all origins for api
            if(strtoupper(self::$request->getRequestMethod()) === IHttpMethods::OPTIONS) {
                self::cors()->allowAnyOrigins()->allowAnyMethods()->allowAnyHeaders();
            }
        }
    
        public function out()
        {
            /* output stuff here */
        }
    }

您可以通过在Controller类和Filter类中使用的filterManager在运行时添加和删除过滤器

    self::$filterManager->add("auth");
    if(self::$filterManager->isActive("time"))
         self::$filterManager->remove("time");

视图

视图是包含主要HTML和一些PHP代码的PHP文件,但你不应该在那里放置业务逻辑。要将内容传递给视图,与发送API数据的方式相同,只需在你的控制器或过滤器中添加self::data[yourkey] = content;即可,然后像这样访问它:<?= $this->yourkey ?>。如果你在视图中访问的内容不存在,它将不会显示任何内容,也不会抛出错误。

由于self::$viewManager可用,你可以通过扩展BaseController类在运行时添加和删除视图。

PreViews是在主内容之前显示的共享视图。

PostViews是在主内容之后显示的共享视图。

ContentView是在预览之后、在postviews之前显示的主要内容视图。

        // getting GET parameter with name ok
        $ok = self::$request->getGet('ok');

        // adding alert shared view before main content of this action
        if($ok === 'true')
        {
            self::$viewManager->addPreView('alerts/success');
        }
        else if($ok === 'false')
        {
            self::$viewManager->addPreView('alerts/failure');
        }
        else
        {
            self::$viewManager->addPreView('alerts/alert');
        }

工具

请求

你可以在任何控制器或过滤器类中通过输入self::$request来访问请求。

DOC TODO

响应

你可以在任何控制器或过滤器类中通过输入self::$response来访问响应。

DOC TODO

文件

文件API允许你轻松处理文件,无论是上传的文件还是其他文件。

DOC TODO

会话

你可以在任何控制器或过滤器类中通过输入self::session()来访问会话管理器。

DOC TODO

CORS

你可以在任何控制器或过滤器类中通过输入self::cors()来访问CORS管理器。

DOC TODO

日志记录器

你可以使用日志记录器直接使用配置文件中的特定模板进行写入Logger::write("Hello World")

DOC TODO

接下来计划做什么

  • 为API响应添加XML输出支持
  • 添加路由参数定制的注解支持,如@RouteParam(name="id", regex="...")
  • 添加WebSocket服务器支持 => (是否需要扩展?)
  • 添加LORM(层对象关系映射)支持 => (是否需要扩展?)