remcosmits/backend-framework

轻量级PHP框架。包括快速安全的数据库QueryBuilder、具有关系的模型、高级路由(动态路由、中间件、分组、前缀、命名路由)。


README

build Total Downloads Latest Stable Version License

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

要设置此PHP后端框架,您需要使用composer安装此软件包

安装

composer require remcosmits/backend-framework

设置

// 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

// 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方法(允许多种请求方法)上执行此操作。您甚至可以在嵌套路由中覆盖它们。

// 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()。当您想要访问动态命名路由时,您需要通过其键传递给定的值。

// 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方法(允许多种请求方法)上指定模式。

// 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方法(允许多种请求方法)进行访问

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

路由分组

分组路由可以在您有中间件/前缀需要应用于多个路由时非常有用。

// 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获取所有请求信息

request()->all();

request()->get()将从GET获取所有请求信息

$_GET['name'] = 'test';
request()->get('name'); // test

request()->post()将从POST(php://input)获取所有请求信息

$_POST['name'] = 'test';
request()->post('name'); // test

request()->file()将从FILES获取所有请求信息

$_FILES['name'] = []; // showing purpose(invalid file array)
request()->file('name'); // will get file array

request()->cookies()将从request|server获取所有cookies

$_SERVER['Cookie'] = 'PHPSESSID=u30vn0lgpmf6010ro4ol9snle1; name=test';
request()->cookies('name'); // test

request()->server()将从SERVER获取所有服务器头信息

$_SERVER['HTTP_METHOD'] = 'GET';
request()->server('HTTP_METHOD'); // GET

request()->headers()将从getallheaders|SERVER获取所有请求头信息

header('Content-Type: application/json;');
request()->headers('Content-Type'); // application/json

请求验证方法

要验证请求输入,请使用request()->validate()

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

自定义验证规则

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令牌是否有效。

if(!request()->validateCsrf()){
    throw new \Exception('Your token is not valid!');
}

响应

响应类/辅助工具可以帮助您发送带有正确头信息和信息的响应。在发送响应时,它将自动选择正确的Content-Type。默认的responsecode200,信息为OK。每个响应代码都包含其自己的信息,将自动包含。您可以将所有方法链接起来,最后的方法链将返回响应。

方法

response()->json()将所有信息转换为json

response()->json(['this is a array to json']);
// headers
Content-Type: application/json; charset=UTF-8;
HTTP/1.1 200 OK

response()->text()允许text/html

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

response()->code(404);
// headers
Content-Type: text/html; charset=UTF-8;
HTTP/1.1 404 Not Found

response()->headers()将在响应中附加头信息

response()->headers(['Test' => 'test']);
// headers
Test: test
Content-Type: text/html; charset=UTF-8;
HTTP/1.1 200 OK

response()->exit()在发送响应后将使用php的exit函数

response()->json(['message' => 'Something went wrong'])->exit();
// headers
Content-Type: Application/json; charset=UTF-8;
HTTP/1.1 200 OK

response()->view()将视图(内容文件)附加到响应

response()->view('index',['userIds' => [1,2,3,4]]);
// headers
Content-Type: text/html; charset=UTF-8;
HTTP/1.1 200 OK

查询构建器

方法

logSql()在页面或ray内部记录查询+绑定

$db->table('users')->logSql()->where('id', '=', 1);

raw(query: string, bindings: array) 当您想使用用户输入值时,您可能希望使用绑定参数

// 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内部记录查询+绑定

// 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)将设置选择列,子查询

$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语句

// 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')

// 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')

$db->table('users')->where('users.id', '=', 1)->orWhere('users.email', '=', 'test@example.com');

whereIn(column: string, value: Closure|array, boolean: string(OR|AND) = 'AND')

// 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)

// 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)

// 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') 请确保您不要使用来自用户的原始输入,因为列将不会进行转义!

// 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') 请确保您不要使用来自用户的原始输入,因为列将不会进行转义!如果您想使用用户输入的值,请确保在闭包(join)内部使用 where()

// 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')

// 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')

// 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)

// SELECT * FROM `users` LIMIT 50 OFFSET 0
$db->table('users')->limit(50)->all([]);

offset(limit: int)

// SELECT * FROM `users` LIMIT 50 OFFSET 10
$db->table('users')->limit(50)->limit(10)->all([]);

orderBy(column: string, direction: string(ASC|DESC) = 'ASC')

$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)

// 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)

$isAdmin = false;
$db->table('posts')->when(!$isAdmin, function(QueryBuilder $query){
    $query->where('user_id', '=', 2);
})->all([]);

paginate(currentPage: int, perPage: int = 15)

// 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)

// 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)

// 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)

$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

$insertId = $db->table('posts')->insert([
    'title' => 'test title',
    'slug' => 'test-title',
    'body' => 'This is an test body'
]);

update(updateData: array<string,mixed>) 根据是否影响了行返回布尔值

$passed = $db->table('users')->where('id', '=', 1)->update([
    'titel' => 'Update title'
]);

delete() 根据是否影响了行返回布尔值

$passed = $db->table('users')->where('id', '=', 1)->delete();

Model

模型将代表一个数据库表。这将允许您仅编写属于单个数据库表的代码。模型将自动尝试根据您的 模型名称 猜测 数据库表名称。当猜测不正确时,您可以通过如下方式指定表名 protected string $table = 'table_name' 默认的 主键id,您可以通过 protected string $primaryKey = 'your_primary_key' 来覆盖它

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) 的方法,这是一个用于查找单个结果的简写。

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->paginatePosts(currentPage: 1, perPage: 25);