Symfony 2 denormalization of nested objects with custom denormalizers - php

Here I'm building Symfony SDK for REST API. Most of data are JSON objects with nested other JSON objects. Like here
{
"id": "eng_pl",
"name": "Premier League",
"_links": {
"self": {
"href": "/tournaments/eng_pl"
},
"seasons": {
"href": "/tournaments/eng_pl/seasons/"
}
},
"coverage": {
"id": "eng",
"name": "England",
"_links": {
"self": {
"href": "/territories/eng"
}
}
}
}
Deserialization must produce an object equal to object produced by the code listed below:
$tournament = new Tournament();
$tournament->setId('eng_pl');
$tournament->setName('Premier League');
$coverage = new Territory();
$coverage->setId('eng');
$coverage->setName('England');
$tournament->setCoverage($coverage);
I'm using my own custom Denormalizers, below the fragment of code of denormalizer for Tournament objects:
class TournamentDenormalizer implements DenormalizerInterface
{
/**
* #inheritdoc
*/
public function supportsDenormalization($object, $type, $format = null)
{
if ($type != Tournament::class) {
return false;
}
return true;
}
/**
* #inheritdoc
* #return Tournament
*/
public function denormalize($object, $class, $format = null, array $context = array())
{
$tournament = new Tournament();
$tournament->setId($object->id);
$tournament->setName($object->name);
if (isset($object->coverage)) {
/** #var Territory $coverage */
$coverage = ???; //HOWTO how to implement Territory denormalization here???
$tournament->setCoverage(
$coverage
);
}
return $tournament;
}
}
The question is how should I access TerritoryDenormalizer inside TournamentDenormalizer? I see two options:
First one (I'm using now) is to add implements DenormalizerAwareInterface to signature of denormalizer class and rely on Symfony\Component\Serializer\Serializer class:
$serializer = new Symfony\Component\Serializer\Serializer(
[
new TournamentDenormalizer(),
new TerritoryDenormalizer()
], [
new Symfony\Component\Serializer\Encoder\JsonDecode()
]
);
$serializer->deserialize($json, Tournament::class, 'json');
So in TournamentDeserializer it will be like here:
if (isset($object->coverage)) {
/** #var Territory $coverage */
$coverage = $this->denormalizer->denormalize(
$object->coverage,
Territory::class,
$format,
$context
);
$tournament->setCoverage(
$coverage
);
}
}
The second approach
Is to inject necessary denormalizers explicitly
$tournamentDenormalizer = new TournamentDenormalizer();
$tournamentDenormalizer->setTerritoryDenormalizer(new TerritoryDenormalizer());
So in TournamentDeserializer it will be like here:
if (isset($object->coverage)) {
/** #var Territory $coverage */
$coverage = $this->territoryDenormalizer->denormalize(
$object->coverage,
Territory::class,
$format,
$context
);
$tournament->setCoverage(
$coverage
);
}
}
Which of approaches is the best? What alternative approaches are possible?

Making your normalizer implement NormalizerAwareInterface (and eventually use NormalizerAwareTrait) is the way to go, this interface has been introduced for this specific use case.

Related

Symfony normalizing array of strings into an Entity

