Reindexing ArrayCollection elements in RESTAPI - json

Imagine, I've an array collection $items, and implemented a method removeItem in my SomeEntity entity class,
public function removeItem(Item $item)
{
$this->items->removeElement($item);
return $this;
}
and a controller action which receives a PATCH request:
public function patchAction(Request $request, SomeEntity $entity) {
$form = ...;
$form->handleRequest($request);
if ($form->isValid()) {
...
$this->get('doctrine.orm.entity_manager')->flush();
return $entity;
}
return $form;
}
Imagine that the instance of the SomeEntity class holds items as [0 => {ITEM}, 1 => {ITEM}, 2 => {ITEM}, 3 => {ITEM}] (indexed array of objects). If you send a request to getAction and receive the SomeEntity object in your front project in JSON format, the type of those items in an object will be an indexed Array of objects (you can iterate over them with array methods), but if you remove one or more of them from an object with PATCH method and receive a JSON response, you get an Object type with objects in it, because some keys have been removed after the PATCH request and the object of SomeEntity class in the response no longer holds an indexed array, instead there will be an object of objects. It is because of, when you convert an array into json object you can get two different results
array of objects(arrays) if keys are indexed (e.g: 0,1,2,3,...)
object of objects(arrays) if keys are not indexed (e.g: 0,2,3,6,...)
At this moment I'm solving this problem with modifying (manually recreating elements) the existing removeItem method in entity class, like this:
public function removeItem(Item $item)
{
$this->items->removeElement($item);
if (!$this->items->isEmpty()) {
$items = new ArrayCollection();
foreach ($this->items as $item) {
$items->add($item);
}
$this->items = $items;
}
return $this;
}
May be there is a better way to solve this? How do you solve this problem?
I'm using FOSRestBundle and JmsSerializerBundle.

This appears to be a common problem - see https://github.com/schmittjoh/JMSSerializerBundle/issues/373 for others having this issue. There is definitely a shorter way to solve it in your functions where you remove items than looping through each element again:
public function removeItem(Item $item)
{
$this->items->removeElement($item);
$this->items= new ArrayCollection($this->items->getValues());
return $this;
}
There are other workarounds listed in that ticket that may or may not suit your needs - if you want a global solution you might be able to override the JsonSerializationVisitor class which casts to an array.

Related

Laravel format resource collection response error Property [product_id] does not exist on this collection instance

