etenil/assegai

一个完整的PHP MVC框架。

v2.2.9 2015-12-06 11:56 UTC

README

更新日期:2014年8月3日

简介

Assegai是一个功能齐全的PHP MVC框架。它是基于MIT许可的免费软件。

该框架曾经依赖于微框架Atlatl。这个依赖关系在2.0版本中被移除。

安装

注意:有一个完整的演示视频,展示了安装和入门部分的内容。请访问youtube观看视频。

要安装Assegai,您首先需要安装composer。然后为您的项目创建一个composer.json文件,其中需要etenil/assegai。一个示例composer.json文件应该如下所示

{
    "name": “YOUR_NAME/helloworld”,
    "require": {
        "etenil/assegai": "2.*"
    }
}

现在保存并退出composer.json文件,然后在您的项目根目录(使用命令行)运行以下命令

$ composer install

一旦Assegai安装完成,您将在项目文件夹中安装一个完全功能的MVC框架。

入门指南

有几种可能的设置和使用Assegai框架的方法。以下部分展示了当前最简单的设置。推荐的设置文档将很快发布。

引导

您需要为项目创建一个引导器或使用默认的引导器。框架附带了一个示例引导器,您可以根据需要进行修改。

要使用默认的引导器,请在您的项目根目录中运行以下命令

$ cp vendor/etenil/assegai/bootstrapper.example.php index.php

这将把已提供的示例引导器文件复制到项目根目录,并将文件重命名为index.php

基本配置

尽管Assegai有默认的配置选项,但如果不创建配置文件,则不会工作,即使它是空的。您可以使用框架附带的conf.example.php文件。为此,从项目根目录使用以下命令

$ cp vendor/etenil/assegai/conf.example.php conf.php

现在您已经完全配置了框架。为了测试设置,下面的部分将展示如何使用Assegai创建应用程序。

Hello World应用程序

在本章中,我们将看到如何为assegai编写一个非常简单的应用程序,它将在HTML页面上显示著名的“Hello, World”消息。

要创建应用程序,首先进入您项目的根目录中的conf.php文件,并将应用程序名称添加到apps数组中。您可以根据需要删除或重命名现有的示例应用程序。

您的配置文件应如下所示

<?php
    $conf['apps_path'] = __DIR__ . '/apps';

    $conf['apps'] = [
        'helloworld',   
    ];

现在让我们创建实际的应用程序。从项目的根目录中输入以下命令

$ vendor/etenil/assegai/assegai app helloworld

这将为您创建应用程序的文件系统结构。默认的应用程序结构如下所示

apps
 - helloworld
   |- conf.php
   |- controllers
   |- exceptions
   |- models
   |- views

现在在controllers目录中创建一个名为Hello.php的文件,并放入以下代码

<?php
    namespace helloworld\controllers;
    
    class Hello extends \assegai\Controller
    {
        function sayHello()
        {
            return "Hello, World!";
        }
    }

请注意在\assegai\Controller上放置第一个反斜杠,否则您会遇到问题。

我们还需要让框架知道当访问网站时需要调用此控制器。这通过将以下内容添加到应用程序的conf.php文件中来实现。请注意,此conf.php文件不同于通用配置文件。

<?php
    $app['route'] = [
        '/' => 'helloworld\controllers\Hello::sayHello',
    ];

测试它

现在,使用以下命令启动PHP内部web服务器(或配置您首选的web服务器)

php -S localhost:8080 -t ./

然后,当您用浏览器访问您的web服务器时,应该会看到打印的 你好,世界 信息。

命名空间约定

Assegai鼓励使用多个专用应用程序来共享模型,以便实现网站。该框架依赖于PSR-0命名标准。

类需要这样命名

app\type\Name

例如

myapp\models\DemoCode

您还可以使用子文件夹来存放模型和控制台,并相应地扩展命名空间

myapp\models\demo\SomeModel

使用模型

现在让我们尝试通过引入一个模型来修改练习。模型通过将所有数据管理委托给专用类,是一种强大且方便的方式来组织代码。

创建一个名为 models/hello.php 的文件,其中包含以下代码

namespace hello\models;

class Hello extends \assegai\Model
{
    function hello()
    {
        return 'Hello, Model';
    }
}

现在我们需要从控制器中加载模型。让我们在 controllers/hello.php 中创建一个新函数

namespace hello\controllers;

