lark/framework

0.27.0 2024-04-09 15:10 UTC

README

Lark 是一个专为开发 REST API 设计的现代、轻量级应用框架。

安装

要求

  • PHP 8
  • PHP 扩展
    • 必需
    • [...]
    • 可选
    • curl - 如果使用 Lark\Http\Client

Composer 安装

composer require lark/framework

路由

路由器用于分发路由操作和中间件。

// bootstrap
// ...

// define routes
router()
    // get([route], [action])
    ->get('/', function() {});

// run app
app()->run();

路由

有多种定义路由的方式。

// routes for HTTP specific methods:
router()->delete('/route', function(){});
router()->get('/route', function(){});
router()->head('/route', function(){});
router()->options('/route', function(){});
router()->patch('/route', function(){});
router()->post('/route', function(){});
router()->put('/route', function(){});

// route for all HTTP methods
router()->all('/route', function(){});

// route for multiple HTTP methods
router()->route(['GET', 'POST'], '/route', function(){});

// a wildcard route "*" can be used to match any route
router()->get('*', function(){}); // all HTTP GET methods
router()->all('*', function(){}); // all HTTP methods (all requests)
router()->route(['GET', 'POST'], '*', function(){}); // all HTTP GET and POST methods

正则表达式路由

正则表达式路由使用 PCRE 模式进行路由匹配。

// match all routes that begin with "/api"
router()->get('/api.*?', function(){});

路由分组

可以使用路由分组来简化定义相似的路由。

router()
    ->group('/api/users') // group([base-route])
    ->get('/', function(){}) // "/api/users"
    ->get('/active', function(){}); // "/api/users/active"

路由分组加载

路由分组可以定义在 路由文件 中,这些文件在路由期间被加载(惰性加载路由)。

// bootstrap routes directory
// ...

router()->load([
    // [base-route] => [file]
    '/api/users' => 'users'
]);

// in routes directory file "users.php" defines routes
// the group('/api/users') method does not need to be called (handled by load() method)
router()
    ->get('/', function(){}) // "/api/users"
    ->get('/active', function(){}); // "/api/users/active"

在路由文件内部,router() 只应调用一次,以避免错误的路由未匹配错误。

// incorrect:
router()->bind(function(){});
router()->get('/', function(){});
// correct:
router()
    ->bind(function(){})
    ->get('/', function(){});

路由控制器

可以使用路由控制器对象与 路由分组路由分组加载

class MyController implements Lark\Router\RouteControllerInterface
{
    public function bind(Router $router): void
    {
        $router->get('/users', function(){}); // "/api/users"
    }
}

// in routes file
router()
    ->group('/api')
    ->controller(new MyController);

路由操作

路由操作是在匹配到路由时执行的。路由操作可以是可调用的函数(Closure)或包含 [class, method] 的数组。第一个匹配的路由是唯一将被执行的路由操作。

// function will be called on route match
router()->get('/example-html', function(): string {
    return 'hello'; // return string to output as html
});

router()->get('/example-json', function(): array {
    return ['message' => 'hello']; // return array|stdClass to output as JSON
    // will auto add header "Content-Type: application/json"
    // and response body will be:
    // {"message": "hello"}
});

// class method "App\Controller\ExampleController::hello()" will be called on route match
router()->get('/example2', [App\Controller\ExampleController::class, 'hello']);

路由未找到操作

如果没有找到匹配的路由,可以定义一个未找到的操作。HTTP 响应状态码自动设置为 404

router()->notFound(function(string $requestMethod, string $requestPath){});

如果没有定义未找到操作,将抛出 Lark\Router\NotFoundException

路由参数

命名参数

路由命名参数是必需的参数,不使用正则表达式。允许多个命名参数。

router()->get('/users/{id}', function($id){});

可选命名参数

路由可选命名参数是不使用正则表达式的可选参数。可选命名参数只能用于路由的末尾。允许多个可选命名参数。

router()->get('/users/{id}/{groupId?}', function($id, $groupId = null){});

在这个例子中,groupId 参数是可选的,因此路由 /users/5/users/5/10 都会匹配。

正则表达式参数

可以使用 PCRE 模式定义参数。允许多个正则表达式参数。

// match digits
router()->get('/users/(\d+)', function(int $id){});
// or match alphanumeric with length of 8
router()->get('/users/([a-z0-9]{8})', function(string $id) {});

中间件

中间件是在调用路由操作之前执行的单个或多个操作。中间件操作可以始终执行或仅在路由匹配时执行。中间件必须在定义路由之前定义。中间件操作的结构与 路由操作 相同。将 Lark\Request $reqLark\Response $res 参数传递给所有中间件操作。

// executed always
router()->bind(function(Lark\Request $req, Lark\Response $res){});
// executed if any route is matched
router()->matched(function(Lark\Request $req, Lark\Response $res){});

// define routes
// ...

可以设置多个中间件操作。

// single action
router()->bind(function(){});
// multiple actions
router()->bind(function(){}, [MyClass::class, 'myMethod']);
// array of actions
router()->bind([
    function(){},
    function(){}
]);

路由中间件

仅当路由匹配时,才会执行特定的路由中间件操作。

// method([methods], [route], [...actions])
router()->map(['GET'], '/api.*?', function(){});

router()->get('/api/users', function(){});

如果 HTTP 请求是 /api/users,则中间件操作和路由操作都会执行。

中间件执行顺序

中间件始终按照以下顺序执行

  1. 始终执行(《router()->bind(...)》)
  2. 在匹配的路由上执行映射(《router()->map(...)》)
  3. 在匹配的路由上执行(《router()->matched(...)》)
  4. 中间件之后(《router()->after(...)》)

路由组中间件

可以将中间件定义为仅在特定路由组中使用。仅当匹配组路由时,才会执行路由组中间件操作。

router()
    // group([base-route], [...actions])
    ->group('/api/users', function(){})
    ->get('/', function(){}) // "/api/users"
    ->get('/{id}', function($id){}) // "/api/users/{id}"

中间件之后

即使路由不存在,中间件之后也会在调用路由操作之后始终运行。

router()->after(function(){}, [MyClass::class, 'myMethod']);

日志记录

Lark\Logger 用于日志记录。可用的辅助函数为 logger()

logger('channel')->critical('message', [context]);
logger('channel')->debug('message', [context]);
logger('channel')->error('message', [context]);
logger('channel')->info('message', [context]);
logger('channel')->warning('message', [context]);

日志记录信息级别记录示例。

// bootstrap log handler
app()->logHandler = new App\LogHandler;
Lark\Logger::handler(app()->logHandler);

// ...

// log info level record
logger('user')->info('User has been authorized', ['userId' => $user->id]);

// ...

// output log example
print_r( app()->logHandler->close() );

可以将全局上下文添加到所有日志记录中发送的上下文中。

Lark\Logger::globalContext(['sessionId' => $session->id]);
// ...
logger('user')->info('User has signed out', ['userId' => $user->id]);
// context is: ['sessionId' => x, 'userId' => y]

异常处理

可以使用异常处理器来处理异常。

// bootstrap
// ...

// define routes
// ...

try
{
    // run app
    app()->run();
}
catch (Throwable $th)
{
    new App\ExceptionHandler($th);
}

示例 App\ExceptionHandler 类。

namespace App;
use Throwable;
class ExceptionHandler
{
    public function __construct(Throwable $th)
    {
        \Lark\Exception::handle($th, function (array $info) use ($th)
        {
            $code = $th->getCode();
            if (!$code)
            {
                $code = 500;
            }

            // log error
            // ...

            // respond with error
            res()
                ->code($code)
                ->json($info);

            // --or-- continue to throw exception
            throw $th;
        });
    }
}

调试器

Lark\Debugger 可用于调试。可用的辅助函数有 debug()x()

use Lark\Debugger;

// append debugger info
Debugger::append(['some' => 'info'])
    ->name('Test info') // this will be displayed as title (optional)
    ->group('test'); // this will group info together (optional)

Debugger::append(['more' => 'info'])
    ->name('More test info')
    ->group('test');

