liuxingwei/simple-api-framework

0.0.53 2019-11-04 13:58 UTC

README

零、概述

SAF(Simple Api Framework)是一个极简单的PHP API开发框架,适用于前后端分离架构的web项目,作为后端PHP API服务的框架。

说其极简,一方面是因为它只有很少的核心类和几个支持文件,另一方面是因为它只支持有限的场景,当然,也是说它非常易于使用。

SAF没有Beautiful URL路由,GET请求的参数是通过形如name=zhangsan&sex=maleQueryString参数传递的。

SAF遵循了惯例优于配置的理念,API必须放在指定的目录(项目目录的Application\Api)下,且GET请求对应的API类要放在Get子目录,而POST请求对应的API类要放在Post子目录,其他类型的HTTP请求对应的API,也放在与其HTTP METHOD相对应的子目录。

数据库方面,SAF有一个简单的DB类,它是以PDO为底层的,理论上它可以支持多种数据库服务,但是目前只在MySQL上做过测试。因此最适合的数据库搭配就是MySQL5.7+

对于PHP,由于7.2.*和其前的版本,在trait特性支持上有缺陷(引用继承了同一trait的多个trait时,会导致重复定义方法的致命错误),因此建议PHP 7.3+

一、环境要求

支持PHP 5.6,建议PHP 7.3+MySQL 5.7+

二、下载

1. composer

composer create-project liuxingwei/simple-api-framework

2. github

git clone https://github.com/Liuxingwei/php-simple-api-framework.git

三、环境搭建

有如下几种方式,任选其一(前两种方式仅适用于开发、测试环境):

1、PHP Built-in Server

使用PHP内建服务时,无需nginxapache,只需PHP

在命令行下,切换至public文件夹(safpathSAF的路径),执行php -S localhost:xxxx index.php即可,其中xxxx为端口号。

示例:

>cd safpath/public
>php -S localhost:xxxx index.php

浏览器打开localhost:xxxx,看到如下内容,服务启动成功:

Please access detail API.

2、借助 VSCode 中 PHP Server 插件

安装PHP Server插件,打开File > Preferences > Settings,找到Extensions > PHP Server Configuration,将Relative Path改为./public

如果PHP可执行文件没加到系统路径中,可以将其填写在PHP Server插件的PHP Path配置项中。

打开public/index.php文件,在文件窗口右键,选PHP Server: Server Project

浏览器打开localhost:xxxx,看到如下内容,服务启动成功:

Please access detail API.

PHP Server: Stop可以停止服务。

PHP Server: Reload Server可以重启服务。

3、使用 WAMP

启动WAMP,点进托盘区WAMP图标,选择Apache > httpd-vhosts.conf文件。

在打开的文件中,复制VirtualHost段,修改端口,并将路径修改为项目目录的public文件夹。

示例(safpath即指SAF的路径):

要复制的段:

<VirtualHost *:80>
  ServerName localhost
  ServerAlias localhost
  DocumentRoot "${INSTALL_DIR}/www"
  <Directory "${INSTALL_DIR}/www/">
    Options +Indexes +Includes +FollowSymLinks +MultiViews
    AllowOverride All
    Require local
  </Directory>
</VirtualHost>

复制后修改IPDocumentRootDirectory路径:

<VirtualHost *:xxxx>
  ServerName localhost
  ServerAlias localhost
  DocumentRoot "safpath/public"
  <Directory "safpath/public">
    Options +Indexes +Includes +FollowSymLinks +MultiViews
    AllowOverride All
    Require local
  </Directory>
</VirtualHost>

修改后的完整文件:

<VirtualHost *:80>
  ServerName localhost
  ServerAlias localhost
  DocumentRoot "${INSTALL_DIR}/www"
  <Directory "${INSTALL_DIR}/www/">
    Options +Indexes +Includes +FollowSymLinks +MultiViews
    AllowOverride All
    Require local
  </Directory>
