hjmediagroep / backend-framework
轻量级PHP框架。包括快速安全的数据库QueryBuilder,高级路由(动态路由,中间件,分组,前缀)
Requires
- opis/closure: ^3.6
- php-curl-class/php-curl-class: ^9.5
Requires (Dev)
- phpstan/phpstan: ^1.2
- phpunit/phpunit: ^9
README
轻量级PHP后端框架。包括快速安全的数据库QueryBuilder,高级路由(动态路由,中间件,分组,前缀,命名路由)
要设置此PHP后端框架,您需要使用composer安装此包
安装
composer require hjmediagroep/backend-framework
设置
<?php
// namespace for App/Route class
use Framework\Http\Route\Route;
use Framework\App;
// include autoloader(composer)
require_once(__DIR__.'/../vendor/autoload.php');
// make instance of App
$app = new App();
// start app load all default(security) settings
$app->start();
// when you want to make a singleton you can do that like:
$app->setInstance(
new Route(),
new ClassThatYouWant()
);
// When you want to use the instance you can do that like this:
/** @var Route */
1. $app->getInstance(Route::class);
2. app()->getInstance(Route::class);
3. app(Route::class);
// make instance of Route class
$route = new Route();
<!-- all routes -->
// init all routes and check witch route is equals to the current uri
$route->init();
路由
基本路由
支持的请求方法: GET
,POST
,PUT
,DELETE
,PATCH
<?php
// supported request types/methods:
$route->get();
$route->post();
$route->put();
$route->patch();
$route->delete();
// route using callback function
$route->get('/account', function () {
echo "Account page";
});
// route using class methods
$route->get('/account', [AccountController::class,'index']);
// route using multi request methods(suports all requests methods)
$route->match('GET|POST','/user/{userID}', function ($userID) {
echo "user information";
});
动态路由
对于动态路由,您可以使用 pattern
方法来允许动态参数只具有特定值。您可以在每个路由方法操作 GET
,POST
,PUT
,DELETE
,PATCH
或 match
方法(允许多个请求方法)上执行此操作。您甚至可以在嵌套路由中覆盖它们。
<?php
// route using dynamic routing(params)
// all params can be accessed with the given name
$route->get('/account/{accountID}', function ($accountID) {
echo "AccountID: {$accountID}";
});
// You can also change the regex pattern of the dynamic params
// Now accountID can be only an number.
$route->get('/account/{accountID}', function ($accountID) {
echo "AccountID: {$accountID}";
})->pattern(['accountID' => '[0-9]+']);
// dynamic route prefix
$route->prefix('account')->group(function (Route $route) {
// group by dynamic prefix param
$route->prefix('/{accountID}')->group(function (Route $route) {
// Route will be: /account/{accountID}/profile
$route->get('/profile', function($accountID){
echo "Account profile page {$accountID}";
})->pattern(['accountID' => '[0-9]+']);
// you can set an other pattern for `{accountID}`
// route will be: /account/{accountID}/profile
$route->get('/profile', function($accountID){
echo "Account profile page {$accountID}";
})->pattern(['accountID' => '[0-9]+\-[a-z]+']);
});
// Route will be: /account
$route->get('/', function(){
echo "Account page";
});
});
命名路由
当您想通过名称获取路由时,可以使用 getRouteByName
方法,或者使用 redirect()->route()
。当您想要访问动态命名路由时,您需要通过其键传递给定的值。
<?php
// show all accounts
route()->getRouteByName('account.index');
// Thhis will get a single route
route()->getRouteByName('account.show', ['accountID' => 1]);
// This will redirect you to the route
redirect()->route('account.index');
// This will redirect you to the route with dynamic param
redirect()->route('account.show', ['accountID' => 1]);
路由前缀
前缀可以用于防止编写长的路由URI,并将具有相同URI部分的分组路由。您可以在 单个路由
或 分组
之前使用此方法。前缀还可以包含动态路由。然后您可以在每个请求方法 GET
,POST
,PUT
,DELETE
,PATCH
或 match
方法(允许多个请求方法)上指定模式。
<?php
// Route will end up like: /account/profile
$route->prefix('account')->get('/profile', function () {
echo "Account profile page";
});
// Grouped routes with prefix
$route->prefix('account')->group(function (Route $route) {
// when pattern was not correct
// Route will end up like: /account/{accountID}
$route->get('/{accountID}', function ($accountID) {
// Get accountID from URL
echo "AccountID: {$accountID}";
});
});
// Grouped routes with dynamic prefix
$route->prefix('account')->group(function (Route $route) {
// dynamic route prefix
$route->prefix('{accountID}')->group(function(Route $route){
// Route will end up like: /account/{accountID}/profile
$route->get('/profile', function ($accountID) {
// Get accountID from URL
echo "AccountID: {$accountID}";
})->pattern(['accountID' => '[0-9]+']);
// you can change the dynamic route prefix pattern after all (get, post, put, delete, patch) methods
});
});
路由中间件
中间件可以用于阻止对请求方法 GET
,POST
,PUT
,DELETE
,PATCH
或 match
方法(允许多个请求方法)的访问
<?php
// Route with single middleware check
$route->middleware(false)->get('/profile', function () {
echo "Account profile page";
});
// Route with array of middleware checks
$route->middleware([true, false])->get('/profile', function () {
echo "Account profile page";
});
// OR
$route->middleware(true, false)->get('/profile', function () {
echo "Account profile page";
});
// Route with custom validate class
$route->middleware(true, CustomMiddlewareClass::class)->get('/profile', function () {
echo "Account profile page";
});
// The class shout like this:
class CustomMiddlewareClass
{
public function handle(array $route, Closure $next){
// return false when middleware need to fail
if(true !== false){
return false;
}
// when middleware is successful
return $next();
}
}
还可以设置自定义中间件失败处理程序或未找到处理程序
<?php
// set the middleware fail callback
$route->onMiddlewareFail(function ($route) {
response()->json([
'status' => 'error',
'code' => 403,
'error' => [
'message' => 'Access denied'
]
])->code(403)->exit();
});
// set the not found callback
$route->onNotFound(function () {
response()->json([
'status' => 'error',
'code' => 404,
'error' => [
'message' => 'Not found'
]
])->code(404)->exit();
});
路由分组
当您有中间件/前缀需要应用于多个路由时,分组路由非常有用。
<?php
// Grouped routes with prefix
$route->prefix('account')->group(function (Route $route) {
// when pattern was not correct
// Route will end up like: /account/{accountID}
$route->get('/{accountID}', function ($accountID) {
// Get accountID from URL
echo "AccountID: {$accountID}";
});
});
// Grouped routes with middleware check
$route->middleware(true)->group(function (Route $route) {
// when pattern was not correct
// Route will end up like: /account/{accountID}
$route->get('/{accountID}', function ($accountID) {
// Get accountID from URL
echo "AccountID: {$accountID}";
});
});
请求
请求方法
request()->all()
将从 GET
,POST
(php://input),FILES
获取所有请求信息
<?php
request()->all();
request()->get()
将从 GET
获取所有请求信息
<?php
$_GET['name'] = 'test';
request()->get('name'); // test
request()->post()
将从 POST
(php://input) 获取所有请求信息
<?php
$_POST['name'] = 'test';
request()->post('name'); // test
request()->file()
将从 FILES
获取所有请求信息
<?php
$_FILES['name'] = []; // showing purpose(invalid file array)
request()->file('name'); // will get file array
request()->cookies()
将从 request
|server
获取所有cookies
<?php
$_SERVER['Cookie'] = 'PHPSESSID=u30vn0lgpmf6010ro4ol9snle1; name=test';
request()->cookies('name'); // test
request()->server()
将从 SERVER
获取所有服务器头信息
<?php
$_SERVER['HTTP_METHOD'] = 'GET';
request()->server('HTTP_METHOD'); // GET
request()->headers()
将从 getallheaders
|SERVER
获取所有请求头信息
<?php
header('Content-Type: application/json;');
request()->headers('Content-Type'); // application/json
请求验证方法
要验证请求输入,请使用 request()->validate()
<?php
// Rules:
// - string
// - int
// - float
// - array
// - min:_NUMBER_
// - max:_NUMBER_
// - regex:_REGEX_ //without / before and after
// - email
// - url
// - ip
// - YourCustomRuleClass::class // that needs to extend `CustomRule` and must have the `validate` method
$_GET['test'] = '';
// this will fail (min:1)
$validated = request()->validate([
'test' => ['required', 'string', 'min:1', 'max:255']
]);
if($validated->failed()){
// do action
$messages = $validated->getErrorMessages(); // get error messages
$failedRules = $validated->getFailedRules();
}
// get validated data
$validatedData = $validated->getData();
自定义验证规则
<?php
class YourCustomRuleClass extends CustomRule {
public function validate(mixed $value): bool {
// check if is valid
if($value === 'test'){
return true;
}
// This message will be combined with the customrule
$this->message('Your value must be test');
return false;
}
}
request()->csrf()
生成一个csrf令牌,您可以使用 request()->validateCsrf()
进行验证。您希望在不是 GET
请求的每个后端请求中都使用此功能。
<input type="hidden" name="_token" value="<?= request()->csrf(); ?>">
request()->validateCsrf()
将验证您的 csrf令牌
是否有效。
<?php
if(!request()->validateCsrf()){
throw new \Exception('Your token is not valid!');
}
响应
响应类/助手可以帮助您发送带有正确头信息和内容的响应。当您发送响应时,它将自动选择正确的 Content-Type
。默认的 responsecode
是 200
,消息为 OK
。每个响应代码都包含其自己的消息,将被自动包含。您可以将所有方法链接起来,响应将在最后一个方法链中返回。
方法
response()->json()
将所有信息转换为 json。
<?php
response()->json(['this is a array to json']);
// headers
Content-Type: application/json; charset=UTF-8;
HTTP/1.1 200 OK
response()->text()
允许文本/html。
<?php
response()->text('this is a normal string|html');
// headers
Content-Type: text/html; charset=UTF-8;
HTTP/1.1 200 OK
response()->code()
将设置响应代码,它使用底层的 http_response_code
。
<?php
response()->code(404);
// headers
Content-Type: text/html; charset=UTF-8;
HTTP/1.1 404 Not Found
response()->headers()
将在响应中添加头信息。
<?php
response()->headers(['Test' => 'test']);
// headers
Test: test
Content-Type: text/html; charset=UTF-8;
HTTP/1.1 200 OK
response()->exit()
当响应发送时将使用 PHP 的 exit
函数。
<?php
response()->json(['message' => 'Something went wrong'])->exit();
// headers
Content-Type: Application/json; charset=UTF-8;
HTTP/1.1 200 OK
response()->view()
将视图(内容文件)添加到响应中。
<?php
response()->view('index',['userIds' => [1,2,3,4]]);
// headers
Content-Type: text/html; charset=UTF-8;
HTTP/1.1 200 OK
QueryBuilder
方法
logSql()
在页面或 ray 内记录查询 + 绑定。
<?php
$db->table('users')->logSql()->where('id', '=', 1);
raw(query: string, bindings: array)
当您想使用用户输入值时,您可能想使用绑定参数。
<?php
// without bindings
$db->raw('SELECT * FROM `users` WHERE `users`.`id` = 1');
// with bindings
$db->raw('SELECT * FROM `users` WHERE `users`.`id` = ?', [1]);
table(table: string, columns: ...string|array)
在页面或 ray 内记录查询 + 绑定。
<?php
// SELECT * FROM `users`
$db->table('users')->all();
// SELECT * FROM `users` LIMIT 1 OFFSET 0
$db->table('users')->one();
// DELETE FROM `users`
$db->table('users')->delete();
// UPDATE `users` SET ...
$db->table('users')->update(['name' => 'test name']);
select(...string|array)
将设置选择列,子查询
。
<?php
$db->table('users', 'id')->where('id', '=', 1);
$db->table('users', 'id', 'email')->where('id', '=', 1);
$db->table('users', ['id', 'email'])->where('id', '=', 1);
// OR
$db->table('users')->select('id')->where('id', '=', 1);
$db->table('users')->select('id', 'email')->where('id', '=', 1);
$db->table('users')->select(['id', 'email'])->where('id', '=', 1);
// OR sub select
// SELECT (SELECT count(posts.id) FROM posts WHERE users.id = posts.user_id) as post_count FROM `users`
$db->table('users')->select([
'post_count' => function(QueryBuilder $query){
$query->table('posts', 'count(posts.id)')->whereColumn('users.id', '=', 'posts.user_id');
}
]);
where(column: Closure|string, operator: array|string, value: mixed = null, boolean: string(OR|AND) = 'AND')
添加 where 语句。
<?php
// SELECT * FROM `users` WHERE `email` = ? // bindings: ['test@example.com']
$db->table('users')->where('email', '=', 'test@example.com');
$db->table('users')->where('email', 'test@example.com');
// SELECT * FROM `users` WHERE `email` = ? OR `email` = ? // bindings: ['test@example.com', 'test@example.com']
$db->table('users')->where('email', '=', 'test@example.com')->where('email', '=', 'test@example.com', 'OR');
// SELECT * FROM `users` WHERE `email` = ? AND `email` = ? // bindings: ['test@example.com', 'test@example.com']
$db->table('users')->where('email', '=', 'test@example.com')->where('email', '=', 'test@example.com', 'AND');
whereRaw(query: string|closure, bindData: array = [], boolean: string(OR|AND) = 'AND')
<?php
// SELECT * FROM `users` WHERE `users`.`email` LIKE '%test@example.com%'
$db->table('users')->whereRaw('`users`.`email` LIKE %test@example.com%');
// SELECT * FROM `users` WHERE `users`.`email` LIKE ? // bindings: ['test@example.com']
$db->table('users')->whereRaw('`users`.`email` LIKE ?', ['test@example.com']);
// SELECT * FROM `users` WHERE `users`.`email` LIKE ? AND `users`.`email` LIKE ? // bindings: ['test@example.com', 'test@example.com']
$db->table('users')->whereRaw('`users`.`email` LIKE ?', ['test@example.com'])->whereRaw('`users`.`email` LIKE ?', ['test@example.com'], 'AND');
orWhere(column: Closure|string, operator: string|null, value: mixed)
Eloquent 版本的 where('column', 'operator', 'value', 'OR')
。
<?php
$db->table('users')->where('users.id', '=', 1)->orWhere('users.email', '=', 'test@example.com');
whereIn(column: string, value: Closure|array, boolean: string(OR|AND) = 'AND')
<?php
// SELECT * FROM `users` WHERE `id` IN (?) // bindings: ['1,2,3,4']
$db->table('users')->whereIn('id', [1,2,3,4]);
// SELECT * FROM `users` WHERE `id` IN (SELECT `user_id` FROM posts)
$db->table('users')->whereIn('id', function(QueryBuilder $query){
$query->table('posts', 'user_id');
});
whereExists(callback: Closure, boolean: string(OR|AND) = 'AND', not: boolean = false)
<?php
// SELECT * FROM `users` WHERE EXISTS (SELECT `created_at` FROM `posts` WHERE `created_at` > ? AND `users`.`id` = `posts`.`user_id` LIMIT 1 OFFSET 0)
$db->table('users')->whereExists(function(QueryBuilder $query){
$query->table('posts', 'created_at')->where('created_at', '>', '2022-01-01')
->whereColumn('posts.id', '=', 'users.id', 'AND')
->limit(1);
});
whereNotExists(callback: closure, boolean: string(OR|AND) = 'AND')
Eloquent 的 whereExists(callback, 'AND', true)
。
<?php
// SELECT * FROM `users` WHERE NOT EXISTS (SELECT `created_at` FROM `posts` WHERE `created_at` > ? AND `users`.`id` = `posts`.`user_id` LIMIT 1 OFFSET 0)
$db->table('users')->whereNotExists(function(QueryBuilder $query){
$query->table('posts', 'created_at')->where('created_at', '>', '2022-01-01')
->whereColumn('posts.id', '=', 'users.id', 'AND')
->limit(1);
});
whereColumn(column: string, operator: string|null, value: string|null, boolean: string(OR|AND) = 'AND')
确保您不要使用来自用户的原始输入,因为列将不会被转义!
<?php
// SELECT (SELECT count(id) FROM `posts` WHERE `users`.`id` = `posts`.`user_id`) as post_count FROM `users`
$db->table()->select([
'post_count' => function(QueryBuilder $query){
$query->table('posts', 'count(id)')->whereColumn('users.id', '=', 'posts.user_id');
}
]);
join(table: string, first: Closure|string, first: string|null, operator: string|null, value: string|null, type: string(INNER|LEFT|RIGHT|CROSS) = 'INNER')
确保您不要使用来自用户的原始输入,因为列将不会被转义!如果您想使用用户输入的值,确保在闭包(连接)内部使用 where()
。
<?php
// SELECT * FROM `users` INNER JOIN `posts` ON `users`.`id` = `posts`.`user_id`
$db->table('users')->join('posts', 'users.id', '=', 'posts.user_id');
// SELECT * FROM `users` INNER JOIN (`posts` ON `users`.`id` = `posts`.`user_id` OR `posts` ON `users`.`id` = `posts`.`user_id`)
$db->table('users')->join('posts', function(JoinClause $join){
$join->on('users.id', '=', 'posts.user_id')->orOn('users.id', '=', 'posts.user_id');
});
// join with user input
// SELECT * FROM `users` INNER JOIN `posts` ON `users`.`id` = ? // bindings [1]
$db->table('users')->join('posts', function(JoinClause $join){
$join->where('users.id', '=', 1);
});
leftJoin()
Eloquent 的 join('table', 'firstColumn', 'operator', 'secondColumn', 'LEFT')
。
<?php
// SELECT * FROM `users` LEFT JOIN `posts` ON `users`.`id` = `posts`.`user_id`
$db->table('users')->leftJoin('posts', 'users.id', '=', 'posts.user_id');
rightJoin()
Eloquent 的 join('table', 'firstColumn', 'operator', 'secondColumn', 'RIGHT')
。
<?php
// SELECT * FROM `users` RIGHT JOIN `posts` ON `users`.`id` = `posts`.`user_id`
$db->table('users')->rightJoin('posts', 'users.id', '=', 'posts.user_id');
limit(limit: int)
<?php
// SELECT * FROM `users` LIMIT 50 OFFSET 0
$db->table('users')->limit(50)->all([]);
offset(limit: int)
<?php
// SELECT * FROM `users` LIMIT 50 OFFSET 10
$db->table('users')->limit(50)->limit(10)->all([]);
orderBy(column: string, direction: string(ASC|DESC) = 'ASC')
<?php
$db->table('users')->orderBy('create_at')->all([]);
$db->table('users')->orderBy('create_at', 'ASC')->all([]);
// OR
$db->table('users')->orderBy('create_at', 'DESC')->all([]);
groupBy(...string)
<?php
// SELECT * FROM `users` GROUP BY `title`
$db->table('posts')->groupBy('title');
// OR
// SELECT * FROM `users` GROUP BY `title`, `user_id`
$db->table('posts')->groupBy('title', 'user_id');
when(when: boolean, callback: Closure)
<?php
$isAdmin = false;
$db->table('posts')->when(!$isAdmin, function(QueryBuilder $query){
$query->where('user_id', '=', 2);
})->all([]);
paginate(currentPage: int, perPage: int = 15)
<?php
// SELECT * FROM `users` LIMIT 50 OFFSET 0
$pagination = $db->table('users')->paginate(1, 50);
// `$pagination` is structures like this:
[
'current_page' => 1,
'first_page' => 1,
'last_page' => ..,
'per_page' => 50,
'total_pages' => .., // number of total pages,
'total_results' => .., // number of results found
'next_page' => [
'exists' => true, // false when there is no next page
'page' => 2 // the next page number
],
'prev_page' => [
'exists' => false, // false when there is no previous page
'page' => 1 // the previous page number
],
'results' => [] // array of results
]
all(fallbackReturnValue: mixed = false, fetchMode: int|null = null)
<?php
// You can use this inside a foreach without using the `all()` method
$db->table('users');
// OR
$db->table('users')->all();
// OR when query fails return value will be `[]`
$db->table('users')->all([]);
// Fetch mode(default fetch mode: \POD::FETCH_ASSOC)
$db->table('users')->all([], \POD::FETCH_ASSOC | \POD::FETCH_COLUMN);
one(fallbackReturnValue: mixed = false, fetchMode: int|null = null)
<?php
// SELECT * FROM `users` LIMIT 1 OFFSET 0
$db->table('users')->one();
// when query fails return value will be `[]`
$db->table('users')->one([]);
// Fetch mode(default fetch mode: \POD::FETCH_ASSOC)
$db->table('users')->one([], \POD::FETCH_ASSOC | \POD::FETCH_COLUMN);
column(fallbackReturnValue: mixed = false, column: int = 0)
<?php
$userInfo = $db->table('users', 'username', 'email')->limit(1);
// to retrieve `username` use
$username = $userInfo->column(0);
// to retrieve `email` use
$email = $userInfo->column(1);
insert(insertData: array<string,mixed>)
当查询 Failed
时,insert 方法将返回 false
,否则方法将返回 insertId
。
<?php
$insertId = $db->table('posts')->insert([
'title' => 'test title',
'slug' => 'test-title',
'body' => 'This is an test body'
]);
update(updateData: array<string,mixed>)
根据是否有影响的行返回布尔值。
<?php
$passed = $db->table('users')->where('id', '=', 1)->update([
'titel' => 'Update title'
]);
delete()
根据是否有影响的行返回布尔值。
<?php
$passed = $db->table('users')->where('id', '=', 1)->delete();
模型
模型将代表数据库表。这允许您只编写属于单个数据库表的代码。模型将自动尝试根据您的 模型名称 猜测 数据库表 名称。当猜测不正确时,您可以像这样指定表名:protected string $table = 'table_name'
默认的 主键 是 id
,您可以通过 protected string $primaryKey = 'your_primary_key'
来覆盖它。
<?php
use Framework\Model\BaseModel;
// table name => `posts`
// primaryKey => 'id'
class Post extends BaseModel
{
}
// table name => `categories`
// primaryKey => 'id'
class Category extends BaseModel
{
}
// table name => `posts`
// primaryKey => 'ID'
class WeirdModelName extends BaseModel
{
protected string $table = 'posts';
protected string $primaryKey = 'ID'
}
当您进行查询时,模型 将自动指定 表名。当您想使用 QueryBuilder
时,可以这样做: BaseModel 有一个名为 find($find: mixed, $key: string|null = null)
的方法,这是一个用于查找单个结果的简写。
<?php
use Framework\Model\BaseModel;
class Post extends BaseModel
{
public function paginatePosts(int $currentPage = 1, int $perPage = 15): array
{
return $this->orderBy('created_at', 'ASC')->paginate($currentPage, $perPage);
}
}
// Make instance of `Post` model
$post = new Post();
// This finds a single result in the database based on the primary key
$singlePost = $post->find(find: 'some_primary_key');
$singlePost = $post->find(find: 'some_title', key: 'title');
// This will contain all posts from the database
$posts = $post->all([]);
// This will get all posts with pagination that where orderd by `created_at` ASC
$paginatedPosts = $post->paginate(currentPage: 1, perPage: 25);