跳转到内容

教程 - Vökuró


Vökuró

Vökuró是一个示例应用,展示了一个使用 Phalcon 编写的典型 Web 应用程序。本应用程序主要关注:- 用户登录(安全性)- 用户注册(安全性)- 用户权限 - 用户管理

注意

您可以将 Vökuró 作为您自己应用程序的起点,并根据需要进一步增强其功能。这绝不是一个完美的应用程序,也无法满足所有需求。

注意

本教程假定您已经熟悉 Model View Controller 设计模式的基本概念。(请参见本教程末尾的参考资料)

注意

注意下面的代码已经格式化以提高可读性

安装

下载中

要安装该应用程序,您可以从以下位置克隆或下载它:GitHub您可以访问 GitHub 页面,下载应用程序,然后将其解压到您机器上的某个目录。或者,您可以使用git clone:

git clone https://github.com/phalcon/vokuro

扩展

运行 Vökuró 有一些先决条件。您需要在您的机器上安装 PHP >= 7.2 以及以下扩展:- ctype - curl - dom - json - iconv - mbstring - memcached - opcache - openssl - pdo - pdo_mysql - psr - session - simplexml - xml - xmlwriter

需要安装 Phalcon。如需帮助安装 Phalcon,请前往安装页面。

最后,您还需要确保更新了 composer 包(请参见下一部分)。

运行

如果以上所有要求都已满足,您可以在终端中运行以下命令来启动内置的 PHP Web 服务器运行该应用程序(在您提取示例的文件夹中执行):

php -S localhost:8080 -t public/ .htrouter.php

上述命令会为localhost端口8080提供网站服务。您可以根据自己的需要更改这些设置。或者,您也可以在 Apache 或 nginX 中通过虚拟主机设置您的站点。有关如何为这些 Web 服务器设置虚拟主机的说明,请查阅相关文档。

Docker

resources文件夹中您将找到一个Dockerfile它允许您快速配置环境并运行应用程序。要使用Dockerfile我们需要决定我们容器化应用程序的名称。在本教程中,我们将使用phalcon-tutorial-vokuro.

从应用程序的根目录,我们需要编译项目(只需一次):

$ docker build -t phalcon-tutorial-vokuro -f docker/Dockerfile .

然后运行它

$ docker run -it --rm phalcon-tutorial-vokuro bash

这将带我们进入容器化环境。检查 PHP 版本:

root@c7b43060b115:/code $ php -v

PHP 8.1.8 (cli) (built: Jul 12 2022 08:28:43) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.8, Copyright (c) Zend Technologies
    with Xdebug v3.1.5, Copyright (c) 2002-2022, by Derick Rethans

并且 Phalcon:

root@c7b43060b115:/code $ php -r 'echo Phalcon\Version::get();'

4.0.0

您现在拥有一个包含所有必要组件的容器化环境来运行 Vökuró。

结构

查看应用程序的结构,我们有如下内容:

vokuro/
    .ci
    configs
    db
        migrations
        seeds
    public
    resources
    src
        Controllers
        Forms
        Models
        Phalcon
        Plugins
        Providers
    tests
    themes
        vokuro
    var
        cache
            acl
            metaData
            session
            volt
        logs
    vendor
目录 描述
.ci 用于为 CI 设置服务的必要文件
configs 配置文件
db 存放数据库迁移
public 应用程序入口、CSS、JS、图片
resources Docker/nanobox 用于设置应用程序的文件
src 应用程序所在的位置(控制器、表单等)
src/Controllers 控制器
src/Forms 表单
src/Models 数据库模型
src/Plugins 插件
src/Providers 提供者:在 DI 容器中设置服务
tests 测试
themes 主题/视图便于自定义
themes/vokuro 应用程序的默认主题
var 各种支持文件
var/cache 缓存文件
var/logs 日志
vendor 第三方/composer 库

配置

.env

Vökuró使用流行的DotenvVance Lucas 编写的库。该库使用位于您根目录中的.env文件,其中保存诸如数据库服务器主机名、用户名、密码等配置参数。Vökuró 自带一个.env.example文件,您可以复制并重命名为.env然后编辑它以匹配您的环境。您需要首先执行此操作,以便应用程序能够正常运行。

可用选项包括:

选项 描述
APP_CRYPT_SALT Phalcon\Encryption\Crypt组件用来生成密码和任何其他安全功能的一个随机且长的字符串
APP_BASE_URI 通常为/如果您的 Web 服务器直接指向 Vökuró 目录。如果您将 Vökuró 安装在子目录中,您可以调整基础 URI
APP_PUBLIC_URL 应用程序的公共 URL。用于电子邮件。
DB_ADAPTER 数据库适配器。可用的适配器包括:mysql, pgsql, sqlite。请确保系统中已安装与数据库相关的扩展。
DB_HOST 数据库主机
DB_PORT 数据库端口
DB_USERNAME 数据库用户名
DB_PASSWORD 数据库密码
DB_NAME 数据库名称
MAIL_FROM_NAME 发送电子邮件时使用的发件人名称
MAIL_FROM_EMAIL 发送电子邮件时使用的发件人邮箱地址
MAIL_SMTP_SERVER SMTP 服务器
MAIL_SMTP_PORT SMTP 端口
MAIL_SMTP_SECURITY SMTP 安全协议(例如tls)
MAIL_SMTP_USERNAME SMTP 用户名
MAIL_SMTP_PASSWORD SMTP 密码
CODECEPTION_URL 测试所用的 Codeception 服务器。如果您在本地运行测试,则应设置为127.0.0.1
CODECEPTION_PORT Codeception 端口