I am receiving a payload that looks like this:
{
"date": "2019-03-14 14:48:26 +0000",
"events": [
"E09FDE82-4CAA-4641-87AF-6C092D6E71C1",
"AE12A6BC-DA37-4C37-BF49-DD0CE096AE00"
],
"location": null
}
The wrapper object is an Animal entity and the events is an array of UUIDs that belong to Event entities. These may or may not exist in the events table.
I want to be able to serialize this into an Animal entity using the symfony serializer like so:
$serializer = $this->get("serializer");
if($request->getMethod() == Request::METHOD_POST) {
$data = $request->getContent();
$entity = $serializer->deserialize($data, $this->type, 'json');
...
...
What I would like to do is during deserialization, I need to look for that particular key and iterate over it, creating new Events (or getting existing ones) and call the setter on the animal with these.
I have had a look at symfony normalizers but I don't think these are the right things? I made this but not sure where to go from here:
<?php
namespace App\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use App\Entity\Event;
class EventNormalizer implements NormalizerInterface {
private $normalizer;
public function __construct(ObjectNormalizer $normalizer){
$this->normalizer = $normalizer;
}
public function normalize($event, $format = null, array $context = [])
{
$data = $this->normalizer->normalize($event, $format, $context);
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof Event;
}
}
According to the documentation, this is how you would edit existing values or add new ones but I have no idea how I would tell the normalizer that "hey, when you see this key, you're up, do your thing".
Any help appreciated.
Here you need a Denormalizer, try to implement DenormalizerInterface
class EventNormalizer implements NormalizerInterface, DenormalizerInterface {
...
public function denormalize($data, string $type, string $format = null, array $context = [])
{
// retrieve your events from $data and return the object (Animal) with related events
// $this->entityManager->find($data['events'][0]) ...
}
public function supportsDenormalization($data, string $type, string $format = null)
{
// your code here
}
}

Denormalize nested structure in objects with Symfony 2 serializer

I'm working on a Symfony 2 project with version 2.8 and I'm using the build-in component Serializer -> http://symfony.com/doc/current/components/serializer.html
I have a JSON structure provided by a web service.
After deserialization, I want to denormalize my content in objects. Here is my structure (model/make in a car application context).
[{
"0": {
"id": 0,
"code": 1,
"model": "modelA",
"make": {
"id": 0,
"code": 1,
"name": "makeA"
}
}
} , {
"1": {
"id": 1,
"code": 2,
"model": "modelB",
"make": {
"id": 0,
"code": 1,
"name": "makeA"
}
}
}]
My idea is to populate a VehicleModel object which contains a reference to a VehicleMake object.
class VehicleModel {
public $id;
public $code;
public $model;
public $make; // VehicleMake
}
Here is what I do:
// Retrieve data in JSON
$data = ...
$serializer = new Serializer([new ObjectNormalizer(), new ArrayDenormalizer()], [new JsonEncoder()]);
$models = $serializer->deserialize($data, '\Namespace\VehicleModel[]', 'json');
In result, my object VehicleModel is correctly populated but $make is logically a key/value array. Here I want a VehicleMake instead.
Is there a way to do that?
The ObjectNormalizer needs more configuration. You will at least need to supply the fourth parameter of type PropertyTypeExtractorInterface.
Here's a (rather hacky) example:
<?php
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
$a = new VehicleModel();
$a->id = 0;
$a->code = 1;
$a->model = 'modalA';
$a->make = new VehicleMake();
$a->make->id = 0;
$a->make->code = 1;
$a->make->name = 'makeA';
$b = new VehicleModel();
$b->id = 1;
$b->code = 2;
$b->model = 'modelB';
$b->make = new VehicleMake();
$b->make->id = 0;
$b->make->code = 1;
$b->make->name = 'makeA';
$data = [$a, $b];
$serializer = new Serializer(
[new ObjectNormalizer(null, null, null, new class implements PropertyTypeExtractorInterface {
/**
* {#inheritdoc}
*/
public function getTypes($class, $property, array $context = array())
{
if (!is_a($class, VehicleModel::class, true)) {
return null;
}
if ('make' !== $property) {
return null;
}
return [
new Type(Type::BUILTIN_TYPE_OBJECT, true, VehicleMake::class)
];
}
}), new ArrayDenormalizer()],
[new JsonEncoder()]
);
$json = $serializer->serialize($data, 'json');
print_r($json);
$models = $serializer->deserialize($json, VehicleModel::class . '[]', 'json');
print_r($models);
Note that in your example json, the first entry has an array as value for make. I took this to be a typo, if it's deliberate, please leave a comment.
To make this more automatic you might want to experiment with the PhpDocExtractor.
In cases when you need more flexibility in denormalization it's good to create your own denormalizers.
$serializer = new Serializer(
[
new ArrayNormalizer(),
new VehicleDenormalizer(),
new VehicleMakeDenormalizer()
], [
new JsonEncoder()
]
);
$models = $serializer->deserialize(
$data,
'\Namespace\VehicleModel[]',
'json'
);
Here the rough code of such denormalizer
class VehicleDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
public function denormalize($data, $class, $format, $context)
{
$vehicle = new VehicleModel();
...
$vehicleMake = $this->denormalizer->denormalize(
$data->make,
VehicleMake::class,
$format,
$context
);
$vehicle->setMake($vehicleMake);
...
}
}
I only have doubts on should we rely on $this->denormalizer->denormalize (which works properly just because we use Symfony\Component\Serializer\Serializer) or we must explicitly inject VehicleMakeDenormalizer into VehicleDenormalizer
$vehicleDenormalizer = new VehicleDenormalizer();
$vehicleDenormalizer->setVehicleMakeDenormalizer(new VehicleMakeDenormalizer());
The easiest way would be to use the ReflectionExtractor if your Vehicle class has some type hints.
class VehicleModel {
public $id;
public $code;
public $model;
/** #var VehicleMake */
public $make;
}
You can pass the Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor as argument to the ObjectNormalizer when you initialize the Serializer
$serializer = new Serializer([new ObjectNormalizer(null, null, null, new ReflectionExtractor()), new ArrayDenormalizer()], [new JsonEncoder()]);
$models = $serializer->deserialize($data, '\Namespace\VehicleModel[]', 'json');
In Symfony4+, you can inject the serializer and it will do the job for you based on either your phpdoc (eg #var) or type hinting. Phpdoc seems safer as it manages collections of objects.
Example:
App\Model\Skill.php
<?php
namespace App\Model;
class Skill
{
public $name = 'Taxi Driver';
/** #var Category */
public $category;
/** #var Person[] */
public $people = [];
}
App\Model\Category.php
<?php
namespace App\Model;
class Category
{
public $label = 'Transports';
}
App\Model\Person.php
<?php
namespace App\Model;
class Person
{
public $firstname;
}
App\Command\TestCommand.php
<?php
namespace App\Command;
use App\Model\Category;
use App\Model\Person;
use App\Model\Skill;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Serializer\SerializerInterface;
class TestCommand extends Command
{
/**
* #var SerializerInterface
*/
private $serializer;
public function __construct(SerializerInterface $serializer)
{
parent::__construct();
$this->serializer = $serializer;
}
protected function configure()
{
parent::configure();
$this
->setName('test')
->setDescription('Does stuff');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$personA = new Person();
$personA->firstname = 'bruno';
$personB = new Person();
$personB->firstname = 'alice';
$badge = new Skill();
$badge->name = 'foo';
$badge->category = new Category();
$badge->people = [$personA, $personB];
$output->writeln(
$serialized = $this->serializer->serialize($badge, 'json')
);
$test = $this->serializer->deserialize($serialized, Skill::class, 'json');
dump($test);
return 0;
}
}
Will give the following expected result:
{"name":"foo","category":{"label":"Transports"},"people":[{"firstname":"bruno"},{"firstname":"alice"}]}
^ App\Model\BadgeFacade^ {#2531
+name: "foo"
+category: App\Model\CategoryFacade^ {#2540
+label: "Transports"
}
+people: array:2 [
0 => App\Model\PersonFacade^ {#2644
+firstname: "bruno"
}
1 => App\Model\PersonFacade^ {#2623
+firstname: "alice"
}
]
}

PHP Monolog doesn't store datetime in ISODate format

The PHP IDS system expose uses Monolog to store logs into MongoDB. The following is how it stores a log:
{
"message": "Executing on data 4f2793132469524563fa9b46207b21ee",
"context": [
],
"level": NumberLong(200),
"level_name": "INFO",
"channel": "audit",
"datetime": "1441721696",
"extra": [
]
}
I want to use the auto-delete function in Mongo, and I need the datetime field to store in ISOdate format, like this:
"datetime":ISODate("2015-09-08T17:43:25.678Z")
I look at the class Mongo in \Expose\Log\Mongo(); and this is the part responsible for storing the datetime in seconds format
public function log($level, $message, array $context = array())
{
$logger = new \Monolog\Logger('audit');
try {
$handler = new \Monolog\Handler\MongoDBHandler(
new \MongoClient($this->getConnectString()),
$this->getDbName(),
$this->getDbCollection()
);
} catch (\MongoConnectionException $e) {
throw new \Exception('Cannot connect to Mongo - please check your server');
}
$logger->pushHandler($handler);
$logger->pushProcessor(function ($record) {
$record['datetime'] = $record['datetime']->format('U');
return $record;
});
return $logger->$level($message, $context);
}
I have changed the $record['datetime'] into this
//$record['datetime'] = $record['datetime']->format('U');
$record['datetime'] = new \MongoDate();;
but the time isn't store as ISOdate but this:
"datetime": "[object] (MongoDate: 0.84500000 1441721683)"
Can anyone tell me how to store the datetime in ISODate format?
You are correct to use MongoDate to generate the datetime object. eg:
$date = new MongoDate(strtotime("2015-11-23 00:00:00"));
If you echo this back you will get the format you describe, but internally Mongo should be storing it correctly. To check, you can test:
echo date(DATE_ISO8601, ($date->sec);
which should return it in a ISO readable format.
Then you can write your own PSR-3 compatible logger that stores data in that manner.
$filters = new \Expose\FilterCollection();
$filters->load();
$logger = new \YourCustom\PSR3Logger();
$manager = new \Expose\Manager($filters, $logger);
On your Mongo instance you will want to set the TTL on your field like such:
db.log.ensureIndex( { "datetime": 1 }, { expireAfterSeconds: 3600 } )
For more info on the TTL setting see the Mongo docs: Expire Data from Collections by Setting TTL
I had the same issue on a Symfony2 configuration. I solved by setting a custom formatter that basically does nothing. It worked pretty well in my case since I was storing just scalar stuff, the only exception was the MongoDate that didn't have to be formatted. So it could be that you may need to do some tuning on your side.
Here's the custom formatter:
<?php
namespace AppBundle\Service\Formatter;
use Monolog\Formatter\FormatterInterface;
/**
* Class MongoLoggerFormatter
*
* #package AppBundle\Service
* #author Francesco Casula <fra.casula#gmail.com>
*/
class MongoLoggerFormatter implements FormatterInterface
{
/**
* {#inheritdoc}
*/
public function format(array $record)
{
return $record;
}
/**
* {#inheritdoc}
*/
public function formatBatch(array $records)
{
return $records;
}
}
And here's an excerpt of my exception listener:
/**
* #return \Symfony\Bridge\Monolog\Logger
*/
public function getLogger()
{
return $this->logger;
}
/**
* #return \Monolog\Handler\HandlerInterface|null
*/
private function getMongoHandler()
{
foreach ($this->getLogger()->getHandlers() as $handler) {
if ($handler instanceof MongoDBHandler) {
return $handler;
}
}
return null;
}
/**
* #return \Monolog\Handler\HandlerInterface|null
*/
private function addDefaultMongoHandlerSettings()
{
$mongoHandler = $this->getMongoHandler();
if ($mongoHandler) {
$mongoHandler->setFormatter(new MongoLoggerFormatter());
$mongoHandler->pushProcessor(function (array $record) {
$record['created_at'] = new \MongoDate(time());
return $record;
});
}
return $mongoHandler;
}

FacebookAds SDK Cursor not working on /reportstats endpoint

I'm using the facebook-php-ads-sdk version 2.2.4 (the newest at the time of this writing). I've noticed that the Cursor that is returned with calls to $adAccount->getReportStats() is broken with the implicit fetch option. The cursor is expecting to see in the response the following structure:
{
"paging": { "cursor": { "after": "<some_url>" } }
}
However, the /reportstats endpoint returns the paging information structured like this:
{
"paging": { "next": "<some_url>" }
}
I could have sworn it was working as expected a few days ago, so perhaps facebook's API has changed?
Here is an example:
$adAccount = new AdAccount('some_id');
// cursor is an instance of FacebookAds\Cursor.
$cursor = $adAccount->getReportStats($someFields, $someParams);
$cursor->setUseImplicitFetch(true);
foreach ($cursor as $item) {
// do stuff
}
// cursor is never advanced to next paged result.
As you can see in this snipped from FacebookAds\Cursor, when implicit fetch is set to true, the cursor only checks for paging.cursor.after|before:
<?php
namespace FacebookAds;
use FacebookAds\Http\RequestInterface;
use FacebookAds\Http\ResponseInterface;
use FacebookAds\Object\AbstractObject;
class Cursor implements \Iterator, \Countable, \arrayaccess {
// ...
/**
* #return string|null
*/
protected function getLastRequestBefore() {
$content = $this->getLastResponse()->getContent();
return isset($content['paging']['cursors']['before'])
? $content['paging']['cursors']['before']
: null;
}
/**
* #return string|null
*/
protected function getLastRequestAfter() {
$content = $this->getLastResponse()->getContent();
return isset($content['paging']['cursors']['after'])
? $content['paging']['cursors']['after']
: null;
}
// ...
}
The actual curl request facebook's sdk generates:
curl -G \
-d "data_columns=["time_start","time_stop","spend","impressions","clicks","unique_clicks","social_clicks","unique_social_clicks","cpm","unique_ctr","reach","frequency","cost_per_unique_click","cost_per_action_type","cost_per_total_action","cpp","cpc","ctr","account_id","account_name","campaign_group_id","campaign_group_name","campaign_id","campaign_name"]" \
-d "date_preset=last_90_days" \
-d "time_increment=1" \
-d "access_token=<nice_try_dude>" \
-d "appsecret_proof=<not_getting_this_either>" \
https://graph.facebook.com/v2.2/act_<account_id>/reportstats
And here is the response:
{
"data": [
{
"campaign_id": "<campaign_id>",
"date_start": "2014-12-18",
"date_stop": "2014-12-18",
"time_start": 1418878800,
"time_stop": 1418965200,
"spend": 39.39,
"impressions": 5127,
"clicks": 65,
"unique_clicks": 55,
"social_clicks": 31,
"unique_social_clicks": 27,
"cpm": 7.6828554710357,
"unique_ctr": 1.0880316518299,
"reach": 5055,
"frequency": 1.0142433234421,
"cost_per_unique_click": 0.71618181818182,
"cost_per_action_type": 0.67913793103448,
"cost_per_total_action": 0.67913793103448,
"cpp": 7.7922848664688,
"cpc": 0.606,
"ctr": 1.2677979325141,
"account_id": "<account_id>",
"account_name": "<account_name>",
"campaign_group_id": "<campaign_group_id>",
"campaign_group_name": "<campaign_group_name>",
"campaign_name": "<campaign_name>"
},
{
"..." : "x49"
}
],
"limit": 50,
"offset": 0,
"paging": {
"next": "https://graph.facebook.com/v2.2/act_<account_id>/reportstats?data_columns=%5B%22time_start%22%2C%22time_stop%22%2C%22spend%22%2C%22impressions%22%2C%22clicks%22%2C%22unique_clicks%22%2C%22social_clicks%22%2C%22unique_social_clicks%22%2C%22cpm%22%2C%22unique_ctr%22%2C%22reach%22%2C%22frequency%22%2C%22cost_per_unique_click%22%2C%22cost_per_action_type%22%2C%22cost_per_total_action%22%2C%22cpp%22%2C%22cpc%22%2C%22ctr%22%2C%22account_id%22%2C%22account_name%22%2C%22campaign_group_id%22%2C%22campaign_group_name%22%2C%22campaign_id%22%2C%22campaign_name%22%5D&date_preset=last_90_days&time_increment=1&access_token=<access_token>&appsecret_proof=<appsecret_proof>&offset=50"
}
}
In the meantime, I've resorted to using the following "wrapper". Better alternatives?
<?php namespace PayPerClick\Market\Facebook\Data;
use FacebookAds\Cursor;
/**
* Class MyReportCursor
*
* #package PayPerClick\Market\Facebook\Data
*/
class MyReportCursor implements \Iterator, \Countable, \ArrayAccess {
/**
* #type int
*/
protected $position = 0;
/**
* #type Cursor[]
*/
protected $cursors = [];
/**
* #param Cursor $cursor
*/
public function __construct(Cursor $cursor) {
$cursor->setUseImplicitFetch(false);
$this->cursors[] = $cursor;
}
/**
* #return Cursor
*/
public function getCursor() {
return $this->cursors[ $this->position ];
}
public function current() {
return $this->getCursor()->current()->getData();
}
public function next() {
$this->getCursor()->next();
if ($this->getCursor()->key() === null) {
$this->advanceCursors();
}
}
protected function advanceCursors() {
if ($this->hasCursor($this->position+1)) {
$this->getCursor()->rewind();
$this->position++;
} else if ($this->hasNextPage()) {
$this->fetchNext();
}
}
/**
* #return bool
*/
protected function hasNextPage() {
return $this->getNextPage() !== null;
}
/**
* #return string|null
*/
protected function getNextPage() {
$content = $this->getCursor()->getLastResponse()->getContent();
return isset($content['paging']['next']) ? $content['paging']['next'] : null;
}
/**
* #param int $offset
* #return bool
*/
protected function hasCursor($offset) {
return isset($this->cursors[ $offset ]);
}
protected function fetchNext() {
parse_str(parse_url($this->getNextPage(), PHP_URL_QUERY), $previousParams);
$objectPrototype = clone $this->getCursor()->offsetGet($this->getCursor()->getIndexRight());
$request = $this->getCursor()->getLastResponse()->getRequest()->createClone();
$request->getQueryParams()->offsetSet('offset', $previousParams['offset']);
$this->getCursor()->rewind();
$this->position++;
$this->cursors[ $this->position ] = new Cursor($request->execute(), $objectPrototype);
}
public function key() {
return $this->getCursor()->key();
}
public function valid() {
return $this->getCursor()->valid();
}
public function rewind() {
$this->position = 0;
}
public function offsetExists($offset) {
return $this->getCursor()->offsetExists($offset);
}
public function offsetGet($offset) {
return $this->getCursor()->offsetGet($offset);
}
public function offsetSet($offset, $value) {
$this->getCursor()->offsetSet($offset, $value);
}
public function offsetUnset($offset) {
$this->getCursor()->offsetUnset($offset);
}
public function count() {
return array_reduce($this->cursors, function ($a, $cursor) {
return $a + $cursor->count();
}, 0);
}
}
What you are seeing is the response when it is a "Time-based Pagination" https://developers.facebook.com/docs/graph-api/using-graph-api/v2.2#paging
I submitted a bug to their github.
It's mostly probably bug in facebook-php-ads-sdk. Even Facebook API examples show it as:
use FacebookAds\Object\AdAccount;
$account = new AdAccount('act_<AD_ACCOUNT_ID>');
$params = array(
'date_preset'=>'last_28_days',
'data_columns'=>"['adgroup_id','actions','spend']",
);
$stats = $account->getReportsStats(null, $params);
foreach($stats as $stat) {
echo $stat->impressions;
echo $stat->actions;
}
Unfortunately - it is, indeed, time-based paging so cursor data won't be returned from it. It's already filled as an issue on GitHub - https://github.com/facebook/facebook-php-ads-sdk/issues/76
EDIT: Oh, it is you who filled that bug :)

Creating php classes tree from a json object

EDIT
Ok it seems I'm really bad at describing my problem. I found this generator on the web, and what I'm looking for it's the exact same thing but for php code. any idea ?
ORIGINAL QUESTION
I am willing to build many php classes from a json representation (API mapping to object), and to do that I'd like to convert this:
{
"success": true,
"domains": [
{
"id": "13",
"manual": "0",
"name": "silo3.mobi",
"lastname": "Doe",
"firstname": "John",
"cid": "1",
"period": "1",
"recurring_amount": "9.95",
"currency_id": "0",
"module": "namesilo",
"next_due": "2012-12-12",
"expires": "2012-12-12",
"status": "Active",
"type": "Register",
"date_created": "2011-12-12",
"autorenew": "1",
"reglock": "1",
"idprotection": "1"
},
{
"id": "11",
"manual": "0",
"name": "netearthorg.org",
"lastname": "Doe",
"firstname": "John",
"cid": "1",
"period": "1",
"recurring_amount": "9.95",
"currency_id": "0",
"module": "NetEarthOne",
"next_due": "2012-11-22",
"expires": "2012-11-22",
"status": "Active",
"type": "Register",
"date_created": "2011-11-22",
"autorenew": "1",
"reglock": "1",
"idprotection": "0"
},
{
"id": "10",
"manual": "0",
"name": "hbappreseller.co.uk",
"lastname": "Blue",
"firstname": "Mike",
"cid": "6",
"period": "2",
"recurring_amount": "9.95",
"currency_id": "0",
"module": "NetEarthOne",
"next_due": "2012-11-22",
"expires": "0000-00-00",
"status": "Pending",
"type": "Register",
"date_created": "0000-00-00",
"autorenew": "1",
"reglock": "0",
"idprotection": "0"
}
],
"call": "getDomains",
"server_time": 1323793581
}
to an object with a bool:success property, an array of "domain" object and so on.
It's not that hard to do, I could develop that myself, but I'm wondering if there is some php libs that take care of that, haven't found any
EDIT
Ok I haven't explained myself so well I guess, what I'd like to do it's build a php class file, with dependencies on other classes and so on so I can match the json structure.
For instance, the given json should generate the following:
class Domain {
protected $id;
protected $manual;
protected $name;
protected $lastname;
protected $firstname;
protected $cid;
protected $period;
protected $recurring_amount;
// and so on
}
The purpose is to serve a WSDL with complex objects, and avoid making the wsdl signature evolve if any modifications are made on the original API (custom classes won't change dinamically, only when wanted so the WSDL will stay the same)
The api generate hundred of json objects, some of them sharing properties, so the purpose of this is to have a global way to handle all json strings and build or get builded objects, for example two json can have the "domains" property, so the first time I want to generate a class named Domain (if property=array then create file with property name -S and fill with attributes then save to file for further usage)
Lets say your JSON object is stored in $json, then you can create a class on the fly like this -
$data = json_decode($json, true);
$class = new Domain();
foreach ($data AS $key => $value) $class->{$key} = $value;
If you want a more generic way, let's say you want to change the class name on the fly -
$data = json_decode($json, true);
$className = "Domain"; // Or assign it to something else like pick from DB, from JSON from anywhere.
$class = new {$className}();
foreach ($data AS $key => $value) $class->{$key} = $value;
Ok, finally I found nothing to do the job of json2csharp tool, so I developed mine:
namespace Hostbill\Api\Generator;
use Zend\Code\Generator\ClassGenerator;
use Zend\Code\Generator\PropertyValueGenerator;
use Zend\Code\Reflection\ClassReflection;
use Zend\Json\Json;
use Zend\Json\Exception\RuntimeException as JsonRuntimeException;
class DataGenerator extends AbstractGenerator
{
const DATA_NAMESPACE = 'Hostbill\Api\Data';
const RESPONSE_SUFFIX = 'Response';
const DATA_ABSTRACT_CLASS = 'AbstractData';
/**
* #var ClassGenerator[]
*/
protected $classes = array();
/**
* #var ClassGenerator
*/
protected $responseClass;
/**
* Build classes from a source json string
* #param string $json
*/
public function fromSource($json)
{
try {
$data = Json::decode($json, Json::TYPE_ARRAY);
} catch (JsonRuntimeException $e) {
$this->err(sprintf('Could not generate classes for given Json, err:"%s"', $e->getMessage()));
return;
}
$this->parse($data);
// write classes files
$this->write($this->responseClass, sprintf('%s/../Data/', __DIR__));
foreach ($this->classes as $class) {
if (self::RESPONSE_SUFFIX === substr($class->getName(), -strlen(self::RESPONSE_SUFFIX))) {
$this->write($class, sprintf('%s/../Data/Response/', __DIR__));
} else {
$this->write($class, sprintf('%s/../Data/', __DIR__));
}
}
}
/**
* Parse json decoded object and generate corresponding classes
* #param array $data associative array retrieved from json_decode
* #return DataGenerator
*/
public function parse($data)
{
$responseClassNamespace = sprintf('%s\%s', self::DATA_NAMESPACE, self::RESPONSE_SUFFIX);
// get "call" property and build Response class name on it: getClientDetails => ClientDetailResponse
$parts = preg_split('/(?=[A-Z])/', $data['call'], -1, PREG_SPLIT_NO_EMPTY);
array_shift($parts); // remove verb
$parts[] = $this->inflector()->singularize(array_pop($parts));
$parts[] = self::RESPONSE_SUFFIX;
$baseResponseClassName = sprintf('%s\%s', self::DATA_NAMESPACE, self::RESPONSE_SUFFIX);
$responseClass = new ClassGenerator(
implode('', $parts),
$responseClassNamespace,
null,
self::RESPONSE_SUFFIX
);
$responseClass->addUse($baseResponseClassName);
$this->addClass($responseClass);
if (!class_exists($baseResponseClassName)) {
$baseResponseClassGenerated = true;
$baseResponseClass = new ClassGenerator(
self::RESPONSE_SUFFIX,
self::DATA_NAMESPACE,
ClassGenerator::FLAG_ABSTRACT
);
} else {
$baseResponseClassGenerated = false;
$baseResponseClass = ClassGenerator::fromReflection(new ClassReflection($baseResponseClassName));
}
$this->responseClass = $baseResponseClass;
foreach ($data as $key => $value) {
$key = $this->inflector()->pascalize($key);
if (is_scalar($value)) {
// thoses properties belongs to the response class
// if we just have generated the "base" response class (Response.php)
// store properties there (there are only 3 basic properties: success, call, serverTime)
// otherwise store them in the child response class, but avoid any overriding of the
// 3 properties which are stored in base Response class
if ($baseResponseClassGenerated) {
$responseClassToUpdate = $baseResponseClass;
} else {
$responseClassToUpdate = $responseClass;
}
// update base response class
if (!$responseClassToUpdate->hasProperty($key) && !$baseResponseClass->hasProperty($key)) {
$responseClassToUpdate->addProperty($key);
}
} else {
// object
if ($this->isArrayAssociative($value)) {
if (!$responseClass->hasProperty($key)) {
$responseClass->addProperty($key);
}
$this->parseObject($key, $value);
// array
} else {
if (!$responseClass->hasProperty($key)) {
$responseClass->addProperty($key, new PropertyValueGenerator(array(), PropertyValueGenerator::TYPE_ARRAY));
}
// if array is simple array, do nothing
if (!is_scalar(reset($value))) {
$this->parseArrayOfObjects($key, $value);
}
}
}
}
return $this;
}
/**
* Parse ordered array and create class object
* #param string $name key name
* #param array $data
* #return DataGenerator
*/
public function parseArrayOfObjects($name, $data)
{
$class = $this->getOrCreateClass($this->inflector()->singularize($name));
foreach ($data as $object) {
foreach ($object as $key => $value) {
if (!$class->hasProperty($key)) {
$class->addProperty($key);
}
}
}
return $this;
}
/**
* Parse associative array and create class object
* #param string $name key name
* #param array $data
* #return DataGenerator
*/
public function parseObject($name, $data)
{
$class = $this->getOrCreateClass($this->inflector()->singularize($name));
foreach ($data as $key => $value) {
if (!$class->hasProperty($key)) {
$class->addProperty($key);
}
}
return $this;
}
/**
* Add class to current stack
* #param ClassGenerator $class
* #return DataGenerator
*/
protected function addClass(ClassGenerator $class)
{
$this->classes[$this->inflector()->lowerize($class->getName())] = $class;
return $this;
}
/**
* Get class from current stack
* #param string $name
* #return false|ClassGenerator False if not found
*/
protected function getClass($name)
{
$id = $this->inflector()->lowerize($name);
if (!isset($this->classes[$id])) {
return false;
}
return $this->classes[$id];
}
/**
* Try to retrievea class from current stack, create it if not found
* #param string $name
* #return ClassGenerator
*/
protected function getOrCreateClass($name)
{
if (!$class = $this->getClass($name)) {
$class = new ClassGenerator(
$this->inflector()->camelize($name),
self::DATA_NAMESPACE,
null,
self::DATA_ABSTRACT_CLASS
);
$this->addClass($class);
}
return $class;
}
/**
* Check if the given array is associative
* #param array $array
* #return bool
*/
protected function isArrayAssociative($array)
{
return (bool)count(array_filter(array_keys($array), 'is_string'));
}
}
This code is so oriented for my needs, but it can easily be adapted to any json file, here the result:
JSON
{
"success": true,
"client": {
"id": "1",
"email": "jondoe#email.com",
"password": "474bf122c92de249ace867a003cb7196",
"lastlogin": "2011-11-25 04:32:40",
"ip": "213.54.21.3",
"host": "cmt-random.uk",
"status": "Active",
"parent_id": "0",
"firstname": "John",
"lastname": "Doe",
"companyname": "",
"address1": "Address 54",
"address2": "",
"city": "Soullans",
"state": "Birmingham",
"postcode": "B33 8TH",
"country": "GB",
"phonenumber": "357755733",
"datecreated": "2011-09-24",
"notes": "",
"language": "spanish",
"company": "0",
"credit": "0.00",
"taxexempt": "0",
"latefeeoveride": "0",
"cardtype": "Visa",
"cardnum": null,
"expdate": null,
"overideduenotices": "0",
"client_id": "1",
"currency_id": "0",
"countryname": "United Kingdom"
},
"call": "getClientDetails",
"server_time": 1323442995
}
GENERATED FILES (docblocks are missing but will be integrated so the WSDL is served correctly)
ClientResponse.php (base object)
namespace Hostbill\Api\Data\Response;
use Hostbill\Api\Data\Response;
class ClientResponse extends Response
{
public $clientId = null;
public $info = array(
);
}
Client.php
namespace Hostbill\Api\Data;
class Client extends AbstractData
{
public $id = null;
public $email = null;
public $password = null;
public $lastlogin = null;
public $ip = null;
public $host = null;
public $status = null;
public $parent_id = null;
public $firstname = null;
public $lastname = null;
public $companyname = null;
public $address1 = null;
public $address2 = null;
public $city = null;
public $state = null;
public $postcode = null;
public $country = null;
public $phonenumber = null;
public $datecreated = null;
public $notes = null;
public $language = null;
public $company = null;
public $credit = null;
public $taxexempt = null;
public $latefeeoveride = null;
public $cardtype = null;
public $cardnum = null;
public $expdate = null;
public $overideduenotices = null;
public $client_id = null;
public $currency_id = null;
public $countryname = null;
public $services = null;
}
I made a PHP class generator that will make models with JSON https://json2php.strikebit.io/. It will recursively inspect your JSON and make corresponding classes.
In my opinion, you should not create objects for generic data like this. You could easily map this against a generic data object.
So your framework would be just standard PHP. Like :
class JsonObject
{
protected $data = array();
public function __construct($data)
{
$this->data = $data;
}
public function __get($var)
{
if (array_key_exists($var, $this->data)) {
return $this->data[$var];
} else {
throw new Exception($var . ' not found in ' . __CLASS__);
}
}
public function __set($var, $val)
{
if (array_key_exists($var, $this->data)) {
return $this->data[$var];
} else {
throw new Exception($var . ' not found in ' . __CLASS__);
}
}
}
class Domain extends JsonObject
{
//some domain specific functionality
}
class getDomainResult
{
public $domains = array();
public $success = false;
public $lastTime = 0;
//some methods to do the calls
public function callback($result)
{
$res = json_decode($result, true);
$this->success = $res['success'];
$this->lastTime = $res['server_time'];
foreach ($res['domains'] as $domain) {
$this->domains[] = new Domain($domain);
}
}
}

Categories