fragment / elemental
一个没有任何附加字符串的PHP框架,但拥有所有神奇的功能。
Requires
- php: >=8.0.0
README
请注意:此存储库包含适合通过Composer安装的Elemental框架的代码。如果您希望克隆应用程序并在没有Composer等依赖项的情况下运行它,请访问主Elemental存储库。
Elemental是一个从头开始开发的PHP框架,旨在提供动态、用户友好的编码体验。它集成了依赖注入等特性,并遵循MVC架构以简化Web开发并提高代码组织性。以简洁和灵活性为设计理念,它邀请开发者进入一个可以行使无与伦比的控制权并获得对可用工具深刻理解的领域。
特性
演示 - Inkwell
为了展示Elemental的功能,已开发了一个名为Inkwell的完整工作平台。Inkwell是一个独特的空间,致力于讲述故事的纯粹本质。与Elemental的目标一样,Inkwell仅使用纯HTML、CSS、JS和PHP构建,没有任何外部依赖。
- 探索演示: Inkwell
- GitHub存储库: Inkwell GitHub
请随意深入了解这两个实时平台和相应的代码库。探索Inkwell的功能,以了解Elemental如何用于您自己的项目。
了解创建Elemental背后的灵感。
为什么选择Elemental?
Elemental的设计目标是没有任何附加条件。没有对外部库或框架的依赖。目的是给开发者一种真正的控制感——一个独立探索和理解框架所蕴含魔力的门户。
总体目标?让开发者完全拥抱并利用DI容器、ORM、中间件等强大抽象的优雅。但这里的关键是——Elemental不仅指出方向,还把钥匙交给你,解开谜团,让你有能力探索这些抽象在代码中的布局。
事实上,我们鼓励你不仅跟随路径,还要另辟蹊径。深入代码库,剖析抽象,了解它们的内部工作原理。请随意修改和实验,因为Elemental不仅仅是一个框架——它是对塑造和塑造可用工具的开放邀请。因为编程不应该是一个迷宫;它应该是一段旅程。让我们共同踏上这段旅程。🚀
文档
入门
创建您的第一个Elemental应用程序
与其他框架不同,Elemental不依赖于Composer或外部库。它就像在您的系统上安装了良好的旧PHP并克隆存储库一样简单。
克隆存储库
打开您的终端并执行以下命令
git clone https://github.com/aneesmuzzafer/elemental.git
无需担心包管理器或依赖关系——Elemental是从零开始构建的,旨在让您摆脱这些担忧。
或者,通过Composer
对于那些更喜欢Composer路径的用户,只需一个命令即可创建新的Elemental应用程序
composer create-project fragment/elemental sample-app
这将会生成一个包含composer.json
文件的项目。
一旦您的项目准备就绪,请使用我们的命令行引擎Candle的ignite
命令启动Elemental本地开发服务器
cd sample-app
php candle ignite
好了!您的应用程序现在可通过http://127.0.0.1:8000访问。
我们已经处理了基本设置,让您可以专注于魔法。
魔法之旅现在开始了!
依赖注入容器
Elemental最重要的功能是其依赖注入容器,它使用该容器来管理类依赖关系和执行依赖注入。
依赖注入是软件开发中的一种设计模式,它处理组件如何获取其依赖关系。在传统系统中,一个类负责创建自己的依赖关系。使用DI后,创建和提供依赖关系的责任被转移到类外部。不是类创建其依赖关系,而是从外部源“注入”到类中。
DI有助于实现松耦合和更易于维护的代码。它通过允许每个类专注于其特定的功能,而不必担心如何创建或获取其依赖关系来促进关注点的分离。
依赖注入是更广泛概念“控制反转”(IoC)的特定实现。IoC代表一种设计范式,其中程序的流程控制被反转或转交给外部实体、容器或框架。
自动依赖解析
在Elemental中,当您使用依赖注入(DI)时,如果类不依赖于任何其他类,或者仅依赖于具体类(不是抽象接口),则无需显式告诉DI容器如何创建该类的实例。DI容器将自动找出。
容器将尝试创建类的实例,如果该类依赖于其他类,则容器将递归地尝试解决这些依赖关系。这个过程一直持续到所有必要的类都成功解析。因此,您不需要手动指定如何创建每个类——DI容器为您处理。
<?php class MailService { public function __construct(private MailerAgent $mailer) { } } // Inside some other class class UserController { public function sendMail(MailService $mailService) { $mailService->sendMail(); } }
在这里,通过在方法参数中对MailService
进行类型提示,Elemental能够解析该类并创建该类的实例,并将其传递给sendMail
,以便您可以在不担心MailService
类需要哪些依赖关系的情况下使用它。如您所见,MailService
本身依赖于其他类MailerAgent
,然而,Elemental在幕后处理了MailerAgent
类的解析,在创建实例时将其传递给MailService
,并为您提供了该实例。
“那么,这种仅通过类型提示类名进行依赖注入的方法在Elemental中可以在哪里工作?”所有类的constructor
函数、所有controller methods
以及命令创建类的handle
方法。
绑定
在幕后,Elemental通过查看已注册的任何绑定来解析类或接口。换句话说,为了显式告诉框架如何解析特定类或接口的实例,您需要使用Application
实例上的bind
方法注册该类或接口的绑定,包括我们希望注册的类或接口名称以及一个返回该类实例的闭包
app()->bind(MailService::class, function () { // Run some logic, for example, decide on the mail agent to be passed to its constructor depending on some factors. return new MailService(app()->make(MailAgent::class)); });
请注意,通常您只需要在需要运行一些额外的逻辑来解决类,或者需要将接口绑定到具体实现时绑定类。否则,Elemental将解决类,而无需您显式绑定它。
绑定单例
单例方法将类或接口与容器绑定,确保它只被解析一次。在初次解析之后,任何对容器的后续调用,如果请求相同的绑定,都将返回同一个对象实例。
app()->singleton(DatabaseConnection::class, function () { return new DatabaseConnection('localhost', 'username', 'password'); }); // Later in the code $databaseConnection1 = app()->make(DatabaseConnection::class); $databaseConnection2 = app()->make(DatabaseConnection::class); // $databaseConnection1 and $databaseConnection2 will reference the same instance
应用服务提供商
虽然在应用的任何地方注册绑定都是完全可以的,但通常需要在应用启动时进行绑定,以便其他应用组件可以开始使用它。Elemental提供了一个特殊位置来注册应用的所有绑定,并执行应用所需的任何其他启动逻辑。这是App\Bootstrap\AppServiceProvider
。应用服务提供商包含一个register
方法和一个boot
方法。
注册方法
在register
方法中,你应该将东西绑定到依赖注入容器中。但是,你不应该在register
方法中尝试解析任何绑定、路由或运行任何其他功能。否则,你可能会错误地使用一个尚未加载的容器中的服务。
启动方法
该方法在所有其他服务提供商注册之后被调用,允许访问框架注册的所有服务。任何希望执行的初始化逻辑都应该放在这里。
<?php namespace App\Bootstrap; use App\Services\Auth; class AppServiceProvider { public function register(): void { app()->singleton(Auth::class, function () { return new Auth(); }); } public function boot(): void { // Additional initialization logic can be placed here } }
解析一个类
你可以使用make
方法从DI容器中解析类实例。应用实例上的make
方法接受你希望解析的类或接口的名称。
use App\Services\MailService; $mailService= app()->make(MailService::class);
你也可以通过在Application
类上直接使用静态方法instance
来获取应用实例。
use Core\Main\Application; use App\Services\MailService; $mailService = Application::instance()->make(MailService::class);
make
方法在尝试从代码组件中解析类时特别有用,在这些组件中,使用类型提示注入依赖项是不切实际的。在这种情况下,你可以明确请求应用依赖注入容器为你解析实例。
路由
路由定义在app\routes.php
文件中,允许开发者轻松注册各种路由以处理不同的HTTP请求。
路由注册
通过调用Route Facade上的相关方法注册路由,例如Route::get()
,涉及指定URI模式作为第一个参数。第二个参数可以是闭包或定义处理请求的控制器和方法的数组。
例如
<?php use App\Controllers\AuthController; use Core\Facade\Route; Route::get("/settings", function () { // handling logic goes here }); Route::post("/register", [AuthController::class, "register"]);
每当请求URI匹配时,相应的闭包或控制器方法将被执行,并生成响应并发送回浏览器。
可用的路由方法
你可以使用以下方法注册响应任何HTTP动词的路由
Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);
路由参数
有时你需要在路由中捕获URI的片段。例如,你可能需要从URL中捕获用户的ID。你可以通过定义路由参数来实现这一点
Route::get('/user/{id}', function (string $id) { return 'User ' . $id; }); Route::get("/story/{id}", function ($id) {/*...*/});
你可以根据需要定义任意多的路由参数
Route::post("story/edit/{id}", [StoryController::class, "edit"]); Route::get("story/{story_id}/comment/{comment_id}", [StoryController::class, "comment"]);
这些将被传递到控制器方法中。
依赖注入
Elemental无缝处理控制器方法所需的必要依赖注入。这允许你通过类型提示在回调签名中指定路由所需的任何依赖项。Elemental负责自动解析和将声明的依赖项注入到回调中。
例如,如果你在回调中类型提示Core\Request\Request
,Elemental将确保当前HTTP请求自动注入到你的路由回调中
<?php use Core\Request\Request; Route::get('/users', function (Request $request) { // ... });
你可以以任何顺序放置类型化的依赖项和路由参数。
路由模型绑定
当您将模型ID作为参数传递给路由或控制器操作时,常见的做法是通过数据库查询以该ID为依据获取相应的模型。Elemental通过路由模型绑定简化了这一过程,为您提供了方便地将模型实例自动注入到路由中的方法。
例如,您可以选择将整个与给定ID对应的用户模型实例注入到路由中,而不仅仅是注入用户的ID。
在路由或控制器操作的上下文中,模型使用与路由特定段匹配的类型提示变量名定义。例如
use App\Models\User; Route::get('/users/{user}', function (User $user) { return $user->email; });
自定义键
有时您可能希望使用除id
之外的列来解析模型。为此,您可以在路由参数定义中指定该列
use App\Models\User; Route::get('/users/{user:email}', function (User $user) { return $user; });
在这种情况下,Elemental将无缝注入具有与请求URI中相应值匹配的电子邮件的模型实例。
当然,路由模型绑定也适用于控制器方法。
找不到模型
如果数据库中没有找到匹配的模型实例,应用将抛出ModelNotFoundException
异常。您可以在ExceptionsHandler
类中处理此类异常以及应用抛出的其他异常,有关详细信息稍后介绍。
后备路由
使用Route::fallback
方法,您可以定义一个路由,当没有其他路由匹配传入请求时执行。
Route::fallback(function () { // ... });
路由列表
route:list
Candle命令将提供应用中定义的所有路由的列表
php candle route:list
控制器
而不是在路由文件中将所有请求处理逻辑都集中在闭包中,考虑通过“控制器”类来组织这种行为。控制器允许您将相关的请求处理逻辑组织到一个连贯的类中。例如,UserController
类可以管理与用户相关的各种传入请求,例如显示、创建、更新和删除用户。这些控制器类通常存储在app/Controllers
目录中。
基本控制器
要生成新的控制器,您可以运行build:controller
Candle命令。
php candle build:controller UserController
这将生成一个名为"UserController.php"的新文件,位于app/Controllers
目录中。
控制器可以有任何数量的公共方法,这些方法将响应传入的HTTP请求
<?php use App\Services\Auth; namespace App\Controllers; class AuthController { public function showRegister() { return view("Register")->withLayout("layouts.DashboardLayout"); } public function logout() { Auth::logout(); redirect("/"); } }
创建控制器类及其方法后,您可以按照以下方式定义到控制器方法的路由
use App\Controllers\UserController; Route::get("/register", [AuthController::class, "showRegister"]);
当接收到的请求与指定的路由URI匹配时,App\Controllers\UserController
类中的showRegister
方法将被调用,并且该方法将接收相应的路由参数。
依赖注入和控制器
构造函数注入
Elemental服务容器负责解析所有控制器的实例。因此,您可以在控制器的构造函数中使用类型提示来指定它可能需要的任何依赖项。所声明的依赖项将被自动解析并注入到控制器实例中
<?php namespace App\Controllers; use Core\Database\Database; class UserController { /** * Create a new controller instance. */ public function __construct( public Database $db, ) {} }
方法注入
除了通过构造函数注入依赖项之外,您还可以在控制器的方法中使用类型提示来指定依赖项。方法注入的一个常见用例是将Core\Request\Request
或任何服务实例注入到控制器方法中
创建和管理控制器以有效地处理请求。
<?php namespace App\Controllers; use Core\Request\Request; use App\Services\Auth; class StoryController { public function create(Request $request) { $data = $request->data(); $user = Auth::user(); $story = Story::create([...]); return redirect("/story/$story->id"); } }
如果您的控制器方法预期从路由参数中接收输入,您可以根据需要以任何顺序列出您的参数。例如,考虑以下路由定义
Route::post("story/update/{id}", [StoryController::class, "update"]);
您仍然可以通过以下方式定义控制器方法来类型提示Core\Request\Request
并通过定义访问您的id
参数
<?php namespace App\Controllers; use Core\Request\Request; class StoryController { public function update(string $id, Request $request) { // Update the story... return redirect("/story/$story->id"); } }
请求
Elemental中的Core\Request\Request
类提供了一种面向对象的方法来与您的应用程序管理的当前HTTP请求进行交互。它简化了检索请求中提交的输入、cookie和文件的过程。
访问请求
要通过依赖注入获取当前的HTTP请求实例,您可以在路由闭包或控制器方法中为Core\Request\Request
类使用类型提示。服务容器将自动注入传入的请求实例。
<?php namespace App\Controllers; use App\Models\Category; use Core\Request\Request; class CategoryController { public function store(Request $request) { $name = $request->data()["name"]; $category = Category::where(["name" => $name]); if ($category) { return view("Category", ["categories" => Category::all(), "msg" => "Category already exists!"])->withLayout("layouts.DashboardLayout"); } Category::create($request->data()); redirect("/category"); } }
服务容器还会自动将传入的请求注入到路由闭包中。
依赖注入和路由参数
如果您的控制器方法预计将从路由参数接收输入,您可以根据需要以任何顺序列出您的参数。例如,考虑以下路由定义
Route::post("story/update/{id}", [StoryController::class, "update"]);
您仍然可以通过以下方式定义控制器方法来类型提示Core\Request\Request
并通过定义访问您的id
参数
<?php namespace App\Controllers; use Core\Request\Request; class StoryController { public function update(string $id, Request $request) { // Update the story... return redirect("/story/$story->id"); } }
请求数据
您可以使用data()
方法获取所有传入请求的输入数据,并将其作为一个array
。无论传入请求是来自HTML表单还是XHR请求,都可以使用此方法
$data = $request->data();
检索输入值
您可以通过Request
实例访问所有用户输入,无需担心请求使用了哪种HTTP动词。无论HTTP动词如何,都可以使用data
方法来检索用户输入
$name= $request->data()["name"];
请求URI、方法和头部
Core\Request\Request
实例提供了各种方法来检查传入的HTTP请求。下面让我们讨论一些最重要的方法。
请求头部
您可以使用headers
方法从Core\Request\Request
实例中检索请求头部。
$headers = $request->headers();
请求方法
您可以通过在Core\Request\Request
实例上调用method
来检索请求方法。
$method = $request->method();
请求URI
您可以使用uri
方法从Core\Request\Request
实例中检索请求URI。
$uri = $request->uri();
请求Cookies
您可以使用cookies
方法从Core\Request\Request
实例中检索请求Cookies。
$cookies = $request->cookies();
原始内容
您可以使用rawContent
方法从Core\Request\Request
实例中检索原始内容。
$content = $request->rawContent();
处理请求的原始内容时要小心。
文件
您可以使用files
方法从Core\Request\Request
实例中检索文件。
$files= $request->files();
请求IP地址
可以使用ip
方法检索向您的应用程序发送请求的客户端的IP地址。
$ipAddress = $request->ip();
端口号
可以使用port
方法检索向您的应用程序发送请求的客户端的端口号。
$port= $request->port();
内容类型
您可以使用contentType
方法从Core\Request\Request
实例中检索内容类型。
$contentType = $request->contentType();
查询字符串
您可以使用queryString
方法检索请求的查询字符串。
$query= $request->queryString();
特定内容类型
文本
如果内容类型设置为text/plain
,则可以使用text
方法检索请求的文本内容。
$text= $request->text();
JavaScript
如果内容类型设置为application/javascript
,则可以使用js
方法检索请求的JavaScript内容。
$js= $request->js();
HTML
如果内容类型设置为text/html
,则可以使用html
方法检索请求的HTML内容。
$js= $request->html();
JSON
如果内容类型设置为application/json
,则可以使用json
方法检索请求的JSON内容。使用$request->data()
返回传递给请求的所有JSON数据。然而,
$jsonData = $request->json();
$request->data()
包含所有JSON数据和通过请求中的查询参数传递的输入。但是,可以使用$request->json()
仅检索JSON内容。
XML
如果内容类型设置为application/json
,则可以使用xml
方法检索请求的XML内容。
$xmlData = $request->xml();
响应
每个路由和控制器都应生成响应以供用户浏览器接收。Elemental提供各种生成响应的方法。最简单的响应形式是从路由或控制器直接返回一个字符串。框架将无缝地将此字符串转换为完整的HTTP响应。
Route::get('/', function () { return 'Hello World'; });
除了从您的路由和控制器中返回字符串外,您还可以返回数组或对象。框架将自动将它们转换为JSON响应
Route::get('/', function () { return [1, 2, 3]; });
响应对象
通常,您不会仅仅从路由动作中返回简单的字符串或数组。相反,您通常会返回Core\Response\Response
或视图的完整实例。
返回完整的Response
实例允许您自定义响应的HTTP状态码和头部。您可以通过在控制器或路由闭包中类型提示响应实例来注入响应实例。
use Core\Response\Response; Route::get('/home', function(Response $response) { $response->setHeader("content-type", "text/plain") ->setStatusCode(200) ->setContent("Hello World"); return response; });
当然,您也可以从控制器中返回一个view
。但是,如果您需要控制响应的状态和头部,同时也需要返回一个view
作为响应的内容,可以按照以下方式操作
use Core\Response\Response; class UserController { public function register(Response $response){ $response->setHeader("x-is_register", "true"); return view("Register"); } }
这将在发送到浏览器的视图响应上自动设置头部。
响应内容
请注意,大多数响应方法都是链式调用的,允许流畅地构建响应实例。
您可以使用响应实例上的setContent
方法设置响应的内容。
$response->setContent("...");
但是,如果您想向响应内容中追加内容,您可以使用响应实例上的appendContent
方法。
$response->appendContent("...");
响应头部
您可以使用setHeader
方法在响应实例上设置一个头部。
$response->setHeader("content-type", "text/plain");
但是,如果您想同时设置多个头部,您可以使用setHeaders
方法并传递一个头部数组。
$response->setHeaders(["content-type" => "text/html", ...]);
响应状态码
您可以直接使用响应实例上的setHeader
方法设置响应的状态码。
$response->setStatusCode(301);
对于常见状态码,将默认设置状态文本。
重定向
您可以通过在Core\Response\Response
类上调用静态方法redirect
生成包含将用户重定向到另一个URL所需的正确头部的重定向响应。
use Core\Response\Response; Route::get('/dashboard', function () { return Response::redirect('home/dashboard'); });
但是,为了简单起见,还有一个全局的辅助方法redirect()
可以实现相同的功能。
use Core\Response\Response; Route::post('/story/create', function () { if(!some condition) return redirect('/story', 204); });
JSON响应
您还可以通过在Core\Response\Response
类上调用静态方法JSON
生成JSON响应。传递给方法的数据将被转换为适当的JSON。您还可以可选地传递状态码和头部数组作为函数的第二个和第三个参数。
use Core\Response\Response; Route::post('/post', function () { $post = (...); return Response::JSON($post, 201, ["header"=>"value"]); });
中间件
中间件提供了一种方便的机制来检查和过滤发送到应用程序的传入HTTP请求。例如,您可以开发中间件来验证应用程序用户的认证状态。如果用户未认证,中间件将它们重定向到登录屏幕。相反,如果用户已认证,中间件将允许请求进一步深入到应用程序。
您有灵活性来创建额外的中间件以执行超出认证的各种任务。例如,一个日志中间件可以记录所有传入应用程序的请求。这些中间件组件位于app/middlewares
目录中。
创建中间件
要创建一个新的中间件,请使用build:middleware
Candle命令
php candle build:middleware IsAuthenticated
执行此命令将在app/middlewares
目录中生成一个名为"IsAuthenticated"的新中间件类。在这个类中,创建了一个名为handle
的方法,您可以在其中表达中间件的逻辑。
在这里,我们只允许用户在认证的情况下访问路由,否则,我们将用户重定向回login
URI。
<?php namespace App\Middlewares; use App\Services\Auth; use Closure; use Core\Request\Request; class IsAuthenticated { public function handle(Request $request, Closure $next) { if (!(/* authentication logic */)) { return redirect("/login"); } return $next($request); } }
要传递请求深入到应用程序,您应该使用$request
调用$next
回调。
将中间件视为一系列“层”,HTTP 请求在到达您的应用程序之前会穿越这些层。每一层都有审查请求并可能拒绝它的能力。
当然,中间件可以在将请求传递到应用程序更深层次之前或之后执行任务。例如,这个中间件会在请求被应用程序处理后执行其任务。
<?php namespace App\Middlewares; use Closure; use Core\Request\Request; class AfterMiddleware { public function handle(Request $request, Closure $next) { $response = $next($request); // Perform action return $response; } }
将中间件分配给路由
如果您想将中间件分配给特定的路由,可以在定义路由时调用 middleware
方法。
Route::get('/profile', function () { // ... })->middleware(IsAuthenticated::class);
您可以通过将中间件名称的数组传递给 middleware
方法来为路由分配多个中间件。
Route::get('/', function () { // ... })->middleware([First::class, Second::class]);
将中间件分配给路由组
您可以通过在定义组时将中间件名称的数组传递给属性 middlewares
来将中间件分配给路由组。
Route::group(["middleware" => [HasSession::class]], function () { Route::get("/", [StoryController::class, "index"]); Route::get("/story/{story}", [StoryController::class, "show"]); });
您可以使用嵌套路由组将中间件与其父组结合。在下面的示例中,“HasSession” 中间件应用于 "/"
和 "/story/{story}"
路由,而“HasSession”、“IsAuth” 和 “Log” 中间件应用于其他路由。
Route::group(["middleware" => [HasSession::class]], function () { Route::get("/", [StoryController::class, "index"]); Route::get("/story/{story}", [StoryController::class, "show"]); Route::group(["middleware" => [IsAuth::class, Log::class]], function () { Route::get("/compose", [StoryController::class, "compose"]); Route::post("/compose", [StoryController::class, "create"]); }); });
视图、布局和组件
视图
在 Elemental PHP 框架中,直接从路由和控制器返回整个 HTML 文档字符串是不切实际的。视图提供了一种方便的方式,将所有 HTML 放入单独的文件中。
视图在将控制器/应用程序逻辑与展示关注点分离中起着至关重要的作用,并存储在 app/views
目录中。这些视图文件,用 PHP 编写,封装了标记。考虑一个基本的视图示例
<html> <body> <h1>Hello, <?= $name ?></h1> </body> </html>
如果这个视图存储在 app/views/Welcome.php
,则可以使用全局 view
辅助器在路由中返回它。
Route::get('/', function () { return view('Welcome', ['name' => 'Ahmed']); });
传递给 view
辅助器的第一个参数对应于 resources/views
目录中视图文件的名字。第二个参数可以是一个数组,将键值对传递给视图。例如,在上面的代码中,$name
将直接可用并包含值 'Ahmed'。
您还可以使用 Core\View\View
类上的静态方法 make
返回视图。
Route::get('/', function () { return View::make("Post", $params); });
嵌套视图目录
视图可以嵌套在 app/views
目录的子目录中。"点" 表示法可以用来引用嵌套视图。例如,如果您的视图存储在 app/views/layouts/MainLayout.php
,则可以像这样从路由/控制器返回它
return view('layouts.MainLayout', $data);
布局
Elemental 提供了一种方便的方法来维护多个视图中的相同布局,从而减少了代码重复。布局本身是一个包含占位符 {{ content }}
的视图文件。当视图与布局一起返回时,最终视图通过将视图放入布局的内容中编译。
Elemental 提供了一种方便的方法来维护多个视图中的相同布局,从而减少了代码重复。布局是一个包含指定占位符的视图文件,由 {{ content }}
表示。当使用特定的布局返回视图时,通过将视图的内容嵌入布局中的指定占位符来达到组合效果。这种方法简化了视图的组织,并通过集中常见的布局元素来提高代码的可维护性。
以下是一个基本的示例
<!DOCTYPE html> <html lang="en"> <head> <!-- Head content --> </head> <body> <?= component("components.Navbar") ?> <div style="min-height: calc(100vh - 140px);"> {{ content }} </div> </body> </html>
可以像这样返回带有布局的视图
public function compose() { return view("Compose")->withLayout("layouts.DashboardLayout"); }
组件
Elemental 提供了一种强大的方法来构建视图。每个视图本质上都是一个组件,任何视图都可以由其他组件组合而成。这是一个由每个部分共同创造和谐且动态整体的交响曲。
示例组件文件(views/components/Logo.php
)
<a class="logo" href="/"> <span class="logo-img"> <img src="logo.png" class="logo-text"> LOGO </span> </a>
此组件可以用于任何其他视图文件中。例如,在 views/Login.php
<div> <?= component("components.Logo") ?> <p>Welcome Back!</p> <!-- Other login form elements --> </div>
因此,Elemental 使您能够同时使用布局和组件结构,允许您以自顶向下和自底向上的方法组合视图。这种灵活性实现了无缝融合,您可以轻松混合和组合元素来为您的应用程序创建优雅且复杂的用户界面。
数据库
在现代网络应用中,数据库交互是一个基本方面。Elemental被设计用来在不同的支持数据库之间无缝地简化这种交互,利用PHP PDO的固有功能。使用Elemental,您可以使用Core\Database\Database
类执行任何复杂的查询或事务。
Elemental提供了一种强大的对象关系映射器(ORM),有效地抽象了许多复杂性,对于大多数数据库查询来说非常有价值。然而,您可以使用Core\Database\Database
运行更高级的SQL查询。
您应用的Elemental App的所有配置都位于您应用的app/config/config.php
配置文件中。在这里,您可以定义所有数据库连接,并指定默认使用哪个连接。此文件中的大多数配置选项由您应用的环境变量值驱动。
数据库配置
您应用的Elemental App的所有配置都位于您应用的app/config/config.php
配置文件中。在这里,您可以定义所有数据库连接,并指定默认使用哪个连接。此文件中的大多数配置选项由您应用的环境变量值驱动。
<?php return [ "db" => [ "driver" => getenv("DB_DRIVER") ?? "mysql", "host" => getenv("DB_HOST") ?? $_SERVER['SERVER_ADDR'], "port" => getenv("DB_PORT") ?? "3306", "database" => getenv("DB_DATABASE") ?? "elemental", "username" => getenv("DB_USERNAME") ?? "root", "password" => getenv("DB_PASSWORD") ?? "", ], ];
Elemental使用PDO作为底层的数据库处理类。所有PDO函数都直接在Core\Database\Database
类上可用。您可以将Core\Database\Database
的实例注入到任何构造函数或控制器方法中来调用PDO方法。Elemental的默认配置是为MySQL数据库设置的,但您可以在配置文件中更改驱动程序。
运行查询
以下是通过Database
实例运行查询的示例
public function tokens(Database $db) { $user_id = 1; $sql = "SELECT * FROM access_tokens WHERE user_id = :user_id"; $stmt = $db->prepare($sql); $stmt->bindValue(":user_id", $user_id); $stmt->execute(); $tokens = $stmt->fetchAll(); }
有关PDO的更多信息,您可以参考PHP的PDO文档
对象关系映射器(模型)
Elemental包含一个定制的对象关系映射器(ORM),使与数据库的交互变得愉快。当使用ORM时,每个数据库表都有一个相应的“模型”用于与该表交互。除了从数据库表中检索记录外,模型还允许您向表中插入、更新和删除记录。
创建模型
模型位于app/models
目录中,并扩展了Core\Model\Model
类。您可以使用build:model
Candle命令生成一个新模型。
php candle build:model Post
约定
通过build:model
命令生成的模型将放置在app/Models
目录中。一个非常基本的模型具有以下结构
<?php namespace App\Models; use Core\Model\Model; class Post extends Model { // ... }
表名:按照约定,类的“snake case”(蛇形命名法)、复数名称将用作表名,除非显式指定其他名称。因此,在这种情况下,Elemental将假定Post
模型将记录存储在posts
表中。
您可以通过在模型上定义tableName
属性来手动指定模型的表名
<?php namespace App\Models; use Core\Model\Model; class Post extends Model { protected $tableName = 'elemental_posts'; }
主键
Elemental还将假定每个模型对应的数据库表都有一个名为id
的主键列。如有必要,您可以在模型上定义一个受保护的$primaryKey
属性来指定作为模型主键的不同的列
<?php namespace App\Models; use Core\Model\Model; class Post extends Model { protected $primaryKey = 'elemental_id'; }
模型操作
您可以将每个模型视为一个强大的查询构建器,允许您流畅地查询与模型关联的数据库表。
所有
模型 的all
方法将检索与模型关联的数据库表中的所有记录
use App\Models\Story; foreach (Story::all() as $story) { echo $story["content"]; }
默认情况下,检索到的记录表示为数组。但是,您可以传递一个模式参数来控制如何表示每个记录。模式参数可以接受任何PDO获取模式。例如,
use App\Models\Story; foreach (Story::all() as $story) { echo $story->content; }
所有条件
allWhere
方法是在模型中的一种强大抽象,允许执行复杂查询。此方法接受三个参数:conditions
、options
和fetchMode
。
方法签名
public static function allWhere(array $conditions, array $options = [], int $fetchMode = PDO::FETCH_ASSOC)
条件: conditions
参数是一组必须满足的记录的子句。每个条件可以是[key => value]
对或[key => [operator, value]]
对。
key
对应于表中的特定列。- 如果条件形式为
[key => value]
,则默认运算符是=
,而value
是该列中记录的数据。 - 如果条件形式为
[key => [operator, value]]
,您可以指定每个条件的运算符。支持的运算符包括['=', '!=', '<', '>', '<=', '>=', 'LIKE', 'IS NULL', 'IS NOT NULL']
.
选项: options
参数是一个数组,用于确定额外的查询参数,例如 order by
、limit
等。在选项参数中支持的结构包括
"orderBy"
"limit"
"offset"
"sortDir"
获取模式: fetchMode
参数控制如何表示每个获取的记录。模式参数采用任何 PDO 获取模式
PDO::FETCH_ASSOC
PDO::FETCH_NUM
PDO::FETCH_BOTH
PDO::FETCH_OBJ
PDO::FETCH_CLASS
PDO::FETCH_INTO
PDO::FETCH_LAZY
PDO::FETCH_KEY_PAIR
以下示例将使它更清晰
use Core\Request\Request; class StoryController { const PAGE_SIZE = 10; public function index(Request $request) { $search = $request->search; $categoryId = $request->category_id; $sortBy = $request->sort_by; // ASC or DESC, Default = ASC $page = $request->page; $orderBy = $request->order_by; return Story::allWhere( [ "category_id" => $categoryId, "title" => ["LIKE", "%$search$"], ], [ "limit" => static::PAGE_SIZE, "orderBy" => $orderBy, "sortDir" => $sortBy, "offset" => ($page - 1) * static::PAGE_SIZE, ], PDO::FETCH_OBJ ); } }
获取单个记录
除了检索与给定查询匹配的所有记录外,您还可以使用 find
和 where
方法检索单个记录。这些方法不是返回记录数组,而是返回单个模型实例
Find: 这将获取与表主键匹配的第一个记录。
$flight = Story::find(1);
Where: where 方法接受一个数组,该数组包含记录必须满足以进行获取的条件。每个条件可以是 [key => value]
对或 [key => [operator, value]]
对。
key
对应于表中的特定列。- 如果条件形式为
[key => value]
,则默认运算符是=
,而value
是该列中记录的数据。 - 如果条件形式为
[key => [operator, value]]
,您可以指定每个条件的运算符。支持的运算符包括['=', '!=', '<', '>', '<=', '>=', 'LIKE', 'IS NULL', 'IS NOT NULL']
.
例如
$user = User::where(["email" => $email]); $liked = Like::where(["user_id" => $user->id, "story_id" => $story_id]);
创建
要将新记录插入数据库,您可以创建一个新的模型实例并设置模型上的属性。然后,在模型实例上调用 save
方法
<?php namespace App\Controllers; use App\Models\Story; use Core\Request\Request; class StoryController { public function store(Request $request) { $story = new Story; $story->name = $request->name; $story->save(); return redirect('/story'); } }
在此示例中,我们将传入的 HTTP 请求中的 name
字段分配给 App\Models\Story
模型实例的 name
属性。当我们调用 save
方法时,将向数据库中插入一个记录。模型的 created_at
时间戳将在调用 save
方法时自动设置,因此无需手动设置。
或者,您可以使用静态 create
方法使用单个 PHP 语句“保存”新模型。由 create
方法返回的插入模型实例将返回给您
use App\Models\Story; $story = Story::create([ 'name' => 'A tale of elemental magic', ]);
更新
您也可以使用 save
方法来更新数据库中已存在的模型。要更新模型,您应该检索它并设置您希望更新的任何属性。然后,您应该调用模型的 save
方法。
use App\Models\Story; $story = Story::find(10); $story->name = 'An elemental tale of magic'; $story->save();
或者,您可以使用静态 update
方法更新模型实例。第一个参数是模型的 id,第二个参数需要是列值对的数组。
use App\Models\Story; $story = Story::update(10, ["name" => "A tale", "content" => "Once upon a time ...."]);
删除
要删除模型,您可以在模型实例上调用 destroy
方法
use App\Models\Story; $story = Story::find(12); $story->destroy();
但是,如果您知道模型的 主键,您可以通过调用 delete
方法来删除模型而无需显式检索它。返回已删除记录的 id
。
use App\Models\Story; Story::delete(12);
数据
您可以通过在模型上调用 data
方法以数组形式检索模型实例的所有属性。
$user = User::find(10); $user_data = $user->data();
烛光(命令行引擎)
烛光是 Element 的命令行引擎,它作为 candle
脚本存在于应用程序的根目录,并提供了许多有助于您进行应用程序开发过程的命令。要查看所有可用的烛光命令列表,您可以使用 help
命令
php candle help
这还将显示您自己创建的自定义命令。
开发服务器
到如今,您应该已经通过ignited
了Elemental的蜡烛来运行您的应用程序。此ignited
命令在IP地址127.0.0.1上提供服务,从8000端口开始寻找可用端口。如果端口8000已被占用,Elemental将自动尝试绑定到下一个可用的端口(例如,8001)等等。
php candle ignite
您可以根据自己的需求灵活定制服务器设置。
自定义主机 使用--host
参数指定特定的IP地址。例如
php candle ingite --host=192.168.1.10
自定义端口 如果您希望绑定到特定的端口,请使用--port
参数
php candle ingite --port=8080
要同时以自定义IP和端口提供服务,请提供--host
和--port
参数
php candle ingite --host=192.168.1.10 --port=8080
--host
和--port
参数可以按任何顺序放置。
列出所有路由
要获取您应用程序中所有已注册路由的全面视图,请使用Candle提供的route:list
命令
bash
php candle route:list
生成文件
您可以使用Candle的build
命令生成模型、控制器、中间件和命令的文件。
生成模型
要创建模型,请执行以下命令
php candle build:model Story
此命令将在app\models
目录中生成一个名为Story.php
的文件,其中包含Story
类。
生成控制器
对于生成控制器,同样使用build
命令
php candle build:controller StoryController
执行此命令将在app\controllers
目录中生成一个名为StoryController.php
的文件,其中包含MyController
类。
生成中间件
要生成中间件,请按照如下方式使用build
命令
php candle build:middleware HasSession
这将创建一个名为HasSession.php
的文件,位于app\middleware
目录中,包含handle
方法。
生成命令
对于命令生成,请使用带有适当参数的build
命令
php candle build:command Migration
执行此命令将在app\commands
目录中生成一个名为Migration.php
的文件,其中包含Migration
类和handle
方法。
Candle中的自定义命令
生成自定义命令是体验Candle强大功能的地方。命令存储在app/commands
目录中,并且对于正确注册到应用程序中,必须在app\commands\Commands.php
中返回的数组中加载它们。
命令结构
在生成命令后,定义类的key
和description
属性的值。`key`用作命令的参数,而`description`将在帮助屏幕中显示。当执行命令时将调用handle
方法,您可以将您的命令逻辑放在此方法中。
您可以为命令处理所需的任何依赖项进行类型提示。Elemental的DI容器将自动将handle
方法签名中类型提示的所有依赖项注入。
让我们看看一个示例命令
<?php namespace App\Commands; use App\Models\User; use App\Service\MailService; use Core\Console\Command; class SendEmails extends Command { protected $key = 'mail:send'; protected $description = 'Send mails to all users'; public function handle(MailService $mailService): void { $mailService->send(User::all()); } }
要执行命令行中的命令
php candle mail:send
检索输入参数
您可以使用Elemental的Core\Console\Commander
检索通过命令行传递的任何输入。`Core\Console\Commander`提供了一个名为`getArgs`的方法,它返回一个从命令行传递的输入数组。Commander实例可以通过处理方法进行类型提示,并按需使用。
一个具体的例子会使它更清楚
<?php namespace App\Commands; use Core\Console\Command; use Core\Console\Commander; class Migration extends Command { protected $key = "migrate"; protected $description = "Custom migration handler."; private $commander; public function handle(Commander $commander, Database $db) { $args = $commander->getArgs(); if (!isset($args[1])) { $this->up(); return; } switch ($args[1]) { case "fresh": $this->downThenUp(); break; case "delete": $this->down(); break; default: $this->up(); } } public function up() { $sql = "CREATE TABLE IF NOT EXISTS users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, bio TEXT, image VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"; try { $db->exec($sql); console_log("Table 'users' created successfully!"); } catch (\PDOException $e) { console_log("Table creation error: " . $e->getMessage()); } } public function down() { $sql = "DROP TABLE IF EXISTS users"; try { $db->exec($sql); console_log("Table 'users' deleted successfully!"); } catch (\PDOException $e) { console_log("Table deletion error: " . $e->getMessage()); } } public function downThenUp() { $this->down(); $this->up(); } }
建议在`handle`方法中而不是在命令类的构造函数中类型提示依赖项。
要执行这些迁移命令
php candle migrate php candle migrate fresh php candle migrate delete
如您所见,生成命令非常强大,可以帮助实现各种功能。在这里,已经构建了一个自定义迁移处理程序。您可以扩展和组织上述结构,或创建一个自定义迁移服务来处理您的迁移逻辑。
命令也可以用于处理任务调度。您可以创建一个执行某些逻辑的命令,然后将该命令传递给操作系统的CRON处理器。
助手
Elemental包含多种全局“辅助”PHP函数。您可以使用这些函数以方便您的方式使用。
app()
app
函数返回Application
实例
$app = app();
当您想注册自己的服务以及解析任何框架或自定义服务时,这非常有用。
app()->bind(CustomService::class, function () { return new CustomService(new anotherService()); }); $service = app()-make(CustomService::class);
dump()
dump
函数将第一个参数传递的变量输出。您还可以传递第二个附加参数,该参数可以作为屏幕上的标识符
dump($value); dump($user, "user");
dd()
dd
函数将给定的变量输出并结束脚本的执行
dd($value); dd($user, "user");
console_log()
console_log
函数是一个独特的日志变量工具,与dump
函数不同。值得注意的是,它不会向浏览器返回输出;相反,它将信息传递到由脚本启动的控制台。您可以向console_log
函数传递任意数量的参数。
console_log($value); console_log($user, $post, $image, $comment);
router()
router
函数返回Router
实例。
view()
view
函数用于从控制器方法返回视图。
return view('Login');
component()
component
函数用于返回作为组件用于另一个视图的视图。
<body> <?= component("Logo")?> //... </body>
redirect()
redirect
函数返回重定向HTTP响应,并用于重定向到任何其他路由。
return redirect('/home');
异常处理器
Elemental提供了一种方便的方式来处理应用抛出的所有异常。
App\Exceptions\Handler
类的handle
方法是在异常渲染给用户之前,所有由您的应用抛出的异常都通过的地方。默认情况下,应用抛出的异常将被格式化,并将结构化响应发送回浏览器。然而,在handle方法中,您可以在响应发送回之前拦截任何异常并执行自定义逻辑。
您甚至可以发送回自定义视图或响应。
处理器类
<?php namespace App\Exceptions; use Core\Exception\ExceptionHandler; class Handler extends ExceptionHandler { public function handle($e) { // Perform some processing here // You can customize the handling of exceptions based on your requirements } }
处理特定异常
Elemental默认定义了一些特定的异常类。
AppException
ModelNotFoundException
RouteNotFoundException
RouterException
ViewNotFoundException
如果您需要以不同的方式处理不同类型的异常,您可以相应地修改handle
方法。
<?php class Handler extends ExceptionHandler { public function handle($e) { if ($e instanceof ModelNotFoundException || $e instanceof RouteNotFoundException) { return view("404")->withLayout("layouts.DashboardLayout"); } if ($e instanceof ViewNotFoundException) { return view("Home"); } // Handle other specific exceptions as needed } }
您可以根据需要从基类Exception
扩展创建自己的异常类,然后按照要求处理。
请根据您应用程序的具体需求自定义handle
方法。
配置
应用程序的所有配置设置都集中存储在app\config\config.php
文件中。这些配置包括数据库连接信息和其他对您的应用程序至关重要的核心设置。
特定环境的配置
为了适应应用程序可能运行的不同环境,在根目录中提供了一个.env.example
文件。此文件概述了可以配置的常见环境变量。如果您在一个团队中工作,建议包含带有占位符值的.env.example
文件。这使得其他开发者清楚哪些环境变量是运行应用程序所必需的。
当您的应用程序收到请求时,所有列出的.env
文件中的变量都将加载到PHP超级全局变量$_ENV
中。然后您可以使用getenv
函数从这些变量中检索值,在您的配置文件中使用。
$appName = getenv("APP_NAME");
访问配置值
要访问配置值,您可以使用类型提示并在构造函数、控制器方法或路由闭包中注入Core\Config\Config
类。
use Core\Config\Config; class YourClass { public function __construct(Config $config) { $driver = $config->db["driver"]; $host = $config->db["host"]; $port = $config->db["port"]; } // Your other methods or code here }
这样做,您在应用程序中检索配置值的方式既干净又有序。
这种方法使配置集中化,并根据环境进行易于更改。它还促进了干净且易于维护的代码库。
外观
Elemental 引入了一个受 Laravel 启发的 Facade 系统,为应用程序的依赖注入(DI)容器中的类提供了一个便捷且富有表达力的静态接口。Facades 作为服务容器中类的静态代理,在简洁的语法与传统的静态方法的可测试性和灵活性之间提供了平衡。
在 Elemental 中,Core\Facade\Route
作为 Facade,提供了一个静态接口,允许您在 routes.php
文件中使用它,如下所示:
// routes.php <?php use Core\Facade\Route; Route::get("/register", [AuthController::class, "showRegister"]); Route::get("/login", [AuthController::class, "showLogin"]); Route::get("/logout", [AuthController::class, "logout"]); Route::post("/register", [AuthController::class, "register"]);
创建您的自定义 Facade
要为任何类创建自定义 Facade,请按照以下步骤操作:
- 创建一个名为
FacadeClass
的类,该类扩展了Core\Facade\Facade
类。 - 在这个类中,实现一个名为
getFacadeAccessor
的静态方法,返回 DI 容器中关联实例的类字符串。
以下是一个创建 PaymentGateway
Facade 的示例:
<?php use Core\Facade\Facade; use Core\Services\PaymentGateway; class PaymentGatewayFacade extends Facade { protected static function getFacadeAccessor() { return PaymentGateway::class; } }
现在,您可以通过调用相应的 FacadeClass
的静态方法来访问自定义类的实例方法。
灵感
LARAVEL 是 神奇的。就像任何无辜的麻瓜一样,它的魔法让您感到恐惧。直到有一天,您鼓起勇气拿起魔杖开始挥舞,然后,您就会 爱上 它。
许可证
Elemental 框架是开源软件,使用 MIT 许可证 发布。
贡献
欢迎所有贡献。请首先为任何功能请求或错误创建一个问题。然后,Fork 存储库,创建一个分支,并对错误进行修复或添加功能,然后创建一个 pull request。就是这样!感谢!
支持
有关错误报告、功能请求或一般问题,请使用 问题跟踪器。