Debugger::dump(); // dump all debugger info and exit
// or use:
// x(); // dump all debugger info and exit

配置与绑定

可以使用 use() 方法设置框架配置和绑定。

调试

启用 Lark 内部附加调试信息以进行调试转储。

app()->use('debug.dump', true);

启用 Lark 内部调试日志。

app()->use('debug.log', true);

数据库连接

使用语法 db.connection.[connectionId] 注册数据库连接,并使用语法 [connectionId]$[database]$[collection] 访问。

// setup default MongoDB database connection with connectionId "default"
// the first registered connection is always the default connection
// regardless of connectionId
app()->use('db.connection.default', [
    'hosts' => ['127.0.0.1'],
    'username' => 'test',
    'password' => 'secret',
    'replicaSet' => 'rsNameHere', // (optional)
    // options can override any global database options
    // (optional, see "Database Global Options" below)
    'options' => []
]);

// register second connection with connectionId "myconn"
app()->use('db.connection.myconn', [...]);

// ...

// use default connection (no connectionId required):
$db = db('dbName$collectionName');
// or: $db = db('dbName', 'collectionName');

// use non-default connection (connectionId required):
$db2 = db('myconn$dbName$collectionName');
// or: $db = db('myConn2', 'dbName', 'collectionName');

有关更多信息,请参阅 数据库 和辅助函数 db()

数据库全局选项

可以使用 db.options 设置数据库全局选项。下面列出了所有默认选项值。

use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\WriteConcern;

app()->use('db.options', [
    'db.allow' => [], // allow access to only specific databases
    'db.deny' => ['admin', 'config', 'local'], // restrict access to databases
    'debug.dump' => false, // will include all database calls/context in debugger dumper
    'debug.log' => false, // add debug level database messages to logger
    'find.limit' => 1_000, // find "limit" for find options
    'read.concern' => new ReadConcern, // MongoDB read concern
    'write.concern' => new WriteConcern(WriteConcern::MAJORITY) // MongoDB write concern
]);

有关写入关注点的更多信息,请参阅 MongoDB 文档PHP 文档

数据库会话

可以使用 Lark\Model 对象将会话存储在数据库中。

app()->use('db.session', new App\Model\Session);

验证器自定义规则

可以使用 validator.rule.[type].[ruleClassName] 注册自定义验证器规则。

app()->use('validator.rule.string.beginWithEndWith', App\Validator\BeginWithEndWith::class);

环境变量与配置

Lark\Env 用于应用程序环境变量和配置。可用的辅助函数为 env()

示例读取 PATH 环境变量。

$path = env('PATH');

// or use default value "/my/path" if environment variable does not exist
$path2 = env('PATH2', '/my/path');

// for required environment vars do not use a default value argument
// which will throw exception if the environment var does not exist
$path2 = env('PATH2');
// Lark\Exception exception thrown: Invalid env variable key "PATH2"

示例 .env 文件。

DB_USER=myuser
DB_PWD=secret

示例 .env 文件使用。

// load from file (bootstrap)
Lark\Env::getInstance()->load(DIR_ROOT . '/.env');

$dbUser = env('DB_USER'); // myuser
$dbPassword = env('DB_PWD'); // secret

其他 Lark\Env 方法:fromArray(array $array)has(string $key): booltoArray(): array

请求

Lark\Request 提供带有输入清理的 HTTP 请求数据。可用的辅助函数是 req()

// example request:
// POST /example
// Content-Type: application/json
// {"name": "Test", "contact": {"email": "test@example.com"}}
$data = req()->json(); // get all as object/array (no auto sanitizing)

// request JSON must be an array or 400 response is sent
$data = req()->jsonArray();
// request JSON must be an object or 400 response is sent
$data = req()->jsonObject();

如果 JSON 方法不存在 HTTP 头 Content-Type: application/json,将发送自动响应,HTTP 状态码为 400,JSON 主体为 {"message": "Invalid JSON: [reason]"}

也可以通过清理访问单个 JSON 字段。

// get individual field
$name = req()->jsonField('name')->string();
if(req()->jsonField('contact.email')->has())
{
    $email = req()->jsonField('contact.email')->email();
}

示例 POST 请求(Content-Type: application/x-www-form-urlencoded)。

if(req()->isMethod('POST'))
{
    $name = req()->input('name')->string();
    if(req()->input('email')->has())
    {
        $email = req()->input('email')->email();
    }
}

示例 GET 请求。

// request "/?id=5&name=Shay"
print_r([
    'id' => req()->query('id')->integer(),
    // use "default" as value if query "name" does not exist
    'name' => req()->query('name', 'default')->string()
]); // Array ( [id] => 5 [name] => Shay )

请求 Cookie 示例。

if(req()->cookie('myCookie')->has())
{
    var_dump( req()->cookie('myCookie')->string() );
}

请求会话

Lark\Request\Session 用于管理会话。

app()->session->set('user.id', 5); // creates session data: [user => [id => 5]]
// ...
if(app()->session->has('user.id'))
{
    $userId = app()->session->get('user.id');
}

可以使用 Lark\Database\Session::handler() 将会话存储在数据库中。

Lark\Request\SessionFlash 可用于存储短期数据,数据在通过以下请求设置时可用,例如

app()->session()->flash()->set('userError', 'Invalid session');
// redirect, then use message
echo app()->session()->flash()->get('userError');
// message is no longer available on next request

请求方法

  • body(bool $convertHtmlEntities = true): string - 获取原始请求体数据
  • contentType(): string - 获取内容类型
  • cookie(string $key, $default = null): Lark\Request\Cookie - 获取 Cookie 输入对象
  • hasHeader(string $key): bool - 检查是否存在头键
  • header(string $key): string - 获取头值
  • headers(): array - 获取所有头
  • host(): string - 获取 HTTP 主机值,如 www.example.com
  • input(string $key, $default = null): Lark\Request\Input - 获取 POST 的输入对象
  • ipAddress(): string - 获取 IP 地址
  • isContentType(string $contentType): bool - 验证请求内容类型
  • isMethod(string $method): bool - 验证请求方法
  • isSecure(): bool - 检查请求是否安全(HTTPS)
  • json(): - 获取 JSON 请求体
  • jsonArray(): array - 获取 JSON 请求体,必须是数组或返回 HTTP 状态码 400 的响应
  • jsonField(string $field, $default = null): Lark\Request\Json - 获取 JSON 请求字段对象
  • jsonObject(): array - 获取 JSON 请求体,必须是对象或返回 HTTP 状态码 400 的响应
  • method(): string - 获取请求方法
  • path(): string - 获取路径,如 /the/path
  • pathWithQueryString(): string - 获取带查询字符串的路径,如 /the/path?x=1
  • port(): int - 获取端口号
  • query(string $key, $default = null): Lark\Request\Query - 获取 GET 的查询输入对象
  • queryString(): string - 获取查询字符串,如 x=1&y=2
  • scheme(): string - 获取 URI 方案,如 http
  • session(): Lark\Request\Session - 获取会话对象
  • uri(): string - 获取 URI,如 http://example.com/example?key=x

请求输入方法

输入方法包括请求输入对象的方法:CookieInputQuery

  • email(array $options = []) - 值获取器,作为电子邮件进行清理
  • float(array $options = ['flags' => FILTER_FLAG_ALLOW_FRACTION]) - 值获取器,作为浮点数进行清理
  • has(): bool - 检查是否存在键
  • integer(array $options = []) - 值获取器,作为整数进行清理
  • string(array $options = []) - 值获取器,作为字符串进行清理
  • url(array $options = []) - 值获取器,作为 URL 进行清理

会话方法

会话方法 clear()get()has()set() 都使用点符号表示法进行键,例如:set('user.isActive', 1) 等同于:[user => [isActive => 1]]

  • clear(string $key) - 清除键
  • static cookieOptions(array $options) - 设置cookie选项
    • 默认选项为:['lifetime' => 0, 'path' => '/', 'domain' => '', 'secure' => false, 'httponly' => false]
  • destroy() - 销毁会话
  • static exists(): bool - 检查会话是否启用并且会话存在
  • get(string $key) - 值获取器
  • has(string $key): bool - 检查键是否存在
  • id(): ?string - 会话ID获取器
  • isSession(): bool - 检查会话是否存在
  • set(string $key, $value) - 键/值设置器
  • toArray(): array - 会话数组获取器