</VirtualHost>


<VirtualHost *:xxxx>
  ServerName localhost
  ServerAlias localhost
  DocumentRoot "safpath/public"
  <Directory "safpath/public">
    Options +Indexes +Includes +FollowSymLinks +MultiViews
    AllowOverride All
    Require local
  </Directory>
</VirtualHost>

重启Apache

浏览器打开localhost:xxxx,看到如下内容,服务启动成功:

Please access detail API.

4. nginx

添加一个server配置:

server {
        listen xxxx default_server;
        listen [::]:xxxx default_server;

        server_name _;

        root safpath/public;

        location / {
                try_files $uri $uri/ =404;
                if (!-e $request_filename) {
                    rewrite  ^(.*)$  /index.php$1  last;
                }
        }

        location ~ \.php(.*)$ {
                include snippets/fastcgi-php.conf;
                fastcgi_split_path_info ^(.+\.php)(/.*)$;
                # With php-fpm (or other unix sockets):
                fastcgi_pass unix:/var/run/php/php7.3-fpm.sock;
                # With php-cgi (or other tcp sockets):
                # fastcgi_pass 127.0.0.1:9000;
        }

        location ~ /\.ht {
                deny all;
        }
}

重启nginx

四、目录结构

初始的项目目录结构如下:

+ application
  + Api
    + Delete
      + Example
        - Index.php
    + Get
      + Example
        - Index.php
    + Patch
      + Example
        - Index.php
    + Post
      + Example
        - Index.php
    + Put
      + Example
        - Index.php
  + Model
    - SafExample.php
  + Validations
    - MyValidation.php
+ conf
  + err_define
    - cn.php.sample
    - default.php
  - config.php.sample
  - di_config.php.sample
  - env.php.sample
+ doc
  - DB-Class-Usage.md
+ lib
  + Core
    - App.php
    - BaseApiInterface.php
    - BaseModel.php
    - bootstrap.php
    - DB.php
    - ErrorCode.php
    - ErrorCodeTrait.php
    - Request.php
    - Response.php
    - SafException.php
  + Validations
    - AbstractValidation.php
    - Length.php
    - Limit.php
    - NotEmpty.php
    - Required.php
    - Rule.php
+ public
  - .htaccess
  - index.html
  - index.php
+ vendor
  + bin
  + composer
  + doctrine
  + jeremeamia
  + liuxingwei
  + nikic
  + php-di
  + psr
  + symfony
  - autoload.php
- .gitignore
- composer.json
- composer.lock
- LICENSE
- README.md

五、创建API

application/Api/Getapplication/Api/Post中根据业务需要创建一个子文件夹(也可以是多级文件夹),在其中创建一个API类。

该类实现Lib\Core\Interfaces\BaseApi接口,并实现run()方法,该方法签名为:run(array $param):mixed

例如,在Get文件夹创建Example文件夹,并在其中创建Index.php,文件内容如下:

<?php
namespace Application\Api\Get\Example;

use Lib\Core\Interfaces\BaseApi;

class Index implements BaseApi
{
  public function run(array $params)
  {
    $result = [
      'code' => 200,
      'message' => 'OK',
      'description' => "I'm a GET request.",
    ];
    return $resut;
  }
}

run()方法的参数即为HTTP请求的参数集合。

此时,向服务器的/example/index发出GET请求,即可收到值为

{
  "code": 200,
  "message": "OK",
  "description": "I'm a GET request."
}

json返回。

此时,向/example/index发出POST请求,收到的则是

{
  "code": 404,
  "message": "API /example/index 不存在"
}

要创建一个接受/example/indexPOST请求的API,需要在application/Api/Post中创建Example文件夹,并在Example中创建Index.php文件:

<?php
namespace Application\Api\Post\Example;

use Lib\Core\Interfaces\BaseApi;