一旦配置文件就绪,访问 IP 地址将显示如下界面:

Database

您还需要初始化数据库。Vökuró使用流行的库Phinx由 Rob Morgan 编写(现属于 Cake 基金会)。该库使用自己的配置文件(phinx.php),但在 Vökuró 中您无需修改任何设置,因为phinx.php会读取.env文件以获取配置设置。这允许您在一个地方设置配置参数。

我们现在需要运行迁移。查看数据库状态:

/app $ ./vendor/bin/phinx status

您将看到如下界面:

要初始化数据库,我们需要运行迁移:

/app $ ./vendor/bin/phinx migrate

屏幕将显示操作过程:

并且status命令现在会显示全部绿色:

配置

acl.php

查看config/文件夹,您会注意到四个文件。您不需要更改这些文件即可启动应用程序,但如果您想进行自定义,这里是需要修改的地方。acl.php文件返回一个路由列表用来控制哪些路由仅对已登录用户可见。

当前的设置会在用户访问以下路由时要求他们必须登录:

  • users/index
  • users/search
  • users/edit
  • users/create
  • users/delete
  • users/changePassword
  • profiles/index
  • profiles/search
  • profiles/edit
  • profiles/create
  • profiles/delete
  • permissions/index

如果你使用 Vökuró 作为你自己的应用程序的起点,你需要修改这个文件来添加或删除路由,以确保你的受保护路由位于登录机制之后。

注意

将私有路由保存在一个数组中,对于中小型应用程序来说高效且易于维护。一旦你的应用程序开始增长,你可能需要考虑其他技术来管理私有路由,例如使用带有缓存机制的数据库。

config.php

此文件包含 Vökuró 所需的所有配置参数。通常你不需要更改此文件,因为该数组中的元素是由.env文件和Dotenv设置的。但是,如果你决定更改目录结构,你可能需要修改日志或其他路径的位置。

在本地机器上使用 Vökuró 时,你可以考虑修改其中一个参数:useMail并将其设置为false。这会指示 Vökuró 在用户注册网站时不要尝试连接邮件服务器并发送电子邮件。

providers.php

此文件包含了 Vökuró 所需的所有提供者(Providers)。这是应用程序中注册到 DI 容器中的类列表。如果你需要向 DI 容器注册新组件,可以将它们添加到此文件的数组中。

routes.php

此文件包含 Vökuró 所理解的路由。路由器已经注册了默认路由,因此在routes.php中定义的路由是特定路由。当你自定义 Vökuró 时,可以在此文件中添加所需的非标准路由。请记住,默认路由如下:

/:controller/:action/:parameters

提供者(Providers)

如前所述,Vökuró 使用名为 Providers 的类来在 DI 容器中注册服务。这只是在 DI 容器中注册服务的一种方式,并不排除你可以将所有这些注册放在一个单独的文件中。

对于 Vökuró,我们决定每个服务使用一个文件以及一个providers.php(见上文)作为这些服务的注册配置数组。这样我们可以将代码分成更小的部分,每个服务分别保存在一个单独的文件中,同时还有一个数组允许我们在不删除文件的情况下注册或取消注册/禁用某个服务。我们所需要做的只是修改providers.php数组。

提供者类位于src/Providers中。每个提供者类都实现了Phalcon\Di\ServiceProviderInterface接口。更多详情,请参阅下面的引导部分。

Composer

Vökuró使用composer用于下载和安装补充 PHP 库。所使用的库包括:

查看composer.json所需依赖包包括:

"require": {
    "php": ">=8.0",
    "ext-openssl": "*",
    "ext-phalcon": "~5.0.0",
    "robmorgan/phinx": "^0.11.1",
    "swiftmailer/swiftmailer": "^5.4",
    "vlucas/phpdotenv": "^3.4"
}

如果这是一个全新安装,你可以运行

composer install

或者如果你想升级已有的上述包安装:

composer update

有关 composer 的更多信息,可以访问他们的文档页面上找到。

引导程序(Bootstrapping)

入口

我们应用程序的入口点是public/index.php。此文件包含启动并运行应用程序所需的代码。它还是我们应用程序的单一入口点,使我们在需要捕获错误、保护文件等操作时更加方便。

让我们看看代码:

<?php

use Vokuro\Application as VokuroApplication;

error_reporting(E_ALL);
$rootPath = dirname(__DIR__);

try {
    require_once $rootPath . '/vendor/autoload.php';

    Dotenv\Dotenv::create($rootPath)->load();

    echo (new VokuroApplication($rootPath))->run();
} catch (Exception $e) {
    echo $e->getMessage(), '<br>';
    echo nl2br(htmlentities($e->getTraceAsString()));
}

首先,我们确保启用了完整的错误报告。当然,如有需要你可以更改此项,或者重新编写代码,使错误报告由.env文件检索消息。

A try/catch块包裹所有操作。这确保了所有错误被捕获并在屏幕上显示。

注意

你需要重新编写代码以增强安全性。当前情况下,如果数据库发生错误,catch代码将在屏幕上输出包含异常信息的数据库凭据。这段代码旨在作为教程用途,而非完整生产环境应用程序

