Traversing through deeply nested JSON object

徘徊边缘 提交于 2021-02-20 03:46:44

问题


When interacting with an API used for building forms, I make an API call to get all the response values associated with my form. The API returns a deeply nested JSON object with all my form values.

One of the many response objects looks like this:

{
      "title":{
        "plain":"Send Money"
      },
      "fieldset":[
        {
          "label":{
            "plain":"Personal Info Section"
          },
          "fieldset":[
            {
              "field":[
                {
                  "label":{
                    "plain":"First Name"
                  },
                  "value":{
                    "plain":"Bob"
                  },
                  "id":"a_1"
                },
                {
                  "label":{
                    "plain":"Last Name"
                  },
                  "value":{
                    "plain":"Hogan"
                  },
                  "id":"a_2"
                }
              ],
              "id":"a_8"
            }
          ],
          "id":"a_5"
        },
        {
          "label":{
            "plain":"Billing Details Section"
          },
          "fieldset":{
            "field":{
              "choices":{
                "choice":{
                  "label":{
                    "plain":"Gift"
                  },
                  "id":"a_17",
                  "switch":""
                }
              },
              "label":{
                "plain":"Choose a category:"
              },
              "value":{
                "plain":"Gift"
              },
              "id":"a_14"
            },
            "fieldset":{
              "label":{
                "plain":""
              },
              "field":[
                {
                  "choices":{
                    "choice":{
                      "label":{
                        "plain":"Other"
                      },
                      "id":"a_25",
                      "switch":""
                    }
                  },
                  "label":{
                    "plain":"Amount"
                  },
                  "value":{
                    "plain":"Other" //(This could also be a dollar amount like 10.00)
                  },
                  "id":"a_21"
                },
                {
                  "label":{
                    "plain":"Other Amount"
                  },
                  "value":{
                    "plain":"200"
                  },
                  "id":"a_20"
                }
              ],
              "id":"a_26"
            },
            "id":"a_13"
          },
          "id":"a_12"
        }
      ]
    }

The goal here is to run a report of all responses and print the data out in a readable way (e.g. "Bob Hogan - $200, Chad Smith - $100").

I'm thinking I'll have to use some sort of map-reduce algorithm because simply nesting a bunch of loops can be unscalable as well as computationally expensive given the increasing time complexity if its a large dataset. Maybe I have to write a recursive function that maps through my dataset, checks the id value, reduces it down to an array if it finds a matching id?

Additionally, I'd like to avoid using a 3rd party library. PHP has enough native functions to facilitate what I'm trying to accomplish.


回答1:


Actually there 's no need for a magic algorithm. Just a bit of php magic in form of entites, hydrators and filters.

In this answer you 'll get an object orientated php approach, which will hydrate the json api response into objects, which you can easily filter. Just keep in mind, that in this oop apporach everything is an object.

The data object - data entity

First of all you have to know, how your data is structured. From this structury you can build php objects. From the given JSON structure you can use the following objects.

namespace Application\Entity;

// Just for recognizing entities as entities later
interface EntityInterface
{

}

class Title implements EntityInterface, \JsonSerializable
{
    public $plain;

    public function getPlain() : ?string
    {
        return $this->plain;
    }

    public function setPlain(string $plain) : Title
    {
        $this->plain = $plain;
        return $this;
    }

    public function jsonSerialize() : array
    {
        return get_object_vars($this);
    }
}

class Fieldset implements EntityInterface, \JsonSerializable
{
    /**
     * Label object
     * @var Label
     */
    public $label;

    /**
     * Collection of Field objects
     * @var \ArrayObject
     */
    public $fieldset;

    // implement getter and setter methods here
}

class Section implements EntityInterface, \JsonSerializable
{
    public $title;

    public $fieldsets;

    public function getTitle() : ?Title
    {
        return $this->title;
    }

    public function setTitle(Title $title) : Section
    {
        $this->title = $title;
        return $this;
    }

    public function getFieldsets() : \ArrayObject
    {
        if (!$this->fieldsets) {
            $this->fieldsets = new \ArrayObject();
        }

        return $this->fieldsets;
    }

    public function setFieldsets(Fieldset $fieldset) : Section
    {
        if (!$this->fieldsets) {
            $this->fieldsets = new \ArrayObject();
        }

        $this->fieldsets->append($fieldset);
        return $this;
    }

    public function jsonSerialize() : array
    {
         return get_object_vars($this);
    }
}

Well, this class depicts the properties of the very first json object given in your example. Why this class implements the JsonSerializable interface? With this implementation you are able to transform the class structure back into a well formed json string. I 'm not sure, if you need that. But for sure it is safe, while communicating with a rest api. The only thing you have to do now is programming entities for every expected complex data structure / json object. You need the title object with a plin property and a fieldset object with label and fieldset properties and so on.

How to get the json data into a php object - hydration

Of course, your given json structure is a string. When we talk about hydration we actually mean converting a json string into an object structure. The above mentioned entites are needed for this approach.

But first the hydrator class itself.

namespace Application\Hydrator;
use \Application\Entity\EntityInterface;

class ClassMethodsHydrator
{
    protected $strategies;