class Index implements BaseApi
{
  public function run(array $params)
  {
    $result = [
      'code' => 200,
      'message' => 'OK',
      'description' => "I'm a POST request.",
    ];
    return $result;
  }
}

命名空间与uri的关系

API类遵循psr4标准,其命名空间Application\Api映射于项目根目录中的application/Api目录。

API类名与APIuri之间的关系是,将类的命名空间中的Application\Api\xxx部分去除,并将\转换为/,就是该APIuri,而其中的xxx即对应了HTTP METHOD

例如类Application\Api\Get\Example\Index对应的APIuri即为/example/index,相应的HTTP METHODGET

而类Application\Api\Post\Example\Index对应的APIuri也为/example/index,但相应的HTTP METHODPOST

PUTPATCHDELETEHTTP METHOD类推。

由于HTTP定义的url对于大小写不敏感,在转换为类名时,会自动将每部分的首字母转换为大写,并将短横线及其后的一个字母转换为大写字母,以对应命名空间中的大写字母。

GET /example/index          =>    Application\Api\Get\Example\Index
POST /user-info/create-user    =>    Application\Api\Post\UserInfo\CreateUser
PUT /user-info/modify-user   =>    Application\Api\Put\UserInfo\ModifyUser

run()方法的参数

run()方法的params参数接收了与HTTP METHOD相对应的request数据。

对于POSTPUTPATCH,如果提交的HEADERContent-Typeapplication/json的情况,接收了json解码后的Request Payload数据。

对于PUTPATCHapplication/x-www-form-urlencoded,接收了(解析后的)Form Data数据,类似于$_POST的值。

对于POSTform-datamultipart/form-dataapplication/x-www-form-urlencoded,则接收了(解析后的)Form Data数据。相当于$_POST的值。

对于GETDELETE,则接收了解析后的Query String键值对。相当于$_GET的值。

其它情况,则直接在paramsBODY元素中存储了提交的Request Payload的原始值。

ErrorCode

可以将返回的基本结构放在配置文件中,通过ErrorCode类读取。

框架提供了一个ErrorCodeTrait,其中定义了实例变量errCode,实例化了ErrorCode类,可以直接在需要使用ErrorCode的类中使用(注意规避一下与errCode变量名的命名冲突):

use Lib\Core\ErrorCodeTrait;

class xxx {
  use ErrorCodeTrait;
  public function xx(...)
  {
    ...
    $res = $this->errCode->OK;
    return $res;
  }
}

ErrorCode使用的配置文件有default.phpxx.php两个,后一个文件的xx指的是配置语言,默认为中文,即cn。配置文件默认放在项目根目录下的conf/err_define文件夹。

文件位置和语言均中在conf/config.php中配置:

return [
  ...
  'err_define_dir' => __DIR__ . '/mydefine',
  'language' => 'en',
  ...
]

配置方式参见conf/err_define/cn.php

default.php中放置的是系统预定义的配置,不建议直接修改,可以在语言文件中定义同名元素覆盖默认配置。

配置中的元素的key,可以当作errCode的属性直接使用:

$this->errCode->OK;
$this->errCode->PARAM_MUST_NOT_EMPTY;

可以这样改写Example\Index

<?php
namespace Application\Api\Get\Example;

use Lib\Core\Interfaces\BaseApi;
use Lib\Core\ErrorCodeTrait;

class Index implements BaseApi
{
  use ErrorCodeTrait;
  public function run(array $params)
  {
    $res = $this->errCode->OK;
    $res['descriptio' => "I'm GET request'];
    return $res;
  }
}

消息定义可以使用占位符,占位符被包含在{{:}}之间。可以使用数组指定要替换的与数组键匹配的值。

例如,定义如下消息:

 PARAM_NOT_EXISTS => ['code' => 403, 'message' => '参数 {{:param}} 的长度必须在 {{:min}} 到 {{:max}} 之间'];

然后在Apirun()方法中这样使用:

$err = self::errCode->mapError(self::errCode->PARAM_NOT_EXISTS, ['param' => 'username', 'min' => 3, 'max' => 16]);
// 最终的输出结果为:
// {
//   "code": 403,
//   "message": "参数 username 的长度必须在 3 到 16 之间"
// }

结束执行

框架最后调用API代码就是run()方法,因此该方法执行的最后一行代码就标志了API的执行结束。

下面的代码示例了在不同条件下的不同输出并结束API的执行:

public function run(array $params)
{
  if ($signinSuccess) {
    return [
      'code' => 200,
      'message' => '登录成功',
    ];
  }
  return [
    'code' => 200,
    'message' => '登录失败',
  ];
}

默认的输出中,HTTP CODE均为200

如果认为登录失败是一种错误,可以定义errorCode的属性:

public $errorCode;
public function run(array $params)
{
  if ($signinSuccess) {
    return [
      'code' => 200,
      'message' => '登录成功',
    ];
  }
  $this->errorCode = '403';
  return [
    'code' => 403,
    'message' => '登录失败',
  ];
}

如果定义的codeHTTP错误码相同,还可以使用SafException类的throw静态方法抛异常:

public function run(array $params)
{
  if (!$signinSuccess) {
    SafException::throw([
      'code' => 403,
      'message' => '登录失败',
    ])
  }
  return [
    'code' => 200,
    'message' => '登录成功',
  ];
}

这两段代码的输出是一样的。

输出类型

默认的输出类型为application/json。无需主动对结果进行json_encode,只需run()方法返回要输出的数组即可。

public function run($param)
{
  ......
  return ['code' => '200', 'message' => 'ok'];
}

也可以定义其它类型的输出,通过API类的$responseType属性指定。

......
class xxx extends BaseApiInterface
{
  public $responseType = 'html';
  return "<div>这是一个html片段</div>";
}

可以使用的类型有:

htmljsonxmltextjavascriptsteam

其中,jsonxml类型,run()方法返回数组;htmltext返回文本;javascript返回js文本。

stream用于输出文件,除了run()方法要返回待输出文件的内容外,还需要通过headers属性指定response头信息。

namespace Application\Api\Get\Test;

use Lib\Core\BaseApiInterface;

class Stream implements BaseApiInterface
{
    public $responseType = 'stream';
    public $headers = [
        'Content-Type: application/vnd.ms-excel',
        'Content-Disposition: attachment;filename=test.csv',
        'Cache-Control: max-age=0',
    ];
    public function run(array $request)
    {
        $csv = "name,age,sex,job\nzhangsan,30,男,程序猿";
        $this->headers[] = 'Content-Lenght: ' . mb_strlen($csv);
        return $csv;
    }
}

配置文件

通用的配置文件放置在conf目录中,文件名为config.php,框架提供了一个示例文件config.php.sample

该文件内容示例如下:

<?php
return [
    'runtime' => 'development', // 运行环境,development 为开发环境,test 为测试环境,product 为生产环境
    'api_path' => '/Api',
    'db' => [ // 数据库配置
        'dbms' => 'mysql',
        'host' => '127.0.0.1',
        'port' => '3306',
        'user' => 'root',
        'password' => '123456',
        'dbname' => 'sampledb',
    ],
    'second_db' => [
        'dbms' => 'mysql',
        'host' => '127.0.0.1',
        'port' => '3306',
        'user' => 'root',
        'password' => '123456',
        'dbname' => 'jol',
    ],
    'di_config' => [ // PHP-DI 定义配置,可以是定义文件名,也可以是定义文件名数组
        __DIR__ . '/di_config.php',
    ],
    'debug' => true, // 是否开启 debug,开启 debug 后,可以在提交中带有 debug 参数,返回的数据中将有 debug 项
];

其中的配置数组将被读入超全局变量$_ENVconfig元素中。

上面示例中的变量可以这样读取:

$_ENV['config']['runtime']; // 'development'
$_ENV['config']['db']['host']; // 'localhost'

为方便使用,该配置也被定义在CONFIG常量中,相同的配置还可以这样读取:

CONFIG['runtime'];
CONFIG['db']['host'];

通常情况下,我们的生产环境和测试、开发环境在配置方面总会有些差别,因此SAF提供了可以覆写conf/config.php文件中的默认配置的方法:

即在conf/env.php文件中放置需要覆写的配置项,比如生产环境的数据库主机地址为10.0.0.1,密码为@77pai*654,则可以在生产服务器的conf/env.php文件作如下配置:

return [
    'db' => [
        'host' => '10.0.0.1',
        'password' => '@77pai*654'
    ],
    'second_db' => [
        'host' => '10.0.0.1',
        'password' => '@77pai*654'
    ]
];

而与conf/config.php相同的配置,则无需在conf/env.php中重复配置。

不要将conf/config.php文件提交到版本库,可以将其放入版本库的忽略文件列表中,而额外提供一个conf/env.php.sample,作为配置的参考。

DBModel

框架实现了一个基于PDODB类,具体使用请参考doc目录的DB-Class-Usage.md文件。

框架提供了一个BaseModel基类,可以以此为基础自定义Model类,继承BaseModel类。建议将Model类定义在Application\Model命名空间中。

// application/Model/User.php
namespace Application\Model;

use Lib\Core\BaseModel;

class User extends BaseModel
{
  public function checkUser($userName, $password)
  {
    return $this->where('user_name = :user_name AND password = :password', [':user_name' => $userName, ':password' => $password])->selectOne();
  }
}

上例中,是假定表名与类名相同,都是user(在MySQL中不区分大小写)。

如果实际的表名与类名不同,则需要使用$table属性自定义表名:

// application/Model/UserInfo.php
namespace Application\Model;

use Lib\Core\BaseModel;

class UserInfo extends BaseModel
{
  protected $table = 'user_info';
  public function checkUser($userName, $password)
  {
    return $this->where('user_name = :user_name AND password = :password', [':user_name' => $userName, ':password' => $password])->selectOne();
  }
}

框架会将类名定义中的大写字母转换「下划线加小写」字母的形式。

注意不要在MySQL中用驼峰法命名表名,而要用下划线命名法。

上例也可以省略表名的显式定义:

// application/Model/UserInfo.php
namespace Application\Model;

use Lib\Core\BaseModel;

class UserInfo extends BaseModel
{
  public function checkUser($userName, $password)
  {
    return $this->where('user_name = :user_name AND password = :password', [':user_name' => $userName, ':password' => $password])->selectOne();
  }
}

如果使用依赖注入方式注入Model类,建议以多实例模式注入,以避免单例引起的问题。

可以在定义Model时即将其指定为多实例模式:

// application/Model/UserInfo.php
namespace Application\Model;

use Lib\Core\BaseModel;

/**
 * @Scope('prototype')
 */
class UserInfo extends BaseModel
{
  .....
}

依赖注入支持

框架提供了对依赖注入的支持。使用了改造过的LIUXINGWEI/LXW-PHP-DI(fork 自PHP-DI/PHP-DI)。主要是增加了Scope注解和scope()方法,以支持非单例注入模式。

Scope注解用于自动装配类的定义,其参数可以是singletonprototype,不写Scope注解,或者不写Scope的参数,均默认为singleton,即单例模式,prototype则为非单例模式。参见Application/Model/SafExample类的定义。

scope()方法用于依赖注入配置,在调用factory()方法之后,链式调用scope()方法,其参数为singletonprototype,分别对应单例和非单例模式。参见conf/config.php.sample文件中的定义。

两种非单例注入模式的注入示例见Application/Api/Get/Example/Index

默认的配置文件是conf/di_config.php,不过可以通过系统配置文件conf/config.php中的di_config项来修改。

该配置项可以是一个PHP-DI配置的路径,也可以是一个包含多个PHP-DI配置文件路径的数组。

框架在conf目录放置了一个依赖注入配置的示例文件conf/di_config.php.sample

有关PHP-DI的详细使用请查阅PHP-DI 文档

示例APIGet\Example\Index中有依赖注入示例。

虚拟子目录支持

如果需要将API部署在虚拟子目录中,需要将请求转发至public\index.php,由其负责路由。

同时,需要修改conf\config.php中的api_path设置,将虚拟目录放在该参数中,例如为所有API提供/Api路径前缀:

'api_path' => '/Api'

AJAX 跨域问题

如果将SAF用于AJAX调用的API服务,可能需要跨域。

跨域配置项如下:

'cross_domain' => [
  'enable' => true,
  'domain' => 'http://192.168.1.25:8080',
  'methods' => 'POST, GET, PUT, DELETE, PATCH',
  'headers' => 'sign, key',
];

enable配置项决定了是否启用跨域。系统默认是不启用。

domain的系统默认值为*

methods的系统默认值为'POST, GET, PUT, DELETE, PATCH'

headers的系统默认值为'x-requested-with, content-type, debug'

由于系统需要使用header的默认值支持,因此此项配置不会覆盖系统默认值,而是会与系统默认值合并。其余三项,则会由用户配置覆盖系统默认值。

参数校验

框架提供了几个基本的校验类,用于对请求参数进行校验。

这些校验方法的使用依赖了annotation(注解)技术。仅需在run()方法上添加注解,即可实现对参数的校验。

每条注解需要声明要校验的参数名,有的还需要带有额外的参数。

已经实现的校验注解及其示例如下(所有注解的字符串类型必须使用双引号作为定界符,使用单引号会引发错误):

1. Required

Required注解用于参数必须的情况。它只要求参数存在,对于参数值则没有要求。

/**
 * ......
 * @Required("user_name")
 * @Required("password")
 */
public function run($params)
{
  ...
}

如上代码要求$params参数数组中必须包含user_namepassword两个元素。

2. NotEmpty

NotEmpty注解用于参数不得为空的情况。

/**
 * ......
 * @NotEmpty("description")
 */
public function run($params)
{
  ...
}

如上代码校验参数$params中的user_name元素不得为空。

需要注意的是NotEmpty注解不对参数是否存在进行校验,它仅在要校验的参数存在的情况下才有效。

如果要求参数必须存在且不得为空,需要联合Required注解共同完成校验:

/**
 * ......
 * @Required("user_name")
 * @NotEmpty("user_name")
 */
public function run($params)
{
  ...
}

NotEmpty支持对要校验的参数去首尾空格:

/**
 * .....
 * @NotEmpty("user_name", trim=true)
 */
public function run($params)
......

不过,trim仅存在于校验过程中,对实际参数没有影响,因此在run()方法内部,仍需自己处理参数的首尾空格问题。

3. 长度校验

对长度的校验有两个注解,Length适用于字符串,Limit适用于数值。

Length注解有maxmin两个可选参数,分别限制最大(含)和最小长度(含),为闭区间。

/**
 * ......
 * @Length("password", max=16, min=9)
 */
public function run($params)
......

Limit注解也是maxmin两个可选参数,闭区间。不过它支持浮点数。

/**
 * ......
 * @Limit("price", min=12.5, max=13.3)
 */
public function run($params)
......

4. 正则校验类

对于电话号码、IP地址、邮编、Email等进行合法性校验时,会使用到正则表达式。

Rule注解就用于这种场合。

/**
 * ......
 * @Rule("ip", rule=".*?@.*?\..*?", error={"message":"参数 \"IP\" 不是合法的 IP 地址"})
 */
public function run($params)
......

与前面的校验类不同,正则校验类允许自定义校验失败时的消息,见上例中的error参数。

自定义校验类

可以自定义校验类,具体写法可以参照Lib\Validatetions中的预置校验类。

简单的说,校验类是依赖doctrine/annotations实现的。

首先,自定义校验类要继承Lib\Validations\AbstractValidation类,并在类前面添加@Annotation@Target({”METHOD"})注解:

use Lib\Validations\AbstractValidation`;

/**
 * @Annotation
 * @Target({"METHOD"})
 */
class MyValidation extends AbstractValidation {
  ......
}

校验类须实现check()方法,其参数即为框架转换后的请求参数(也即run()方法接收到的参数,见前言run()方法的参数。

在校验通过时,check()方法返回true,失败时返回false

并且在失败时,要设置err变量,其类型为数组,包括codemessage两个元素,对应失败的编码和原因。

......
class MyValidation extends AbstractValidation
{
  ......
  public function check(array $params)
  {
    if (...) { // 校验失败的处理
      $this->err = [
        'code' => 10086,
        'message' => '客服小姐姐脾气太大'
      ];
      return false;
    }
    return true;
  }
}

校验类的蕨类定义了一个变量value,它对应于校验注解的同名参数,如果该参数位于注解的第一位,可以不标名:

......
class MyValidation extends AbstractValidation
{
  public function check(array $params)
  {
    $this->value;
    ......
  }
}
......
class MyApi implements BaseApiInterface
{
  /**
   * @MyValidation(value="username")
   */
  public function run($params)
  {

  }
}
class MyApi1 implements BaseApiInterface
{
  /**
   * @MyValidation("username")
   */
  public function run($params)
  {
    ......
  }
}

上面例子中的两种注解,其效果是一样的,在MyValidation类中的value获取的值均为username

校验类的其它公有变量,对应于注解中的同名参数。

书写注解时,要注意,如果参数类型为string,则对应的参数值要使用双引号,而不能用单引号。如果参数类型是array,要放在一对花括号中,其格式与标准json基本一致。

以下是一个比较完整的示例:

namespace Application\Validations;

use Lib\Validations\AbstractValidation;

/**
 * 利用正则校验参数是否符合规则
 * @Annotation
 * @Target({"METHOD"})
 */
class MyValidation extends AbstractValidation
{
  /**
   * 出错时的自定义消息
   * @var array
   */
  public $error = null;

  /**
   * 校验用的正则表达式
   * @var string
   */
  public $rule;

  public function check(array $params)
  {
    if (key_exists($this->value, $params)) { // 判断要校验的参数在给出的参数中是否存在,存在才需要校验
      if (preg_match('/' . $this->rule . '/', $params[$this->value])) { // 用给定的正则进行匹配,成功返回 true
        return true;
      } else { // 失败对 $this->err 进行设置,并返回 false
        $code = (key_exists('code', $this->error)) ? $this->error['code'] : 10086;
        $message = (key_exists('message', $this->error)) ? $this->error['message'] : '参数格式不符合要求';
        $this->err = [
          'code' => $code,
          'message' => $message
        ];
        return false;
      }
    } else { // 要校验的参数不存在,无需校验直接返回 true
      return true;
    }
  }
}
use Application\Api\Get\Test;

use Lib\Core\BaseApiInterface;
use Lib\Core\ErrorCodeTrait;

class Index implements BaseApiInterface
{
  use ErrorCodeTrait;

  /**
   * ......
   * @MyValidation("ip", rule="^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", error={"code":"10090","message":"不是合法的IP地址"})
   */
  public function run($params)
  {
    ......
  }
}

不建议将自定义校验类放在Lib\Validations命名空间,在升级框架时可能会受影响。

可以将自定义校验类放在框架可识别的任意命名空间中,并在配置文件中使用validation_namespaces对其进行标识。上面的示例就是将校验类放在Application\Validations命名空间中,其在配置文件中的定义如下:

return [
  ...
  'validation_namespaces' => [
    'Application\Validations',
  ],
  ...
];