响应

Lark\Response 用于控制HTTP响应。

// set header, status code 200, content-type and send JSON response
res()
    ->header('X-Test', 'value')
    ->code(Lark\Response::HTTP_OK)
    ->contentType('application/json') // not required when using json()
    ->json(['ok' => true]);
// {"ok": true}

响应方法

  • cacheOff(): Lark\Response - 使用缓存控制禁用缓存
  • contentType(string $contentType): Lark\Response - 设置content-type
  • cookie($key, $value, $expires, $path, $domain, $secure, $httpOnly): bool - 设置cookie
  • cookieClear(string $key, string $path = '/'): bool - 删除cookie
  • header(string $key, $value): Lark\Response - 设置header
  • headerClear(string $key): Lark\Response - 删除header键
  • headers(array $headers): Lark\Response - 使用数组设置headers
  • json($data) - 以JSON有效载荷响应(headers中的content-type设置为application/json
  • redirect(string $location, bool $statusCode301 = false) - 发送重定向
  • send($data) - 以原始数据有效载荷响应
  • code(int $code): Lark\Response - 设置响应状态码

数据库

Lark\Database 用于访问MongoDB数据库和集合实例。提供辅助函数 db()

// bootstrap
// setup default MongoDB database connection with connectionId "default"
app()->use('db.connection.default', [...]);

// register second connection with connectionId "myconn"
app()->use('db.connection.myconn', [...]);

// ...

// get Database object instance
$db = db('myDb$users');

插入文档

// insert documents
$docIds = $db->insert([
    ['name' => 'Test', 'role' => 'admin'],
    ['name' => 'Test2', 'role' => 'admin']
]);
// Array ( [0] => 62ba4fd034faaf6fc132ef54 [1] => 62ba4fd034faaf6fc132ef55 )

// insert single document
$docId = $db->insertOne(['name' => 'Test3', 'role' => 'readonly']);

查找文档

// find documents
$docs = $db->find(['role' => 'admin']);
// Array ( [0] => Array ( [id] => 62ba4fd034faaf6fc132ef54 [name] => Test [role] => admin )
// [1] => Array ( [id] => 62ba4fd034faaf6fc132ef55 [name] => Test2 [role] => admin ) )

// find documents with "name" staring with "Test"
$docs = $db->find(['name' => ['$regex' => '^Test']]);

// find documents by IDs
$docs = $db->findIds(['62ba4fd034faaf6fc132ef54', '62ba4fd034faaf6fc132ef55']);

// find single document
$doc = $db->findOne(['name' => 'Test2']);

// find single document by ID
$doc = $db->findId('62ba4fd034faaf6fc132ef54');

更新文档

// update documents
$affected = $db->update(['role' => 'admin'], ['role' => 'admin2']);

// update bulk
$docIds = $db->updateBulk([
    ['id' => '62ba4fd034faaf6fc132ef55', 'role' => 'admin'],
    [...]
]);
// Array ( [0] => 62ba4fd034faaf6fc132ef55 [1] => ... )

// update single document by ID
$newDoc = $db->updateId('62ba4fd034faaf6fc132ef55', ['role' => 'admin2']);

// update single document
$newDoc = $db->updateOne(['name' => 'Test2'], ['role' => 'admin']);

默认情况下,更新方法使用$set运算符进行更新,例如['$set' => ['role' => 'admin']]。此运算符可以更改,例如

// increment visits by 1
$newDoc = $db->updateOne(['name' => 'Test2'], ['visits' => 1], operator: '$inc');

替换文档

// replace bulk
$docIds = $db->replaceBulk([
    ['id' => '62ba4fd034faaf6fc132ef55', 'name' => 'Test222'],
    [...]
]);
// Array ( [0] => 62ba4fd034faaf6fc132ef55 [1] => ... )

// replace single document by ID
$newDoc = $db->replaceId('62ba4fd034faaf6fc132ef55',
    ['name' => 'Test2222', 'role' => 'admin']);

// replace single document
$newDoc = $db->replaceOne(['name' => 'Test2222'], ['name' => 'Test2', 'role' => 'admin']);

删除文档

// delete documents (note: filter cannot be empty)
$affected = $db->delete(['role' => 'admin']);

// delete documents by IDs
$affected = $db->deleteIds(['62ba4fd034faaf6fc132ef54', '62ba4fd034faaf6fc132ef55']);

// delete single document
$affected = $db->deleteOne(['name' => 'Test2']);

// delete all documents in collection
$affected = $db->deleteAll();

集合字段方法

// create a new field
// set default value to empty array
$affected = $db->collectionField('tags')->create([]);

// delete a field
$affected = $db->collectionField('tags')->delete();

// check if a field exists on all documents
$exists = $db->collectionField('tags')->exists();

// check if a field exists on any document
$exists = $db->collectionField('tags')->exists(false);

// remove value "mytag" from field "tags" array
$affected = $db->collectionField('tags')->pull(
    ['id' => '62ba4fd034faaf6fc132ef54'],
    'mytag'
);

// append values "mytag1" and "mytag2" to field "tags" array
// these values will only be appended if they
// don't already exists in the array
// use $unique=false to always append
$affected = $db->collectionField('tags')->push(
    ['id' => '62ba4fd034faaf6fc132ef54'],
    ['mytag1', 'mytag2']
);

// rename a field
$affected = $db->collectionField('tags')->rename('tagsNew');

使用点表示法表示嵌套字段名称,如field1.field2

数据库方法

  • collectionField(string $field): Database\Field - 集合字段对象获取器
  • count(array $filter = [], array $options = []): int - 计算匹配过滤器条件的文档数
  • delete(array $filter, array $options = []): int - 删除匹配过滤器的文档
  • deleteAll(array $options = []): int - 删除所有文档
  • deleteIds(array $ids, array $options = []): int - 根据ID删除文档
  • deleteOne(array $filter, array $options = []): int - 删除匹配过滤器条件的单个文档
  • drop(): bool - 删除集合
  • exists(): bool - 检查集合是否存在
  • find(array $filter = [], array $options = []): array - 查找匹配过滤器的文档
  • findId($id, array $options = []): ?array - 通过ID查找文档
  • findIds(array $ids, array $options = []): array - 通过ID查找文档
  • findOne(array $filter = [], array $options = []): ?array - 查找匹配过滤器条件的单个文档
  • has(array $filter, array $options = []): bool - 检查是否存在匹配过滤器的文档
  • hasIds(array $ids, array $options = []): bool - 检查是否存在具有指定ID的文档
  • insert(array $documents, array $options = []): array - 插入文档
  • insertOne($document, array $options = []): ?string - 插入单个文档
  • ping(): bool - ping命令
  • replaceBulk(array $documents, array $options = []): int - 批量替换
  • replaceId($id, $document, array $options = []): int - 替换单个文档
  • replaceOne(array $filter, $document, array $options = []): int - 替换单个文档
  • update(array $filter, $update, array $options = []): int - 更新匹配过滤器条件的文档
  • updateBulk(array $documents, array $options = []): int - 批量更新
  • updateId($id, $update, array $options = []): int - 通过ID更新文档
  • updateOne(array $filter, $update, array $options = []): int - 更新单个匹配过滤器条件的文档

数据库字段方法

  • create($defaultValue = null): int - 创建带有默认值的字段
  • delete(): int - 删除集合字段
  • exists(bool $allDocs = true): bool - 检查字段是否存在或检查字段是否在任何文档中存在(如果 !$allDocs
  • pull(array $filter, $value): int - 从字段数组中移除值
  • push(array $filter, $value, $unique = true): int - 将值追加到字段数组,如果 $unique 为真,则只有当值不在字段数组中时才会追加
  • rename(string $newName): int - 重命名字段

模式

Lark\Schema 用于创建用于创建实体、实体验证和数据库集合创建的架构。

use Lark\Schema;
$schema = new Schema([
    // create an index when creating a database collection
    '$index' => [
        'name' => 1, 'age' => 1, '$name' => 'idxNameAge'
    ],
    // or create multiple indexes
    // '$indexes' => [
    //    ['username' => 1, '$name' => 'idxUsername', '$unique' => true],
    //    ['name' => 1, 'age' => 1, '$name' => 'idxNameAge']
    // ],

    // auto database projection (filter password by default)
    '$filter' => ['password' => 0],

    // schema fields
    'name' => ['string', 'notEmpty'],
    'username' => ['string', 'notEmpty'],
    'password' => ['string', 'notEmpty'],
    'age' => ['int', 'notEmpty'],
    'isAdmin' => ['bool', 'notNull', ['default' => false]]
]);

架构使用 验证类型与规则 进行字段定义。

$index$indexes 中的选项是任何以 $ 开头的字段,如 $unique,更多选项可以在 MongoDB 文档 中找到。

默认字段值也可以动态设置。对于嵌套字段,使用点表示法,如 field.nestedfield

$schema->default('isAdmin', false);

可以使用字段值回调。对于嵌套字段,使用点表示法,如 field.nestedfield

$schema->apply('name', function($name): string {
    return strtoupper($name);
});

字段架构导入

可以将架构文件导入为架构字段。首先,在架构文件中创建部分架构,例如:[DIR_SCHEMAS]/partials/users.info.php

<?php
return [
    'object',
    [
        'fields' => [
            'age' => 'int',
            'tags' => 'array'
        ]
    ]
];

然后,使用字段名和文件添加导入。

$schema = new Schema([
    '$import' => [
        // field => file (in schemas directory)
        'info' => 'partials/users.info'
    ],
    'name' => ['string', 'notEmpty'],
    // field for schema import (optional, does not need to be set here)
    'info' => null
]);

打印 $schema->toArray() 将输出

Array
(
    [name] => Array
        (
            [0] => string
            [1] => notEmpty
        )

    [info] => Array
        (
            [0] => object
            [1] => Array
                (
                    [fields] => Array
                        (
                            [age] => int
                            [tags] => array
                        )

                )

        )

)

嵌套字段(使用点表示法)也可以使用。

$schema = new Schema([
    '$import' => [
        // field => file (in schemas directory)
        'info.1.fields' => 'partials/users.info'
    ],
    'name' => ['string', 'notEmpty'],
    'info' => [
        'object',
        ['fields' => null]
    ]
]);

示例部分架构在:[DIR_SCHEMAS]/partials/users.info.php

<?php
return [
    'age' => 'int',
    'tags' => 'array'
];

模型

Lark\Model 是一个模型:一种简化数据库调用和创建/验证实体的方法。

namespace App\Model;
use App\Model;
use Lark\Schema;

class User extends Model
{
    const DBS = 'default$app$users';
    public static function &schema(): Schema
    {
        return parent::schema([
            'name' => ['string', 'notEmpty'],
            'age' => ['int', 'notEmpty'],
            'isAdmin' => ['bool', 'notNull', ['default' => false]]
        ]);
    }
}

App\Model\User 类可以用于创建实体和验证。

$user = (new App\Model\User)->make([
    'name' => 'Bob',
    'age' => 25
]);
var_dump($user);
// array(3) { ["name"]=> string(3) "Bob" ["age"]=> int(25) ["isAdmin"]=> bool(false) }

// or an array can be used
$user = (new App\Model\User)->makeArray([
    ['name' => 'Bob', 'age' => 25],
    ['name' => 'Jane', 'age' => 21]
]);

可以使用 $mode 参数来更改验证器模式,例如使用 replace+idupdate+id 要求文档ID。

// schema: ['id' => ['string', 'id'], 'name' => ['string', 'notEmpty']]
$user = (new App\Model\User)->make([
    'name' => 'Bob'
], 'update+id');
// throws Lark\Validator\ValidatorException:
// Validation failed: "User.id" must be a string

可以使用 $mode 参数来允许 updateupdate+id 与部分文档一起使用的缺失字段。

$user = (new App\Model\User)->make([
    'name' => 'Bob'
], 'update');
var_dump($user); // array(1) { ["name"]=> string(3) "Bob" }

可以使用 Model::db() 方法来访问模型数据库集合(必须设置 Model::DBS)。

// ...
class Model extends Model
{
    const DBS = 'default$app$users';
    public function get(string $id): ?array
    {
        return $this->db()->findId($id);
    }
}

// get user document
$user = (new App\Model\User)->get('62ba4fd034faaf6fc132ef55');

// external calls: get documents
$docs = (new \App\Model\User)->db()->find(['role' => 'admin']);

重要:模型类不应在其 Model::__construct() 方法中包含任何必需参数,因为模型在使用模型/数据库绑定时会自动实例化,并且任何必需参数都不会存在。

模型架构方法

可以使用多种方式使用 Model::schema() 方法。默认情况下,该方法将使用 Model::SCHEMA 架构文件常量从文件中加载架构。

创建架构的另一种方法是覆盖父方法并传递架构数组。

class ExampleModel extends Model
{
    public static function &schema(): Schema
    {
        return parent::schema([
            'id' => ['string', 'id'],
            // ...
        ]);
    }
}

上述方法缓存架构对象,因此当再次调用架构方法时,它将返回引用的 Schema 对象。

还可以传递回调来访问由父方法创建的 Schema 对象,例如

class ExampleModel extends Model
{
    const SCHEMA = 'users.php';
    public static function &schema(): Schema
    {
        return parent::schema(function(Schema &$schema)
        {
            $schema->apply('name', function($name)
            {
                return strtoupper($name);
            });
        });
    }
}

模型数据库查询

可以使用模型 Lark\Database\Query 类来使用查询参数。

use Lark\Database\Query;
use App\Model\User;

$query = new Query(new User, [
    'name' => 'test'
]);

// Database::find()
$results = $query->find();

// Database::count()
$count = $query->count();

查询选择器

查询选择器可以用作查询参数。匹配字段与字段值

$query = [
    'name' => 'test'
];

MongoDB 比较选择器 $eq$gt$gte$in$lt$lte$ne$nin 可以使用如下

$query = [
    'age' => ['$gte' => 18]
];

使用 $in 选择器

$query = [
    'name' => ['$in' => ['test', 'test2', 'test3']]
];

使用多个选择器

$query = [
    'age' => ['$gt' => 20, '$lt' => 100]
];

查询选项

默认情况下,使用多个选择器的查询将执行逻辑AND操作。可以使用$or选项来使用逻辑OR操作。

$query = [
    // age is greater than 20 OR less than 100
    'age' => ['$gt' => 20, '$lt' => 100],
    '$or' => true
];

可以使用$filter(或$projection)选项来过滤从数据库返回的文档字段。

$query = [
    // only include fields "id" and "name" for each document
    '$filter' => ['id' => 1, 'name' => 1],
    'name' => 'test',
    'age' => ['$gte' => 18]
];

// or fields can be excluded for each document
$query = [
    // exclude fields "age" and "info" for each document
    '$filter' => ['age' => 0, 'info' => 0]
];

可以使用$page选项进行分页。

// fetch first page
$query = [
    '$page' => 1
];

// fetch second page
$query = [
    '$page' => 2
];

默认情况下,每页文档的限制由数据库选项find.limit确定。

$page选项的默认文档排序顺序为["id" => 1],这可以使用$sort选项进行覆盖。

可以使用$limit选项来设置返回的文档数量,或在使用$page选项时覆盖默认的每页文档数量。

$query = [
    '$limit' => 100
];

$limit选项的值不能超过数据库选项find.limit的值。

可以使用$sort选项来设置文档的排序顺序。

// sort by "name" ASC and "age" DESC
$query = [
    '$sort' => ['name' => 1, 'age' => -1]
];

可以使用$skip选项来设置查询跳过的值。

$query = [
    '$skip' => 10
];

当与$page选项一起使用时,$skip选项将始终被覆盖。

自动创建和更新字段值

创建和更新的字段值可以用于自动设置带有创建和更新日期/时间的字段。示例模式

[
    '$created' => 'createdAt',
    '$updated' => 'updatedAt',
    'name' => ['string', 'notNull'],
    'createdAt' => ['timestamp', 'notNull'],
    'updatedAt' => ['timestamp', 'notNull']
]

现在createdAtupdatedAt字段将自动设置为当前时间戳(time())。默认情况下,值可以设置为timestamp,也可以设置为datetime用于DateTimedbdatetime用于MongoDB\BSON\UTCDateTime,示例

[
    '$created' => [
        'createdAt' => 'dbdatetime'
    ],
    // ...
]

在上面的示例中,createdAt字段将只设置一次(使用模式默认值),而updatedAt字段将在每次创建文档时设置。

数据库模型模式约束

数据库模型模式约束可以用作数据库约束,例如验证外键和通过引用删除文档。

引用外键约束

$refs.fk约束验证外键,可以在任何模型模式中设置,并与Database方法:insert()insertOne()replaceBulk()replaceId()replaceOne()update()updateBulk()updateId()updateOne()一起使用。

class UserLog extends Model
{
    const DBS = 'default$app$users.log';
    public static function &schema(): Schema
    {
        return parent::schema([
            '$refs' => [
                'fk' => [
                    // collection => [localField => foreignField, ...]
                    'users' => ['userId' => 'id']
                ]
            ],
            'id' => ['string', 'id'],
            'userId' => ['string', 'notEmpty'],
            'message' => ['string', 'notEmpty']
        ]);
    }
}

users.log中的示例文档

{
    "id": "abc",
    "userId": "62ba4fd034faaf6fc132ef54",
    "message": "test"
}

现在当调用模型数据库的插入/替换/更新方法时,上述$refs.fk约束将验证集合users.log的字段userId值是否存在为users集合字段id_id)的外键。

如果外键约束验证失败,将抛出带有类似消息的Lark\Database\Constraint\DatabaseConstraintException异常:“插入或更新文档失败,外键约束“userId”失败”。

$refs.fk外键字段(foreignField)必须始终是MongoDB ObjectId,任何其他类型的外键验证都将失败。

$refs.fk约束将始终验证外键,即使本地字段值为null,但可以通过在本地字段名前使用nullable$前缀来禁用此功能,例如nullable$userId,这意味着所有本地字段null值将不会进行外键验证。

$refs.fk约束还可以用于数组中的外键数组

// class UserGroup (model)
$schema = new Schema([
    '$refs' => [
        'fk' => [
            // collection => [localField => foreignField, ...]
            'users' => ['users.$' => 'id']
        ]
    ],
    // ...
]);

users.groups中的示例文档

{
    "id": "abc",
    "name": "group name",
    "users": ["62ba4fd034faaf6fc132ef54", "62ba4fd034faaf6fc132ef55"]
}

现在当调用模型数据库的插入/替换/更新方法时,上述$refs.fk约束将验证集合users.groups字段users数组中的每个值是否存在为users集合字段id_id)的外键。

