fragment/elemental

一个没有任何附加字符串的PHP框架,但拥有所有神奇的功能。

v1.0.0 2024-01-16 14:48 UTC

This package is auto-updated.

Last update: 2024-09-16 18:45:22 UTC


README

License

请注意:此存储库包含适合通过Composer安装的Elemental框架的代码。如果您希望克隆应用程序并在没有Composer等依赖项的情况下运行它,请访问主Elemental存储库

Elemental是一个从头开始开发的PHP框架,旨在提供动态、用户友好的编码体验。它集成了依赖注入等特性,并遵循MVC架构以简化Web开发并提高代码组织性。以简洁和灵活性为设计理念,它邀请开发者进入一个可以行使无与伦比的控制权并获得对可用工具深刻理解的领域。

特性

演示 - Inkwell

为了展示Elemental的功能,已开发了一个名为Inkwell的完整工作平台。Inkwell是一个独特的空间,致力于讲述故事的纯粹本质。与Elemental的目标一样,Inkwell仅使用纯HTML、CSS、JS和PHP构建,没有任何外部依赖。

请随意深入了解这两个实时平台和相应的代码库。探索Inkwell的功能,以了解Elemental如何用于您自己的项目。

了解创建Elemental背后的灵感。

为什么选择Elemental?

Elemental的设计目标是没有任何附加条件。没有对外部库或框架的依赖。目的是给开发者一种真正的控制感——一个独立探索和理解框架所蕴含魔力的门户。

总体目标?让开发者完全拥抱并利用DI容器、ORM、中间件等强大抽象的优雅。但这里的关键是——Elemental不仅指出方向,还把钥匙交给你,解开谜团,让你有能力探索这些抽象在代码中的布局。

事实上,我们鼓励你不仅跟随路径,还要另辟蹊径。深入代码库,剖析抽象,了解它们的内部工作原理。请随意修改和实验,因为Elemental不仅仅是一个框架——它是对塑造和塑造可用工具的开放邀请。因为编程不应该是一个迷宫;它应该是一段旅程。让我们共同踏上这段旅程。🚀

文档

  1. 入门
  2. 依赖注入容器
  3. 路由
  4. 控制器
  5. 请求
  6. 响应
  7. 中间件
  8. 视图、布局和组件
  9. 数据库
  10. 模型(ORM)
  11. Candle
  12. 助手
  13. 异常处理器
  14. 配置
  15. 外观

入门

创建您的第一个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文件的项目。

一旦您的项目准备就绪,请使用我们的命令行引擎Candleignite命令启动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方法是在模型中的一种强大抽象,允许执行复杂查询。此方法接受三个参数:conditionsoptionsfetchMode

方法签名

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 bylimit 等。在选项参数中支持的结构包括

  • "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
        );
    }
}

获取单个记录

除了检索与给定查询匹配的所有记录外,您还可以使用 findwhere 方法检索单个记录。这些方法不是返回记录数组,而是返回单个模型实例

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中返回的数组中加载它们。

命令结构

在生成命令后,定义类的keydescription属性的值。`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,请按照以下步骤操作:

  1. 创建一个名为 FacadeClass 的类,该类扩展了 Core\Facade\Facade 类。
  2. 在这个类中,实现一个名为 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。就是这样!感谢!

支持

有关错误报告、功能请求或一般问题,请使用 问题跟踪器