connora/basic

一个全栈PHP框架,为你提供一个开始网络项目的必备基础。

v2.1.1 2023-05-07 17:39 UTC

README

关于

一个全栈PHP框架,为你提供一个开始网络项目的必备基础。

需要PHP 8。

主要特性

  • MVC架构
  • 简单路由
  • 使用Plates视图模板
  • 类自动加载和命名空间
  • 依赖注入容器
  • PDO数据库包装类

文档

安装

使用Composer下载。

composer create-project connora/basic your-project-name

使用Composer安装时,应创建项目根目录下的.env文件。同时还包括了.env.example文件。

本地服务网站

如果您想在本地上快速测试或开发网站,并且您的机器上已安装PHP,请在项目根目录下使用“serve”命令。

注意:这将只使用PHP提供服务,不使用MySQL。

php basic serve

或者,使用Laragon、Docker或XAMPP,并配置虚拟主机。

路由

注册

默认情况下,您将在/routes/main.php中注册路由。

您将能够访问$this->router属性,该属性提供App\Core\Router类的实例。

App\Core\Router类提供了以下与常见HTTP动词相对应的注册方法:

$this->router->get($uri, $callback);
$this->router->post($uri, $callback);
$this->router->put($uri, $callback);
$this->router->patch($uri, $callback);
$this->router->delete($uri, $callback);

这些方法将您的路由注册为应用程序内的有效端点。如果您尝试访问未注册的URL/路由,将显示404页面。

回调函数

路由的回调将是闭包(匿名函数),您可以直接在其中执行端点逻辑,或者是一个数组的引用,该数组包含要调用的控制器方法的引用,其中第一个元素是要引用的完全限定类,第二个元素是要调用的方法名。

// Basic route using a closure
$this->router->get('/home', function () {
    return 'Hello World';
});
// Alternatively, use a controller class and a method to store your logic in
$this->router->get('/home-alt', [HomeController::class, 'index']);

动态参数

您可以在路由URI中设置动态参数,通过在slug前加井号#。参数的值将通过您的回调函数作为参数变量可用。

在控制器中使用

// Ex: yoursite.com/blog/post/123
// within /routes/main.php
$this->router->get('/blog/post/#id', [BlogPostController::class, 'show']);

// within /app/controllers/BlogPostController.php
public function show($id)
{
    // $id = '123'
    return View::render('pages.blog.post.single', [
        'blogPostData' => $this->blogPostModel->findById($id)
    ]);
}

或者使用基本的闭包

$this->router->get('/example/#id', function ($id) {
    return $id
});

批量注册

您可以批量注册具有类似特性的路由,例如控制器、URI前缀或两者。这里的目的是减少样板代码,并提供更好的组织。

在批量路由时,需要在末尾链式调用batch()方法,它接受一个闭包参数,其中包含要应用批量属性的路线。

可用的批量相关方法

$this->router->controller(string $className);
$this->router->prefixUri(string $uri);
$this->router->batch(callable $closure);

当使用prefixUri()方法批量路由时,闭包内的路由都将添加您定义的URI前缀。

例如

$this->router
    ->prefixUri('/users')
    ->batch(function () {
        $this->router
            // /users GET (show all users)
            ->get('/', [UserController::class, 'index'])
            // /users/create GET (form to create a user)
            ->get('/create', [UserController::class, 'create'])
            // /users POST (endpoint to store a new user)
            ->post('/', [UserController::class, 'store'])
            // /users/123 GET (show a single user)
            ->get('/#id', [UserController::class, 'show'])
            // /users/123/edit GET (form to edit user properties)
            ->get('/#id/edit', [UserController::class, 'edit'])
            // /users/123 PATCH (endpoint to update user properties)
            ->patch('/#id', [UserController::class, 'update'])
            // /users/123 DELETE (endpoint to remove a user record)
            ->delete('/#id', [UserController::class, 'destroy']);
    });

当使用controller()方法批量路由时,请注意

  • 闭包内的注册方法的第二个参数将是一个引用端点方法的字符串,而不是默认的数组语法
  • $this->router->view()方法在批量闭包中不可用,因为我们必须引用控制器方法

例如

$this->router
    ->controller(UserController::class)
    ->batch(function () {
        $this->router
            ->get('/users', 'index')
            ->get('/users/create', 'create')
            ->post('/users', 'store')
            ->get('/users/#id', 'show')
            ->get('/users/#id/edit', 'edit')
            ->patch('/users/#id', 'update')
            ->delete('/users/#id', 'destroy');
    });