$refs.fk约束还可以用于数组中的具有外键的对象

// class UserAllowed (model)
$schema = new Schema([
    '$refs' => [
        'fk' => [
            // collection => [localField => foreignField, ...]
            'users' => ['users.$.id' => 'id']
        ]
    ],
    // ...
]);

users.allowed中的示例文档

{
    "id": "abc",
    "role": "admin"
    "users": [
        {"id": "62ba4fd034faaf6fc132ef54", "name": "test"}
        {"id": "62ba4fd034faaf6fc132ef55", "name": "test2"}
    ]
}

现在当调用模型数据库的插入/替换/更新方法时,上述的$refs.fk约束将验证集合users.allowed字段的users数组,以确保每个对象字段的id值存在于users集合的id(_id)字段中。

$refs.fk约束可以与多个集合和字段一起使用。

$schema = new Schema([
    '$refs' => [
        'fk' => [
            // collection => [localField => foreignField, ...]
            'users' => [
                'userId' => 'id',
                'users.$' => 'id',
                'usersAllowed.$.id' => 'id'
            ]
        ]
    ],
    // ...
]);

$refs.fk约束也可以与同一模型一起使用。

// class User (model)
$schema = new Schema([
    '$refs' => [
        'fk' => [
            // allow managerId to be null (no manager)
            // verify FK users.id exists when users.managerId exists
            'users' => ['nullable$managerId' => 'id']
        ]
    ],
    'id' => ['string', 'id'],
    'managerId' => 'string'
]);

