Render Doctrine entities including recursive relations as JSON - json

I am building a JSON-based REST API using Symfony2.4 with Doctrine2.
EDIT : with JsonNormalizer, I can disabled some attributes, but what if I would like to set them, without recursivity ?
Basically, what I have (working) now is :
{
"tasks": [
{
"id": 1,
"name": "first task"
},
{
"id": 2,
"name": "second task"
}
]
}
What I would like is :
{
"tasks": [
{
"id": 1,
"name": "first task",
"category": {
"id": 1,
"name": "first category"
}
},
{
"id": 2,
"name": "second task",
"category": {
"id": 1,
"name": "first category"
}
}
]
}
What was my initial problem is :
{
"tasks": [
{
"id": 1,
"name": "first task",
"category": {
"id": 1,
"name": "first category",
"tasks": [
{
"id": 1,
"name": "first task",
"category": {
"id": 1,
"name": "first category",
"tasks": [...] // infinite...
}
},
{
"id": 2,
"name": "second task",
"category": {
"id": 1,
"name": "first category",
"tasks": [...] // infinite...
}
}
]
}
},
{
"id": 2,
"name": "second task",
"category": {
"id": 1,
"name": "first category",
"tasks": [
{
"id": 1,
"name": "first task",
"category": {
"id": 1,
"name": "first category",
"tasks": [...]
}
},
{
"id": 2,
"name": "second task",
"category": {
"id": 1,
"name": "first category",
"tasks": [...]
}
}
]
}
}
]
}
I have a entity A with a manyToOne relation to another entity B.
I have implemented the reverse-side, to be able to retrieve the related A entities on the B one.
class Task
{
/**
* #ORM\ManyToOne(targetEntity="List", inversedBy="task")
* #ORM\JoinColumn(name="list_id", referencedColumnName="id")
*/
private $list;
public function toArray($recursive = false)
{
$entityAsArray = get_object_vars($this);
if ($recursive) {
foreach ($entityAsArray as &$var) {
if ((is_object($var)) && (method_exists($var, 'toArray'))) {
$var = $var->toArray($recursive);
}
}
}
return $entityAsArray;
}
}
use Doctrine\Common\Collections\ArrayCollection;
class List
{
/**
* #ORM\OneToMany(targetEntity="Task", mappedBy="list")
*/
private $tasks;
public function __construct()
{
$this->tasks = new ArrayCollection();
}
}
Then I am building the different API routes and controllers,
Rendering the output as JsonResponses,
And I would like to render, for a given list, the different tasks using the route :
/api/v1/lists/1/tasks
The task action of my controller :
public function tasksAction($id)
{
$em = $this->getDoctrine()->getManager();
$list = $em->getRepository('MyRestBundle:List')->findOneActive($id);
if (!$list) {
throw $this->createNotFoundException('List undefined');
}
$tasks = $list->getTasks()->toArray();
foreach ($tasks as &$task) {
// recursively format the tasks as array
$task = $task->toArray(true);
}
$serializer = $this->get('serializer');
return $this->generateJsonResponse($serializer->normalize($tasks), 200);
}
But unfortunately, I always get a memory leak, because the call of toArray() is recursive, so each task has a list property which has a tasks collection etc.
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 130968 bytes) in src/Symfony/Component/Serializer/Serializer.php on line 146
I am wondering what would be the cleanest way to render entities with relations as JSON objects with Symfony2 ?
Do I really have to loop on my tasks to execute the "toArray()" method ?
I have also tried without it, without more success, except that the leak in in the file : src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php...
I have also tried without the JMSSeralizer, and the memory leak is thrown in my own php file.
Of course, I could increase the memory limit, but as it is an infinite loop problem of toArray() calls, it will not solve my problem.
How to format it properly ?