或者两者都用上

$this->router
    ->controller(UserController::class)
    ->prefixUri('/users')
    ->batch(function () {
        $this->router
            ->get('/', 'index')
            ->get('/create', 'create')
            ->post('/', 'store')
            ->get('/#id', 'show')
            ->get('/#id/edit', 'edit')
            ->patch('/#id', 'update')
            ->delete('/#id', 'destroy');
    });

注意:您不能在batch()方法中嵌套batch()方法。

表单请求

标准的HTML <form> 标签只接受 GETPOST 作为有效的请求方法。我们可以通过使用 method_spoof(string $method) 辅助函数来克服这一点。这要求我们的表单使用 POST 方法请求,并在表单内部指定“伪造”的方法,使用 PUTPATCHDELETE

例如

$this->router->patch('/update-example', [ExampleClass::class, 'updateMethod']);
<!-- Example form request to update data -->
<form action="/update-example" method="POST">
    <?= csrf() ?>
    <?= method_spoof('PATCH') ?>
    <label class="form-label">Field to update</label>
    <input name="exampleField" value="<?= $originalValue ?>" required>
    <button type="submit" name="updateSubmit">Update</button>
</form>

还建议使用包含的 csrf()csrf_valid() 辅助函数,以确保您的请求不受任何潜在的 跨站请求伪造 的威胁。

组织

随着您的应用程序的增长,您可能会希望更好地组织您的路由,而不是将它们都放在 /routes/main.php 文件中。默认情况下,您可以在 /routes 目录中的任何PHP文件中访问到 $this->router 属性。所以请随意组织您希望的结构!

控制器

基础

控制器是用于处理传入HTTP请求逻辑的类。请确保将控制器方法的访问修饰符设置为 public,以便路由器可以成功执行它们。

最佳实践是在控制器方法中仅处理用户输入和响应逻辑。如果您在端点中涉及更复杂的企业逻辑,建议将其抽象到另一个类(通常是 Service 类)中。

控制器应根据请求的主题进行命名和组织。虽然建议但不强制要求在类名中包含“Controller”一词。

提供了一个示例控制器类。

注意:控制器方法应仅接受动态路由参数参数,包含的依赖注入容器将只解析在构造函数中建立的类。

请求/用户输入

框架提供了一个默认的 App\Core\Request 类,可以在控制器中用于与用户输入交互。该类将提供对传入请求输入的基本清洁,以及用于与数据交互的方法,而不是直接使用PHP的超级全局变量。

可用的 App\Core\Request 方法

/**
 * Return an array of all sanitized inputs from the request
 */
$request->all();

/**
 * Return the sanitized $_GET value by it's key, optional default value
 */
$request->get(string $key, string $default = null);

/**
 * Return the sanitized $_POST value by it's key, optional default value
 */
$request->post(string $key, string $default = null);

/**
 * Return the sanitized $_REQUEST value by it's key, optional default value
 */
$request->input(string $key, string $default = null);

可以在需要它的每个控制器方法中实例化 App\Core\Request 类,或使用包含的 request() 辅助函数。

<?php

namespace App\Controllers;

use App\Core\Request;

class ExampleController
{
    public function update()
    {
        // standard instantiation
        $request = new Request();
        $data = $request->input('data');

        // or with the helper
        $data = request()->input('data');
    }
}

依赖注入容器

默认情况下,您可以在控制器方法的 __construct() 方法中输入任何类,容器将为您构建类及其依赖关系。容器将使用反射和递归自动实例化和设置您的类可能需要的所有依赖关系。

<?php

namespace App\Controllers;

use App\Core\View;
use App\Models\UserModel;

class UserController
{
    private $userModel;

    // utilizing the containers automatic resolution
    // by type hinting the class we want
    public function __construct(UserModel $userModel)
    {
        $this->userModel = $userModel;
    }

    public function index()
    {
        $users = $this->userModel->getAll();

        return View::render('pages.users.list', ['users' => $users]);
    }
}

可用的 App\Core\Container 类方法

$this->container->get(string $id);
$this->container->set(string $id, callable $callback);
$this->container->setOnce(string $id, callable $callback);

默认情况下,容器用于轻松实例化所需的类,而无需您担心实例化其依赖关系。然而,在某些情况下,您的类可能无法由容器解析(需要原始构造函数参数等),或者您可能需要容器返回类的更自定义实现。