清除约束

$refs.clear约束允许清除字段值,可以在任何模型模式中设置,并与Database::deleteIds()方法一起使用。

class User extends Model
{
    const DBS = 'default$app$users';
    public static function &schema(): Schema
    {
        return parent::schema([
            '$refs' => [
                'clear' => [
                    // collection => [foreign fields]
                    'users.log' => ['userId']
                ]
            ],
            'id' => ['string', 'id'],
            'name' => ['string', 'notEmpty']
        ]);
    }
}

users.log中的示例文档

{
    "id": "abc",
    "userId": "62ba4fd034faaf6fc132ef54",
    "message": "test"
}

现在当调用模型数据库方法deleteIds()时,上述的$refs.clear约束将触发数据库清除(更新操作),以清除users.log集合中所有具有userId: {$in: [ids]}的文档的userId字段。

MongoDB shell中的等效操作为

db.users.delete( { _id: { $in: [ids] } } )
db.users.log.updateMany( { userId: { $in: [ids] } }, { $set: { userId: null } } )

删除约束

$refs.delete约束允许删除文档,可以在任何模型模式中设置,并与Database::deleteIds()方法一起使用。

class User extends Model
{
    const DBS = 'default$app$users';
    public static function &schema(): Schema
    {
        return parent::schema([
            '$refs' => [
                'delete' => [
                    // collection => [foreign fields]
                    'users.log' => ['userId']
                ]
            ],
            'id' => ['string', 'id'],
            'name' => ['string', 'notEmpty']
        ]);
    }
}

users.log中的示例文档

{
    "id": "abc",
    "userId": "62ba4fd034faaf6fc132ef54",
    "message": "test"
}

现在当调用模型数据库方法deleteIds()时,上述的$refs.delete约束将触发数据库删除操作,删除users.log集合中所有具有userId: {$in: [ids]}的文档。

MongoDB shell中的等效操作为

db.users.delete( { _id: { $in: [ids] } } )
db.users.log.delete( { userId: { $in: [ids] } } )

$refs.delete约束也可以用于从数组中拉取($pullAll) ID。

$schema = new Schema([
    '$refs' => [
        'delete' => [
            // collection => [foreign fields]
            'users.groups' => ['users.$']
        ]
    ],
    // ...
]);

users.groups中的示例文档

{
    "id": "abc",
    "name": "group name",
    "users": ["62ba4fd034faaf6fc132ef54", "62ba4fd034faaf6fc132ef55"]
}

现在当调用模型数据库方法deleteIds()时,上述的$refs.delete约束将触发数据库更新操作,从集合users.groups字段的users中拉取所有ID。

MongoDB shell中的等效操作为

db.users.delete( { _id: { $in: [ids] } } )
db.users.groups.updateMany(
    { users: { $in: [ids] } },
    { $pullAll: { users: [ids] } },
    { multi:true }
)

注意:即使在一个文档的字段上从数组中拉取了多个值,MongoDB仍然会返回modifiedCount: 1

$refs.delete约束还可以用于根据对象字段值从数组中拉取($pull)对象。

$schema = new Schema([
    '$refs' => [
        'delete' => [
            // collection => [foreign fields]
            'users.allowed' => ['users.$.id']
        ]
    ],
    // ...
]);

users.allowed中的示例文档

{
    "id": "abc",
    "role": "admin"
    "users": [
        {"id": "62ba4fd034faaf6fc132ef54", "name": "test"}
        {"id": "62ba4fd034faaf6fc132ef55", "name": "test2"}
    ]
}

现在当调用模型数据库方法deleteIds()时,上述的$refs.delete约束将触发数据库更新操作,从集合users.groups字段的users中根据对象字段id值拉取所有对象。