class Demo extends \assegai\Controller
{
    function hello()
    {
        return "Hello, World!";
    }

    function hello_model()
    {
        $hello = $this->model('Hello');
        return $hello->hello();
    }
}

最后,我们需要在 conf.php 中创建一个指向此新函数的路由

$app['route'] = [
    '/' => 'hello\controllers\Demo::hello',
    '/model' => 'Demo::hello_model',
    ];

现在尝试使用 /model 段访问您的安装,例如 https:///index.php/model。您应该看到显示的消息“Hello, Model”。

请注意,我们为'/model'路径使用了隐式命名空间路由。这很方便,可以节省打字。

视图

现在让我们尝试用视图来做我们之前做过的事情。我们将从现有的模型中获取数据,然后将其输入到视图中并显示。

我们首先创建视图。创建一个名为 views/hello.phtml 的文件,并包含以下代码

<DOCTYPE html>
<html>
    <head>
        <title>Assegai Tutorial</title>
    </head>
    <body>
        <p><?=$vars->message?></p>
    </body>
</html>

注意 $vars->message 变量。

让我们在控制器体内创建另一个函数

function hello_view()
{
    $hello = $this->model('Hello');
    return $this->view('hello', array('message' => $hello->hello()));
}

最后,我们将在这个新函数中创建一个路由 conf.php

$app['route'] = [
    '/' => 'Demo::hello',
    '/model' => 'Demo::hello2',
    '/view' => 'Demo::hello_view',
];

尝试访问带有 /view 段的url,例如 https:///index.php/view,您应该看到带有 Hello, Model 替换消息变量的视图。

视图助手

视图助手是方便的函数,它们返回或输出一些HTML,并在视图中用于格式化和显示数据。考虑以下

<?php $h->form->input('text', 'foobar') ?>
=> <input type="text" name="foobar" id="foobar"/>

您可能会这样实现这个函数

function input($type, $name) {
    echo "<input type=\"$type\" name=\"$name\" id=\"$name\"/>";
}

默认情况下,助手不会被加载;视图必须声明它所使用的必要助手,如下所示

<?php $load_helper('form'); ?>

目前,Assegai不提供任何助手。但您可以轻松实现自己的。

助手始终对所有应用程序可用,并作为 helpers_path 文件夹(默认情况下为项目根目录中的 helpers 文件夹)中的类的一部分声明。

助手类必须遵循通常的约定,并命名为 helpers\SomeName。将相关的助手打包在一起是个好主意。以下是一个示例

namespace helpers;

class Form {
    function input($type, $name) {
        echo "<input type=\"$type\" name=\"$name\" id=\"$name\"/>";
    }
}

路由

路由基于每个应用程序定义,并且较后加载的应用程序会覆盖冲突的路由。

路由是基于正则表达式的。因此,可以很容易地通配任何路由的一部分,并将其指向同一处理器。路由中的捕获花括号映射为处理器的参数。因此,可以使用以下

// Handler for route '/foo/([0-9]+)'
function foo($num)
{
    return $num;
}

您还可以通过在路由前加上所需方法的前缀来为特定HTTP方法定义路由

$app['route'] = [
    'GET:/bar' => 'app\controllers\Foo::bar_get',
    'POST:/bar' => 'app\controllers\Foo::bar_post',
    '/bar' => 'app\controllers\Foo::bar',
];

请注意,路由也支持隐式命名空间,如下所示

$app['route'] = [
    '/bar' => 'Foo::bar', // This will be assumed as 'app\controllers\Foo'.
];

在大型路由表中,使用路由组很方便。它们使表格更清晰,并减少打字量

$app['route'] = [
    '@/test' => [
        '/foo' => 'Test::testFoo', // This is understood as '/test/foo'.
        '/bar' => 'Test::testBar',
    ],
    '@/real' => [
        '/foo' => 'Real::foo',
        '/bar' => 'Real::bar',
    ],
];

首先搜索方法特定的路由,如果没有找到,再搜索其他路由。

URL前缀

如果您的网站从子文件夹运行,那么路由最终会有一个常数前缀,例如,网站将在 foo 文件夹中运行,并产生类似 http://host.com/foo/myroute 的URL。在这种情况下,请按如下方式设置配置中的URL前缀

$conf['prefix'] = '/foo'

然后路由将被自动解析为 /myroute

通用路由

捆绑的路由器包含几个内置处理程序。这些处理程序可以用于对URL执行即时操作。