我们通过加载 Composer 的自动加载器来确保能够访问所有支持库。在composer.json中,我们还定义了autoload条目,指导自动加载器从Vokuro命名空间类加载自src文件夹。

"autoload": {
    "psr-4": {
        "Vokuro\\": "app/"
    },
    "files": [
        "app/Helpers.php"
    ]
}

然后我们通过调用.env file by calling the

Dotenv\Dotenv::create($rootPath)->load();

Finally, we run our application.

应用程序

All the application logic is wrapped in the Vokuro\Application类封装了所有的应用程序逻辑。让我们看看它是如何完成的:

<?php
declare(strict_types=1);

namespace Vokuro;

use Exception;
use Phalcon\Application\AbstractApplication;
use Phalcon\Di\DiInterface;
use Phalcon\Di\FactoryDefault;
use Phalcon\Di\ServiceProviderInterface;
use Phalcon\Mvc\Application as MvcApplication;

class Application
{
    const APPLICATION_PROVIDER = 'bootstrap';

    /**
     * @var AbstractApplication
     */
    protected $app;

    /**
     * @var DiInterface
     */
    protected $di;

    /**
     * @var string
     */
    protected $rootPath;

    /**
     * @param string $rootPath
     *
     * @throws Exception
     */
    public function __construct(string $rootPath)
    {
        $this->di       = new FactoryDefault();
        $this->app      = $this->createApplication();
        $this->rootPath = $rootPath;

        $this->di->setShared(self::APPLICATION_PROVIDER, $this);

        $this->initializeProviders();
    }

    /**
     * @return string
     * @throws Exception
     */
    public function run(): string
    {
        return (string) $this
            ->app
            ->handle($_SERVER['REQUEST_URI'])
            ->getContent()
        ;
    }

    /**
     * @return string
     */
    public function getRootPath(): string
    {
        return $this->rootPath;
    }

    /**
     * @return AbstractApplication
     */
    protected function createApplication(): AbstractApplication
    {
        return new MvcApplication($this->di);
    }

    /**
     * @throws Exception
     */
    protected function initializeProviders(): void
    {
        $filename = $this->rootPath 
                 . '/configs/providers.php';
        if (!file_exists($filename) || !is_readable($filename)) {
            throw new Exception(
                'File providers.php does not exist or is not readable.'
            );
        }

        $providers = include_once $filename;
        foreach ($providers as $providerClass) {
            /** @var ServiceProviderInterface $provider */
            $provider = new $providerClass;
            $provider->register($this->di);
        }
    }
}

该类的构造函数首先创建一个新的 DI 容器,并将其存储在本地属性中。我们使用的是Phalcon\Di\FactoryDefaultDI 容器,其中已经注册了许多服务。

接着我们创建了一个新的Phalcon\Mvc\Application并同样存储在一个属性中。我们还存储了根路径,因为它在整个应用程序中都很有用。

然后我们将此类(即Vokuro\Application)以名称bootstrap注册到 DI 容器中。这使我们可以通过 DI 容器从应用程序的任何部分访问此类。

最后一件事是注册所有提供者。虽然Phalcon\Di\FactoryDefault对象已经注册了许多服务,但我们仍需注册适合我们应用程序需求的提供者。如上所述,每个提供者类都实现了Phalcon\Di\ServiceProviderInterface接口,因此我们可以加载每个类并使用 DI 容器调用其register()方法来注册每个服务。因此,我们首先加载配置数组config/providers.php然后遍历各个条目,依次注册每个提供者。

可用的提供者如下:

提供商 描述
AclProvider 权限
AuthProvider 身份验证
ConfigProvider 配置值
CryptProvider 加密
DbProvider 数据库访问
DispatcherProvider 调度器 - 用于根据 URL 调用对应的控制器
FlashProvider 用于向用户反馈的闪现消息
LoggerProvider 用于记录错误和其他信息的日志记录器
MailProvider 邮件支持
ModelsMetadataProvider 模型的元数据
RouterProvider 路由
SecurityProvider 安全
SessionBagProvider 会话数据
SessionProvider 会话数据
UrlProvider URL 处理
ViewProvider 视图和视图引擎

run()现在将处理REQUEST_URI,对其进行处理并返回内容。应用程序内部将根据请求计算路由,并分派相关的控制器和视图,然后将该操作的结果作为响应返回给用户。

数据库

如上所述,Vökuró 可以使用 MariaDB/MySQL/Aurora、PostgreSql 或 SQLite 作为数据库存储进行安装。在本教程中,我们使用的是 MariaDB。应用程序使用的表包括:

表名 描述
email_confirmations 注册的电子邮件确认
failed_logins 登录失败尝试
password_changes 密码更改的时间及更改人
permissions 权限矩阵
phinxlog Phinx 迁移表
profiles 每个用户的个人资料
remember_tokens 记住我(Remember Me)功能令牌
reset_passwords 重设密码令牌表
success_logins 登录成功尝试
users 用户

模型

遵循模型-视图-控制器模式,Vökuró 每个数据库表(不包括 Phinx 的迁移表)对应一个模型。这些模型允许我们通过一种简单面向对象的方式与数据库表进行交互。模型位于phinxlog目录中,每个模型都定义了相关的字段、源表以及该模型与其他模型之间的关系。一些模型还实现了验证规则,以确保数据正确地存储到数据库中。/src/Models目录下。

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;