MongoDB shell中的等效操作为

db.users.delete( { _id: { $in: [ids] } } )
db.users.allowed.updateMany(
    { users.id: { $in: [ids] } },
    { $pull: { users: { id: { $in: [ids] } } } },
    { multi:true }
)

注意:即使在一个文档的字段上从数组中拉取了多个对象,MongoDB仍然会返回modifiedCount: 1

$refs.delete约束可以与多个集合和字段一起使用。

$schema = new Schema([
    '$refs' => [
        'delete' => [
            // collection => [foreign fields]
            'users.log' => ['userId', 'userId2'],
            'users.groups' => ['users.$'],
            'users.allowed' => ['users.$.id']
        ]
    ],
    // ...
]);

验证器

Lark\Validator用于验证和创建实体。

use Lark\Validator;

$isValid = (new Validator([
    // data
    'name' => 'Bob',
    'age' => 25
], [
    // schema
    'name' => ['string', 'notEmpty'],
    'age' => ['int', 'notNull'],
    'phone' => null, // no type (any type allowed), optional
    'title' => 'string' // string, optional
]))->validate(); // true

可以在验证过程中使用断言。

(new Validator([
    'name' => null
], [
    'name' => ['string', 'notNull']
]))->assert();
// throws Lark\Validator\ValidatorException:
// Validation failed: "name" must be a string

使用验证创建实体。

// validation will pass because no field is required
var_dump(
    (new Validator([], [
        'name' => ['string'],
        'age' => ['int']
    ]))->make()
);
// array(2) { ["name"]=> NULL ["age"]=> NULL }

验证类型与规则

规则notNullnotEmpty,以及有时id,是不允许值null的所有类型的规则。规则voidable可以用于任何可能缺失的字段。

  • 任何类型(默认)- 允许任何类型
    • notNull - 值不能为null
  • array(或arr)- 值可以是arraynull
    • allowed - 数组值必须是允许的[allowed => [...]]
    • length - 数组项数必须是[length => x]
    • max - 数组值不能超过最大值[max => x]
    • min - 数组值不能低于最小值[min => x]
    • notEmpty - 必须是非空array
    • notNull - 必须是array
    • unique - 数组值必须是唯一的
  • boolean(或bool)- 必须是booleannull
    • notNull - 必须是boolean
  • datetime - 必须是DateTime实例或null
    • notNull - 必须是DateTime实例
  • dbdatetime - 必须是MongoDB\BSON\UTCDateTime实例或null
    • notNull - 必须是MongoDB\BSON\UTCDateTime实例
  • float - 必须是 floatnull
    • between - 必须在两个值之间 [between => [x, y]]
    • max - 必须是最大值 [max => x]
    • min - 必须是最小值 [min => x]
    • notEmpty - 必须是大于零的 float
    • notNull - 必须是 float
  • integer (或 int) - 必须是 integernull
    • between - 必须在两个值之间 [between => [x, y]]
    • id - 当设置 ENTITY_FLAG_ID 标志时,必须是 integer
    • max - 必须是最大值 [max => x]
    • min - 必须是最小值 [min => x]
    • notEmpty - 必须是大于零的 integer
    • notNull - 必须是 integer
  • number (或 num) - 必须是数字或 null
    • between - 必须在两个值之间 [between => [x, y]]
    • id - 当设置 ENTITY_FLAG_ID 标志时,必须是数字
    • max - 必须是最大值 [max => x]
    • min - 必须是最小值 [min => x]
    • notEmpty - 必须是大于零的数字
    • notNull - 必须是数字
  • object (或 obj) - 必须是 objectnull
    • notEmpty - 必须是非空的 object
    • notNull - 必须是 object
  • string (或 str) - 必须是 stringnull
    • allowed - 值必须是允许的 [allowed => [...]]
    • alnum - 必须只包含字母数字字符
    • 或,必须只包含字母数字字符和空格 [alnum => true]
    • alpha - 必须只包含字母字符
    • 或,必须只包含字母字符和空格 [alpha => true]
    • contains - 必须包含值 [contains => x]
    • 或,必须包含值(不区分大小写) [contains => [x, true]]
    • email - 必须是有效的电子邮件地址
    • hash - 哈希值必须相等(时间攻击安全) [hash => x]
    • id - 当设置 ENTITY_FLAG_ID 标志时,必须是 string
    • ipv4 - 必须是有效的 IPv4 地址
    • ipv6 - 必须是有效的 IPv6 地址
    • json - 必须是有效的 JSON
    • length - 长度必须是字符数 [length => x]
    • regex - 值必须是正则表达式匹配 [regex => x]
    • max - 长度必须是最大字符数 [max => x]
    • min - 长度必须是最小字符数 [min => x]
    • notAllowed - 值必须是允许的 [notAllowed => [...]]
    • notEmpty - 必须是非空的 string
    • notNull - 必须是 string
    • password - 密码必须匹配 [password => x]
    • url - 必须是有效的 URL
  • timestamp - 必须是时间戳或 null
    • notNull - 必须是时间戳

嵌套字段

可以使用 fields 属性定义嵌套字段。

$isValid = (new Validator([
    // data
    'name' => 'Bob',
    'contact' => [
        'email' => 'bob@example.com',
        'phone' => [
            'cell' => '555-5555',
            'office' => '555-6666'
        ]
    ]
], [
    // schema
    'name' => ['string', 'notEmpty'],
    'contact' => [
        'array',
        [
            'fields' => [
                'email' => ['string', 'email'],
                'phone' => [
                    'array',
                    [
                        'fields' => [
                            'cell' => 'string',
                            'office' => 'string'
                        ]
                    ]
                ]
            ]
        ]
    ]
]))->validate(); // true

嵌套模式

可以使用 schema:arrayschema:object 属性定义数组数组的嵌套模式或对象。

$isValid = (new Validator([
    'name' => 'test',
    'tags' => [
        // these must be arrays because "schema:array" is used
        // if "schema:object" is used these must be objects
        ['id' => '1', 'name' => 'test2'],
        ['id' => 2, 'name' => 'test3'],
    ]
], [
    'name' => ['string', 'notEmpty'],
    'tags' => [
        'array', 'notEmpty',
        [
            'schema:array' => [
                'id' => ['int', 'notNull'],
                'name' => 'string'
            ]
        ]
    ]
]))->assert();
// throws Lark\Validator\ValidatorException:
// Validation failed: "tags.0.id" must be an integer or null

在上面的示例中,如果 schema:arrayschema:object 属性之前没有使用 notEmpty 模式规则,并且数组或对象数组为空,则不会验证/断言任何规则。

不允许在嵌套模式对象或数组中包含部分文档。

断言回调

可以使用 assert() 方法与回调一起使用。

(new Validator([
    'name' => null
], [
    'name' => ['string', 'notNull']
]))->assert(function(string $field, string $message, string $name = null){
    // handle error
    //...

    // return true to halt
    // return false to continue to throw validation exception
    return true;
});

自定义验证规则

可以创建自定义验证规则。

// validator.rule.[type].[name]
app()->use('validator.rule.string.beginWithEndWith', App\Validator\BeginWithEndWith::class);

// App\Validator\MyRule class:
namespace App\Validator;
class BeginWithEndWith extends \Lark\Validator\Rule
{
    private string $beginWith;
    private string $endWith;

    protected string $message = 'must begin with value and end with value';

    public function __construct(string $beginWith, string $endWith)
    {
        $this->beginWith = $beginWith;
        $this->endWith = $endWith;
    }

    public function validate($value): bool
    {
        $beginsWith = substr($value, 0, strlen($this->beginWith));
        $endsWith = substr($value, -(strlen($this->endWith)));

        return $beginsWith === $this->beginWith && $endsWith === $this->endWith;
    }
}

// validation example
(new Validator([
    'alias' => '123testXYZ'
], [
    'alias' => ['string', ['beginWithEndWith' => ['123', 'XYZ']]]
]))->validate(); // true

也可以覆盖现有规则。