I have a feeling that we might be overthinking this. Would that work for you?
// In your controller
$repo = $this->getDoctrine()->getRepository('MyRestBundle:List');
$list = $repo->findActiveOne($id);
$tasks = $list->getTasks()->toArray();
$serializer = $this->get('serializer');
$json = $serializer->serialize($tasks, 'json');
Why is Task Entity recursive? A task can not include another task. Only a List can include an array of Tasks. So basically all we should do is get this array from the List entity and serialize it. Unless I am missing something.
EDIT:
You can ask the serializer to ignore certain attributes as mentioned by the documentation:
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
$normalizer = new GetSetMethodNormalizer();
$normalizer->setIgnoredAttributes(array('age'));
$encoder = new JsonEncoder();
$serializer = new Serializer(array($normalizer), array($encoder));
$serializer->serialize($person, 'json'); // Output: {"name":"foo"}
Try to follow that example and just ignore the $list attribute in the Task entity.
EDIT2:
You don't need 'list' inside each task since it's the same. Your json should have 'list' and 'tasks' at the same level. then 'tasks' would be an array of tasks which will not contain 'list'. to achieve that you can have something like array('list' => $list, 'tasks' => $tasks) and serialize that.

EDIT
If I understand what your code is doing, I think the toArray($recursive) function goes into infinite recursion both directly (whenever $var = $this) and indirectly (i.e. by a sibling iterating through its own list and calling toArray of the original task again). Try keeping track of what's been processed to prevent infinite recursion:
/**
* #ORM\ManyToOne(targetEntity="List", inversedBy="task")
* #ORM\JoinColumn(name="list_id", referencedColumnName="id")
*/
private $list;
private $toArrayProcessed = array();
public function toArray($recursive = false)
{
$this->toArrayProcessed[$this->getId()] = 1;
$entityAsArray = get_object_vars($this);
if ($recursive) {
foreach ($entityAsArray as &$var) {
if ((is_object($var)) && (method_exists($var, 'toArray')) && !isset($this->toArrayProcessed[$var->getId()]) {
$var = $var->toArray($recursive);
}
}
}
return $entityAsArray;
}

Related

how can I insert variables into json file when using terraform

this is the module folder structure
the json definition file (machine_definition.json)
{
"Comment": "A Hello World example of the Amazon States Language using Pass states",
"StartAt": "Hello",
"States": {
"Hello": {
"Type": "Pass",
"Result": "Hello",
"Next": "World"
},
"World": {
"Type": "${var.test}",
"Result": "World",
"End": true
}
}
}
for example I'm trying to enter var.test in here.
how to make the json file detect my variables?
here is the step function definition
module "step-functions" {
source = "terraform-aws-modules/step-functions/aws"
name = "env-${var.environment}-state-machine"
definition = file("${path.module}/machine_definition.json")
tags = var.tags
service_integrations = {
xray = {
xray = true
}
}
cloudwatch_log_group_name = "env-${var.environment}-state-machine-logGroup"
attach_policies = true
number_of_policies = 2
policies = ["arn:aws:iam::aws:policy/AmazonS3FullAccess", "arn:aws:iam::aws:policy/AWSLambda_FullAccess"]
}
Variables cannot be added to a file that way. In order to achieve what you want, you need to use the templatefile [1] built-in fuction. To achieve this you need a bit of code change:
definition = templatefile("${path.module}/machine_definition.json", {
type = var.test
})
Then, in the JSON file, you need to reference the templated variable (type) like this:
{
"Comment": "A Hello World example of the Amazon States Language using Pass states",
"StartAt": "Hello",
"States": {
"Hello": {
"Type": "Pass",
"Result": "Hello",
"Next": "World"
},
"World": {
"Type": "${type}",
"Result": "World",
"End": true
}
}
}
This should render the file properly.
[1] https://developer.hashicorp.com/terraform/language/functions/templatefile

How do I add names to objects in JSON using PowerShell?

I have the following situation below. I'd like to add a name to each item in the array.
First item needs to have a name instead of being an anonymous object. How do I do this?
Items need to have an name derived from the Technical Name, so 988 instead of N988AB1.
Structure of array is changed to an object in the result.
Current situation:
{
"Category": [{
"TechnicalName": "N988AB1",
"Name": "House"
},
{
"TechnicalName": "H181AG3",
"Name": "Apartment"
},
{
"TechnicalName": "X123XY5",
"Name": "Villa"
}
]
}
Desired situation:
{
"Data": {
"988": {
"TechnicalName": "N988AB1",
"Name": "House"
},
"181": {
"TechnicalName": "H181AG3",
"Name": "Apartment"
},
"0123": {
"TechnicalName": "X0123XY5",
"Name": "Villa"
}
}
}
One way of tackling this: (You can copy and paste the full contents of the code box to try it out. I've commented what various steps are doing)
$ExistingJson = #"
{
"Category": [{
"TechnicalName": "N988AB1",
"Name": "House"
},
{
"TechnicalName": "H181AG3",
"Name": "Apartment"
},
{
"TechnicalName": "X123XY5",
"Name": "Villa"
}
]
}
"#
# Convert the json string to an object we can manipulate.
$ExistingObj = $ExistingJson | ConvertFrom-Json
# Create an empty hashtable for the new items we are creating.
$HT = #{}
foreach ($Category in $ExistingObj.Category) {
# Match any digits, grouped as "name", after any character [A-Za-z] which are followed by [A-Za-z] - this is pretty basic and may need to be changed if your TechnicalName string changes, or you want different information from it.
[System.Void]($Category.TechnicalName -match "(?<=[A-Za-z])(?<name>\d+)(?=[A-Za-z])")
$NewItem = [PSCustomObject]#{
TechnicalName = $Category.TechnicalName
Name = $Category.Name
}
# Add a new entry to the hashtable with the discovered digits by it's group name "name" and the object created above with the existing information as it's value.
$HT.Add($Matches["name"], $NewItem)
}
# Create a new object with a Data property with the value as the hashtable.
$NewObj = [PSCustomObject]#{
Data = $HT
}
# Convert it back to Json
$NewObj | ConvertTo-Json
Results in:
{
"Data": {
"988": {
"TechnicalName": "N988AB1",
"Name": "House"
},
"181": {
"TechnicalName": "H181AG3",
"Name": "Apartment"
},
"123": {
"TechnicalName": "X123XY5",
"Name": "Villa"
}
}
}

