Yii2 envelope single data in JSON response - php

I went through offical guide and found a way to envelop JSON data like this.
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
}
This works perfect when I have a collection and then I have a response like this.
{
products:....
}
But what I want to do is that i have a envelope for single data. For example if I do products/10 GET request to get.
{
product:
}
Hope somebody figured it out.

Single Data Envelope is not supported by \yii\rest\Serializer. At least until Yii 2.0.6 only collections get enveloped in order to add _links and _meta data objects to the response.
To envelope single data resource objects you'll need to override ActiveController's default view action within your Controller :
public function actions()
{
$actions = parent::actions();
unset($actions['view']);
return $actions;
}
public function actionView($id)
{
$model = Product::findOne($id);
return ['product' => $model];
}

Old, but I just bumped into here with the same problem.
And found a better (I think) solution: create your own serializer class extending \yii\rest\Serializer:
class Serializer extends \yii\rest\Serializer
{
public $itemEnvelope;
public function serializeModel($model)
{
$data = parent::serializeModel($model);
if($this->itemEnvelope)return [$this->itemEnvelope=>$data];
return $data;
}
}
And then use it like this:
public $serializer = [
'class' => '[your-namespace]\Serializer',
'collectionEnvelope' => 'list',
'itemEnvelope' => 'item'
];

Related

Controller does not send data to view, arrives as null

I've been trying to send data from controller to view in CodeIgniter 4, let me just...
Model:
class KomikModel extends Model
{
protected $table = 'komik';
protected $useTimestamps = true;
protected $allowedFields = ['judul', 'slug', 'penulis', 'penerbit', 'sampul'];
public function getKomik($slug = false)
{
if ($slug == false) {
return $this->findAll();
}
return $this->where(['slug' => $slug])->first();
}
}
Controller:
public function edit($slug)
{
$data = [
'title' => 'Form Ubah Data',
'validation' => \Config\Services::validation(),
'komik' => $this->komikModel->getKomik($slug)
];
return view('komik/edit', $data);
}
View:
<?= dd($komik); ?>
After all that, $komik that arrived in the view comes out as null. Why, what did I do wrong? Thanks in advance.
PS: Somehow, it works fine with other methods. currently there are 3 other methods using the model to send data from controller towards the view. but only at this edit method the problem occurs.
The issue is with this particular line.
return $this->where(['slug' => $slug])->first();
Model Class doesn't have any "where" method defined in it. It borrows it from Builder Class. This is called object composition. You can certainly try to have your own method with a similar implementation as in the Model class but I would recommend against it since it is an antipattern.
A better approach would be to have a single class that is responsible for all the querying or filtering as in Repository Pattern. You can do a bit of research on that.

Fractal transformer with different Serializers for nested items and collections

To change the JSON output send by a Laravel 5.4 RESTful API, I make use of the Fractal package by thephpleague. Because pagination will be added in the future, it is important that collections make use of the default DataArraySerializer and single items use the ArraySerializer. It is also needed that deeper nested objects are given the same structure. How can I achieve this (globally or not)?
class TreeTransformer extends TransformerAbstract {
protected $defaultIncludes = [
'type',
'branches'
];
public function transform(Tree $tree) {
return [
'id' => (int)$tree->id,
'name' => (string)$tree->name
];
}
public function includeType(Tree $tree) {
return $this->item($tree->type, new TypeTransformer()); // Should be ArraySerializer
}
public function includeBranches(Tree $tree) {
return $this->collection($tree->branches, new BranchTransformer()); // Should stay DataArraySerializer
}
}
Unfortunately, I think what you are trying to do is not possible yet. More information here: https://github.com/thephpleague/fractal/issues/315
You can still change the serializer for an entire output like this: https://github.com/spatie/fractalistic#using-a-serializer. But it is not what you want to achieve.
Actually you can.
It may look quite more verbose, but the trick is just not using $defaultIncludes. Use the fractal helper instead.
class TreeTransformer extends TransformerAbstract {
public function transform(Tree $tree) {
return [
'id' => (int)$tree->id,
'name' => (string)$tree->name,
'type' => $this->serializeType($tree)
'branches' => $this->serializeBranches($tree)
];
}
private function serializeType(Tree $tree) {
return fractal()
->serializeWith(new ArraySerializer())
->collection($tree->type)
->transformWith(TypeTransformer::class)
->toArray(); // ArraySerializer
}
private function serializeBranches(Tree $tree) {
return fractal()
->serializeWith(new DataArraySerializer())
->collection($tree->branches)
->transformWith(BranchesTransformer::class)
->toArray(); // DataArraySerializer
}
}
It's working for me with ArraySerializer. Didn't try DataArraySerializer.

