phpluna / framework
一个简单的PHP MVC框架
Requires
- php: >=7.1
- illuminate/database: ^8.83
README
Luna是一个以PHP开发的框架,受到Laravel、CodeIgniter和JavaScript的Express等其他框架的启发,专注于Web开发,具有以下功能:
- 路由映射;
- ORM数据库(Illuminate/Eloquent);
- 中间件队列;
- 实用性、安全性和敏捷性;
- 简化托管;
- 缓存存储;
- 组件化;
- 分页;
- 搜索引擎优化(SEO)。
摘要
学习Luna
安装
在开始Luna项目之前,您需要安装PHP(版本7.1或更高版本)和Composer。
使用以下命令以Luna启动项目
composer create-project phpluna/luna {project-name}
初始化
将文件.env.example
重命名为.env
并按需配置URL
mv .env.example .env
如果您愿意,可以复制该文件并保留.env.example
以保留环境变量的示例。
项目的一般设置可以在同一文件
.env
中定义,例如,可以定义第三方API的认证密钥。
路由
应在/routes
目录下的某个文件中创建路由(假定您正在遵循这里展示的模式)。您可以创建分离的文件,例如pages.php
用于页面路由或api.php
用于API路由。
<?php use App\Controllers\Pages; $router->get('/', [ function($request, $response) { return Pages\Home::homePage($request, $response); } ]);
可用的路由方法
$router->get($uri, [$callback]); $router->post($uri, [$callback]); $router->put($uri, [$callback]); $router->patch($uri, [$callback]); $router->delete($uri, [$callback]); $router->options($uri, [$callback]);
可以为同一个$uri
和$callback
定义多个方法。
$router->match(['get', 'post'], $uri, [$callback]); $router->any($uri, [$callback]);
路由参数
路由可以接收自定义参数。
$router->get('/products/{id}', [ function($request, $response) { return Pages\Product::getPage($request, $response); } ]);
可以在执行函数中获取参数。
class Product extends Page { public static function getPage($request, $response) { $id = $req->param('id'); } }
如果您愿意,也可以通过显式变量获取URL参数。
$router->get('/products/{id}', [ function($id, $request, $response) { return Pages\Product::getPage($id, $request, $response); } ]); class Product extends Page { public static function getPage($id, $request, $response) { // ... } }
可选参数
可以使用?
创建可选参数。
$router->get('/cart/{id?}', [ function($request, $response) { // ... } ]); $router->get('/product/{id}/{slug?}', [ function($request, $response) { // ... } ]);
未在请求中提供的选择参数将被定义为NULL。
错误路由
可以在定义路由时直接处理一些常见的错误,以自定义返回页面。
use \App\Controllers\Errors; $router->error(404, [ function($request, $response) { return Errors\PageNotFound::getPage($request, $response); } ]);
还可以为任何错误定义一个默认路由。
$router->error('default', [ function($request, $response) { return Errors\General::getPage($request, $response); } ]);
重定向路由
要执行路由重定向,请使用redirect()
函数。
$router->get('/redirect', [ function($request, $response) { return $request->getRouter()->redirect('/destination'); } ]);
中间件
中间件提供了一种方便的机制来验证特定路由的请求。
$router->get('/', [ 'middlewares' => [ 'maintenance' ], function($request, $response) { // ... } ]);
中间件类必须包含一个名为handle
的函数,该函数将在访问路由时执行。
namespace App\Middlewares; class Maintenance { public function handle($request, $response, $next) { // ... return $next($request, $response); } }
handle
函数必须接受$request
、$response
和$next
参数,并必须返回$next($request, $response)
以继续队列。
创建中间件类后,需要用别名定义它,以便在定义路由时使用。
use Luna\Http\Middleware; Middleware::setMap([ 'maintenance' => \App\Middlewares\Maintenance::class ]);
可以定义标准中间件
,这些中间件将在创建的所有路由中执行。
Middleware::setDefault([ 'maintenance' ]);
缓存
将路由返回存储在缓存中可以减少相同路由未来请求的返回时间
$router->get('/', [ 'cache' => 10000, function($request, $response) { // ... } ]);
缓存时间以毫秒为单位定义
缓存配置可以在.env
文件中定义
当路由缓存被设置为true时,CACHE_TIME的值被定义为缓存时间(也以毫秒为单位)
$router->get('/', [ 'cache' => true, function($request, $response) { // ... } ]);
控制器
路由(大多数情况下)执行控制器(Controllers)
namespace App\Controllers; class Product { public static function productPage($request, $response) { // ... } }
获取请求数据
可以通过$request
变量获取请求数据,如headers、查询参数、body等
$request->header(); // Obter parâmetros do header $request->query(); // Obter parâmetros da query $request->body(); // Obter parâmetros do corpo $request->param(); // Obter parâmetros da URL $request->getUri(); // Obter URI $request->getHttpMethod(); // Obter método HTTP
可以使用以下函数获取特定参数:
$request->query('id')
。不指定参数将导致所有参数作为数组返回。
响应请求
每个请求都必须被响应,并且其响应应该在控制器的函数的return
中完成
class Product { public static function productPage($request, $response) { return $response->send(200, "Sucesso"); } }
建议遵循此处列出的HTTP状态码标准(例如,示例中的200):链接
请求响应类型
请求的响应可以返回text/html
、application/json
(更常见)或其他(较少见)的值
public static function getProduct($request, $response) { // ... return $response->send(200, ["data" => "Sucesso"], "application/json"); }
还可以使用别名来返回HTML或JSON中的请求
$res->send(200, $content, 'json'); // Ao invés de 'application/json' $res->send(200, $content, 'html'); // Ao invés de 'text/html'
仅在DEFAULT_CONTENT_TYPE
的.env
文件值与控制器期望的不同时,才应该在函数中指定响应类型
服务
服务(Services)帮助在数据库和控制器之间获取和处理数据
namespace App\Services; class Product { public static function find($id) { // ... } }
服务使用
use App\Services\Product as ProductService; class Product { public static function getProduct($request, $response) { $id = $request->param("id"); $product = ProductService::find($id); return $response->send(200, $product); } }
辅助函数
助手(Helper)将一些小的、未被定义为服务的有用函数组合在一起
namespace App\Helpers; class Uuid { public function generate() { // ... } }
助手使用
use App\Helpers\Uuid as UuidHelper; class User { public function find($id) { return UuidHelper::generate(); } }
视图
视图可以在resources/views
中创建为.html
文件,并在渲染中使用
namespace App\Controllers\Pages; use Luna\Utils\View; class Product { public static function productPage($request, $response) { $content = View::render('pages/product', [ 'name' => "Produto nome", 'description' => "Produto descrição" ]); return $response->send(200, $content); } }
resources/view/page/product.html
文件
<h1>{{name}}</h1> <p>{{description}}</p> <!-- Resultado: --> <!-- <h1>Produto nome</h1> --> <!-- <p>Produto descrição</p> -->
要访问数组的不同级别,请使用->
,例如
<p>{{name->first}}</p> <p>{{phone->main->number}}</p>
将获得以下值
$data = [ 'phone' => [ 'main' => [ 'number' => '12345' ] ], 'name' => [ 'full' => 'Fulano de Tal', 'first' => 'Fulano', ], ]
未发送的变量可以使用??
接收预定义的值
<p>{{name ?? Nome não definido}}</p>
视图变量遵循与组件相同的规则,因此两者都可以使用所有资源
页面规范
Page
类具有一些函数,可以用于使用header
、footer
和其他标准项来标准化页面,并更改content
的值
namespace App\Controllers\Pages; use Luna\Utils\View; class Product extends Page { public static function productPage($request, $response) { $content = View::render('pages/product', [ 'name' => "Produto nome", 'description' => "Produto descrição" ]); $content = parent::getPage("Produto Título", $content); return $response->send(200, $content); } }
使用Page
类时,变量$content
将包含page.html
、header.html
和footer.html
文件(已存在于/resources/view
中)的合并
还可以添加新的标准文件到头部和底部,例如可以为公共区域和管理区域创建不同的头部
$content = parent::getPage("Produto Título", $content, [ 'header' => 'header-admin', 'footer' => 'footer-admin' ]);
要防止添加文件,将其定义为false
。
标准变量
最常用的变量可以在index.html
的View::define()
中定义,并可以在任何视图中使用
<img src="{{PUBLIC}}/assets/img/php-logo.png" /> <a href="{{URL}}"><button>Início</button></a> <a href="{{URL}}/products"><button>Produtos</button></a>
Flash消息
Flash消息可以用于以动态方式将消息返回到视图
namespace App\Controllers\Pages; use Luna\Utils\Flash; class Product { public static function productPage($request, $response) { // ... Flash::create("productNotFound", "Produto não encontrado", 'error'); } }
创建消息后,可以将其渲染并添加到视图中
Flash::create("productNotFound", "Produto não encontrado", 'error'); $flash = Flash::render("productNotFound");
还可以渲染尚未预先创建的消息
$flash = Flash::render(false, "Produto não encontrado", 'error');
消息存储在会话变量
$_SESSION
中,如果消息在其他地方未使用,则预先创建它可能是有用的。
如果需要,可以一次渲染多个消息(仅适用于预先创建的消息)
$flashs = Flash::renderAll(["productNotFound", "productOutOfStock"]);
一旦渲染,就可以像其他参数一样将其添加到视图中
$content = View::render('pages/product', ['flash' => $flash]);
请确保在将要使用的视图中添加{{flash}}或相应的参数。
Flash消息组件可以在/resources/components/flash/alert.html
中更改。
如果需要,可以在同一目录中创建一个组件,并在渲染时选择它
Flash::create("productNotFound", "Produto não encontrado", 'error', 'alert-new'); Flash::render("Produto não encontrado", 'error', 'alert-new');
示例中存在的error
值应用于组件的{{type}}
变量,并且可以自定义任何值以进行样式化。
常见类型包括:
error
、danger
、warning
、info
、success
。
组件
视图中的小结构如果是重复的(或不重复的)则可以用作组件。
namespace App\Controllers\Pages; use Luna\Utils\Component; class Product { public static function productsPage($request, $response) { // ... $productCard = Component::render('product-card', $product); $content = View::render('pages/product', ['productCard' => $productCard]); } }
组件应创建在.html
文件中,与view
相同,位于resources/components
目录下。
也可以创建子目录来组织,例如:
resources/components/product/card
,并使用Component::render('product/card', $product)
进行渲染。
多渲染
在需要多次从数组渲染相同组件的情况下
$productCards = Component::multiRender('product-card', $products); $content = View::render('pages/product', ['productCards' => $productCards]);
分页
可以使用Pagination
类来实现数组的分页
namespace App\Controllers\Pages; use Luna\Utils\Pagination; class Product { public static function productsPage($request, $response) { // ... $pagination = new Pagination($products, $page, $limit); $products = $pagination->get(); } }
get()
函数将返回已分页的列表和其他分页数据。
可以获取特定分页数据
$pagination->getCount(); // Obter quantidade de itens da página atual $pagination->getList(); // Obter lista de itens da página atual $pagination->getPages(); // Obter quantidade de páginas $pagination->getPage(); // Obter página atual $pagination->getLimit(); // Obter quantidade de itens por página $pagination->getTotal(); // Obter quantidade total de itens
分页模板
分页控制可以渲染显示在视图中
namespace App\Controllers\Pages; use Luna\Utils\Pagination; class Product { public static function productsPage($request, $response) { // ... $pagination = new Pagination($products, $page, $limit); $paginationRender = $pagination->render($req); $content = View::render('pages/products', ['pagination' => $paginationRender]); } }
用于创建分页的组件可以在resources/components/pagination
中进行修改,也可以在渲染时进行修改
$paginationRender = $pagination->render($req, [ 'last' => 'last.html' // ... ]);
项目的
href
始终使用{{page}}参数来定义目标页面。
如果需要删除某个项目,请将参数设置为false
。
要限制每侧显示的项目数量,请使用
$paginationRender = $pagination->render($req, [], 3);
渲染结果示例,每侧显示3个项目
样式应单独进行设计
数据库
Luna项目的数据库访问通过在Laravel中使用的对象关系映射(ORM)实现,称为Illuminate/Eloquent,它允许使用简单的快速函数来编写复杂的SQL查询。
数据库访问凭据的配置应在.env
文件中进行,并通过以下方式建立连接:
use Luna\Db\Database; Database::boot();
使用
bootstrap.php
文件时,连接将默认配置。例如:require __DIR__ . '/bootstrap.php';
。
可以在此处访问完整的Eloquent文档[点击访问]。
迁移
迁移可以用来以程序化和版本化的方式对数据库进行修改。
要创建迁移,请使用Luna CLI
。
php luna make:migration {{name}}
值name
应包含迁移文件的名称,例如
php luna make:migration create_users_table
也可以通过特定的表创建迁移
php luna make:migration --table=users add_role_id_column_to_users_table
要执行数据库迁移,请使用以下命令
php luna migrate
这将检查哪些迁移尚未执行,并执行它们。
要执行特定的迁移,请使用以下命令
php luna migrate {{name}}
如果需要撤销最后一次执行的迁移,请使用以下命令
php luna migrate:rollback
每次执行
migrate:rollback
都会返回一个批次,执行最后一个批次将撤销。
要清理数据库并执行迁移,请使用以下命令
php luna migrate:fresh
出于安全考虑,需要通过
--confirm
或-c
来确认执行migrate:fresh
。
模型
模型遵循Illuminate/Eloquent ORM的标准
namespace App\Models; use Luna\Db\Model; class Product extends Model { // ... }
模型的使用
namespace App\Services; use App\Models\Product; class Product { public function find() { return Product::find(1); } public function list() { return Product::all(); } }
SEO
可以为视图显示创建搜索引擎优化(SEO)
namespace App\Controllers; use Luna\Utils\Seo; class Product { public static function getProduct($request, $response) { // ... $seo = new Seo(); $seo->setTitle("Produto nome"); $seo->setDescription("Produto descrição"); $seo->setKeywords(["produto-chave-1", "produto-chave-2"]); $seo->setImage("produto.png"); $seo->setAuthor("Autor nome"); $seoRender = $seo->render(); $content = View::render('pages/product', ['seo' => $seoRender]); } }
函数
$seo->setKeywords()
可以接收数组或字符串中的关键字,例如:$seo->setKeywords("chave-1, chave-2")
。
如果在视图的渲染中未单独定义标题,则在
parent::getPage
中使用$seo->render(true)
以确保<title>
标签由SEO渲染。
Twitter和Meta OpenGraph
Twitter和Meta OG的配置可以单独进行
$seo = new Seo(); $seo->setTitle("Produto nome"); $seo->twitter()->setTitle("Produto nome (Twitter)"); $seo->meta()->setTitle("Produto nome (Meta)");
可以单独配置每个网络的全部tags
$seo->twitter()->setTitle($title); $seo->twitter()->setDescription($description); $seo->twitter()->setCard($card); $seo->twitter()->setSite($site); $seo->twitter()->setImage($image); $seo->twitter()->setUrl($url); $seo->meta()->setTitle($title); $seo->meta()->setDescription($description); $seo->meta()->setUrl($url); $seo->meta()->setImage($image); $seo->meta()->setType($type);
如果Twitter和Meta OG的数据相同,只需以下方式之一告知
$seo = new Seo(); $seo->twitter(); $seo->meta(); $seo->setTitle("Produto nome") // ...
$seo = new Seo(['twitter', 'meta']); $seo->setTitle("Produto nome") // ...
如果在使用
$seo->twitter()
或$seo->meta()
之后使用$seo->setTitle()
和其他设置,标题、描述和图像的定义将被共享。要禁用此功能,请在每个使用时使用$seo->twitter(false)或$seo->meta(false)。
如果类 Seo
未指定 Twitter 或 Meta OG 则会使用 .env
文件中的 DEFAULT_SEO
中的值。
机器人
可以在渲染时添加 Robots 的配置
$seo = new Seo(); $seo->setRobots($index, $follow);
变量 $index
和 $follow
必须是 Boolean
类型。
Robots 的定义示例
// Página indexada e com links seguidos: $seo->setRobots(); // Página não indexada: $seo->setRobots(false); // Página com links não seguidos: $seo->setRobots(true, false); // Página não indexada e links não seguidos: $seo->setRobots(false, false);
默认情况下,索引使用跟随的链接,因此如果使用
$seo->setRobots()
函数而不传递任何参数,它将是可选的。
环境
可以使用 .env
文件来定义项目配置值,这些值可以从文件中获取
use Luna\Utils\Environment; Environment::get($key); // Obter item específico Environment::get(); // Obter todos os items
也可以存储不在 .env
文件中的动态值
Env::set($key, $value);
为项目做出贡献
感谢您考虑为 Luna 做出贡献!贡献指南仍在开发中,不久您将了解如何进行贡献。
在此期间,您可以自行在仓库中进行贡献。
许可证
Luna 是一个遵循 MIT 许可协议 的开源软件。