// validator.rule.[type].[name]
// overwrite existing string rule "email"
app()->use('validator.rule.string.email', 'App\\Validator\\Email');

// App\Validator\Email class:
namespace App\Validator;
class Email extends \Lark\Validator\TypeString\Email
{
    public function validate($value): bool
    {
        // must be valid email and domain "example.com"
        return parent::validate($value)
            && preg_match('/@example\.com$/i', $value) === 1;
    }
}

// validation example
(new Validator([
    'email' => 'test@example.com'
], [
    'email' => ['string', 'email']
]))->validate(); // true

过滤器

Lark\Filter 用于过滤值。

$cleanStr = filter()->string($str);

按数组键过滤。

$arr = ["one" => 1, "two" => 2, "three" => 3];

// exclude filter
print_r(
    filter()->keys($arr, ["two" => 0])
); // Array ( [one] => 1 [three] => 3 )

// include filter
print_r(
    filter()->keys($arr, ["one" => 1, "two" => 1])
); // Array ( [one] => 1 [two] => 2 )

过滤方法

  • email($value, array $options = []): string - 使用电子邮件过滤器净化值
  • float($value, array $options = ['flags' => FILTER_FLAG_ALLOW_FRACTION]): float - 使用浮点过滤器对值进行清理
  • integer($value, array $options = []): int - 使用整数过滤器对值进行清理
  • keys(array $array, array $filter): array - 根据包含或排除过滤器过滤键
  • string($value, array $options = ['flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH]): string - 使用字符串过滤器对值进行清理
  • url($value, array $options = []): string - 使用URL过滤器对值进行清理

HTTP 客户端

Lark\Http\Client 是一个HTTP客户端。

use Lark\Http\Client;
$client = new Client;
try
{
    $res = $client->get('http://example.com');
    $headers = $client->headers();
    $statusCode = $client->statusCode();

    if($statusCode === 200)
    {
        // ok
    }
    else
    {
        // handle
    }
}
catch (Lark\Http\HttpException $ex)
{
    // handle request/curl error
}

提供各种HTTP方法。

// DELETE request
$client->delete('http://example.com', ['field1' => 'value']);
// GET request
$client->get('http://example.com', ['param' => 1]); // http://example.com?param=1
// HEAD request
$client->head('http://example.com');
// OPTIONS request
$client->options('http://example.com');
// PATCH request
$client->patch('http://example.com', ['field1' => 'value']);
// POST request
$client->post('http://example.com', ['field1' => 'value']);
// PUT request
$client->put('http://example.com', ['field1' => 'value']);

字符串也可以用来发送JSON。

$client = new Client([
    'headers' => ['content-type' => 'application/json']
]);
// POST request with JSON string
$client->post('http://example.com', json_encode(['field1' => 'value']));

可以为所有方法设置选项(将覆盖默认选项)。

use Lark\Http\Client;
$client = new Client(['url' => 'http://example.com', 'timeout' => 8]);
$res = $client->get('/api/items'); // http://example.com/api/items
$res2 = $client->post('/api/items', ['name' => 'My Item']);

可以为单个方法设置选项(将覆盖所有方法的默认选项和选项)。

$res = $client->get('/api/items', ['timeout' => 5]);

可以为 curl 设置选项。

use Lark\Http\Client;
$client = new Client([
    'curl' => [
        CURLOPT_RESOLVE => ['test.loc:127.0.0.1']
    ]
]);

HTTP客户端选项

  • curl - 使用 CURLOPT_[...] 选项设置curl选项
  • headers - 设置HTTP头,可以使用两种方法设置
    • ['headers' => ['My-Header' => 'value']]
    • ['headers' => ['My-Header: value']]
  • port - 设置自定义端口号
  • proxy - 使用HTTP代理
  • redirects - 允许重定向
  • timeout - 连接和执行的秒数超时
  • url - 请求方法的基URL
  • verify - 验证对等证书和通用名称

命令行界面

Lark\Cli 用于创建命令行应用程序。

// bootstrap
// ...

$cli = Lark\Cli::getInstance();

// add command
$cli->command('files', 'Print files in directory')
    ->arg('dir', 'Read directory')
    ->action(function(string $dir) {
        // print files in directory $dir

        // optional, exit with any code by returning an int
        // return 1; // same as $cli->exit(1);
    });
    // or use class/method:
    // ->action([MyClass::class, 'methodName'])

// run app
$cli->run($_SERVER['argv']);

可以为命令设置参数和选项,每个参数和选项都有可选设置。

// set global option (separate from command options)
$cli->option('-d, --debug', 'Enable debug mode', function() {
    // enable here
});

$cli->command('files', 'Print files in directory')
    ->arg('dir', 'Read directory') // required by default
    // set another optional argument that can have multiple values (array)
    ->arg('subdirs', 'Read subdirectories', ['optional', 'array'])
    // add option for output file
    ->option('-o, --outputfile', 'Output to file')
    // option test
    ->option('-t, --test', 'Run test', ['optional'])
    // add command action
    ->action(function(string $dir, ?array $subdirs, ?string $outputfile, ?bool $isTest) {
        var_dump($dir, $subdirs, $outputfile, $isTest);
    });

// $ php ./app/cli.php files mydir subdir1 subdir2 --outputfile=/my/file -t
// string(5) "mydir"
// array(2) { [0] => string(7) "subdir1" [1] => string(7) "subdir2" }
// string(8) "/my/file"
// bool(true)

CLI Lark\Cli\Output 类用于输出和样式化输出。

$o = $cli->output();

// output green text
$o->colorGreen->echo('This is green text');
// use multiple styles
$o->colorBlue->styleUnderline->echo('More text');

// style methods for common styles
$o->error('Error'); // red background
$o->info('Info'); // blue text
$o->ok('Success'); // green text
$o->warn('Warning'); // yellow text
$o->dim('Muted'); // dim text

// custom style methods can be registered
$o::register('blink', function ($text, $end = PHP_EOL) use ($out) {
    $out->styleBlink;
    $out->echo($text, $end);
});
$o->bink('Blinking text'); // blinking text

// override existing style methods
$o::register('error', function ($text, $end = PHP_EOL) use ($out) {
    $out->colorRed; // text color red (instead of bg red)
    $out->stderr($text, $end); // send to stderr
});
$o->error('Oops'); // red text

可以使用 grid() 输出方法来均匀地间隔列。

$data = [
    [1, "one"],
    [2, "two"],
    [100, "one hundred"],
    [3, "three"],
];

$out->grid($data, ['indent' => 2]);

上面的例子将输出

  1      one
  2      two
  100    one hundred
  3      three

使用 confirm() 进行确认。

// "Continue? (y/N)"
if($cli->confirm("Continue?")) // ...
// or yes by default: "Continue? (Y/n)"
if($cli->confirm("Continue?", true)) // ...

使用 input() 进行输入。

// "Enter value [DEFAULT]:"
$val = $cli->input("Enter value:", "DEFAULT");
// if no value is entered the value would be "DEFAULT"

CLI方法

  • abort($status = 0) - 显示命令中止消息并退出应用程序
  • command(string $name, string $description, array $aliases = []): Command - 注册命令
  • confirm(string $question, bool $isDefaultYes = false) - 确认是/否
  • exit($status = 0) - 退出应用程序
  • header(callable $callback) - 注册用于 help() 方法的头部回调
  • help() - 显示帮助(由 Cli 自动调用)
  • input(string $text, $default = null) - 输入
  • option(string $option, string $description, callable $action) - 设置全局选项
  • output() - CLI Output 对象获取器
  • run() - 运行CLI应用程序

CLI命令方法

  • action($callbackOrClassArray): Command - 设置命令操作
  • arg(string $arg, string $description, array $options = []): Command - 设置参数
    • 选项
      • array - 具有多个值的参数(必须放在参数列表的最后一个位置)
      • default - 默认值,例如:['default' => 'the value']
      • optional - 参数是可选的
  • option(string $option, string $description = '', array $options = []): Command - 设置选项
    • 选项
      • default - 默认值,例如:['default' => 'the value']

CLI输出属性

  • bgBlack - 设置背景为黑色
  • bgBlue - 设置背景为蓝色
  • bgCyan - 设置背景为青色
  • bgGray - 设置背景为灰色
  • bgGreen - 设置背景为绿色
  • bgPurple - 设置背景为紫色
  • bgRed - 设置背景为红色
  • bgWhite - 设置背景为白色
  • bgYellow - 设置背景为黄色
  • bgLigthBlue - 设置背景为浅蓝色
  • bgLightCyan - 设置背景为浅青色
  • bgLightGray - 设置背景为浅灰色
  • bgLightGreen - 设置背景为浅绿色
  • bgLightPurple - 设置背景为浅紫色
  • bgLightRed - 设置背景为浅红色
  • bgLightYellow - 设置背景为浅黄色
  • colorBlack - 设置颜色为黑色
  • colorBlue - 设置颜色为蓝色
  • colorCyan - 设置颜色为青色
  • colorGray - 设置颜色为灰色
  • colorGreen - 设置颜色为绿色
  • colorPurple - 设置颜色为紫色
  • colorRed - 设置颜色为红色
  • colorWhite - 设置颜色为白色
  • colorYellow - 设置颜色为黄色
  • colorLightBlue - 设置颜色为浅蓝色
  • colorLightCyan - 设置颜色为浅青色
  • colorLightGray - 设置颜色为浅灰色
  • colorLightGreen - 设置颜色为浅绿色
  • colorLightPurple - 设置颜色为浅紫色
  • colorLightRed - 设置颜色为浅红色
  • colorLightYellow - 设置颜色为浅黄色
  • styleBlink - 设置闪烁效果
  • styleBold - 设置加粗效果
  • styleDim - 设置暗淡效果
  • styleHidden - 设置隐藏效果
  • styleInvert - 设置反转效果
  • styleUnderline - 设置下划线效果

CLI 输出方法

  • dim(string $text, string $end = PHP_EOL): Output - 打印暗淡风格文本
  • echo(string $text = '', string $end = PHP_EOL): Output - 将文本打印到 stdout
  • error(string $text, string $end = PHP_EOL): Output - 打印错误文本
  • grid(array $data, array $options = []): Output - 打印网格
    • 选项
      • indent - 缩进空格数
      • padding - 列填充(默认:4
      • style - 将样式应用于列,如 ['style' => ['name' => 'colorBlue']]
  • info(string $text, string $end = PHP_EOL): Output - 打印信息文本
  • ok(string $text, string $end = PHP_EOL): Output - 打印成功文本
  • warn(string $text, string $end = PHP_EOL): Output - 打印警告文本
  • static register(string $name, callable $callback) - 注册样式方法
  • stderr(string $text, string $end = PHP_EOL): Output - 输出到 stderr
  • stdout(string $text = '', string $end = PHP_EOL): Output - 输出到 stdout
  • styleIndent(int $number): Output - 缩进样式

文件

Lark\File 用于处理文件。

use Lark\File;

$file = new File('./my-file.txt');
if($file->write('contents'))
{
    // ...
}

$contents = $file->read();

Lark\Json\File 用于处理 JSON 文件。

use Lark\Exception as LarkException;
use Lark\Json\File as JsonFile;

$file = new JsonFile('./my-file.json');
$file->write(['name' => 'test']);

try
{
    $value = $file->read();
}
catch(LarkException $ex)
{
    // exception is throw on JSON decode error
    echo 'Failed to decode JSON file: ' . $ex->getMessage();
}

文件方法

  • delete(): bool - 删除文件
  • exists(): bool - 检查文件是否存在
  • existsOrException() - 如果文件不存在则抛出异常
  • path(): string - 获取文件路径
  • read() - 读取文件内容
  • write($data, $append = false, $lock = true): bool - 写入文件内容

计时器

Lark\Timer 作为计时器使用。

$timer = new Lark\Timer;

usleep(500000);
echo $timer->elapsed(); // 0.5001s

sleep(1);
echo $timer->elapsed(); // 1.5014s

sleep(2);
// get elapsed since last Timer::elapsed()
// or Timer::elapsedSinceLast() was invoked
echo $timer->elapsedSinceLast(); // 2.0003s

echo $timer->elapsed(); // 3.5018s

助手

助手是全局助手函数。

助手 app()

使用 app() 函数访问主 App 实例。

app()->use('[...]');

助手 db()

db() 函数是数据库集合实例助手。

// when using default connection ID
// "[database]$[collection]"
$db = db('app$users');
// or "database", "collection"
$db = db('app', 'users');

// when using non-default connection ID
// "[connectionId].[database].[collection]"
$db = db('myDb$app$users')
// or "connectionId", "database", "collection"
$db = db('myDb', 'app', 'users');

// when using a App\Model class with DBS (database string)
$db = db(App\Model\User::class);

更多请参阅 数据库连接

助手 dbdatetime()

dbdatetime() 函数返回一个 MongoDB\BSON\UTCDateTime 对象。

$dbDt = dbdatetime();
$dt = $dbDt->toDateTime(); // DateTime object

// with milliseconds
$dbDt = dbdatetime(strtotime('-1 day') * 1000);

助手 debug()

debug() 函数是调试和日志助手。当调用 debug() 函数时,将附加调试信息并发送到日志记录器 (Logger::debug())。

debug('test');
// same as:
// Debugger::append('test');
// (new Logger)->debug('test');

// title/name can be used:
debug('test', ['info' => 'here');
// same as:
// Debugger::append(['info' => 'here'])->name('test');
// (new Logger)->debug('test', ['info' => 'here']);

// title/name and group/channel can be used:
debug('test', ['info' => 'here'], 'name');
// same as:
// Debugger::append(['info' => 'here'])->name('test')->group('name');
// (new Logger('name'))->debug('test', ['info' => 'here']);

另请参阅 x() 助手函数。

更多请参阅 调试器日志记录

助手 env()

env() 函数是环境变量助手。

$dbName = env('DB_NAME');

更多请参阅 环境变量 & 配置

助手 f()

f() 函数返回一个格式化的字符串。

echo f('First value: {}, second value: {}', 'one', 'two');
// First value: one, second value: two

// placeholder names can be used
echo f('Name: {name}, age: {age}', 'Test', 25);
// Name: Test, age: 25

// array keys and placeholder names can be used
// regardless of key/value order
echo f('Name: {name}, age: {age}', ['age' => 25, 'name' => 'Test']);
// Name: Test, age: 25

助手 halt()

halt() 函数可用于立即返回 HTTP 响应状态代码和可选的 JSON 消息。

halt(404, 'Resource not found');
// returns HTTP response status code 404
// with JSON body {"message": "Resource not found"}

// use custom JSON response
halt(500, ['error' => 'message', 'context' => ['test']]);

// halt without message
halt(500);

助手 logger()

使用 logger() 函数获取 Logger 实例。

logger('channel')->info('message', ['context']);

有关日志记录的更多信息,请参阅日志记录

辅助函数 p()

p() 函数输出格式化的(HTML/CLI)变量。

p('test', 'this');
p(['my' => 'array']);

辅助函数 pa()

pa() 函数是一个变量打印器。

pa('test', 'this', ['and' => 'this'], 'end');
// test this
// Array
// (
//    [and] => this
// )
// end

辅助函数 req()

使用 req() 函数访问 Lark\Request 实例。

var_dump(
    req()->path()
);
// string(1) "/"

有关请求的更多信息,请参阅请求

辅助函数 res()

使用 res() 函数访问 Lark\Response 实例。

res()->contentType('application/json');

有关响应的更多信息,请参阅响应

辅助函数 router()

使用 router() 函数访问 Lark\Router 实例。

router()->get('/', function() {
    return 'home';
});

有关路由的更多信息,请参阅路由

辅助函数 x()

x() 函数是一个调试器和转储辅助工具。当调用 x() 函数(或 Lark\Debugger::dump())时,将转储所有调试器信息对象并停止执行。

x('value', ['test' => 'this']);

还可以查看debug() 辅助函数。

有关调试器的更多信息,请参阅调试器