janatzend/zend-expressive-legacy-bridge

该软件包提供桥接功能,以便在 Zend Expressive 中运行旧版应用程序

0.2.0 2016-11-09 21:35 UTC

This package is not auto-updated.

Last update: 2024-09-28 19:47:30 UTC


README

Zend Expressive Legacy Bridge 是一个库,允许基于 Zend Framework 1 和 Symfony 1 的旧版应用程序作为中间件层在 Zend Expressive 中运行。目标是提供这些应用程序的 REST API,而无需更改原始应用程序的代码。

先决条件

Zend Expressive Legacy Bridge 需要 PHP 7

安装

使用 composer 安装

composer require janatzend/zend-expressive-legacy-bridge

加载旧版桥接器

旧版桥接器提供多个服务和中间件实现。为了将它们加载到 Zend Expressive 应用程序中,必须在配置容器的 dependencies 部分插入 ConfigProvider 服务提供的数组。

示例

如果配置是按照此处文档创建的 https://zendframework.github.io/zend-expressive/features/container/zend-servicemanager/#configuration-driven-container,则应在 config/autoload 目录中放置一个名为 legacybridge.global.php 的文件,并包含以下内容

<?php
use Zend\Expressive\LegacyBridge\ConfigProvider;

return (new ConfigProvider())();

此代码也用于以下项目 https://bitbucket.org/account/user/janatzendteam/projects/ELMhttps://bitbucket.org/account/user/janatzendteam/projects/EX

用法

由于现代化问题可能没有一刀切的解决方案,因此旧版桥接器旨在提供丰富且大多数情况下可用的功能集,同时允许修改所有组件以适应需求。换句话说,它不是一键或一步解决方案,而是为框架提供扩展旧版功能的灵活选项。旧版桥接器负责提供一个中间件实现,其中(几乎)封装了 ZF1 或 Symfony 1 MVC 应用程序的全部执行流程。但是,还需要一些手动工作。一般来说,必须提供三个不同的组件

初始化

旧版应用程序的初始化详细信息必须可定制。例如,设置包含路径、配置文件路径、环境变量等。旧版桥接器提供了一个选项,可以在其中定义一个服务来指定这些详细信息。通常,它类似于从原始 index.php 中复制和粘贴。请参阅下面的示例部分了解如何实现。

路由

在使用旧版桥接器之前必须回答的一个重要问题是:“新 REST API 的路由应该是什么样的?”有几个选项

  • 在 URL 路径前缀添加类似 /api/rest 的内容
  • 使用 Accept-Header:如果它是 application/json,则应该提供来自 API 的 JSON 响应,否则提供正常的 HTML 输出
  • 以上两种方法的组合

示例 1 展示了如何使用 Accept-Header,示例 2 是为使用前缀路径 /rest 编程的

Hydrator

由于遗留桥不是屏幕抓取器,所以遗留桥的功能是负责关闭遗留应用程序视图组件的渲染。此外,还需要捕获/禁用重定向。视图对象由一个Hydrator或Hydrator链处理,负责以结构化格式输出所需的数据。多亏了Hydrator,可以过滤数据,特别是可以按自定义方式解决传递给遗留应用程序视图对象的那些对象。

示例

由于没有使用遗留桥的最佳实践,为了方便起见,提供了两个示例,这两个示例都使用相同的遗留桥核心,但以非常不同的方式和设置。这两个示例的共同之处在于遗留应用程序与新API功能并行运行。

Zend Framework 1应用程序MyTracking

MyTracking是一个Zend Framework 1应用程序,最初是在几年前客户合作中开发的原型。MyTracking是一个CRUD实现,用于在数据库中管理(跟踪)图像。此外,还可以使用存储的数据生成PDF。

MyTracking还可以在IBM i上运行DB2

初始化

在启动引导之前,Zend Framework 1应用程序需要一些信息。例如,include_path通常在index.php中设置。但是,环境变量(例如,是否为生产系统或开发系统)也在早期以常量的形式设置。由于Expressive应用程序不应这样做,遗留桥允许在桥中间件的第一步执行可调用的函数。这个可调用的函数必须在配置容器中以zf1-prereq的名称找到。在这个示例中,一个名为zf1-prereq.global.php的文件定义了可调用的函数如下

