Search code examples
phpjsoncode-generation

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)


Solution

  • 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;
    }