Fractal Transformers Optional Model Columns

I am using Fractal Transformers in Laravel 5. I have:
namespace App\Transformers;
use App\Models\Cake;
use League\Fractal\TransformerAbstract;
class CakeTransformer extends TransformerAbstract
{
protected $availableIncludes = [
'user',
'description'
];
public function transform(Cake $cake)
{
$ar = [
'name' => $cake->name,
'url_name' => $cake->url_name,
'user' => $cake->user->screenname,
'date_created' => $cake->created_at
];
return $ar;
}
public function includeUser(Cake $cake)
{
return $this->item($cake->user, new UserTransformer());
}
public function includeDescription(Cake $cake) {
return $cake->description;
}
}
The above doesn't work because includeDescription doesn't return the right kind of object, but from the above you can see what I'm trying to do.
For instance in my search I want to bring back much less data than if I were to load a whole page about the search item. E.g. for search I don't want to load the description, but for the page that contains details about the product I would want to.
How can I achieve this?

Symfony After Filters - modify json response

Does anyone knows how to add new data field to existing JsonResponse (Symfony\Component\HttpFoundation\JsonResponse) in after filter (onKernelResponse) without deleting old data?
For example, I have custom controller
<?php
class MessageController extends Controller
{
public function getAllMessagesAction(Request $request)
{
(...)
return new JsonResponse(array(
'count' => count($messages),
'total_count' => $allMessagesCount,
'messages' => $messages,
));
}
}
and some listener
<?php
class NotesListener
{
public function onKernelResponse(FilterResponseEvent $event)
{
(...)
$response = $event->getResponse();
if ($response instanceof JsonResponse) {
$response->setData(array('foo' => 'bar'));
}
}
}
Problem
The problem is that $response->setData in listener will override data passed in controller. Moreover, JsonResponse doesn't have method like addData(). Unfortunately there is not method getData(), so I can not get old data, modifying it, and set new data.
Does anyone know good solution?
Thanks in advance!
You can use getContent() and decode the the data, then modify it and set the data again.
Alternatively, you could change your controller to just return the data and create the JsonResponse in an event listener.

ZF2: How to get Zend\Navigation inside custom route?

