使用 PHP 数据库框架编写 CRUD 操作

正如您可能从第九章 “添加服务器端数据库” 中回忆到的,CRUD 代表创建(create)、读取(read)、更新(update)和删除(delete)。在那一章中,我们使用 MongoDB 来创建 CRUD 操作。在本节中,我们将使用 MySQL 来创建后端身份验证。我们将在刚刚使用 PSR 创建的 PHP 应用程序中使用带有 PHP 的 MySQL。因此,让我们首先创建 MySQL 数据库中所需的表。

创建 MySQL 表

请确保您已在本地计算机上安装了 MySQL 服务器并创建了一个名为 nuxt-php 的数据库。完成此操作后,请按照以下步骤完成我们 API 的第一部分:

  1. 插入以下 SQL 查询以在数据库中创建表:

    CREATE TABLE user (
        uuid varchar(255) NOT NULL,
        name varchar(255) NOT NULL,
        slug varchar(255) NOT NULL,
        created_on int(10) unsigned NOT NULL,
        updated_on int(10) unsigned NOT NULL,
        UNIQUE KEY slug (slug)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

    您首先会注意到,我们使用的是 uuid 而不是像我们在第十二章 “创建用户登录和 API 身份验证” 中使用的 id。UUID 代表通用唯一标识符(Universally Unique Identifier)。在数据库表中为记录建立索引时,选择 UUID 而不是自动递增键可能存在一些原因和好处。例如,您可以在不连接到数据库的情况下创建 UUID。它在不同的应用程序中几乎是唯一的,因此您可以轻松地合并来自不同数据库的数据,而永远不会发生冲突。要在 PHP 应用程序中生成 UUID,我们可以使用 Ben Ramsey 的 ramsey/uuid 来帮助我们生成 RFC 4122 (https://tools.ietf.org/html/rfc4122) 版本 1、3、4 和 5 的 UUID。

  2. 因此,让我们通过 Composer 安装 ramsey/uuid

    $ composer require ramsey/uuid
  3. 现在,您可以按如下方式使用此包生成版本 1 的 UUID:

    use Ramsey\Uuid\Uuid;
    
    $uuid1 = Uuid::uuid1();
    echo $uuid1->toString();

如果您想了解有关此包的更多信息,请访问 https://github.com/ramsey/uuid。

现在,让我们学习如何使用 PHP 来操作 MySQL 数据库,并了解为什么我们需要一个数据库框架来加快我们在 PHP 中的开发速度。

使用 Medoo 作为数据库框架

在 PHP 的早期,开发者使用 MySQL 函数 (https://www.php.net/manual/en/ref.mysql.php) 来管理 MySQL 数据库。然后,MySQLi 扩展 (https://www.php.net/manual/en/book.mysqli.php) 取代了现已弃用的 MySQL 函数。然而,现在,开发者被鼓励使用 PHP 数据对象 (PDO) (https://www.php.net/manual/en/book.pdo.php)。PDO 是一个内置的 PHP 接口抽象,就像 PSR-7 和 PSR-15 一样。它是一个数据访问抽象层,为访问和管理数据库(例如 MySQL 和 PostgreSQL)提供一致的接口(统一的 API),这意味着无论您使用哪个数据库,您都使用相同的函数来查询和获取数据。它支持以下数据库:

  • CUBRID

  • MS SQL Server

  • Firebird

  • IBM

  • Informix

  • MySQL

  • Oracle

  • ODBC and DB2

  • PostgreSQL

  • SQLite

  • 4D

请注意,PDO 是一个数据访问抽象层,而不是数据库抽象层。因此,根据您使用的数据库,必须安装该数据库的 PDO 驱动程序才能使用 PDO。我们正在使用 MySQL 数据库,因此我们必须确保已安装 PDO_MYSQL 驱动程序。在 Ubuntu 中,您可以使用以下命令检查是否启用了 PDO 扩展并且您的环境中安装了 PDO_MYSQL 驱动程序:

$ php -m

您应该会看到 PHP 模块列表。查找 PDOpdo_mysql

[PHP Modules]
...
PDO
pdo_mysql
...

您可以使用另一个更具体的选项来检查 PDO 及其驱动程序,如下所示:

$ php -m|grep -i pdo
PDO
pdo_mysql

如果您只想搜索 PDO 驱动程序,请执行以下操作:

$ php -m|grep -i pdo_
pdo_mysql

您还可以创建一个包含 phpinfo()PHP 页面来查找它们。或者,您可以使用 getAvailableDrivers 方法,如下所示:

print_r(PDO::getAvailableDrivers());

您应该会看到 PDO 驱动程序列表,如下所示:

Array
(
    [0] => mysql
)

或者,还有一些内置的 PHP 函数可以帮助您:

extension_loaded ('PDO'); // 返回布尔值
extension_loaded('pdo_mysql'); // 返回布尔值
get_loaded_extensions(); // 返回数组

如果您没有看到任何 PDO 驱动程序,则必须安装 MySQL 支持的驱动程序。请按照以下步骤操作:

  1. 搜索软件包名称(Ubuntu):

    $ apt-cache search php7.4|grep mysql
    php7.4-mysql - MySQL module for PHP
  2. 安装 php7.4-mysql 并重新启动您的 Apache 服务器:

    $ sudo apt-get install php7.4-mysql
    $ sudo service apache2 restart

一旦您安装了 PDO_MYSQL 驱动程序,就可以立即开始编写 CRUD 操作。例如,让我们编写一个插入操作,如下所示:

  1. 创建 MySQL 数据库连接:

    $servername = "localhost";
    $username = "<username>";
    $password = "<password>";
    $dbname = "<dbname>";
    $connection = new PDO(
        "mysql:host=$servername;dbname=$dbname",
        $username,
        $password
    );

    请注意,<username>、<password> 和 <dbname> 是实际连接详细信息的占位符。您必须根据自己的数据库设置更改它们。

  2. 准备 SQL 查询并绑定(bind)参数:

    $stmt = $connection->prepare("
        INSERT INTO user (
            uuid,
            name,
            slug,
            created_on,
            updated_on
        ) VALUES (
            :uuid,
            :name,
            :slug,
            :created_on,
            :updated_on
        )
    ");
    $stmt->bindParam(':uuid', $uuid);
    $stmt->bindParam(':name', $name);
    $stmt->bindParam(':slug', $slug);
    $stmt->bindParam(':created_on', $createdOn);
    $stmt->bindParam(':updated_on', $updatedOn);
  3. 插入新行:

    $uuid = "25769c6c-d34d-4bfe-ba98-e0ee856f3e7a";
    $name = "John Doe";
    $slug = "john-doe";
    $createdOn = (new DateTime())->getTimestamp();
    $updatedOn = $createdOn;
    $stmt->execute();

这不是很理想,因为您每次都必须准备语句并在需要的地方绑定参数,这需要相当多的代码行才能操作。因此,我们应该选择一个 PHP 数据库框架来加速开发。Medoo (https://medoo.in/) 是一个不错的选择。它轻巧且非常易于集成和使用。

让我们安装它并将其连接到我们的应用程序:

  1. 通过 Composer 安装 Medoo

    $ composer require catfan/medoo
  2. 如果一切设置就绪,您可以导入 Medoo 并传入一个配置数组来启动数据库连接,就像我们之前在原生方法中所做的那样:

    use Medoo\Medoo;
    
    $database = new Medoo([
        'database_type' => 'mysql',
        'database_name' => '<dbname>',
        'server' => 'localhost',
        'username' => '<username>',
        'password' => '<password>'
    ]);

这就是通过这个数据库框架建立与 MySQL 数据库连接的全部内容。您可以在本书的 GitHub 存储库中的 /chapter-16/nuxtphp/proxy/backend/core/mysql.php 中找到此代码片段的实际用法。我们将在下一节中向您展示如何实现它,但现在,让我们探索如何使用 Medoo 编写一些基本的 CRUD 操作。

插入记录

当您想向表中插入新记录时,可以使用 insert 方法,如下所示:

$database->insert('user', [
    'uuid' => '41263659-3c1f-305a-bfac-6a7c9eab0507',
    'name' => 'Jane',
    'slug' => 'jane',
    'created_on' => '1568072289'
]);

如果您想了解有关此方法的更多详细信息,请访问 https://medoo.in/api/insert。

查询记录

当您想从表中列出记录时,可以使用 select 方法,如下所示:

$database->select('user', ['uuid', 'name', 'slug', 'created_on', 'updated_on']);

select 方法会返回一个记录列表。如果您只想选择特定的行,可以使用 get 方法,如下所示:

$database->get('user', ['uuid', 'name', 'slug', 'created_on', 'updated_on'], ['slug' => 'jane']);

如果您想了解更多详细信息,请访问 https://medoo.in/api/select 以获取 select 方法的信息,以及 https://medoo.in/api/get 以获取 get 方法的信息。

更新记录

当您想修改表中记录的数据时,可以使用 update 方法,如下所示:

$database->update('user', [
    'name' => 'Janey',
    'slug' => 'jane',
    'updated_on' => '1568091701'
], [
    'uuid' => '41263659-3c1f-305a-bfac-6a7c9eab0507'
]);

如果您想了解有关此方法的更多详细信息,请访问 https://medoo.in/api/update。

删除记录

当您想从表中删除记录时,可以使用 delete 方法,如下所示:

$database->delete('user', [
    'uuid' => '41263659-3c1f-305a-bfac-6a7c9eab0507'
]);

如果您想了解有关此方法的更多详细信息,请访问 https://medoo.in/api/delete。

这就是关于如何使用 Medoo 和 PDO 编写基本 CRUD 操作的全部内容。

请查看 Medoo 的文档 https://medoo.in/doc 以了解您可以使用的其余方法。Medoo 还有其他替代方案,例如 Doctrine DBAL (https://github.com/doctrine/dbal) 和 Eloquent (https://github.com/illuminate/database)。

在本节中,您学习了一些 PSR 和 CRUD 操作。接下来,我们将介绍如何将所有这些组合在一起并将它们与 Nuxt 集成。由于 PHP 和 JavaScript 是两种不同的语言,它们相互通信的唯一方式是通过 API 中的 JSON。

但是在编写启用此功能的脚本之前,我们应该研究这两个程序的跨域应用程序结构。自第十二章 “创建用户登录和 API 身份验证” 以来,我们的 Nuxt 应用程序一直使用跨域应用程序结构,因此您应该对此很熟悉。让我们开始吧!

组织跨域应用目录

同样,就像构建跨域应用程序目录一样,以下是我们对 Nuxt 和我们的 PHP API 的整体视图:

// Nuxt 应用
front-end
├── package.json
├── nuxt.config.js
└── pages
    ├── index.vue
    └── ...

// PHP API
backend
├── composer.json
├── vendor
│   └── ...
├── ...
└── ...

单独而言,Nuxt 的目录结构保持不变。我们只需要对 API 目录的结构进行稍微修改,如下所示:

// PHP API
backend
├── composer.json
├── middlewares.php
├── routes.php
├── vendor
│   └── ...
├── public
│   └── index.php
├── static
│   └── ...
├── config
│   └── ...
├── core
│   └── ...
├── middleware
│   └── ...
└── module
    └── ...

PHP API 的目录结构只是一个建议。您始终可以设计一个您更喜欢且最适合您的结构。因此,概括来说,我们有以下内容:

  • /vendor/ 目录用于存放所有第三方软件包或依赖项。

  • /public/ 目录仅包含一个用于启动我们 API 的 index.php 文件。

  • /static/ 目录用于存放静态文件,例如网站图标。

  • /config/ 目录存储配置文件,例如 MySQL 文件。

  • /core/ 目录存储我们可以在整个应用程序中使用的通用对象和函数。

  • /middleware/ 目录存储我们的 PSR-15 中间件。

  • /module/ 目录存储我们稍后将创建的自定义模块,就像我们在第十二章“创建用户登录和 API 身份验证”中使用 Koa 所做的那样。

  • composer.json 文件始终位于根级别。

  • middlewares.php 文件是导入来自 /middleware/ 目录的中间件的核心位置。

  • routes.php 文件是导入来自 /module/ 目录的路由的核心位置。

准备好结构后,您可以开始编写顶级代码,该代码将把来自不同位置和目录的其他代码粘合到 /public/ 目录的 index.php 文件中的单个应用程序中。因此,让我们开始吧:

  1. foreach 循环放入 routes.php 文件中,以迭代您稍后将创建的每个模块:

    // backend/routes.php
    $modules = require './config/routes.php';
    foreach ($modules as $module) {
        require './module/' . $module . 'index.php';
    }
  2. /config/ 目录中创建一个 routes.php 文件,该文件将列出您的模块的文件名,如下所示:

    // backend/config/routes.php
    return [
        'Home/',
        'User/',
        // ...
    ];
  3. 在这个 PHP API 中,middlewares.php 文件将会导入一个中间件,

    // backend/middlewares.php
    require './middleware/outputDecorator.php';

    这个装饰器将会把 CRUD 操作的输出打印成以下 JSON 格式:

    {"status":<status code>,"data":<data>}
  4. /middleware/ 目录下创建一个名为 outputDecorator.php 的文件,其中包含以下代码。这段代码将会把操作的输出包装成前面的格式:

    // backend/middleware/outputDecorator.php
    use function Zend\Stratigility\middleware;
    $router->middleware(middleware(function ($request, $handler) {
        $response = $handler->handle($request);
        $existingContent = (string) $response->getBody();
        $contentDecoded = json_decode($existingContent, true);
        $status = $response->getStatusCode();
        $data = [
            "status" => $status,
            "data" => $contentDecoded
        ];
        $payload = json_encode($data);
        $response->getBody()->rewind();
        $response->getBody()->write($payload);
        return $response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus($status);
    }));

    在这里,我们使用来自 zend-stratigility 组件的 middleware 方法来创建装饰器中间件。然后,我们通过使用来自 The League of Extraordinary 的 league/route 的 router 来将整个应用程序锁定在这个中间件中。

  5. 在 /core/ 目录下创建一个名为 mysql.php 的文件,该文件返回用于 MySQL 连接的 Medoo 实例:

    // backend/core/mysql.php
    $dbconfig = require './config/mysql.php';
    $mysql = new Medoo\Medoo([
        'database_type' => $dbconfig['type'],
        'database_name' => $dbconfig['name'],
        'server' => $dbconfig['host'],
        'username' => $dbconfig['username'],
        'password' => $dbconfig['password']
    ]);
    return $mysql;
  6. 正如我们之前提到的,/public/ 目录只包含一个 index.php 文件。这个文件用于启动我们的程序,因此它包含您之前学习过的关于 PSRs 的脚本:

    // backend/public/index.php
    chdir(dirname(__DIR__));
    require_once 'vendor/autoload.php';
    $request = Zend\Diactoros\ServerRequestFactory::fromGlobals(
        //...
    );
    $router = new League\Route\Router;
    try {
        require 'middlewares.php';
        require 'routes.php';
        $response = $router->dispatch($request);
    } catch(Exception $exception) {
        // handle errors
    }
    (new Zend\HttpHandlerRunner\Emitter\SapiEmitter)->emit($response);

在这里,您可以看到 middlewares.php 和 routes.php 文件被导入到这个文件中以生成一个 PSR-7 响应。它们被包裹在 try 和 catch 代码块中,以捕获任何 HTTP 错误,例如 404 和 506 错误。因此,来自模块的任何输出和任何错误都将通过最后一行发送到浏览器。希望这已经为您提供了这个简单 API 的概览。现在,让我们继续深入 /module/ 目录,更详细地了解如何创建模块和路由。

创建 API 的公共路由及其模块

创建 API 的公共路由及其模块与您在本书前几章中学习构建的 API 非常相似;主要的区别在于语言。之前我们使用 JavaScript 和 Node.js 框架 Koa,而本章的 API,我们使用 PHP 和 PSR 来创建一个与框架无关的 API。让我们开始吧:

  1. 在 /module/ 目录中创建两个目录:一个名为 Home,另一个名为 User。这两个子目录是此 API 中的模块。在每个模块中,创建一个 /_routes/ 目录和一个 index.php 文件,该文件将导入来自 /_routes/ 目录的路由,如下所示:

    └── module
        ├── Home
        │   ├── index.php
        │   └── _routes
        │       └── hello_world.php
        └── User
            ├── index.php
            └── _routes
                └── ...
  2. 在 Home 模块中,输出一条 "Hello world!" 消息并将其映射到 / 路由,如下所示:

    // module/Home/_routes/hello_world.php
    use Psr\Http\Message\ResponseInterface;
    use Psr\Http\Message\ServerRequestInterface;
    
    $router->get('/', function (ServerRequestInterface $request) : ResponseInterface {
        return new Zend\Diactoros\Response\JsonResponse(
            'Hello world!'
        );
    });
  3. 在 User 模块中,编写 CRUD 操作,以便我们可以创建、读取、更新和删除我们的用户。因此,在 /_routes/ 目录中,创建五个文件,分别名为 fetch_user.php、fetch_users.php、insert_user.php、update_user.php 和 delete_user.php。在每个文件中,我们将为 /Controller/ 目录中的每个 CRUD 操作映射路由:

    └── User
        ├── index.php
        ├── _routes
        │   ├── delete_user.php
        │   ├── fetch_user.php
        │   └── ...
        └── Controller
            └── ...
  4. 例如,在 fetch_users.php 文件中,我们将定义一个 /users 路由来列出所有用户,如下所示:

    // module/User/_routes/fetch_users.php
    use Psr\Http\Message\ResponseInterface;
    use Psr\Http\Message\ServerRequestInterface;
    
    $router->get('/users', function (ServerRequestInterface $request) : ResponseInterface {
        $database = require './core/mysql.php';
        $users = (new Spectre\User\Controller\Fetch\Users($database))->fetch();
        return new Zend\Diactoros\Response\JsonResponse($users);
    });

    在这里,您可以看到我们将 Medoo 实例导入为 $database,并将其传递给将执行读取操作的控制器,然后调用 fetch 方法来获取所有可用用户。

  5. 因此,接下来我们将创建一些 CRUD 目录:Insert、Fetch、Update 和 Delete。在每个 CRUD 目录中,我们将 PSR-4 类存储在 /Controller/ 目录中,如下所示:

    └── Controller
        ├── Controller.php
        ├── Insert
        │   └── User.php
        ├── Fetch
        │   ├── User.php
        │   └── Users.php
        ├── Update
        │   └── User.php
        └── Delete
            └── User.php
  6. 首先,创建一个抽象类,CRUD 目录中的类可以继承它。此类在其构造函数中仅接受 Medoo\Medoo 数据库,如下所示:

    // module/User/Controller/Controller.php
    namespace Spectre\User\Controller;
    
    use Medoo\Medoo;
    
    abstract class Controller
    {
        protected $database;
    
        public function __construct(Medoo $database)
        {
            $this->database = $database;
        }
    }
  7. 导入前面的抽象类并将其扩展到任何需要连接到 MySQL 数据库的其他类,如下所示:

    // module/User/Controller/Fetch/Users.php
    namespace Spectre\User\Controller\Fetch;
    
    use Spectre\User\Controller\Controller;
    
    class Users extends Controller
    {
        public function fetch()
        {
            $columns = [
                'uuid',
                'name',
                'slug',
                'created_on',
                'updated_on',
            ];
            return $this->database->select('user', $columns);
        }
    }

在此类中,我们使用 select 方法从 MySQL 数据库的 user 表中获取所有用户。Medoo 将返回一个包含用户列表的数组,如果没有用户则返回一个空数组。然后,此结果将在 fetch_users.php 文件中使用 zend-diactoros 的 JsonResponse 方法转换为 JSON。最后,它将由 /middleware/ 目录中的中间件进行修饰。这将产生以下输出:

{"status":200,"data":[{"uuid":"...","name":"Jane","slug":"jane",...},{...},{...}]}

这就是关于 PHP API 的全部内容。它相当简单,不是吗?在本练习中,我们将跳过在 API 端处理 CORS 的任务,因为我们将使用 Nuxt Axios 和 Proxy 模块在我们将要创建的 Nuxt 应用程序中无缝且轻松地处理 CORS。让我们开始吧!

您可以在本书的 GitHub 存储库中的 /chapter-16/nuxtphp/proxy/backend/ 中找到此 PHP API,而此 API 的其余 CRUD 类可以在 /chapter-16/nuxtphp/proxy/backend/module/User/Controller/ 中找到。