强意见的 MVC 框架,用于快速开发

0.9.9 2020-03-05 09:46 UTC

README

     _                 _                                  
 ___(_)_ __ ___  _ __ | | ___  _ __     ___ ___  _ __ ___ 
/ __| | '_ ` _ \| '_ \| |/ _ \| '_ \   / __/ _ \| '__/ _ \
\__ \ | | | | | | |_) | | (_) | | | | | (_| (_) | | |  __/
|___/_|_| |_| |_| .__/|_|\___/|_| |_|  \___\___/|_|  \___|
                |_|                                       

简化版/Core

简化版/core 包含一系列有明确意见的库,这些库构成了一个基于组件的应用的核心。此包需要 PHP7.1+,并基于 PSR-7 与中间件层构建。

  1. 应用结构
    1.1 注册表
    1.2 上下文
    1.3 路由
    1.4 存储
    1.5 外出请求
  2. 骨架
    2.1 生成默认应用
    2.2 添加组件
    2.3 向组件添加视图
    2.4 向组件添加存储
  3. 中间件
    3.1 异常处理
    3.2 地区设置
    3.3 路由
    3.4 身份验证
  4. 控制器
    4.1 视图控制器
    4.2 REST 控制器
  5. 视图
    5.1 模板
    5.2 构建页面
  6. 表单辅助器
    6.1. 定义表单字段
    6.2. 创建表单视图
    6.3. 在主视图中实现
    6.4. 控制器实现

1. 应用结构

应用主要由一些应用级别的类和一些组件组成,而组件本身是小程序。一般思路是,如果将应用分解成更小的部分,则更容易沟通和维护,因此重点关注组件。此外,当涉及 configlocale 模块时,应用使用继承。继承从应用级别到组件级别,这样组件就可以访问应用级别的数据。

主要概念是 MVC,这意味着你的 controller 将接收请求,在所需帮助的辅助下处理数据,并将其传递给视图或重定向到另一个资源。Simplon\Core 区分了两种类型的控制器:ViewController 是第一种类型,用于处理应生成网站的请求。第二种类型是 RestController,用于处理任何与 api 相关数据的请求。后者将以 JSON 结构化 的数据响应。

组件只能通过 事件 进行通信。它们可以通过其他组件的 提供 拉取信息,或者 订阅 事件。组件在其自己的事件类中描述其事件。提供订阅 定义为类常量。

需要注意的是,所有请求都将通过一系列预定义的 Middleware,其中 RouteMiddleware 是唯一必需的,因为它处理所有传入的请求。

1.1. 注册

每个组件都需要通过其自己的注册类进行注册。它需要一个接收 Context 类的方法,并且可以可选地接收引用 路由身份验证规则事件 定义的 方法

1.2. 上下文

应用程序和组件都有自己的 Context 类,这些类包含所有基本实例。基本意味着这些实例在应用程序中(分别是在组件中)的不同类之间共享。例如,如果您有一个组件的存储,您将把该存储的实例创建放在组件的上下文类中。上下文类还包含对您的配置和区域数据的引用。

1.3. 路由

所有组件相关的路由都在基于组件的 Route 类中定义。此类包含所有路由 模式 和用于构建相应路由的静态方法。

1.4. 存储

存储通过 CRUD 类进行处理。目前仅提供 MySQL 适配器。存储由其存储和模型类描述。如果您想与数据交互,应通过存储类进行,避免直接访问。

1.5. 出站请求

与组件相关的 出站请求 都收集在其自己的 Requests 类中,主要是为了辅助透明度和结构。显然,只有当您有任何这类请求时,此类才是必需的。

2. 骨架

核心有一个命令行工具,允许您创建代码骨架,以帮助您设置应用程序、组件或组件的部分,例如 CrudStore/CrudModel 类。

您可以通过在终端运行以下命令来查找所有可能的命令,在您使用 composer install 安装 simplon/core 之后:

vendor/bin/core -h

2.1. 生成默认应用程序

让我们创建一个名为 MyApp 的默认应用程序。我们想为我们的应用程序使用 视图,因此我们将使用选项 --with-view。如果您只想使用 REST 接口,则不需要此选项。

vendor/bin/core init MyApp --with-view 

2.2. 添加组件

由于核心是组件化的,因此我们需要至少有一个组件。让我们添加一个,命名为 Cars。同样,我们想要使用 视图,因此我们必须添加该选项,但这次我们必须添加我们第一个 ViewController 的名称。

vendor/bin/core component Cars --with-view=Car

还有一个用于 REST 接口的选项

vendor/bin/core component Cars --with-rest

也可以结合这两个选项

vendor/bin/core component Cars --with-view=Car --with-rest

2.3. 向组件添加视图

如果您有任何组件需要另一个 视图,您可以运行以下命令,这将添加一个新的 ViewController 和默认的 视图模板 集合。

对于以下示例,假设我们有一个名为 Team 的组件。现在我们想要为资源添加一个骨架视图

vendor/bin/core view Team Resources 

这将创建以下文件

  • App/Components/Team/Controllers/TeamViewController.php
  • App/Components/Team/Views/Resources/ResourcesView.php
  • App/Components/Team/Views/Resources/ResourcesTemplate.phtml

您仍然需要添加一个 路由 并在 注册表 中注册此路由。

2.4. 向组件添加存储

如果您的任何组件需要 CrudStore,您可以运行以下命令,这将构建一个默认的存储/模型类集。您可以添加设置 storemodel数据库表 名称的选项。默认情况下,它将派生自 组件名称

vendor/bin/core store Cars

3. 中间件

中间件帮助我们处理/组织我们的请求/响应处理。它还通过预处理请求来简化某些操作,例如在它到达实际控制器之前进行身份验证。最后,它是一个非常出色的工具,因为它具有可扩展性和灵活性。到目前为止,核心提供了四个类。

3.1. 异常

包装所有后续处理并使用 Whoops 处理异常。

3.2. 区域设置

如果集成,它将检测请求路由中的两位字母定义的区域设置(例如 /en/)或甚至带有额外三位字母的区域特定区域设置(例如 /en-us/)。如果后者是情况,则区域特定文件(例如 en-us-locale.php)将从主区域设置(例如 en-locale.php)继承。此中间件期望一个接受的区域设置代码数组(例如 ['en', 'de']),但如果未提供,则回退到 ['en']

3.3. 路由

路由中间件将请求的路由与所有定义的路由(仅注册的组件)进行对比。它还将启动这些组件的 事件处理

3.4. 认证

AuthMiddleware 帮助对某些 路由用户角色临时令牌 进行认证。为了使此操作正常工作,我们需要创建一个 AuthContainer,这是 AuthMiddleware 所必需的,同样也是 ComponentsCollection。后者是必需的,因为所有认证规则都是由每个组件定义的。

AuthContainer 是您必须设置的一个类,它应该扩展核心的抽象 AuthContainer 类。AuthMiddleware 将使用此容器通过调用 fetchUser 来认证当前请求,该请求应处理实际的认证。因此,您可以根据自己的意愿选择如何认证请求。

如果存在,AuthMiddlware 将调用 onSuccessonError 回调。这两个回调都将收到一个 ResponseInterface 对象,而 onSuccess 将收到 AuthUserInterface 作为第二个参数。请确保在两种情况下都返回 ResponseInterface 对象。

最后,您需要将所有这些放在一起。以下是一个粗略的引导示例

$appContext = new AppContext();

$components = new ComponentsCollection();
$components->add(new FooRegistry($appContext());

$authContainer = new AuthContainer();

$middleware = new MiddlewareCollection();
$middleware->add(new AuthMiddleware($authContainer, $components));

(new Core())->run($components, $middleware);

3.4.1. 基于会话的 AuthContainer 示例

这只是粗略地说明了讨论的内容

namespace App\Components\Auth\Managers;

use App\Components\Auth\AuthRoutes;
use App\Components\Auth\Data\AuthSessionUser;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Simplon\Core\Interfaces\AuthUserInterface;
use Simplon\Core\Middleware\Auth\AuthContainer;

/**
 * @package App\Components\Auth\Managers
 */
class AuthViewContainer extends AuthContainer
{
    /**
     * @param ServerRequestInterface $request
     *
     * @return null|AuthUserInterface
     */
    public function fetchUser(ServerRequestInterface $request): ?AuthUserInterface
    {    
        if (!empty($_SESSION['session']))
        {                    
            return new AuthSessionUser($_SESSION['session']);
        }

        return null;
    }

    /**
     * @return callable|null
     */
    protected function getOnError(): ?callable
    {
        return function (ResponseInterface $response) {
            if (empty($response->getHeaderLine('Location')))
            {
                $response = $response->withAddedHeader('Location', AuthRoutes::toSignIn());
            }

            return $response;
        };
    }
}

3.4.2. 基于REST的 AuthContainer 示例

这是一个带有 bearer token 和在用户数据库中查找的粗略示例

namespace App\Components\Auth\Managers;

use App\Components\Auth\AuthRoutes;
use App\Components\Auth\Data\AuthRestUser;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Simplon\Core\Interfaces\AuthUserInterface;
use Simplon\Core\Middleware\Auth\AuthContainer;
use Simplon\Mysql\Mysql;
use Simplon\Mysql\MysqlException;

/**
 * @package App\Components\Auth\Managers
 */
class AuthRestContainer extends AuthContainer
{
    /**
     * @var Mysql
     */
    private $mysql;

    /**
     * @param Mysql $mysql
     */
    public function __construct(Mysql $mysql)
    {
        $this->mysql = $mysql;
    }

    /**
     * @param ServerRequestInterface $request
     *
     * @return null|AuthUserInterface
     * @throws MysqlException
     */
    public function fetchUser(ServerRequestInterface $request): ?AuthUserInterface
    {    
        if ($bearer = $this->fetchAuthBearer($request))
        {
            list($token, $secret) = explode(':', $bearer);

            $query = '
            -- noinspection SqlDialectInspection
            select * from ' . AuthStore::TABLE_NAME . ' where ' . AuthModel::COLUMN_TOKEN . ' = :token
            ';

            if ($row = $this->mysql->fetchRow($query, ['token' => $token]))
            {
                $authUser = new AuthRestUser($row);

                if ($secret)
                {
                    $authUser->validateSecret($secret);
                }

                return $authUser;
            }
        }

        return null;
    }

    /**
     * @return callable|null
     */
    protected function getOnError(): ?callable
    {
        return function (ResponseInterface $response) {
            if (empty($response->getHeaderLine('Location')))
            {
                $response = $response->withAddedHeader('Location', AuthRoutes::toSignIn());
            }

            return $response;
        };
    }
}

4. 控制器

每个标识的路由最终都会进入一个控制器。有两种类型的控制器,它们只在几个 媒体相关方法 和它们的 响应内容类型 方面有所不同。两种类型都期望一个 __invoke 方法,该方法接收一个空数组或一组可能的参数。这些参数是您定义的路由的部分结构,导致连接的控制器。例如,一个如 /some/{foo}/stuff 的路由将与请求的路由 /some/more/stuff 匹配。对于该示例,您的控制器参数将包含 ['foo' => 'more']

4.1. ViewController

此控制器类型用于所有导致渲染 HTML 页面的请求。

class SomeViewController extends ViewController
{
    /**
    * @param array $params
    *
    * @return ResponseViewData
    */
    public function __invoke(array $params): ResponseViewData
    {
        // some code

        //
        // you can handle redirects
        //
        
        if($shouldRedirect)
        {
            $this->getFlashMessage()->setFlashSuccess('Some flash message');

            return $this->redirect('/some/route');
        }

        //
        // or respond with view data
        //
        
        return $this->respond(new SomeView());
    }

    /**
    * @return SomeRegistry
    */
    public function getRegistry(): SomeRegistry
    {
        return $this->registry;
    }
}

4.2. RestController

class SomeRestController extends RestController
{
    /**
     * @param array $params
     *
     * @return ResponseViewData
     */
    public function __invoke(array $params): ResponseRestData
    {
        // some code
        
        //
        // respond with array data which will be
        // transformed into JSON
        //
        
        return $this->respond(['foo' => 'bar']);
    }

    /**
     * @return SomeRegistry
     */
    public function getRegistry(): SomeRegistry
    {
        return $this->registry;
    }
}

5. 视图

视图只了解它们接收到的依赖项。第一个依赖项和唯一的要求是 CoreViewData,它包含 LocaleFlashMessagesDevice 的实例。《Locale》的功能很明确。《FlashMessages》显示控制器中定义的警告、错误或成功消息。《Device》用于根据 mobiletablet其他任何东西 检测定义的模板。视图期望一个模板和一些可选数据,这些数据将被注入到模板中。上述提到的 CoreViewData 实例将自动注入。

5.1. 模板

如前所述,每个模板默认接收三个变量:$locale$flash$device。视图类还提供了一些静态辅助方法,例如 View::renderWidget,它有助于渲染较小的模板片段或我们称之为 widgets。下面是一个模板的简单示例。

/**
 * @var FlashMessage $flash
 * @var Locale $locale
 * @var Device $device
 *
 * @var string $content
 */
use Simplon\Core\Views\FlashMessage;
use Simplon\Core\Views\View;
use Simplon\Device\Device;
use Simplon\Locale\Locale;

?>
<?php if ($flash->hasFlash()): ?>
    <?= $flash->getFlash('huge') ?>
<?php endif ?>

<div>
    Some content
</div>

<div>
    <?= View::renderWidget(__DIR__ . '/SomeWidget.phtml', ['foo' => 'bar']) ?>
</div>

使用设备模板

默认情况下,视图使用定义的模板。然而,它还尝试通过查找这些特定模板来检测与设备相关的模板。

例如,假设我们的默认模板是 DefaultTemplate.phtml,并且我们正在使用 平板设备。在这种情况下,我们的视图将查找现有的 DefaultTemplateTablet.phtml。如果该模板存在,它将优先使用该模板而不是定义的模板。同样的情况也适用于 手机设备。在这种情况下,我们的视图将查找 DefaultTemplateMobile.phtml

侧记:如果不存在平板模板,平板设备也会偏好 手机模板

5.2. 构建页面

构建页面非常重要,因为我们嵌套视图:组件有自己的视图,但可能也有几个子视图。这些子视图将在组件视图模板中 实现,作为注入的变量。组件视图本身将在 应用程序视图会话包装视图 中实现。原则始终相同:所有底层视图包裹上层视图,使用多少层取决于你。

此过程将由我们的组件控制器处理。

protected function buildPage(ViewInterface $view, ComponentViewData $componentViewData, GlobalViewData $globalViewData): ViewInterface
{
    $appContext = $this->getContext()->getAppContext();
    
    $componentView = new AccountsPageView($this->getCoreViewData(), $componentViewData);
    $componentView->implements($view, 'content');
    
    $sessionView = new SessionPageView($this->getCoreViewData(), $appContext->getUserSessionManager()->read());
    $sessionView->implements($componentView, 'content');
    
    $appView = $appContext->getAppPageView($this->getCoreViewData(), $globalViewData);
    $appView->implements($sessionView, 'content');
    
    return $appView;
}

您还可以看到两个数据类:ComponentViewDataGlobalViewData。这些是帮助将数据传输到我们的组件视图和应用程序视图的辅助工具,因为我们的组件中可能有多个子视图。它有助于我们组织和描述我们的数据。

6. 表单助手

核心提供了一些表单助手类,以简化并结构化应用程序中的使用。以下段落将展示如何使用这些助手的全例。

6.1. 定义表单字段

namespace App;

use Simplon\Core\Utils\Form\BaseForm;
use Simplon\Form\Data\FormField;
use Simplon\Form\Data\Rules\RequiredRule;
use Simplon\Form\Data\Rules\EmailRule;

class CreateForm extends BaseForm
{
    const NAME = 'name';
    const EMAIL = 'email';

    /**
     * @return FormField[]
     */
    protected function buildFields(): array
    {
        return [
            $this->getName(),
            $this->getEmail(),
        ];
    }

    /**
     * @return FormField
     */
    private function getName(): FormField
    {
        return (new FormField(self::NAME))->addRule(new RequiredRule());
    }

    /**
     * @return FormField
     */
    private function getEmail(): FormField
    {
        return (new FormField(self::EMAIL))->addRule(new EmailRule());
    }
}

6.2. 创建表单视图

namespace App;

use App\CreateFormFields;
use Simplon\Core\Utils\Form\BaseFormView;
use Simplon\Form\FormError;
use Simplon\Form\View\Elements\DropDownElement;
use Simplon\Form\View\Elements\InputTextElement;
use Simplon\Form\View\FormViewBlock;
use Simplon\Form\View\FormViewRow;

class CreateFormView extends BaseFormView
{
    /**
     * @return FormViewBlock[]
     * @throws FormError
     */
    protected function getBlocks(): array
    {
        return [
            $this->buildFormViewBlock(self::BLOCK_DEFAULT)
                ->addRow(
                    $this->>buildFormViewRow()
                        ->autoColumns($this->getNameElement())
                        ->autoColumns($this->getEmailElement())
                ),
        ];
    }

    /**
     * @return string
     */
    protected function getSubmitLabel(): string
    {
        return $this->getLocale()->get('form-create-submit-label');
    }

    /**
     * @return InputTextElement
     * @throws FormError
     */
    private function getNameElement(): InputTextElement
    {
        $element = new InputTextElement($this->getFields()->get(CreateFormFields::NAME));

        $element
            ->setLabel($this->getLocale()->get('form-create-name-label'))
            ->setPlaceholder($this->getLocale()->get('form-create-name-placeholder'))
        ;

        return $element;
    }

    /**
     * @return InputTextElement
     * @throws FormError
     */
    private function getEmailElement(): InputTextElement
    {
        $element = new InputTextElement($this->getFields()->get(CreateFormFields::EMAIL));

        $element
            ->setLabel($this->getLocale()->get('form-create-email-label'))
            ->setPlaceholder($this->getLocale()->get('form-create-email-placeholder'))
        ;

        return $element;
    }
}

6.3. 在主视图中实现

namespace App;

use Simplon\Core\Utils\Form\ViewWithForm;

class CreateView extends ViewWithForm
{
    /**
     * @return string
     */
    protected function getTemplate(): string
    {
        return __DIR__ . '/CreateTemplate.phtml';
    }
}

6.3.1. 主模板

/**
 * @var Locale $locale
 * @var FlashMessage $flash
 * @var Device $device
 *
 * @var FormView $formView
 */

use App\AppContext;
use Simplon\Core\Views\FlashMessage;
use Simplon\Core\Views\View;
use Simplon\Device\Device;
use Simplon\Form\View\FormView;
use Simplon\Locale\Locale;

?>
<div class="ui grid">
    <div class="sixteen wide column">
        <div class="section-content">
            <?= $formView->render(__DIR__ . '/FormTemplate.phtml', ['locale' => $locale]) ?>
        </div>
    </div>
</div>

6.3.2. 表单模板

/**
 * @var Locale $locale
 * @var FormView $formView
 */
use App\CreateFormView;
use Simplon\Form\View\FormView;
use Simplon\Locale\Locale;

?>

<div class="ui basic segment">
    <?= $formView->getBlock(CreateFormView::BLOCK_DEFAULT)->render() ?>
</div>

<?= $formView->getSubmitElement()->renderElement() ?>

6.4. 控制器实现

namespace App;

use App\CreateForm;
use App\CreateFormView;
use App\CreateView;
use Simplon\Core\Controllers\ViewController;
use Simplon\Core\Utils\Form\FormWrapper;
use Simplon\Core\Data\ResponseViewData;
use Simplon\Form\FormFields;

class CreateViewController extends ViewController
{
    /**
     * @param array $params
     *
     * @return ResponseViewData
     * @throws \Simplon\Form\FormError
     */
    public function __invoke(array $params): ResponseViewData
    {
        $formWrapper = $this->buildFormWrapper(
            new CreateForm($this->getLocale())
        );

        if ($formWrapper->getValidator()->validate()->isValid())
        {
            // do something with the form data
            
            return $this->redirect('/some/other/url');
        }

        $formView = new CreateFormView($this->getLocale(), $formWrapper->getFields());

        return $this->respond(
            new CreateView($this->getCoreViewData(), $formView)
        );
    }
}