通用路由是在路由器内部定义的,不能扩展或修改。它们接受参数,因此看起来与普通路由不同。

一个典型的通用路由可能很简单,就像

'::access_denied'

或者使用参数作为

['::view', 'baz', ['foo' => 'bar']]

重定向

重定向只是实现301 HTTP重定向的一种简单方式,无需触摸Web服务器的配置文件,或创建专用控制器。重定向路由接受两个参数,第一个是重定向目标,第二个是重定向的HTTP代码(通常是301或有时是302)。如果省略第二个参数,则默认为301。

$app['route'] = [
    '/foo' => ['::redirect', '/bar', 301],
    '/bar' => 'app\\controllers\\Bar::bar',
];

视图

视图通用路由非常适合所有网站都有的准静态文件。视图路由接受两个参数,第一个是视图的名称,最后一个是一个传递给视图的关联数组。

$app['route'] = [
    '/foo' => ['::view', 'foo', ['text' => 'Hello, world!']],
    '/bar' => ['::view', 'bar'],
];

配置

框架和应用配置作为服务器属性的一部分轻松访问。您可以像这样访问它们

$this->server->main->get('apps');
$this->server->app->get('my_very_important_setting');

这些配置字典只是包装了解析后的配置文件,因此您可以轻松定义自己的设置。

控制器

控制器是Assegai应用程序的核心。它们提供对模型、视图和模块的访问。

初始化

Assegai为其控制器提供了一个基本的实现,您的控制器应该扩展此实现。此实现包含一个不能重载的最终构造函数。因此,您应该将初始化代码放在提供的 _init() 方法中,该方法始终由父构造函数调用。

视图

控制器包含一个名为 view() 的辅助方法,它加载一个视图并返回填充的内容。该 view() 辅助方法接受视图的名称作为参数,以及一个值关联数组作为第二个参数。

模型

控制器有一个名为 model() 的辅助函数,可以轻松地加载模型。该函数接受模型类名作为参数,并返回实例化的模型。

模型辅助函数支持隐式命名空间,从而节省了大量输入。

class MyController extends \assegai\Controller
{
    function foo()
    {
        $my_model = $this->model('app\models\Foo');
        $my_short_model = $this->model('Foo');
    }
}

模块

模块在应用程序之间共享,成员变量 modules 从控制器提供了对这些模块的轻松访问。有关更多信息,请参阅专用章节。

其他实用程序

dump() 方法允许轻松地将值包装在一个 pre HTML块中,从而使其易于阅读。

appPath() 方法可以用来获取到应用程序相关路径的绝对路径。该方法期望一个相对路径作为参数。

模型

模型是提供对某些数据提供程序抽象的对象。它们的角色通常是确保数据有效性、存储和检索。

Assegai让您自由地以您喜欢的任何方式组织您的模型,基 Model 类只提供了一个 _init() 方法,以及成员变量 $modules 以访问加载的模块。

应用程序模型

每个应用程序都可以在其 models 文件夹中拥有自己的模型。这些模型遵循命名约定

<application>\models\<Model>

共享模型

如果您的网站足够小,只需一个应用程序,那么您可能只想在您的应用程序中使用模型。然而,对于非平凡的网站,通常使用共享模型会更容易。

所有共享模型都位于由 models_path 配置确定的单个文件夹中,默认情况下是assegai根目录中的 models 文件夹。它们可以由任何应用程序轻松加载。

通常,您在共享文件夹中会有很多模型。根据您的组织方式,您可能为单个模型拥有多个类。因此,共享模型的命名约定略有不同,下划线字符用作分隔符,以确定模型源文件的位置。这样,您可以在文件夹中组织您的模型。

models\polls\Negative\Mapper
models/poll/negative/Mapper.php

异常

应用程序包含一个异常文件夹。该文件夹旨在包含可能在应用程序的任何地方抛出并处理的异常类。

异常的命名约定很简单

<app>\exceptions\<Name>

单元测试

Assegai支持对您的应用程序进行单元测试,并提供必要的粘合代码,以便PHPUnit能够正常运行。

为了测试您的代码,您需要安装PHPUnit,并在测试文件夹中创建一个phpunit.xml和一个引导文件。

phpunit.xml通常是这样的

<phpunit bootstrap="./bootstrap.php" colors="true">
    <testsuite name="Unit">
        <directory>./</directory>
    </testsuite>
</phpunit>