class SuccessLogins extends Model
{
    /**
     * @var integer
     */
    public $id;

    /**
     * @var integer
     */
    public $usersId;

    /**
     * @var string
     */
    public $ipAddress;

    /**
     * @var string
     */
    public $userAgent;

    public function initialize()
    {
        $this->belongsTo(
            'usersId', 
            Users::class, 
            'id', 
            [
                'alias' => 'user',
            ]
        );
    }
}

在上面的模型中,我们将表中的所有字段定义为公共属性以便于访问:

echo $successLogin->ipAddress;

注意

如果你留意,属性名称的大小写与相关表中字段名的大小写是一致的。

initialize()方法中,我们还定义了此模型与Users模型的关系。我们指定了字段(本地/远程),以及该关系的一个alias别名标识符(alias identifier)。

echo $successLogin->user->name;

注意

你可以随意打开每个模型文件并识别模型之间的关系。请查阅我们的文档了解不同类型的关联之间的区别。

控制器

再次遵循模型-视图-控制器模式,Vökuró 每个控制器负责处理特定的路由。这意味着AboutController处理/about路由。所有控制器都位于/src/Cotnrollers目录下。

目录中。IndexControllerController。Controller所有控制器类都带有后缀Action每个控制器都有以后缀indexAction结尾的方法,默认动作是IndexController方法将被调用,并执行indexActionindexAction。

此外,除非你注册了特定的路由,否则默认路由(自动注册的)将尝试匹配以下格式:

/profiles/search

/src/Controllers/ProfilesController.php -> searchAction

Vökuró 的可用控制器、动作和路由如下:

控制器 动作 路由地址 描述
About index /about 显示about页面
Index index / 默认动作 - 主页
Permissions index /permissions 查看/修改某一配置级别的权限
Privacy index /privacy 显示隐私页面
Profiles index /profiles 查看配置主页
Profiles create /profiles/create 创建配置
Profiles delete /profiles/delete 删除配置
Profiles edit /profiles/edit 编辑配置
Profiles search /profiles/search 搜索配置
Session index /session 默认会话动作
Session forgotPassword /session/forgotPassword 忘记密码
Session login /session/login 登录
Session logout /session/logout 登出
Session signup /session/signup 注册
Terms index /terms 显示服务条款页面
UserControl confirmEmail /confirm 验证邮箱
UserControl resetPassword /reset-password 重置密码
Users index /users 用户主界面
Users changePassword /users/changePassword 更改用户密码
Users create /users/create 创建用户
Users delete /users/delete 删除用户
Users edit /users/edit 编辑用户

视图

的最后一个组成部分是视图。模型-视图-控制器模式中的视图部分。Vökuró 使用Volt作为其视图引擎。

注意

目录下。views文件夹下的/src文件夹中看到一个名为/themes/vokuro.

views 的文件夹。但 Vökuró 采用稍微不同的方法,将所有视图文件都存储在.volt视图目录包含映射到每个控制器的目录。在这些目录中,每个动作都对应一个

/profiles/create

文件。例如,路由:

ProfilesController -> createAction

对应:

/themes/vokuro/profiles/create.volt

并且视图的位置为:

控制器 动作 视图 描述
About index /about/index.volt 显示about页面
Index index /index/index.volt 默认动作 - 主页
Permissions index /permissions/index.volt 查看/修改某一配置级别的权限
Privacy index /privacy/index.volt 显示隐私页面
Profiles index /profiles/index.volt 查看配置主页
Profiles create /profiles/create.volt 创建配置
Profiles delete /profiles/delete.volt 删除配置
Profiles edit /profiles/edit.volt 编辑配置
Profiles search /profiles/search.volt 搜索配置
Session index /session/index.volt 默认会话动作
Session forgotPassword /session/forgotPassword.volt 忘记密码
Session login /session/login.volt 登录
Session logout /session/logout.volt 登出
Session signup /session/signup.volt 注册
Terms index /terms/index.volt 显示服务条款页面
Users index /users/index.volt 用户主界面
Users changePassword /users/changePassword.volt 更改用户密码
Users create /users/create.volt 创建用户
Users delete /users/delete.volt 删除用户
Users edit /users/edit.volt 编辑用户

The /index.volt文件包含页面的主要布局,包括样式表、JavaScript 引用等。/layouts目录包含应用程序中使用的不同布局,例如,未登录用户使用publiclogin 布局,而登录用户则使用privatemain 布局。各个视图会被注入到布局中,并构建成最终的页面。

组件

我们在 Vökuró 中使用了许多组件,它们在整个应用程序中提供了功能。所有这些组件都位于/src/Plugins目录下。

Acl

Vokuro\Plugins\Acl\Acl是一个实现了访问控制列表的组件,用于我们的应用程序。ACL 控制着哪些用户可以访问哪些资源。您可以在我们的专用页面.

在这个组件中,我们定义了被认为是私有的资源。这些资源保存在一个内部数组中,其中控制器作为键,动作为值,用于标识哪些控制器/动作需要身份验证。它还存储了在整个应用程序中使用的动作的可读性描述。

该组件提供了以下方法:

