ServicesIO is a Symfony bundle that provide you the ability to turn your project into an efficient service oriented element very easily
ServicesIOBundle is a Symfony bundle that provides a way to easily build a services provider., (*1)
ServicesIO basically introduces two components :, (*2)
a reader. the reader build for you a model that wrap the input data., (*3)
a view layer to scaffold an object tree view of the reponse. The view layer is called by your controller, build the tree you want to render, and actually render it (json only so far)., (*4)
First of all, add and enable ServicesIOBundle in your project., (*5)
Add it in composer :, (*6)
composer require redgem/servicesio-bundle:1.0.*
, (*7)
All set., (*8)
The model will help you to read and decode trees structures (such as json) passed, for instance, as requests to your controllers., (*9)
documentation of the Model component to come later, (*10)
You are probably used to Twig to build and render your HTML views on your projects., (*11)
The aim of ServicesIO is to be able to get something as powerful and efficient for data trees that Twig could be for HTML., (*12)
ServicesIO view provide you the ability to create :, (*13)
Here is what you need to know before starting :, (*14)
The view rendering system runs by a two-steps system :, (*15)
1 - you will build a tree in your view on a language agnostic datastructure by creating connected nodes. a node is composed of 3 available elements :, (*16)
2 - the rendering of the tree. Once it's fully build, we will be able to turn it into a data langage (only json so far) and send it to output., (*17)
Now, let's see that in action with a short example., (*18)
To make it easy to read, I will remove all code that is not related to our topic., (*19)
Let's create a small example project : 2 entities and 2 controllers., (*20)
The Doctrine entities :, (*21)
``` php class User { /** * @MongoDB\Id */ public $id;, (*22)
/** * @MongoDB\Field(type="string") */ public $name; }, (*23)
``` php class Message { /** * @MongoDB\Id */ public $id; /** * @MongoDB\Field(type="string") */ public $title; /** * @MongoDB\Field(type="string") */ public $description; /** * @MongoDB\Field(type="id") */ public $user; }
I assume to have 2 users : author and visitor and 3 entries for messages : message1, message2, message3., (*24)
Here are the controllers (without their return calls) :, (*25)
``` php class MessageController { public function singleAction(int $id, DocumentManager $documentManager) { $single = $documentManager ->getRepository(Message::class)->find($id); }, (*26)
public function listingAction(DocumentManager $documentManager) { $listing = $documentManager ->getRepository(Message::class)->findAll(); } }, (*27)
Based on that, we now have to create the _View_ elements for _decorators_ and _entities_, and call the rendering from the _controllers_. Let's complete first the _messageAction_'s View, with a _ServicesIO view_ class. A basic `View` class has to : - extends `Redgem\ServicesIOBundle\Lib\View\View` - location is up to you, we recommend using `<APP_OR_YOUR_BUNDLE>\View namespace` (and therefore in the `<APP_OR_YOUR_BUNDLE>/View` directory) to make a clear organisation. - implements the `content()` that is the place to build the view tree. - class name is also up to you, we recommend to name it `<NAME>View`. ``` php 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) ); } }
We are here creating a simple tree with an Item object that containt 3 childrens : id, title, description to display our 3 corresponding message fields., (*28)
Finally, let's make the controller calling and rendering it :, (*29)
The rendering service to call in ServicesIOBundle is called Redgem\ServicesIOBundle\Lib\View\Service
., (*30)
The call takes 2 arguments :, (*31)
``` php use Redgem\ServicesIOBundle\Lib\View\Service as View;, (*32)
class MessageController { public function singleAction(int $id, DocumentManager $documentManager, View $view) { $single = $documentManager ->getRepository(Message::class)->find($id);, (*33)
return $view->render( SingleView::class, ['single' => $single] );
} }, (*34)
The _single_ variable sent as a parameter in the controller is accessible as _$this->params['single']_ in the view class. Let's call it for the first _message_ for example ! ``` json { "message" : { "id" : "1", "title" : "message 1 title", "description" : "description 1 title" } }
Nice, we now have the json representation of the MessageView class tree !, (*35)
We can now replicate it for the listing action :, (*36)
``` php namespace MyBundle\View;, (*37)
use Redgem\ServicesIOBundle\Lib\View\View;, (*38)
class ListingView extends View { public function content() { $collection = $this->createCollection();, (*39)
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);
} }, (*40)
``` php 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] ); } }
Let's call it, and :, (*41)
``` json { "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" } ] }, (*42)
Excellent ! ### Partials views and fragment controllers We have a problem. We did repeat ourselves between the 2 classes to create the partial view of a _message_. Fortunately, there is a solution, : to create a reusable element. ``` php 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) ); } }
As yu can see, a reusable element is a totaly regular View class. That mean, you may use it to render directly a controller if you want to., (*43)
Finally, we use it on our SingleView and ListingView :, (*44)
``` php namespace MyBundle\View;, (*45)
use Redgem\ServicesIOBundle\Lib\View\View;, (*46)
class SingleView extends View { public function content() { return $this->createItem() ->set('message', $this->partial(MessageView::class, ['message' => $this->params['single']])); } }, (*47)
``` php 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); } }
When calling the partial()
method to get the subtree from there, it will pass for you the current context (i.e params you sent from the controller) merged with the params you add in the second method argument., (*48)
Of course, the json final rendering is still exactly the same., (*49)
Let's now enrich the data response everywhere with a new UserView :, (*50)
``` php namespace MyBundle\View;, (*51)
use Redgem\ServicesIOBundle\Lib\View\View;, (*52)
class UserView extends View { public function content() { return $this->createItem() ->set('id', $this->params['user']->id) ->set('name', $this->params['user']->name); ); } }, (*53)
and ``` php 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']]) ) ); } }
A single request will now display :, (*54)
``` json { "message" : { "id" : "1", "title" : "message 1 title", "description" : "description 1 title", "user": { "id": "1", "name": "author" } } }, (*55)
In addition to to `partial()` method, a `controller()` method is available. Instead of calling just a view, it will call a whole Symfony controller as a _fragment_. Its prototype is : `function controller($controller, $params = array())` with : - `$controller` : a regular Symfony controller name (as a string, with full class name) - `$params` : an array, that will be merged with current params context and passed to the new controller. The response will be handle on that way : - If this new controller return a _ServicesIO view_ response, the fragment tree will be merged on the main tree at the right place. - Otherwise, the response will be threated as a string and merged on the main tree at the right place. And one more thing : - `get($service)` - string $service : a Symfony service name. - `getParameter($parameter)` - string $parameter : a parameter name. methods are available as well. They just use the _container_ to call a _Symfony service_ or a _parameter_. ### View extensions We now want to decorate our response with the connected user on the top of it. It's easy to do with this `controller()` method. I assume that the user is correctly authenticated by the _Security component_ : ``` php public function visitorAction(View $view) { return $view->render( UserView::class, ['user' => $this->getUser()] //returns a User object ); }
And I add that to my views :, (*56)
``` php namespace MyBundle\View;, (*57)
use Redgem\ServicesIOBundle\Lib\View\View;, (*58)
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']])) ); } }, (*59)
``` php 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) ); } }
A single request will now display :, (*60)
``` json { "visitor": { "id": "2", "name": "visitor" }, "content": { "message" : { "id" : "1", "title" : "message 1 title", "description" : "description 1 title", "user": { "id": "1", "name": "author" } } } }, (*61)
And my problem of repeating myself is back... on the global decorator. I have created twice the main object with _visitor_ and _response_. We can solve it by changing our way of thinking. Instead of having only one class to build the tree, let's split it into 2 elements : - a class to build the main decorator node (i.e, with _visitor_ and _response_) - filling the fields of the _response_ node by each view classes. First of all, let's create the decorator thing. It's still a regular _View_ class : ``` php 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'); } }
There is a new third argument on the set()
method. This third argument is a string, and set up a name for the placeholder option. The placeholder is an entry on the tree that can be replaced later by an another value., (*62)
In case of placeholder, the second value (_null_ here) is a default value that will be displayed if the placeholder is not filled., (*63)
We can now transform our SingleView and ListingView to use this decorator :, (*64)
``` php namespace MyBundle\View;, (*65)
use Redgem\ServicesIOBundle\Lib\View\View;, (*66)
class SingleView extends View { public function getParent() { return DecoratorView::class; }, (*67)
public function blockFullResponse() { return $this->createItem() ->set('message', $this->partial(MessageView::class, ['message' => $this->params['single']])); } }, (*68)
``` php 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); } }
We can see two differences :, (*69)
getParent()
method appeared : that means that the root node will be deported in this view class. All the context is passed to this object, and obviously, this is a standard View class you can reuse where you want.content()
method is replaced by the blockFullResponse(). The content()
method is the method that create the root node of the tree. It's therefore not compatible with the getParent() method. A class with a getParent()
method will only fill the placeholders defined above it. This is the purpose of blockXXX()
where XXX is the placeholder name in CamelCase. methods (i.e blockFullResponse here).You can of course chain how many levels oh hierarchy you want with getParent()
and define placeholders into all of them., (*70)
getParent()
usually return a string. It can also return an array :, (*71)
``` php public function getParent() { return array(MyFriendBundleView::class, MyBundleDecoratorView::class); }, (*72)
In this case, the first actually implemented View class will be chosen. ### Why all those fancy stuffs ? Why doing that ? You may say it would be easier to use the regular extends PHP word for classes, and avoiding using _viewpath_, and you would be right. But _ServicesIO view_ do provides you an easy, flexible, and clear way to build some view reusables pieces. So Finally, we can call our controllers : ``` json { "visitor": { "id": "2", "name": "visitor" }, "content": { "message" : { "id" : "1", "title" : "message 1 title", "description" : "description 1 title", "user": { "id": "1", "name": "author" } } } }
json
{
"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"
}
}
]
}
}
, (*73)