以及写入bootstrap.php文件的引导代码是这样的

<?php
define('APP_PATH', dirname(__DIR__) . '/');
define('ROOT_PATH', dirname(dirname(dirname(__DIR__))) . '/');
require(ROOT_PATH . 'lib/testloader.php');

您可能需要更新APP_PATH和ROOT_PATH的定义,以便与您的实际设置相匹配。

然后您可以使用以下命令运行您的测试

phpunit -c phpunit.xml

模块

模块提供从提供的$modules辅助变量在ControllerModel中,或者从挂钩上的动作来修改框架行为的高级功能。

模块在Assegai中全局可用,尽管您也可以在应用程序配置以及主配置中声明它们。如果当前应用程序包含已加载模块的配置,则该配置将具有优先级,高于全局配置文件。

Assegai附带了一些预安装的模块。以下各节将描述每个模块。

ACL

此模块为Assegai提供简单的访问控制列表支持。

列表必须在应用程序的conf.php文件中声明。控制列表实际上是某些角色对资源的操作权限。首先必须定义角色和资源,然后定义它们的交互。以下是一个简短的示例列表。

$app['acl'] = array(
    'roles' => array(
        'user' => null,
        ),
    'resources' => array(
        'article' => null,
        ),
    'privileges' => array(
        'user' => array(
            'article' => array('view', 'comment'),
        ),
    ),
);

角色和资源也支持继承。以下是一个示例。

$app['acl'] = array(
    'roles' => array(
        'user' => null,
        'author' => array('user'),
        ),
    'resources' => array(
        'article' => null,
        'admin' => array('article'),
        ),
    'privileges' => array(
        'user' => array(
            'article' => array('view', 'comment'),
        ),
        'admin' => array(
            'article' => array('edit'),
            'admin' => array('access'),
        ),
    ),
);

一旦建立了列表,您就可以使用模块的isAllowed()辅助函数来查找角色是否允许在资源上执行操作。以下是一个简短的示例。

$article = new Article();
$user = new User();

if($this->modules->acl->isAllowed('user', 'article', 'view')) {
    $article->show();
}

Mustache

此模块用修改后的Mustache引擎替换标准视图。请注意,此模块期望视图文件具有.tpl扩展名,而不是常用的.phtml

有关模板引擎语法的更多信息,请参阅项目网站上的官方文档。

PDO

这提供了PDO数据库连接的抽象。

数据库连接必须在应用程序的conf.php文件中定义,如下所示

$app['pdo'] = array(
    'myNiceConnection' => array(
        'dsn' => 'mysql:host=localhost;dbname=somedb',
        'username' => 'root',
        'password' => 'somepassword!',
    ),
    'conn2' => array(
        'dsn' => 'mysql:host=localhost;dbname=otherdb',
        'username' => 'root',
        'password' => 'somepassword!',
    ),
);

这将实例化两个连接,可以像这样访问

$this->modules->pdo->myNiceConnection->exec('INSERT INTO demo VALUES('1', '2', '3')');

有关PDO连接的更多信息,请参阅PHP文档。

Paginator

此模块提供方便的分页器以处理结果。该模块目前仅支持对数组进行分页。

以下提供了以下函数

  • getPage($num) 获取第$num页
  • getCurrentPage() 返回当前页
  • count() 计算元素数量。
  • setPage($num) 将分页器设置为第$num页
  • setPageLength($length) 将页长设置为$length。默认为10。
  • getPageNum() 获取当前页码
  • getPages() 返回可用页面的总数
  • getPagesList($length) 获取当前页周围的页面列表

用法示例

$data = range(0, 100);
$paginator = new \assegai\modules\paginator\Paginator::fromArray($data);
$paginator->setPage(3);
foreach($paginator->getCurrentPage() as $item) {
    echo $item;
}
foreach($paginator->getPagesList() as $page) {
    echo $page;
}

ESI

ESI模块将ESI兼容性引入Assegai。ESI是一个标准,该标准允许将页面片段单独缓存,并由边缘服务重新构建最终的页面。例如,Squid实现了ESI。

此模块目前尚不可用。如果您有时间并且有动力,欢迎您对其进行开发。

验证器

验证器模块允许您轻松地将表单数据与一组格式和过滤器进行校验。

快速示例

以下示例展示了如何使用自定义异常抛出验证异常。然后您可以从调用方法中检索错误信息。在控制器中验证数据不是好的做法,这应该在模型中处理。这只是一个快速示例。