I have custom router and I have to get access to Zend\Navigation inside this custom router. I was googling, asking and searching and no results :/
All I need is to find nodes with 'link' param using Zend\Navigation in my Alias::match function.
Here is my module.config.php:
'navigation' => array(
'default' => array(
'account' => array(
'label' => 'Account',
'route' => 'node',
'pages' => array(
'home' => array(
'label' => 'Dashboard',
'route' => 'node',
'params' => array(
'id' => '1',
'link' => '/about/gallery'
),
),
),
),
),
),
[...]
And here is my Alias class:
// file within ModuleName/src/ModuleName/Router/Alias.php
namespace Application\Router;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Mvc\Router\Http;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class Alias extends Http\Segment implements ServiceLocatorAwareInterface
{
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function match(Request $request, $pathOffset = null)
{
[...]
return parent::match($request, $pathOffset);
}
}
EDITED:
Now i know that I should inject service manager into my custom router. Let me know if You know how to do this :)
EDITED:
Ok, its not custom router but route. My bad. I was talking on #zftalk irc chanell and AliasSegment class should implements ServiceLocatorAwareInterface. Ok I've tried it but now there is another problem.
In setServiceLocator function i can't get service locator. It returns null object, however $serviceLocator is class Zend\Mvc\Router\RoutePluginManager.
public function setServiceLocator(ServiceLocatorInterface $serviceLocator){
$sl = $serviceLocator->getServiceLocator();
var_dump($sl); // NULL
}
Any ideas how to get Zend navigation from it ?
EDITED
Corresponding to what #mmmshuddup said, I've changed my custom router class. (New version is above). Also in my Module.php, within onBootstrap function, I added this line:
$sm->setFactory('Navigation', 'Zend\Navigation\Service\DefaultNavigationFactory', true);
Navigation works and its instantiated before route so it should be visible within my Alias class but it's not.
I've put into my match function in Alias class this line:
$servicesArray = $this->getServiceLocator()->getRegisteredServices();
and $servicesArray is almost empty. There is no service, no factories. The same line inserted into onBootstrap, just after setting new factory (as above) returns array with navigation and other services.
The question is: how can i share this array (or ServiceManager) with my custom router: Alias ?
I have to say that all I want to do was possible in ZF1 and it was quite easy.
EDIT
I found a solution. The answer is below
That is because the object itself really doesn't have any properties declared. But if you do this:
echo get_class($sl);
You will see that it is indeed an instance of Zend\ServiceManager\ServiceManager
You should be able to get your navigation instance by doing something like:
$nav = $sl->get('Navigation');
EDIT:
I just notice you have some stuff in the wrong location of your code. You're calling getServiceLocator() on $serviceLocator which is already the instance of that. Also you're calling it within setServiceLocator(). You should change it to:
// EDIT - file within ModuleName/src/Router/Alias.php
namespace Application\Router;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Mvc\Router\Http;
class Alias extends Http\Segment implements ServiceLocatorAwareInterface
{
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function match(Request $request, $pathOffset = null)
{
$nav = $this->getServiceLocator()->get('Navigation');
// ...
return parent::match($request, $pathOffset);
}
}
I found the solution but this is NOT elegant solution i think. However everything works perfectly. If somebody knows disadvantages of this solution, please comment this answer or add another, better. I had to modify #mmmshuddup's idea (you can read the conversation).
First of all, the implementation of ServiceLocatorAwareInterface in custom route class is no more necessary.
In Module.php within onBootstrap function:
$app = $e->getApplication();
$sm = $app->getServiceManager();
$sm->get('translator');
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$sm->setFactory('Navigation',
'Zend\Navigation\Service\DefaultNavigationFactory', true);
$nav = $sm->get('Navigation');
$alias = $sm->get('Application\Router\Alias');
$alias->setNavigation($nav);
First we instantiate Navigation factory in ServiceManager and then our custom route. After that we can pass Navigation class into custom route using setNavigation function.
To complete instantiate of our custom route we need in getServiceConfig in the same file:
return array(
'factories' => array(
'Application\Router\Alias' => function($sm) {
$alias = new \Application\Router\Alias('/node[/:id]');
return $alias;
},
'db_adapter' => function($sm) {
$config = $sm->get('Configuration');
$dbAdapter = new \Zend\Db\Adapter\Adapter($config['db']);
return $dbAdapter;
},
)
);
And here is a tricky part. This instance is temporary. While routing, this class will be instantiated one more time and this is why, I think, it's not very elegant. We have to insert parameter into constructor however at this moment value of this parameter is not important.
The custom route class:
// file within ModuleName/src/ModuleName/Router/Alias.php
namespace Application\Router;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Mvc\Router\Http;
class Alias extends Http\Segment
{
private static $_navigation = null;
public function match(Request $request, $pathOffset = null)
{
//some logic here
//get Navigation
$nav = self::$_navigation;
return parent::match($request, $pathOffset);
}
public function setNavigation($navigation){
self::$_navigation = $navigation;
}
}
Because first instance is temporary, we have to collect our Navigation class in static variable. It's awful but works nice. Maybe there is a way to instantiate it only once and in route configuration get instance of it, but at this moment this is best answer for my question. Simply enough and working correctly.

Categories