'zf1-prereq' => function() {
    defined('APPLICATION_ENV')
        || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));
    // ...
    set_include_path(implode(PATH_SEPARATOR, array(
        realpath(APPLICATION_PATH . '/../src'),
        // ...
        get_include_path(),
    )));
}

路由

MyTracking的index.php设置Zend Expressive应用程序;原始的index.php已被重命名为zf.php

ApiDecider

为了提供API,原始应用程序的路由没有被修改。相反,请求的Accept-Header的值决定输出应该是HTML还是JSON。这个决策由ApiDecider中间件做出,该中间件位于Zend\Expressive\Application::pipeRoutingMiddleware()Zend\Expressive\Application::pipeDispatchMiddleware()之间

$app = AppFactory::create($container, $container->get(RouterInterface::class));
$app->pipeRoutingMiddleware();
$app->pipe('/', ApiDecider::class);
$app->pipeDispatchMiddleware();

这意味着ApiDecider在成功路由后立即变得活跃。它检查Accept-Header的值,如果它等于application/json,则调用管道中的下一个中间件。否则,它使用LocationRedirector服务,并停止管道执行。LocationRedirector将路径重写为例如从/tracking/list/zf.php/tracking/list,并将其重定向到该位置。LocationRedirector期望一个名为pathCreator的服务,将请求对象传递给它。借助该服务,可以重写URL路径。在这个示例中,该服务定义在config/autoload/redirectory.global.php文件中

<?php
return [
    'dependencies' => [
        'services' => [
            'pathCreator' => function ($req) {
                $url  = $req->getUri();

                return '/zf.php' . $url->getPath();
            }
        ]
    ]
]

API定义

index.php的最后定义,在实际运行应用程序之前,指定了支持的API/REST调用。

$app->get('/tracking/list', Zf1\Bridge::class);
$app->delete('/tracking/delete/id/{id:[0-9]+}', Zf1\Bridge::class);
$app->get('/tracking/new', Zf1\Bridge::class);
$app->post('/tracking/new', Zf1\Bridge::class);

如我们所见,所有的路由都已经存在于遗留应用程序中。这是必要的,因为路由被传递到MVC,如果它与原始路由不同,则不会找到匹配项。然而,通过一些附加功能(见示例2),也可以使用新的路由,多亏了自定义路由映射机制。

尽管定义给人一种印象,认为直接调用 Zf1\Bridge 来提供中间件,但这并不正确。相反,Zf1\Bridge 是用于加载 Zf1\BridgeFactory 的标识符。原因可以在文件 ConfigProvider.php 中的 Legacy Bridge 库代码中找到 - Legacy Bridge 服务在此处定义。这种机制使得覆盖库核心功能变得非常容易。

Hydrator

由于视图没有渲染,自定义 Hydrators 必须将视图元素转换为可以输出为 JSON 的结构。Zend_View 对象持有所有视图变量作为公共实例变量。在 MyTracking 示例中,路由为 /tracking/list 的视图对象只包含一个值,即 list。遗憾的是,由于 list 包含不应出现在 JSON 响应中的二进制数据(图像存储在数据库中),因此无法直接输出其内容。为了提供筛选功能,已创建文件 config/autoload/hydrator.global.php。它包含一个服务,该服务将路由映射到一组视图项(在此示例中我们只有一个:list)及其相应的 Hydrator 实现。

'services' => [
    'hydratorstrategies' => [
        '/tracking/list^GET' => [
            'list' => 'HydratorStrategyTrackingList'
        ],
        // ...
    ]
]

hydratorstrategies 数组键是由 Legacy Bridge 保留的。HydratorStrategyTrackingList 指向同一文件中定义的 Hydrator 实现。