In laravel we can format our json response from resource class as seen below
class ProductsResource extends JsonResource
{
public function toArray($request)
{
return [
'id'=> $this->product_id ,
'code'=> $this->product_code,
'shortdescription'=> $this->product_short_description,
'image'=> $this->product_image,
];
}
}
But when returning resources collection i can't format my collection error Property [product_id] does not exist on this collection instance
class ProductsResource extends ResourceCollection
{
public function toArray($request)
{
return [
'id'=> $this->product_id ,
'code'=> $this->product_code,
'shortdescription'=> $this->product_short_description,
'image'=> $this->product_image,
];
}
}
thanks.
It's because ResourceCollection expects a collection of items instead of a single item. The collection resource expects you to iterate through the collection and cannot perform single enitity routines directly from $this (as it's a collection).
See resource collections in documentation
What you are probably looking for is to cast a custom mutation which examples can be found here:
See custom casts in documentation
Look/search for Value Object Casting. It is explained thoroughly how to mutate attributes on get and set, which is probably better than a resource collection (if this is the only thing you wish to do with it). This will modify the collection immediately and saves you from having to manually instantiate the resource collection every time you need it (as you are modifying at model level).
Coming from the docs:
Value Object Casting
You are not limited to casting values to primitive types. You may also cast values to objects. Defining custom casts that cast values to objects is very similar to casting to primitive types; however, the set method should return an array of key / value pairs that will be used to set raw, storable values on the model.
But to get back on topic...
If you dump and die: dd($this); you will see there is an attribute called +collection
In case you wish to transform keys or values, you have to iterate through $this->collection in order to transform the collection values or keys to your requirements.
As you can see in the parent class Illuminate\Http\Resources\Json\ResourceCollection the method toArray() is already a mapped collection.
In which you can see it's pointing to $this->collection
/**
* Transform the resource into a JSON array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return $this->collection->map->toArray($request)->all();
}
You could use something like the following. And update the item/key values within this collection map.
return $this->collection->map(function($item, $key){})->toArray();
if you wish to transform the values before returning it to an array.
Or a simple foreach like this (have not tested it and there are far better ways of doing this) But for the sake of sharing a simple-to-grasp example:
$result = [];
// Map the associations to be modified
$resultMap = [
'product_id' => 'id',
'product_code' => 'code',
'product_short_description' => 'shortdescription',
'product_image' => 'image'
];
// Iterate through the collection
foreach ($this->collection as $index => $item)
foreach ($item as $key => $value)
$result[$index][$resultMap[$key]] = $value;
return $result;

Casting JSON to complex types

I am trying to cast my http.get response to actual object -> in my specific case array of complex objects.
In a normal scenario, where you don't need any specific casting, you could do the following (simplified):
return this.httpClient.get(api, this._options_get)
.pipe(
map((response: any) => {
return response.value as NewProduct[];
})
);
As my need is to actually cast this to an object, I have created this static method which does that:
static toProduct(otherProduct: any): NewProduct {
let item = new NewProduct();
Object.keys(otherProduct).forEach(prop => {
if (typeof otherProduct[prop] === "object" && otherProduct[prop]) {
if (!item.hasOwnProperty(prop))
item[prop] = otherProduct[prop];
Object.assign(item[prop], otherProduct[prop]);
}
else
item[prop] = otherProduct[prop];
})
return item;
}
Under Object.assign I am taking already existing object which was initialized under first line and I am simply copying all the properties from the otherProduct to it. However I start to face problem when it comes to array of objects. Example (with simplified class):
export class Person {
name:string;
age:number;
addresses:Address[] = [];
}
export class Address {
street:string;
city:string;
fullAddress() : string { return this.street + this.city; }
}
As soon as I have this sort of array, I don't have any initial object in item. This means that there is no initial constructor of a class which results in simple Object. This is no error for JavaScript or TypeScript; however when I am trying to access internal method of a class (in our simplified case fullAddress(), I won't be able to.
The reason why I need that is that I am overriding toString() method on my sub-classes, which is necessary for MatTableDataSource when you use the filter method (which works with strings).
Is there a way how to retrieve elements from http.get() and properly map results to typed objects?
You're being too generic. You're creating objects of objects, not objects of Product with children of Addresses.
If you want to create a new product you're going to have to understand the relationship between the api's results and the data you want in the UI.
Because you're using classes and not interfaces and want to inherit the functions, the only way to get new Addresses into the new object is with the new keyword.
And you'll have to loop through. You're not going to find a shortcut for this. You're going to need to loop through the data and transform it. If your api is giving you an ApiPerson then you'll want to do something like this:
const addresses = apiPerson.addresses.map((apiAddress) => {
const address = new Address();
// map properties of apiAddress to address...
return address;
});
Now that you have the addresses, you can map the apiPerson to a new Person()'s properties and then set the newPerson.addresses = address.

Using Fractal Transformer with ember-data

I am using PHP league's Fractal as the transformer for my API. However, I think I must be doing something wrong as the item transformer wraps everything in an array like it would a collection which is against the JSON API standard I believe.
So for a user with ID of one I get something like this:
{
"users":[
{
"id":1,
"firstName":"Jacob",
"surname":"Windsor",
}
]
}
When surely it should be this?
{
"users":
{
"id":1,
"firstName":"Jacob",
"surname":"Windsor",
}
}
I am using ember.js and this is causing problems with naming conventions.
I am using Laravel and in my userController I have something like this:
public function show($id)
{
$user = User::find($id);
return $this->respondItem($user);
}
Then in the apiController that everything extends from:
public function respond($response, $status = 200){
return Response::make($response, $status);
}
public function respondTransform($resource){
$fractal = new Fractal\Manager();
$fractal->setSerializer(new JsonApiSerializer());
return $this->respond($fractal->createData($resource)->toJson());
}
public function respondItem($data, $transformer = null, $namespace = null){
! isset($transformer) ? $transformer = $this->transformer : $transformer = $transformer;
! isset($namespace) ? $namespace = $this->namespace : $namespace = $namespace;
$resource = new Item($data, $transformer, $namespace);
return $this->respondTransform($resource);
}
I must be doing something wrong. The fractal docs have no examples specifically for items only collections so I am unsure what I have done.
So it seems that Fractal doesn't quite obey ember-data's conventions which is an annoying problem but very easily overcome using custom serialziers.
I have a psr-4 autoloaded file named CustomJsonSerializer which I have included in my ApiController class. If you follow the article on php league's site (posted above) its fairly easy to do. I have these two methods.
public function collection($resourceKey, array $data)
{
return array($resourceKey ?: 'data' => $data);
}
/**
* Serialize an item resource
*
* #param string $resourceKey
* #param array $data
*
* #return array
*/
public function item($resourceKey, array $data)
{
return [$resourceKey => $data];
}
You can see that the collection is responding as it normally would, i.e I haven't changed it. But the item method just responds without the extra array. Simple! You have to include all the other methods as well and I haven't got round to sorting out pagination but it should be fairly simple.
I hope this helps anyone wanting to use ember-data with Fractal. I highly recommend it, fractal has made my life so much easier. You could build transformers yourself but it makes it so much easier and more easily modified in the future.
Edit:
Please make sure you keep the $resourceKey in both the methods. You need to be using it and setting it when calling the transformer. |Ember-data requires a resource key.
Assuming your userController extends ApiController, you could simply do:
public function show($id)
{
$user = User::findOrFail($id);
return $this->setStatusCode(200)->withItem($user, new UserTransformer);
}
You do need to implement the UserTransformer class. If you need help with that, let me know in the comments.
I actually found that a much simpler adjustment of JsonApiSerializer did what I needed for Ember:
(I just took out the count($data) check)
<?php
namespace Acme\Serializer;
use RuntimeException;
use League\Fractal\Serializer\JsonApiSerializer;
class EmberSerializer extends JsonApiSerializer
{
/**
* Serialize the top level data.
*
* #param string $resourceKey
* #param array $data
*
* #return array
*/
public function serializeData($resourceKey, array $data)
{
if (! $resourceKey) {
throw new RuntimeException('The $resourceKey parameter must be provided when using '.__CLASS__);
}
return array($resourceKey => $data);
}
}