$validator = new \assegai\modules\validator\Validator($post);
$validator
    ->required('You must supply a name.')
    ->validate('name', 'Name');
$validator
    ->required('You must supply an email address.')
    ->email('You must supply a valid email address')
    ->validate('email', 'Email');

// check for errors
if ($validator->hasErrors()) {
    throw new Validator_Exception(
        'There were errors in your form.',
        $validator->getAllErrors()
        );
}

可用的验证方法

  • exists($message = null) - 字段必须存在,无论其内容如何。
  • required($message = null) - 字段值是必需的。
  • email($message = null) - 字段值必须是一个有效的电子邮件地址字符串。
  • float($message = null) - 字段值必须是一个浮点数。
  • integer($message = null) - 字段值必须是一个整数。
  • digits($message = null) - 字段值必须是一个数字(没有上限的整数)。
  • min($limit, $include = TRUE, $message = null) - 字段值必须大于 $limit(数值)。$include 定义值是否可以等于限制。
  • max($limit, $include = TRUE, $message = null) - 字段值必须小于 $limit(数值)。$include 定义值是否可以等于限制。
  • between($min, $max, $include = TRUE, $message = null) - 字段值必须在 $min 和 $max(数值)之间。$include 定义值是否可以等于 $min 和 $max。
  • minLength($length, $message = null) - 字段值必须大于或等于 $length 个字符。
  • maxLength($length, $message = null) - 字段值必须小于或等于 $length 个字符。
  • length($length, $message = null) - 字段必须是 $length 个字符长。
  • matches($field, $label, $message = null) - 一个字段与另一个字段匹配(例如密码匹配)
  • notMatches($field, $label, $message = null) - 字段值必须不匹配 $field 的值。
  • startsWith($sub, $message = null) - 字段必须以字符串 $sub 开头。
  • notStartsWith($sub, $message = null) - 字段必须不以字符串 $sub 开头。
  • endsWith($sub, $message = null) - 字段必须以字符串 $sub 结尾。
  • notEndsWith($sub, $message = null) - 字段必须不以字符串 $sub 结尾。
  • ip($message = null) - 字段值是一个有效的 IP,使用 filter_var 确定。
  • url($message = null) - 字段值是一个有效的 URL,使用 filter_var 确定。
  • date($message = null) - 字段值是一个有效的日期,可以是 DateTime() 接受的任何格式。
  • minDate($date, $format, $message = null) - 日期必须大于 $date。$format 必须是页面上的格式 https://php.ac.cn/manual/en/datetime.createfromformat.php
  • maxDate($date, $format, $message = null) - 日期必须小于 $date。$format 必须是页面上的格式 https://php.ac.cn/manual/en/datetime.createfromformat.php
  • ccnum($message = null) - 字段值必须是一个有效的信用卡号。
  • oneOf($allowed, $message = null) - 字段值必须是 $allowed 中的其中一个值。$allowed 可以是一个数组或以逗号分隔的值列表。如果以逗号分隔,除非有意用于匹配,否则不要包含空格。
  • callback($callback, $message = '', $params = null) - 定义您自己的自定义回调验证函数。$callback 必须通过 is_callable() 检查。$params 可以是任何值,或者如果必须传递多个参数,则是一个数组。
验证数组和数组索引

此验证类已扩展,允许验证数组以及多维数组的嵌套索引。

要验证数组的特定索引,请使用点表示法,例如。

// load the validator
$validator = new \assegai\modules\validator\Validator($this->request->allPost());

// ensure $_POST['field']['nested'] exists
$validator
  ->required('The nested field is required.')
  ->validate('field.nested');

// ensure we have the first two numeric
// indices of $_POST['links'][]
$validator
  ->required('This field is required')
  ->validate('links.0');
$validator
  ->required('This field is required')
  ->validate('links.1');
可用的预验证过滤

您可以将预验证过滤器应用于您的数据(例如,trim、strip_tags、htmlentities)。只要通过 is_callable() 检查,这些过滤器也可以自定义。

  • filter($callback)

示例

// standard php filter for valid user ids.
$validator
  ->filter('intval')
  ->min(1)
  ->validate('user_id');

// custom filter
$validator
  ->filter(function($val) {
    // bogus formatting of the field
    $val = rtrim($val, '/');
    $val .= '_custom_formatted';
  })
  ->validate('field_to_be_formatted');