redgem / servicesio-bundle
ServicesIO是一个提供轻松构建服务提供者方式的Symfony组件
Requires
- php: >=7.0
README
ServicesIOBundle是一个提供轻松构建服务提供者方式的Symfony组件。
ServicesIO主要引入了两个组件
-
一个读取器。读取器为您构建一个包装输入数据的模型。
- 来自标准输入(例如请求对象)。
- 来自您想要请求的第三方服务。
-
一个视图层,用于构建响应的对象树视图。视图层由您的控制器调用,构建您想要渲染的树,并实际渲染它(目前仅支持JSON)。
安装组件
首先,在您的项目中添加并启用ServicesIOBundle。
在composer中添加它
composer require redgem/servicesio-bundle:1.0.*
设置完成。
ServicesIO模型
该模型将帮助您读取和解码传递给您的控制器的树结构(例如json),例如。
模型组件的文档将在以后提供
ServicesIO视图
您可能已经习惯了在项目中使用Twig来构建和渲染HTML视图。
ServicesIO的目标是能够为数据树提供与Twig为HTML提供的一样强大和高效的东西。
概述
ServicesIO视图提供您创建的能力
- 可重用的视图组件,可通过路径配置调用。
- 可扩展的视图组件,具有数据占位符和填充它们的智能方式。
- 与组件扩展的集成,可以通过扩展组件来覆盖视图组件。
在开始之前,您需要了解以下信息
-
视图渲染系统通过两步系统运行
1 - 您将在您的视图中使用语言无关的数据结构创建一个树,通过创建连接的节点。一个节点由3个可用的元素组成
- 一个集合,用于处理其他节点的列表。
- 一个项,用于处理带有节点附加的命名字段。
- 一个标量,将是要渲染的数据片段(例如string、int、float)。
2 - 树的渲染。一旦完全构建,我们就可以将其转换为数据语言(目前仅支持json)并发送到输出。
基本操作:创建您的视图
现在,让我们用一个简短的例子来看看它是如何工作的。
为了便于阅读,我将删除与我们的主题无关的所有代码。
让我们创建一个小的示例项目:2个实体和2个控制器。
实体Doctrine
class User { /** * @MongoDB\Id */ public $id; /** * @MongoDB\Field(type="string") */ public $name; }
class Message { /** * @MongoDB\Id */ public $id; /** * @MongoDB\Field(type="string") */ public $title; /** * @MongoDB\Field(type="string") */ public $description; /** * @MongoDB\Field(type="id") */ public $user; }
假设我有2个用户:author和visitor,以及3条消息:message1、message2、message3。
以下是控制器(不包括它们的返回调用)
class MessageController { public function singleAction(int $id, DocumentManager $documentManager) { $single = $documentManager ->getRepository(Message::class)->find($id); } public function listingAction(DocumentManager $documentManager) { $listing = $documentManager ->getRepository(Message::class)->findAll(); } }
基于此,我们现在需要创建装饰器和实体的视图元素,并从控制器中调用渲染。
让我们先完成messageAction的视图,使用一个ServicesIO视图类。
一个基本的View
类必须
- 扩展
Redgem\ServicesIOBundle\Lib\View\View
- 位置由您自己决定,我们建议使用
<APP_OR_YOUR_BUNDLE>\View命名空间
(因此在<APP_OR_YOUR_BUNDLE>/View
目录中),以便进行清晰的组织。 - 实现
content()
,这是构建视图树的场所。 - 类名由您自己决定,我们建议将其命名为
<NAME>View
。
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class SingleView extends View { public function content() { return $this->createItem() ->set('message', $this->createItem() ->set('id', $this->params['single']->id) ->set('title', $this->params['single']->title); ->set('description', $this->params['single']->description) ); } }
我们在这里创建一个简单的树,其中包含一个包含3个子项的Item对象:id、title、description,以显示我们的3个相应的message字段。
最后,让我们调用并渲染控制器。
在ServicesIOBundle中要调用的渲染服务名为Redgem\ServicesIOBundle\Lib\View\Service
。
调用需要两个参数
- viewpath,视图类名。
- 要发送的参数数组。
use Redgem\ServicesIOBundle\Lib\View\Service as View; class MessageController { public function singleAction(int $id, DocumentManager $documentManager, View $view) { $single = $documentManager ->getRepository(Message::class)->find($id); return $view->render( SingleView::class, ['single' => $single] ); } }
在控制器中作为参数发送的single变量在视图类中可通过$this->params['single']访问。
以第一个消息为例来调用它!
{ "message" : { "id" : "1", "title" : "message 1 title", "description" : "description 1 title" } }
很好,现在我们有MessageView类树的json表示了!
现在我们可以将其复制到listing操作中。
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class ListingView extends View { public function content() { $collection = $this->createCollection(); foreach($this->params['listing'] as $message) { $collection->push( $this->createItem() ->set('id', $this->params['message']->id) ->set('title', $this->params['message']->title); ->set('description', $this->params['message']->description) ); ); } return $this->createItem() ->set('listing', $collection); } }
use Redgem\ServicesIOBundle\Lib\View\Service as View; class MessageController { public function listingAction(DocumentManager $documentManager, View $view) { $listing = $documentManager ->getRepository(Message::class)->findAll(); return $view->render( ListingView::class, ['listing' => $listing] ); } }
让我们调用它。
{ "listing" : [ { "id" : "1", "title" : "message 1 title", "description" : "description 1 title" }, { "id" : "2", "title" : "message 2 title", "description" : "description 2 title" }, { "id" : "3", "title" : "message 3 title", "description" : "description 3 title" } ] }
太棒了!
部分视图和片段控制器
我们遇到了一个问题。我们在两个类之间重复了创建message部分视图的操作。
幸运的是,有一个解决方案:创建一个可重用元素。
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class MessageView extends View { public function content() { return $this->createItem() ->set('id', $this->params['message']->id) ->set('title', $this->params['message']->title); ->set('description', $this->params['message']->description) ); } }
正如你所见,一个可重用元素完全是一个普通的视图类。这意味着,如果你想的话,你可以直接用它来渲染控制器。
最后,我们在SingleView和ListingView中使用它。
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class SingleView extends View { public function content() { return $this->createItem() ->set('message', $this->partial(MessageView::class, ['message' => $this->params['single']])); } }
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class ListingView extends View { public function content() { $collection = $this->createCollection(); foreach($this->params['listing'] as $message) { $collection->push( $this->partial(MessageView::class, ['message' => $message]) ); } return $this->createItem() ->set('listing', $collection); } }
当调用partial()
方法从那里获取子树时,它将为你传递当前上下文(即从控制器发送的参数)与在第二个方法参数中添加的参数合并后的参数。
当然,最终的json渲染仍然是完全相同的。
现在让我们在所有地方用新的UserView丰富数据响应。
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class UserView extends View { public function content() { return $this->createItem() ->set('id', $this->params['user']->id) ->set('name', $this->params['user']->name); ); } }
和
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class MessageView extends View { public function content() { return $this->createItem() ->set('id', $this->params['message']->id) ->set('title', $this->params['message']->title); ->set('description', $this->params['message']->description) ->set('user', ($this->params['message']->user == null) ? null : $this->partial(UserView:class, ['user' => $this->params['message']]) ) ); } }
现在单个请求将显示
{ "message" : { "id" : "1", "title" : "message 1 title", "description" : "description 1 title", "user": { "id": "1", "name": "author" } } }
除了partial()
方法之外,还有一个controller()
方法可用。它不仅调用视图,还会调用整个Symfony控制器作为片段。
它的原型是
function controller($controller, $params = array())
其中
$controller
:一个常规的Symfony控制器名称(作为字符串,带有完整类名)$params
:一个数组,它将与当前参数上下文合并,并传递给新的控制器。
将以这种方式处理响应
- 如果这个新的控制器返回一个ServicesIO视图响应,片段树将在主树的正确位置合并。
- 否则,响应将被处理为一个字符串,并在主树的正确位置合并。
还有一件事
get($service)
- 字符串 $service:一个Symfony服务名称。getParameter($parameter)
- 字符串 $parameter:一个参数名称。
这些方法也是可用的。它们只是使用container调用一个Symfony服务或一个参数。
视图扩展
现在我们想在响应的顶部装饰连接的用户。使用这个controller()
方法来做这件事很简单。
我假设用户已经被Security组件正确认证。
public function visitorAction(View $view) { return $view->render( UserView::class, ['user' => $this->getUser()] //returns a User object ); }
我将它添加到我的视图中
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class SingleView extends View { public function content() { return $this->createItem() ->set('visitor', $this->controller('App\MyController\VisitorAction')) ->set('response', $this->createItem() ->set('message', $this->partial(MessageView::class, ['message' => $this->params['single']])) ); } }
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class ListingView extends View { public function content() { $collection = $this->createCollection(); foreach($this->params['listing'] as $message) { $collection->push( $this->partial(MessageView::class, ['message' => $message]) ); } return $this->createItem() ->set('visitor', $this->controller('App\MyController\VisitorAction')) ->set('response', $this->createItem() ->set('listing', $collection) ); } }
现在单个请求将显示
{ "visitor": { "id": "2", "name": "visitor" }, "content": { "message" : { "id" : "1", "title" : "message 1 title", "description" : "description 1 title", "user": { "id": "1", "name": "author" } } } }
我的重复问题又出现了...在全局装饰器上。我用visitor和response创建了两次主要对象。
我们可以通过改变我们的思维方式来解决这个问题。而不是只有一个类来构建树,让我们将其分成两个元素
- 一个类来构建主要装饰节点(即,用visitor和response)
- 填充每个视图类中response节点的字段。
首先,让我们创建装饰器。它仍然是一个普通的View类
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class DecoratorView extends View { public function content() { return $this->createItem() ->set('visitor', $this->controller('App\MyController\VisitorAction')) ->set('response', null, 'fullResponse'); } }
在set()
方法上有一个新的第三个参数。这个第三个参数是一个字符串,用于设置占位符选项的名称。占位符是树上的一个条目,以后可以被另一个值替换。
如果使用占位符,第二个值(此处为null)是默认值,如果占位符未填充,则将显示此默认值。
现在我们可以将我们的SingleView和ListingView转换为使用此装饰器
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class SingleView extends View { public function getParent() { return DecoratorView::class; } public function blockFullResponse() { return $this->createItem() ->set('message', $this->partial(MessageView::class, ['message' => $this->params['single']])); } }
namespace MyBundle\View; use Redgem\ServicesIOBundle\Lib\View\View; class ListingView extends View { public function getParent() { return DecoratorView::class; } public function blockFullResponse() { $collection = $this->createCollection(); foreach($this->params['listing'] as $message) { $collection->push( $this->partial(MessageView::class, ['message' => $message]) ); } return $this->createItem() ->set('listing', $collection); } }
我们可以看到两个区别
- 出现了一个新的
getParent()
方法:这意味着根节点将在这个视图类中移动。所有上下文都传递给此对象,显然,这是一个标准视图类,您可以在任何地方重用它。 content()
方法被blockFullResponse()替换。content()
方法是创建树根的方法。因此,它与getParent()方法不兼容。具有
getParent()
方法的类将仅填充其上定义的占位符。这就是blockXXX()
方法的目的,其中XXX是占位符名称的驼峰式命名。方法(即这里的blockFullResponse)。
当然,您可以使用getParent()
链多少层您想要的层次结构,并在其中定义所有占位符。
getParent()
通常返回一个字符串。它也可以返回一个数组
public function getParent() { return array(MyFriendBundleView::class, MyBundleDecoratorView::class); }
在这种情况下,将选择实际实现的第一视图类。
为什么有这么多花哨的东西?
为什么这样做?您可能会说,使用常规的PHP类的extends关键字会更简单,避免使用viewpath,您是对的。但是,ServicesIO视图提供了一个简单、灵活和清晰的构建一些视图可重用组件的方法。
所以最后,我们可以调用我们的控制器
{ "visitor": { "id": "2", "name": "visitor" }, "content": { "message" : { "id" : "1", "title" : "message 1 title", "description" : "description 1 title", "user": { "id": "1", "name": "author" } } } }
{ "visitor": { "id": "2", "name": "visitor" }, "content": { "listing" : [ { "id" : "1", "title" : "message 1 title", "description" : "description 1 title", "user": { "id": "1", "name": "author" } }, { "id" : "2", "title" : "message 2 title", "description" : "description 2 title", "user": { "id": "1", "name": "author" } }, { "id" : "3", "title" : "message 3 title", "description" : "description 3 title", "user": { "id": "1", "name": "author" } } ] } }