方法 返回 描述
getActionDescription($action) string 根据简化名称返回动作的描述
getAcl() ACL object 返回 ACL 列表
getPermissions(Profiles $profile) array 返回分配给某个配置文件的权限
getResources() array 返回所有可用的资源及其动作
isAllowed($profile, $controller, $action) bool 检查当前配置文件是否被允许访问某个资源
isPrivate($controllerName) bool 检查一个控制器是否为私有
rebuild() ACL object 重建访问列表到文件中

认证(Auth)

Vokuro\Plugins\Auth\Auth是一个管理身份验证并提供Vökuró中的身份管理的组件。

该组件提供了以下方法:

方法 描述
check($credentials) 验证用户凭据
saveSuccessLogin($user) 创建“记住我”环境设置,用于相关 Cookie 并生成令牌
registerUserThrottling($userId) 实现登录节流功能。减少暴力破解攻击的有效性
createRememberEnvironment(Users $user) 创建“记住我”环境设置,用于相关 Cookie 并生成令牌
hasRememberMe(): bool 检查会话是否有“记住我”Cookie
loginWithRememberMe(): Response 使用 Cookie 中的信息进行登录
checkUserFlags(Users $user) 检查用户是否被禁止/停用/暂停
getIdentity(): array / null 返回当前身份
getName(): string 返回用户名
remove() 从会话中移除用户身份信息
authUserById($id) 通过用户的 ID 进行身份认证
getUser(): Users 获取与当前活跃身份相关联的用户实体
findFirstByToken($token): int / null 返回当前令牌用户
deleteToken(int $userId) 删除会话中的当前用户令牌

邮件 (Mail)

Vokuro\Plugins\Mail\Mail是对Swift Mailer的封装。它暴露了两个方法,send()getTemplate()可以让您从视图中获取模板并使用数据填充它。生成的 HTML 随后可以在send()方法中连同收件人及其他参数一起使用来发送电子邮件消息。

注意

注意,只有在启用useMail后才会使用此组件.env文件中。您还需要确保 SMTP 服务器和凭证有效。

注册 (Sign Up)

控制器

要访问 Vökuró 的所有区域,您需要拥有一个账户。Vökuró 允许您通过点击Create an Account按钮注册网站。

这样做将导航到/session/signupURL,该 URL 将调用SessionControllersignupAction。让我们看看signupAction:

<?php
declare(strict_types=1);

namespace Vokuro\Controllers;

use Phalcon\Flash\Direct;
use Phalcon\Http\Request;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Security;
use Phalcon\Mvc\View;
use Vokuro\Forms\SignUpForm;
use Vokuro\Models\Users;

/**
 * @property Dispatcher $dispatcher
 * @property Direct     $flash
 * @property Request    $request
 * @property Security   $security
 * @property View       $view
 */
class SessionController extends ControllerBase
{
    public function signupAction()
    {
        $form = new SignUpForm();

        // ....

        $this->view->setVar('form', $form);
    }
}

应用程序的工作流程如下:

  • 访问/session/signup
    • 创建表单,将表单发送到视图,渲染表单
  • 提交数据(非 POST 请求)
    • 表单再次显示,没有其他操作发生
  • 提交数据(POST 请求)
    • 错误
      • 表单验证器出现错误,将表单发送至视图并重新渲染(错误将显示出来)
    • 无错误
      • 数据被清理
      • 新模型创建
      • 数据保存到数据库
        • 错误
          • 显示屏幕上的消息并刷新表单
        • 成功
          • 记录已保存
          • 在屏幕上显示确认信息
          • 发送电子邮件(如适用)

表单

为了对用户提供的数据进行验证,我们使用了Phalcon\Forms\FormPhalcon\Filter\Validation*类。这些类允许我们创建 HTML 元素并将验证器附加到它们上。然后将表单传递给视图,在那里实际的 HTML 元素被渲染到屏幕上。

当用户提交信息时,我们将发布的数据发回给表单,相关的验证器对输入进行验证并返回任何潜在的错误消息。

注意

所有 Vökuró 的表单都位于/src/Forms

首先,我们创建一个SignUpForm对象。在此对象中,我们定义我们需要的所有带有各自验证器的 HTML 元素:

<?php
declare(strict_types=1);

namespace Vokuro\Forms;

use Phalcon\Forms\Element\Check;
use Phalcon\Forms\Element\Hidden;
use Phalcon\Forms\Element\Password;
use Phalcon\Forms\Element\Submit;
use Phalcon\Forms\Element\Text;
use Phalcon\Forms\Form;
use Phalcon\Validation\Validator\Confirmation;
use Phalcon\Validation\Validator\Email;
use Phalcon\Validation\Validator\Identical;
use Phalcon\Validation\Validator\PresenceOf;
use Phalcon\Validation\Validator\StringLength;