Symfony2 doctrine and response

I have a little problem, I'm trying to send a json response, but all I get is a empty object all the time.
So here is my code:
//Get the data from DB
$template = $this->getDoctrine()
->getRepository('EVRYgroBundle:Template')
->findOneBy(
array('active' => 1)
);
if (!$template) {
throw $this->createNotFoundException(
'No product found for id '
);
}
//Send the response
$response = new Response();
$response->setContent(json_encode($template));
return $response;
And when I'm viewing it all it shows is {}
And I have also tried with the jsonResponse and with this code:
$response = new JsonResponse();
$response->setData($template);
And I have no idea what i'm doing wrong!
json_encode expects an array as first parameter to be given in. When you call it with an object the public properties will may be displayed. To keep the properties protected (as they should be) you can add a expose function to your entity:
/**
* delivers all properties and values of the entity easily
*
* #return array
*/
public function expose()
{
return get_object_vars($this);
}
and then call
$response->setData(json_encode($template->expose()));
This way you keep your entity clean with only access via getter and setter methods and you can access all properties via json still.
Well I found the problem, the problem was that some variables that holds the information from the db was set to protected and not public.

serialize an entity that extends FOS user model

I have a user entity that extends the entity model of FOsUserBundle (FOS\UserBundle\Entity\User), as they recommend it.
Then I'd like to get all the users I have and pass them to twig as a json:
$em = $this->getDoctrine()->getManager();
$user_array = em->getRepository('MyBundle:user')->findByCustomer($customerID);
So I have an array which contains objects.
if I do:
json_encode($user_array);
or
json_encode($user_array[0]);
I get an empty string {}. I was at least expecting to get the array defined in the FOS User class
public function serialize()
{
return serialize(array(
$this->password,
$this->salt,
$this->usernameCanonical,
$this->username,
$this->expired,
$this->locked,
$this->credentialsExpired,
$this->enabled,
$this->id,
));
}
but it seems actually FOS doesn't implements Jsonserialize so it doesn't work.
When I change the FOS user class to implement Jsonserialize, it stops working (I can't connect anymore for example...).
Is there any way to get this work with FOS ?
All fields in the FriendsOfSymfony User entity are protected.
Which means you can simply reference them in the User class that is extending it just as you would a normal field.
This also means you can add another method in your own User class that will return an json encoded array containing all the values of the User.
Example of this would be:
public function json_encode()
{
return json_encode(array(
$this->password,
$this->salt,
$this->usernameCanonical,
$this->username,
$this->expired,
$this->locked,
$this->credentialsExpired,
$this->enabled,
$this->id,
));
}
You can't just simply json encode the whole array so the way to do it is:
$jsonEncodedUserArray = array();
foreach($user_array as $user) {
$jsonEncodedUser = $user->json_encode();
array_push($jsonEncodedUserArray, $jsonEncodedUser);
}