How to verify stringified json in pact

I am trying to build a pact between two services using asynchronous communication.
This is the code I used for generate the pact:
#ExtendWith(PactConsumerTestExt.class)
#PactTestFor(providerName = "provider", providerType = ProviderType.ASYNCH)
public class StringifiedPactTest {
#Pact(consumer = "consumer", provider = "provider")
public MessagePact generatePact(MessagePactBuilder builder) {
return builder.hasPactWith("provider")
.expectsToReceive("A valid aws sns event")
.withContent(new PactDslJsonBody().stringType(new String[]{"MessageId", "TopicArn"}).stringValue("Message", new PactDslJsonBody().stringType("Value", "Foo").toString()))
.toPact();
}
#Test
#PactTestFor(pactMethod = "generatePact")
public void buildPact(List<Message> messages) {
}
}
And the generated pact is
{
"consumer": {
"name": "consumer"
},
"provider": {
"name": "provider"
},
"messages": [
{
"description": "A valid aws sns event",
"metaData": {
"contentType": "application/json"
},
"contents": {
"TopicArn": "string",
"Message": "{\"Value\":\"Foo\"}",
"MessageId": "string"
},
"matchingRules": {
"body": {
"$.MessageId": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.TopicArn": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.0.10"
}
}
}
This means the producer should have a "Message" that matches {"Value" : "Foo"}, any other combination like {"Value" : "Bar" } won't be successful.
Is there any way to add matching rules inside a stringified json?
Thanks!
Here's an anonymised example from a test we have. Hope it's useful. This creates a pact that matches only on type. So on the provider side, when I test against the contract, it doesn't matter what value I have for categoryName for example, as long as it's a stringType:
#PactTestFor(providerName = "provider-service", providerType = ProviderType.ASYNCH)
public class providerServiceConsumerPactTest {
private static String messageFromJson;
#BeforeAll
static void beforeAll() throws Exception {
messageFromJson = StreamUtils.copyToString(new ClassPathResource("/json/pact/consumer-service_provider-service.json").getInputStream(), Charset.defaultCharset());
}
#Pact(provider = "provider-service", consumer = "consumer-service")
public MessagePact providerServiceMessage(MessagePactBuilder builder) {
DslPart body = new PactDslJsonBody()
.object("metaData")
.stringType("origin", "provider-service")
.datetimeExpression("dateCreated", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.closeObject()
.minArrayLike("categories", 0, 1)
.stringType("id", "example data")
.stringType("categoryName", "example data")
.booleanType("clearance", false)
.closeObject()
.closeArray();
return builder
.expectsToReceive("a provider-service update")
.withContent(body)
.toPact();
}
#Test
#PactTestFor(pactMethod = "providerServiceMessage")
public void testProviderServiceMessage(MessagePact pact) {
// State
final String messageFromPact = pact.getMessages().get(0).contentsAsString();
// Assert
JSONAssert.assertEquals(messageFromPact, messageFromJson, false);
}
I'm having exactly the same issue, and unfortunately I don't think it's possible to tell Pact to parse the stringified JSON and look inside it (e.g. to verify that parse(Message).Value === "Foo" in your example).
The best you can do is write a regular expression to match the string you're expecting. This kind of sucks because there's no easy way to ignore the ordering of the JSON keys (e.g. "{\"a\":\"1\", \"b\":\"2\"}" and "{\"b\":\"2\", \"a\":\"1\"}" will compare different) but AFAIK Pact simply lacks the parsing functionality we're looking for, so the only tool it provides is regex.

How to remove brakets on Json respone

im working on Laravel Rest Api with passeport ,
in return response()->json() i want to trim the brackets
I've tried trim($json,'[]') function but it's not what i want
public function getOffers()
{
$offers = Offer::where('type', 'primary')->where('active', 1)->get();
$paks = Offer::where('type', 'pack')->where('active', 1)->get();
return response()->json([
'offersList' => $offers,
'packsList' => $paks,
], 200);
}
i expect the output will be
{
"offersList": {
{
"id": 3,
"name": "Gold",
"description": null
}
},
"packsList":[]
}
but the actual result is
{
"offersList": [
{
"id": 3,
"name": "Gold",
"description": null
}
],
"packsList":[]
}
$offers is a collection, and thus an array in JSON.
If $offers should be a single item, use first() instead of get() and it will be rendered as a single object in your JSON instead of an array of objects.
$offers = Offer::where('type', 'primary')->where('active', 1)->first();
If $offers should, at times, contain multiple offers, leave it as-is; it's correct!
Braces {} nested in another object is not valid JSON.
Objects can be used in property values and as array elements.
Not valid JSON
{
"offersList": {
{
"id": 3,
"name": "Gold",
"description": null
}
}
}
Valid option 1
{
"offersList": [
{
"id": 3,
"name": "Gold",
"description": null
}
]
}
Valid option 2
{
"offersList": {
"id": 3,
"name": "Gold",
"description": null
}
}
You can use online linters to quickly validate your JSON structure.
https://jsonformatter.curiousconcept.com/

Loading TreeStore with JSON that has different children fields

I am having a JSON data like below.
{
"divisions": [{
"name": "division1",
"id": "div1",
"subdivisions": [{
"name": "Sub1Div1",
"id": "div1sub1",
"schemes": [{
"name": "Scheme1",
"id": "scheme1"
}, {
"name": "Scheme2",
"id": "scheme2"
}]
}, {
"name": "Sub2Div1",
"id": "div1sub2",
"schemes": [{
"name": "Scheme3",
"id": "scheme3"
}]
}
]
}]
}
I want to read this into a TreeStore, but cannot change the subfields ( divisions, subdivisions, schemes ) to be the same (eg, children).
How can achieve I this?
When nested JSON is loaded into a TreeStore, essentially the children nodes are loaded through a recursive calls between TreeStore.fillNode() method and NodeInterface.appendChild().
The actual retrieval of each node's children field is done within TreeStore.onNodeAdded() on this line:
dataRoot = reader.getRoot(data);
The getRoot() of the reader is dynamically created in the reader's buildExtractors() method, which is what you'll need to override in order to deal with varying children fields within nested JSON. Here is how it's done:
Ext.define('MyVariJsonReader', {
extend: 'Ext.data.reader.Json',
alias : 'reader.varijson',
buildExtractors : function()
{
var me = this;
me.callParent(arguments);
me.getRoot = function ( aObj ) {
// Special cases
switch( aObj.name )
{
case 'Bill': return aObj[ 'children' ];
case 'Norman': return aObj[ 'sons' ];
}
// Default root is `people`
return aObj[ 'people' ];
};
}
});
This will be able to interpret such JSON:
{
"people":[
{
"name":"Bill",
"expanded":true,
"children":[
{
"name":"Kate",
"leaf":true
},
{
"name":"John",
"leaf":true
}
]
},
{
"name":"Norman",
"expanded":true,
"sons":[
{
"name":"Mike",
"leaf":true
},
{
"name":"Harry",
"leaf":true
}
]
}
]
}
See this JsFiddle for fully working code.