hjmediagroep/backend-framework

轻量级PHP框架。包括快速安全的数据库QueryBuilder,高级路由(动态路由,中间件,分组,前缀)

1.0.7 2022-07-28 10:48 UTC

This package is auto-updated.

Last update: 2024-09-28 15:18:40 UTC


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();

路由

基本路由

支持的请求方法: GETPOSTPUTDELETEPATCH

<?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 方法来允许动态参数只具有特定值。您可以在每个路由方法操作 GETPOSTPUTDELETEPATCHmatch 方法(允许多个请求方法)上执行此操作。您甚至可以在嵌套路由中覆盖它们。

<?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部分的分组路由。您可以在 单个路由分组 之前使用此方法。前缀还可以包含动态路由。然后您可以在每个请求方法 GETPOSTPUTDELETEPATCHmatch 方法(允许多个请求方法)上指定模式。

<?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
    });
});

路由中间件

中间件可以用于阻止对请求方法 GETPOSTPUTDELETEPATCHmatch 方法(允许多个请求方法)的访问

<?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() 将从 GETPOST (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。默认的 responsecode200,消息为 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);