要手动将类绑定到容器中,请使用 set() 方法,传递一个字符串引用 $id(通常是完全限定的类名),以及一个闭包作为 $callback,该闭包应返回新的类实例。每当您的设置类需要由容器解析时,它将使用您注册的回调来返回其实例。

如果您希望配置的类仅实例化一次并用于容器中的所有后续引用,可以使用 setOnce() 方法。默认情况下,有一些核心类通过使用 setOnce() 方法设置为仅实例化一次,以供框架全局使用。

要从一个容器中返回/解析类实例,请使用 get() 方法。这是容器在自动解析注入类时内部使用的。

在设置手动绑定时,您可以使用$container参数访问闭包内的容器。这允许您在闭包内部使用$container->get()来解析您要返回的类所需的任何依赖。

如果您需要手动设置类或接口及其绑定,您可以在App\Core\App类的containerSetup()方法中这样做。

/**
 * Establish any container class bindings for the application
 */
public function containerSetup(): self
{
    // included by default
    $this->container->setOnce(Config::class, function ($container) {
        return new Config($_ENV);
    });
    $this->container->setOnce(Request::class, function ($container) {
        return new Request();
    });
    $this->container->setOnce(DB::class, function ($container) {
        $dbConfig = config('database', 'main');
        return new DB(
            $dbConfig['name'],
            $dbConfig['username'],
            $dbConfig['password'],
            $dbConfig['host']
        );
    });

    // reference the actual repository whenever the interface is referenced/injected
    $this->container->set(UserRepositoryInterface::class, function ($container) {
        return new UserRepository($container->get(UserModel::class));
    });

    return $this;
}

如果您不想在控制器构造函数中解析某些类,您可以使用包含的container()辅助函数来访问get()方法。

<?php

namespace App\Controllers;

use App\Core\View;
use App\Models\UserModel;

class ExampleController
{
    public function index()
    {
        // get/resolve the UserModel class from the container without injecting into the constructor
        // dependencies automatically resolved, pretty neat
        $userModel = container(UserModel::class);
        $user = $userModel->getById(request()->input('id'));

        return View::render('pages.example', ['user' => $user]);
    }
}

注意:您不需要使用包含的依赖注入容器。请随意使用传统的依赖注入技术手动实例化您的类,并在需要的地方传递它们。

视图

默认情况下,框架使用Plates作为它的视图模板系统。使用App\Core\View类作为基本包装器。

静态页面?

此外,App\Core\Router类还有一个直接调用您视图的方法,因此您无需为更简单的页面烦恼闭包或控制器。

$this->router->view('/', 'pages.welcome');

在您的控制器方法中

当在控制器中引用视图时,请使用静态App\Core\View::render()方法来返回模板内容。该方法接受视图文件引用(使用点.作为嵌套分隔符,没有文件扩展名)和您希望在视图中可访问的数据变量数组。

public function index()
{
    $foo = 'bar';

    return View::render('pages.example', ['foo' => $foo]);
}

模型和数据库

模型是用于与您的数据库交互的类。

数据库类

包含的App\Core\DB类作为PDO的包装器,旨在使连接到数据库和执行查询更加容易。如前所述,App\Core\DB类默认使用database.main配置设置绑定到容器中。您可以使用/config/database.php和应用程序的.env文件更改默认选项或设置多个连接,有关详细信息,请参阅后续内容。

建立连接

在PHP Web应用程序中创建和使用数据库连接有多种方法。

如前所述,App\Core\DB类创建数据库连接。在App\Core\App::containerSetup()中添加了一个默认的容器绑定:默认情况下已注释。这种方法可以确保在请求生命周期中只为每个请求创建一个数据库类/连接,并且可以轻松地在应用程序中引用。

为了方便起见,您的模型类应扩展包含的App\Core\Model抽象类以包含$this->db属性。

<?php

namespace App\Models;

use App\Core\Model;

/**
 * $this->db available to use for your queries
 */
class ExampleModel extends Model
{
    private $table = 'example';

    public function getAll()
    {
        $sql = "SELECT * FROM $this->table";
        return $this->db->query($sql);
    }
}
<?php

namespace App\Controllers;

use App\Core\View;
use App\Models\ExampleModel;

class ExampleController
{
    private $exampleModel;

    public function __construct(ExampleModel $exampleModel)
    {
        $this->exampleModel = $exampleModel;
    }

    public function index()
    {
        return View::render('pages.example', [
            'data' => $this->exampleModel->getAll();
        ]);
    }
}

但是,这可能在您的应用程序中不是您想要的/需要的方法,因此请随意移除绑定或使用更传统的依赖注入技术来处理数据库/模型。