    public function hydrate(array $data, EntityInterface $entity) : EntityInterface
    {
        foreach ($data as $key => $value) {
            if (!method_exists($entity, 'set' . ucfirst($key)) {
                throw new \InvalidArgumentException(sprintf(
                    'The method %s does not exist in %s',
                    get_class($entity)
                ));
            }

            if ($this->strategies[$key]) {
                $strategy = $this->strategies[$key];
                $value = $strategy->hydrate($value);
            }

            $entity->{'set' . ucfirst($key)}($value);
        }

        return $entity;
    }

    public function addStrategy(string $name, StrategyInterface $strategy) : Hydrator
    {
        $this->strategies[$name] = $strategy;
        return $this;
    }
}

Well, this is the class where all the magic happens. I guess this is what you mentioned as algorithm. The hydrator takes your data from the json response and pushes it into your entities. When having the entites hydrated, you can easily access the given data by calling the get methods of the entities. As the json data is complex and nested, we have to use hydrator strategies. A common pattern in hydration concerns. A strategy can be hooked into a object property and executes another hydrator. So we make sure that we represent the nested data in an identical object structure.

Here 's an example of a hydrator strategy.

namespace Application\Hydrator\Strategy;
use \Application\Entity\EntityInterface;

interface HydratorStrategy
{
    public function hydrate(array $value) : EntityInterface;
}

use \Application\Entity\Title;
class TitleHydratorStrategy implements HydratorStrategy
{
    public function hydrate(array $value) : EntityInterface
    {
        $value = (new ClassMethods())->hydrate($value, new Title);
        return $value;
    }
}

// Use case of a strategy
$data = json_decode$($response, true);
$section = (new ClassMethods())
    ->addStrategy('title', new TitleHydratorStrategy())
    ->hydrate($data, new Section());

So what a hydrator strategy actually does? While iterating over our json api response, there are severel elements, which are an object or contain objects. To hydrate this multidimensional structure correctly, we use strategies.

To stay with your example of the JSON response I 've added a simple use case. First we decode the json response into an assiciative, multidimensional array. After that we use our entities, hydrators and hydrator strategies to get an object, which contains all the data. The use case knows, that the title property in the JSON response is an object that should hydrated into our title entity, which contains the plain property.

At the end our hydrated object has a structure like this ...

\Application\Entity\Section {
     public:title => \Application\Entity\Title [
         public:plain => string 'Send Money'
     }
     ...
}

Actually you can access the properties with the getter methods of our entities.

echo $section->getTitle()->getPlain(); // echoes 'Send money'

Knowing how to hydrate our classes leads us to the next step. Aggregation!

Getting the full string with aggregation

Actually aggregation is a common design pattern in modern object orientated programming. Aggregation means no more and no less than the allocation of data. Let 's have a look at your posted JSON response. As we can see the fieldset property of our root object contains a collection of fieldset objects, that we can access via our getter and setter methods. With this in mind, we can create additional getter methods in our section entity. Let us expand our section entity with a getFullName method.

...
public function getFullName() : string
{
    $firstname = $lastname = '';

    // fetch the personal info section
    if ($this->getFieldsets()->offsetExists(0)) {
         $personalInfoFieldset = $this->getFieldsets()->offsetGet(0)->getFiedlset()->offsetGet(0);
         $firstname = $personalInfoFieldset->getField()->offsetGet(0)->getValue();
         $lastname = $personalInfoFieldset->getField()->offsetGet(1)->getValue();
    }

    return $this->concatenate(' ', $firstname, $lastname);
}

public function concatenate(string $filler, ...$strings) : string
{
    $string = '';
    foreach ($strings as $partial) {
        $string .= $partial . $filler;
    }

    return trim($string);
}

This example assumes, that both the first name and the last name are available in the very first item of the fieldset collection of the section entity. So we get Bob Hogan as return value. The concatenate method is just a little helper, which concatenates a number of strings with a filler (space).

Filter data using our entities and the FilterIterator class

Further you mentioned, that you have to find specific data by id. One possible solution coould be filtering our entities by a specific item with the Filter Iterator class.

namespace Application\Filter;

class PersonIdFilter extends \FilterIterator
{
    protected $id;

    public function __construct(Iterator $iterator, string $id)
    {
        parent::__construct($iterator);
        $this->id = $id;
    }

    public function accept()
    {
        $person = $this->getInnerIterator()->current();
        return ($person->getId() == $this->id) ? true : false;
    }
}

Because of using ArrayObject classes for our collections we are able to use iterators to filter for a specific argument. In this case we filter for the id in our personal info fieldsets.

Starting from our hydration example we it could be something like the following code.

$personalIterator = $section->getFieldsets()->offsetGet(0)->getFieldset()->getIterator();
$filter = new PersonIdFilter($personalIterator, 'a_8');
foreach ($filter as $result) {
    var_dump($result); // will output the first fieldset with the personal data
}

Too complex? Absolutely not!

As you said you want a scalable solution without nested iterations in a huge loop. In my eyes it makes sense not writing just a huge single function, wich iterates the json response and returns the data you want. Working with objects in this case makes mch more sense because of the high scalability. You can access all the data you want in a glimpse by calling the right getter methods. Furthermore the code is mich more readable than a huge function which is recursivly iterating again and again. In the above shown approach you only code once and reuse all the objects again and again.

Please keep in mind, the the above shown code is just a theoretical suggestion. It is not tested.



来源:https://stackoverflow.com/questions/53421756/traversing-through-deeply-nested-json-object

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!