'factories' => [
    'HydratorStrategyTrackingList' => function() {
        return new StrategyChain([
            new ClosureStrategy(function ($data) {
                if (!is_a($data, 'ArrayAccess')) return $data;

                return $data->toArray();
            }),

            new ClosureStrategy(function ($data) {
                /* HACK for specific situation, Rowset conatins binary data */
                foreach ($data as $rowNr => $row) {
                    foreach ($row as $key => $value) {
                        if (preg_match('~[^\x20-\x7E\t\r\n]~', $value) > 0) unset($data[$rowNr][$key]);
                    }
                }

                return $data;
            })
        ]);
    }
],

这里有趣的部分不是实际的代码,因为它是实现细节,而是两个所谓的 Hydrator 策略被组合到一个 Hydrator 链中;因此,list 值中的数据被转换两次。

Symfony 1 应用程序 Jobeet

Jobeet 是一个在 Symfony 1 在线培训教程的背景下开发的应用程序。

见: https://symfony.com.cn/legacy/doc/jobeet?orm=Doctrine

初始化

Jobeet 应用程序只需做最小的工作来初始化,实际上只需要包含正确的路径的 ProjectConfiguration 类文件。因此,文件 config/autoload/sf1-prereq.global.php 包含以下代码

'sf1-prereq' => function() {
    require_once(dirname(__FILE__).'/../../config/ProjectConfiguration.class.php');
}

Legacy Bridge 在 Bridge 中间件的第一步执行可调用的 sf1-prereq

路由

在此示例中,Zend Expressive 应用程序设置是在名为 web/expressive.php 的文件中完成的。原始文件 web/index.php 没有被修改。新的 Jobeet API 依赖于 URL 中的路径前缀:/rest。因此,在 web/.htaccess 文件中添加了一个重写规则,确保在请求新路径时调用 web/expressive.php

RewriteRule ^rest/(.*)$ expressive.php [L,NC]

API定义

web/expressive.php 中实际运行应用程序之前,最后的定义指定了支持的 API/REST 调用。

$app->get('/rest/categories/', Bridge::class);
$app->post('/rest/search/',  Bridge::class);

尽管定义给人一种印象,认为 Bridge 直接用于提供中间件,但这并不正确。相反,Bridge 是用于加载 BridgeFactory 的标识符。原因可以在文件 ConfigProvider.php 中的 Legacy Bridge 库代码中找到 - Legacy Bridge 服务在此处定义。这种机制使得覆盖库核心功能变得非常容易。

第一眼可能令人困惑的另一件事是,原始 Jobeet 应用程序不提供显示所有(职位)类别的功能。然而,上面的第一个路由正好允许这样做。为什么这是可能的?首先,Jobeet 应用程序的开始页面,或者更确切地说,开始页面的视图对象包含有关不显示的类别的信息。为了显示此信息,我们需要一个 hydrator(见下文),还需要从 /rest/categories/ 到开始页面路由 /en/ 的映射,以在 Bridge 中间件中加载正确的操作。此映射可以在文件 config/autoload/route-mapping.global.php 中找到。

'route-mapping' => [
    '/rest/categories/^GET' => '/en/',
    // ...
]

Legacy Bridge 使用此配置项将 API 路由映射到提供必要信息的遗留应用程序的路由。

Hydrator

由于视图未渲染,自定义Hydrators必须将视图元素转换为可以输出为JSON的结构。在Symfony 1中,视图变量存储在动作视图对象的varHolder容器中。在Jobeet示例中,对于路由/rest/categories//en/,视图对象包含一个categories变量。该变量以结构化格式包含对象数组而不是标量值。因此,需要在文件config/autoload/hydrator.global.php中定义一个特定的Hydrator,以便将数据转换为所需的格式。

'services' => [
    'hydratorstrategies' => [
        '/rest/categories/^GET' => [
            'categories' =>  'HydratorStrategyCategories'
        ],
        // ...
    ]
]

hydratorstrategies数组键已被Legacy Bridge保留。HydratorStrategyCategories指向同一文件中的Hydrator定义。

'factories' => [
    'HydratorStrategyCategories' => HydratorStrategyCategoriesFactory::class,
    // ...
],

指定的类在src文件夹中可用。该类或相应的方法用于处理对象数组,以过滤所需的数据。