class SignUpForm extends Form
{
    /**
     * @param string|null $entity
     * @param array       $options
     */
    public function initialize(
        string $entity = null, 
        array $options = []
    ) {
        $name = new Text('name');
        $name->setLabel('Name');
        $name->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The name is required',
                    ]
                ),
            ]
        );

        $this->add($name);

        $email = new Text('email');
        $email->setLabel('E-Mail');
        $email->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The e-mail is required',
                    ]
                ),
                new Email(
                    [
                        'message' => 'The e-mail is not valid',
                    ]
                ),
            ]
        );

        $this->add($email);

        $password = new Password('password');
        $password->setLabel('Password');
        $password->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The password is required',
                    ]
                ),
                new StringLength(
                    [
                        'min'            => 8,
                        'messageMinimum' => 'Password is too short. ' .
                                            'Minimum 8 characters',
                    ]
                ),
                new Confirmation(
                    [
                        'message' => "Password doesn't match " .
                                     "confirmation",
                        'with'    => 'confirmPassword',
                    ]
                ),
            ]
        );

        $this->add($password);

        $confirmPassword = new Password('confirmPassword');
        $confirmPassword->setLabel('Confirm Password');
        $confirmPassword->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The confirmation password ' .
                                     'is required',
                    ]
                ),
            ]
        );

        $this->add($confirmPassword);

        $terms = new Check(
            'terms', 
            [
                'value' => 'yes',
            ]
        );

        $terms->setLabel('Accept terms and conditions');
        $terms->addValidator(
            new Identical(
                [
                    'value'   => 'yes',
                    'message' => 'Terms and conditions must be ' .
                                 'accepted',
                ]
            )
        );

        $this->add($terms);

        $csrf = new Hidden('csrf');
        $csrf->addValidator(
            new Identical(
                [
                    'value'   => $this->security->getRequestToken(),
                    'message' => 'CSRF validation failed',
                ]
            )
        );
        $csrf->clear();

        $this->add($csrf);

        $this->add(
            new Submit(
                'Sign Up', 
                [
                    'class' => 'btn btn-success',
                ]
            )
        );
    }

    /**
     * @param string $name
     *
     * @return string
     */
    public function messages(string $name)
    {
        if ($this->hasMessagesFor($name)) {
            foreach ($this->getMessagesFor($name) as $message) {
                return $message;
            }
        }

        return '';
    }
}

initialize我们正在此方法中设置所需的所有 HTML 元素。这些元素包括:

元素 类型 描述
name Text 用户姓名
email Text 账户的邮箱地址
password Password 账户密码
confirmPassword Password 密码确认
terms Check 接受条款复选框
csrf Hidden CSRF 保护元素
Sign Up Submit 提交按钮

添加元素非常简单直接:

<?php
declare(strict_types=1);

$email = new Text('email');
$email->setLabel('E-Mail');
$email->addValidators(
    [
        new PresenceOf(
            [
                'message' => 'The e-mail is required',
            ]
        ),
        new Email(
            [
                'message' => 'The e-mail is not valid',
            ]
        ),
    ]
);

$this->add($email);

首先,我们创建一个Text对象,并将其名称设为email。我们还将元素的标签设置为E-Mail。之后,我们向该元素附加各种验证器。当用户提交数据并将数据传入表单后,将调用这些验证器。

如上所示,我们在PresenceOf验证器附加到了email元素,并附带提示信息The e-mail is required。验证器将检查用户在点击提交按钮时是否提交了数据,如果验证失败将产生该提示信息。验证器会检查传入的数组(通常是$_POST),对于该特定元素,它会检查$_POST['email'].

我们还附加了Email验证器,负责检查有效的电子邮件地址。如您所见,验证器位于数组中,因此您可以轻松地为任意特定元素附加多个验证器。

最后一步是将该元素添加到表单中。

您会注意到terms元素没有附加任何验证器,因此我们的表单不会检查该元素的内容。

特别注意passwordconfirmPassword元素。您会注意到这两个元素的类型都是Password。其目的是要求您两次输入密码,并且密码必须一致才能避免错误。

The password字段包含两个内容验证器:PresenceOf即它是必填项,并且StringLength:我们需要密码长度超过8个字符。此外,我们还附加了一个名为Confirmation的第三个验证器。此特殊验证器将password元素与confirmPassword元素关联起来。当触发验证时,它将检查两个元素的内容是否一致,如果不一致,则会显示错误信息,即验证失败。

视图

现在我们已经完成了表单的所有设置,接下来我们将表单传递给视图:

$this->view->setVar('form', $form);

我们的视图现在需要渲染渲染这些元素:

{# ... #}
{% 
    set isEmailValidClass = form.messages('email') ? 
        'form-control is-invalid' : 
        'form-control' 
%}
{# ... #}

<h1 class="mt-3">Sign Up</h1>

<form method="post">
    {# ... #}

    <div class="form-group row">
        {{ 
            form.label(
                'email', 
                [
                    'class': 'col-sm-2 col-form-label'
                ]
            ) 
        }}
        <div class="col-sm-10">
            {{ 
                form.render(
                    'email', 
                    [
                        'class': isEmailValidClass, 
                        'placeholder': 'Email'
                    ]
                ) 
            }}
            <div class="invalid-feedback">
                {{ form.messages('email') }}
            </div>
        </div>
    </div>

    {# ... #}
    <div class="form-group row">
        <div class="col-sm-10">
            {{ 
                form.render(
                    'csrf', 
                    [
                        'value': security.getToken()
                    ]
                ) 
            }}
            {{ form.messages('csrf') }}

            {{ form.render('Sign Up') }}
        </div>
    </div>
</form>

<hr>

{{ link_to('session/login', "&larr; Back to Login") }}

我们在视图中为SignUpForm对象设置的变量名是form。因此我们直接使用它并调用它的方法。Volt 中的语法略有不同。在 PHP 中我们会使用$form->render(),而在 Volt 中我们将使用form.render().

视图顶部包含一个条件判断,检查我们的表单是否有任何错误,如果有的话,就为元素附加is-invalidCSS 类。此样式类会在元素旁边添加一个红色边框,突出显示错误并显示相应的提示信息。

此后,我们编写普通 HTML 标签并应用相应的样式。要显示每个元素的 HTML 代码,我们需要调用render()form与相关元素名称一起使用。还要注意我们也会调用form.label()使用相同的元素名称,这样我们就可以分别创建<label>标签中。

在视图末尾渲染CSRF隐藏字段以及提交按钮Sign Up.

发布

如上所述,当用户填写表单并点击Sign Up按钮时,表单将自我提交也就是说,它会将数据发布到相同的控制器和动作(在我们的例子中是/session/signup)。现在该操作需要处理这些提交的数据:

<?php
declare(strict_types=1);

namespace Vokuro\Controllers;

use Phalcon\Flash\Direct;
use Phalcon\Http\Request;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Security;
use Phalcon\Mvc\View;
use Vokuro\Forms\SignUpForm;
use Vokuro\Models\Users;

/**
 * @property Dispatcher $dispatcher
 * @property Direct     $flash
 * @property Request    $request
 * @property Security   $security
 * @property View       $view
 */
class SessionController extends ControllerBase
{
    public function signupAction()
    {
        $form = new SignUpForm();

        if (true === $this->request->isPost()) {
            if (false !== $form->isValid($this->request->getPost())) {
                $name     = $this
                    ->request
                    ->getPost('name', 'striptags')
                ;
                $email    = $this
                    ->request
                    ->getPost('email')
                ;
                $password = $this
                    ->request
                    ->getPost('password')
                ;
                $password = $this
                    ->security
                    ->hash($password)
                ;

                $user = new Users(
                    [
                        'name'       => $name,
                        'email'      => $email,
                        'password'   => $password,
                        'profilesId' => 2,
                    ]
                );

                if ($user->save()) {
                    $this->dispatcher->forward([
                        'controller' => 'index',
                        'action'     => 'index',
                    ]);
                }

                foreach ($user->getMessages() as $message) {
                    $this->flash->error((string) $message);
                }
            }
        }

        $this->view->setVar('form', $form);
    }
}

如果用户已提交数据,则以下代码行将执行,并且我们将执行if语句中的代码:

if (true === $this->request->isPost()) {

这里我们检查用户的请求,判断是否为POST。现在确实是这种情况,我们需要使用表单验证器并检查是否存在错误。[Phalcon\Http\Request][request] 对象允许我们通过使用以下方式轻松获取这些数据:

$this->request->getPost()

现在我们需要将这些提交的数据传递给表单并调用isValid。这将触发每个元素的所有验证器,如果其中任何一个失败,表单会填充内部消息集合并返回false

if (false !== $form->isValid($this->request->getPost())) {

如果一切正常,我们将再次使用 [Phalcon\Http\Request][request] 对象来检索提交的数据,并对其进行清理。以下示例剥离了提交的name字符串中的标签:

$name     = $this
    ->request
    ->getPost('name', 'striptags')
;

请注意,我们从不存储明文密码。相反,我们使用Phalcon\Encryption\Security组件并对其调用hash方法,将提供的密码转换为单向哈希后再存储。这样一来,就算有人入侵我们的数据库,至少也无法获得明文密码。

$password = $this
    ->security
    ->hash($password)
;

现在我们需要将用户提交的数据存储到数据库中。为此,我们创建一个新的Users模型,将经过清理后的数据传入模型,然后调用save:

$user = new Users(
    [
        'name'       => $name,
        'email'      => $email,
        'password'   => $password,
        'profilesId' => 2,
    ]
);

if ($user->save()) {
    $this
        ->dispatcher
        ->forward(
            [
                'controller' => 'index',
                'action'     => 'index',
            ]
        );
}

如果$user->save()返回true,用户将会被转至首页 (index/index) 并显示成功消息。

模型

关系

现在我们需要检查Users模型,因为在模型中有一些我们定义的逻辑,特别是afterSavebeforeValidationOnCreate事件中附加了一些代码。

核心方法,也就是初始化设置发生在initialize方法中。这是我们设置所有关联关系的位置。对于Users类,我们定义了几个关联关系。你或许会问为什么需要关联关系?Phalcon 提供了一种简便的方式用来检索与某个模型相关的数据。

例如,如果我们想查看某个用户的全部登录记录,可以使用如下代码片段实现:

<?php
declare(strict_types=1);

use Vokuro\Models\SuccessLogins;
use Vokuro\Models\Users;

$user = Users::findFirst(
    [
        'conditions' => 'id = :id:',
        'bind'       => [
            'id' => 7,
        ] 
    ]
);

$logins = SuccessLogin::find(
    [
        'conditions' => 'userId = :userId:',
        'bind'       => [
            'userId' => 7,
        ] 
    ]
);

上面这段代码获取 ID 为7的用户,然后从相应的表中获取该用户的所有登录记录。

使用关联关系,我们可以让 Phalcon 替我们完成大量工作。所以上面的代码可以简化为:

<?php
declare(strict_types=1);

use Vokuro\Models\SuccessLogins;
use Vokuro\Models\Users;

$user = Users::findFirst(
    [
        'conditions' => 'id = :id:',
        'bind'       => [
            'id' => 7,
        ] 
    ]
);

$logins = $user->successLogins;

$logins = $user->getRelated('successLogins');

最后两行代码的功能完全相同。这只是你选择使用的语法问题。Phalcon 将查询相关表,并使用用户的 id 过滤相关表。

对于我们的Users表,我们定义了以下的关系:

名称 源字段 目标字段 模型
passwordChanges id usersId PasswordChanges
profile profileId id Profiles
resetPasswords id usersId ResetPasswords
successLogins id usersId SuccessLogins
<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    // ...

    public function initialize()
    {
        $this->belongsTo(
            'profilesId', 
            Profiles::class, 
            'id', 
            [
                'alias'    => 'profile',
                'reusable' => true,
            ]
        );

        $this->hasMany(
            'id', 
            SuccessLogins::class, 
            'usersId', 
            [
                'alias'      => 'successLogins',
                'foreignKey' => [
                    'message' => 'User cannot be deleted because ' .
                                 'he/she has activity in the system',
                ],
            ]
        );

        $this->hasMany(
            'id', 
            PasswordChanges::class, 
            'usersId', 
            [
                'alias'      => 'passwordChanges',
                'foreignKey' => [
                    'message' => 'User cannot be deleted because ' .
                                 'he/she has activity in the system',
                ],
            ]
        );

        $this->hasMany(
            'id', 
            ResetPasswords::class, 
            'usersId', [
            'alias'      => 'resetPasswords',
            'foreignKey' => [
                'message' => 'User cannot be deleted because ' .
                             'he/she has activity in the system',
            ],
        ]);
    }

    // ...
}

如你所见,在定义的关系中,我们有一个belongsTo和三个hasMany。所有的关系都设置了别名,以便更容易地访问它们。belongsTo关系还设置了reusable标志为启用状态。这意味着如果在同一请求中多次调用该关系,Phalcon 只会在第一次调用时执行数据库查询,并将结果集缓存起来,后续调用都会使用缓存的结果集。

另外值得注意的是,我们定义了特定的外键消息。如果违反了某条关系约束,就会抛出预先定义的消息。

事件

[Phalcon\Mvc\Model][db-models] 被设计为能够触发特定的事件这些事件方法可以位于监听器中,也可以直接定义在模型中。

对于UsersafterSavebeforeValidationOnCreate事件中附加了一些代码。

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    public function beforeValidationOnCreate()
    {
        if (true === empty($this->password)) {
            $tempPassword = preg_replace(
                '/[^a-zA-Z0-9]/', 
                '', 
                base64_encode(openssl_random_pseudo_bytes(12))
            );

            $this->mustChangePassword = 'Y';

            $this->password = $this->getDI()
                                   ->getSecurity()
                                   ->hash($tempPassword)
            ;
        } else {
            $this->mustChangePassword = 'N';
        }

        if ($this->getDI()->get('config')->useMail) {
            $this->active = 'N';
        } else {
            $this->active = 'Y';
        }

        $this->suspended = 'N';

        $this->banned = 'N';
    }
}

The beforeValidationOnCreate每次新增记录(Create)时,都会在任何验证之前触发该事件。我们首先检查是否设置了密码,如果没有,就生成一个随机字符串,然后使用Phalcon\Encryption\Security对其进行哈希处理,并将生成的哈希值存储在password属性中。同时还将更改密码的标志设为 true。

如果密码不为空,我们只需将mustChangePassword字段设置为N。最后,我们设置一些默认值以确定用户是否是active, suspendedbanned。这确保了记录在插入数据库之前已准备好。

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    public function afterSave()
    {
        if ($this->getDI()->get('config')->useMail) {
            if ($this->active == 'N') {
                $emailConfirmation          = new EmailConfirmations();
                $emailConfirmation->usersId = $this->id;

                if ($emailConfirmation->save()) {
                    $this->getDI()
                         ->getFlash()
                         ->notice(
                            'A confirmation mail has ' .
                            'been sent to ' . $this->email
                        )
                    ;
                }
            }
        }
    }
}

The afterSave事件在记录保存到数据库之后立即触发。在这个事件中,我们检查是否启用了邮件发送功能(参见.env文件useMail设置),若启用则会在EmailConfirmations表中创建一条新记录,并保存这条记录。完成后,屏幕上会出现一条提示信息。

注意

注意,EmailConfirmations模型还有一个afterCreate事件,负责实际向用户发送电子邮件。

验证

模型还有一个validate方法,它允许我们将一个验证器附加到模型中的任意字段上。对于Users表来说,email字段必须唯一。因此我们为其添加了Uniqueness 验证器。在对模型执行任何保存操作之前,该验证器会被触发,如果验证失败则返回相应的消息。

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    public function validation()
    {
        $validator = new Validation();

        $validator->add(
            'email', 
            new Uniqueness(
                [
                    "message" => "The email is already registered",
                ]
            )
        );

        return $this->validate($validator);
    }
}

结论

Vökuró 是我们用来演示 Phalcon 提供的一些功能的一个示例应用程序。当然它未必能适用于所有需求。不过你可以将其作为开发你自己的应用的起点。

参考资料

无噪 Logo
无噪文档
25 年 6 月翻译
版本号 5.9
文档源↗