<?php

namespace App\Controllers;

use App\Core\DB;
use App\Models\ExampleModel;

class ExampleController
{
    private $db;

    public function __construct(DB $db)
    {
        // Ex.1
        // Use the main DB connection that is configured in the container
        // via the type-hinted constructor argument
        $this->db = $db;

        // Ex.2
        // Create an alternative DB class binding in the container
        // Useful for models that need a different database connection
        $this->db = container('db_alt');

        // Ex.3
        // Create a connection on the fly
        $dbConfig = config('database', 'alt');
        $this->db = new DB(
            $dbConfig['name'],
            $dbConfig['username'],
            $dbConfig['password'],
            $dbConfig['host']
        );
    }

    public function index()
    {
        $exampleModel = new ExampleModel($this->db);

        return View::render('pages.example', [
            'data' => $exampleModel->getAll();
        ]);
    }
}

App\Core\DB类提供以下方法:

/**
 * Get the established PDO connection
 */
$this->db->pdo();

/**
 * Prepares the query, binds the params, executes, and runs a fetchAll()
 */
$this->db->query(string $sql, array $params = []);

/**
 * Prepares the query, binds the params, executes, and runs a fetch()
 */
$this->db->single(string $sql, array $params = []);

/**
 * Prepares the query, binds the params, and executes the query
 */
$this->db->execute(string $sql, array $params = []);

示例

<?php

namespace App\Models;

use App\Core\Model;

class UserModel extends Model
{
    private $table = 'users';

    public function getAll()
    {
        $sql = "SELECT * FROM $this->table";
        return $this->db->query($sql);
    }

    public function getById($id)
    {
        $sql = "SELECT * FROM $this->table WHERE id = ?";
        return $this->db->single($sql, [$id]);
    }

    public function getByEmail($email)
    {
        $sql = "SELECT * FROM $this->table WHERE email = ?";
        return $this->db->single($sql, [$email]);
    }

    public function create($name, $email, $username, $password)
    {
        $hashedPwd = password_hash($password, PASSWORD_DEFAULT);
        $sql = "INSERT INTO $this->table(name, email, username, password) 
            VALUES(:name, :email, :username, :password)";

        return $this->db->execute($sql, [
            'name' => $name,
            'email' => $email,
            'username' => $username,
            'password' => $hashedPwd,
        ]);
    }

    public function update(int $userId, array $properties)
    {
        $setString = '';
        foreach ($properties as $property => $value) {
            $setString .= $property . ' = ' . ':' . $property;
            if ($property != array_key_last($properties)) {
                $setString .= ', ';
            } else {
                $setString .= ' ';
            }
        }
        $properties['id'] = $userId;
        $sql = "UPDATE $this->table
            SET $setString
            WHERE id = :id";

        return $this->db->execute($sql, $properties);
    }

    public function delete(int $userId)
    {
        $sql = "DELETE FROM $this->table WHERE id = ?";
        return $this->db->execute($sql, [$userId]);
    }
}

为了遵循MVC约定,并在您的应用程序中获得更好的组织,强烈建议您只使用DB类并在模型类中执行查询。

辅助函数

辅助函数旨在在任何地方访问。框架中包含了一些辅助函数,您也可以添加自己的辅助函数。

/app/helpers.php

环境和配置数据

.env文件

使用composer安装项目时,应创建.env文件。如果没有,则提供了一个示例文件以创建新的文件。

此文件用于存储可能因网站使用环境(本地、预发布、生产)而不同的设置变量,以及存储不希望提交到源代码存储库的私人信息,如API密钥、数据库访问凭证等。默认情况下,它添加到.gitignore中。

# Site Environment
ENV=local

# When the data has spaces
EXAMPLE_DATA="Example Data"

配置数据

可以将配置数据视为您的网站“设置”。这可能包括数据库连接、邮件服务器信息、网站元数据等。这些数据将使用位于/config目录中的多维数组.php文件存储。

当您需要设置与您的私有 .env 数据相关的配置设置时,可以使用 $this->env 属性来访问其值。

config(string $file, string $key) 辅助函数用于在应用程序中访问所需数据。将配置文件引用(不包含文件扩展名)作为第一个参数,将值键位置字符串作为第二个参数(使用点 . 作为配置数组中嵌套值的分隔符)。

// get the main database connection host name
$host = config('database', 'main.host');

CLI工具

创建控制器

php basic new:controller YourControllerName

创建模型

php basic new:model YourModelName

本地托管您的网站

php basic serve