<?php
namespace com\mikebevz\xsd2php;

use QuickBooksOnline\API\Core\CoreConstants;

use com\mikebevz\xsd2php\wsdl;

set_include_path(get_include_path().PATH_SEPARATOR.
                realpath("../lib/ZF/1.10.7").PATH_SEPARATOR.
                realpath("../src"));

require_once 'Zend/Soap/Wsdl.php';
require_once 'com/mikebevz/xsd2php/Common.php';
require_once 'com/mikebevz/xsd2php/wsdl/WsdlFactory.php';

class Wsdl extends Common
{

    /**
     * Service class to be exposed
     * @var object
     */
    private $class;
    /**
     * Instance of Zend_Soap_Wsdl
     *
     * @todo consider replace this class - it's not that useful
     * @var com\mikebevz\xsd2php\wsdl\AbstractWsdl
     */
    private $wsdl;

    /**
     * Reflection of the $class
     * @var \ReflectionClass
     */
    private $refl;

    /**
     * Soap binding style: rpc|document
     * @var string
     */
    private $soapBindingStyle = "document";

    /**
     * Soap binding transport: which transport of SOAP this binding corresponds to
     * @var string
     */
    private $soapBindingTransport = "http://schemas.xmlsoap.org/soap/http";

    /**
     * Port type
     * @var DomNode
     */
    private $portType;

    /**
     * Binding
     * @var DomNode
     */
    private $binding;

    /**
     * Service location
     *
     * @var string
     */
    private $location;

    /**
     * Prefix of target namespace
     * @var string
     */
    private $targetNsPrefix = "tns";

    /**
     * Prefix for XML Schema namespace
     * @var string
     */
    private $xmlSchemaPreffix = "xsd";
    /**
     * Suffix for generated service tag name
     * @var string
     */
    private $serviceNameSuffix = "_Service";

    /**
     * Suffix for generated port tag name
     * @var string
     */
    private $portNameSuffix = "_Port";
    /**
     * Suffix for generated binding tag name
     * @var unknown_type
     */
    private $bindingNameSuffix = "_Binding";

    /**
     * Response items suffix
     * @var string
     */
    private $responseSuffix = "Response";

    /**
     * Request
     * @var unknown_type
     */
    private $requestSuffix = "Request";

    /**
     * Path to schema to be imported/included in XSD
     * @var string
     */
    private $schemasPath = null;

    /**
     * Path to publicly accessable folder to store imported/included schemas
     *
     * @var string
     */
    private $publicPath = null;

    private $publicUrl = null; // Relative to / without domain name

    /**
     * Namespaces already imported to XML Schema
     * @var array
     */
    private $importedNamespaces = array();

    /**
     * Creates new wsdl for the $class given
     * @param string|object $class
     *
     * @return void
     */
    public function __construct($class = null)
    {
        if ($class != null) {
            $this->class = $class;
        }
    }

    /**
     * Ger WSDL file contents
     *
     * @param string|object $class
     *
     * @return string
     *
     * @throws RuntimeException If class not defined
     * @throws RuntimeException If given class is neither class nor string
     * @throws RuntimeException If given class is not found, ie was not included before
     */
    public function getWsdl($class = null)
    {
        /*
        if ($class != null) {
            $this->class = $class;
        } elseif ($this->class == null) {
            throw new \RuntimeException("No class defined");
        }

        if (!is_object($this->class) && !is_string($this->class)) {
            throw new \RuntimeException("Given class is neither class nor string");
        }

        if (is_string($this->class)) {
            if (!class_exists($this->class)) {
                throw new \RuntimeException("Class ".$class." is not found. Did you forget to include it?");
            }
            $this->class = new $this->class();
        }

        */

        //$this->refl = new \ReflectionClass($this->class);

        //$this->wsdl = new \Zend_Soap_Wsdl($this->refl->getShortName(),
        //                                 $this->namespaceToUrn($this->refl->getNamespaceName()));

        $factory = new wsdl\WsdlFactory($class, wsdl\WsdlFactory::WSDL_1_1);
        $this->wsdl = $factory->getImplementation();
        $this->wsdl->debug = true;
        /*
        $this->addTypes();
        $this->addMessages();
        $this->addPortType();
        $this->addBindings();

        $this->addServices();

        $dom = $this->wsdl->toDomDocument();
        if ($this->debug) {
            $dom->formatOutput = true;
        }*/

        return $this->wsdl->toXml();//$dom->saveXml();
    }

    /**
     * Add bindings to WSDL
     *
     * @return DomNode Binding node
     */
    private function addBindings()
    {
        $this->binding = $this->wsdl->addBinding($this->refl->getShortName().$this->bindingNameSuffix,
                                                 $this->targetNsPrefix.":".$this->getBindingTypeName());
        $this->wsdl->addSoapBinding($this->binding,
                                    $this->getSoapBindingStyle(),
                                    $this->getSoapBindingTransport());

        $this->addBindingOperations();
        return $this->binding;
    }

    /**
     * Get binding type name
     *
     * @return string
     */
    private function getBindingTypeName()
    {
        return $this->refl->getShortName().$this->portNameSuffix;
    }

    /**
     * Add port type
     *
     * @return DOMNode portType node
     */
    private function addPortType()
    {
        $this->portType = $this->wsdl->addPortType($this->getPortName());
        $this->addPortOperations();

        return $this->portType;
    }

    /**
     * Add XML Schema types
     *
     * @return void
     */
    public function addTypes()
    {
        $methods = $this->getClassMethods();
        foreach ($methods as $method) {
            $data = $this->getMethodIO($method->name);
            $element = array('name' => $method->name,
                             'sequence' => array() );

            if (array_key_exists("params", $data)) {
                foreach ($data['params'] as $param) {
                    if ($this->isLocalType($this->getTypeName($param['type']))) {
                        array_push($element['sequence'], array('name' => $param['name'], 'type' => $this->getTypeName($param['type'])));
                    } else {
                        array_push($element['sequence'], array('ref' => $this->getTypeName($param['type'])));
                    }
                }
                $this->wsdl->addElement($element);
            }

            if (array_key_exists("return", $data)) {
                $return = "";
                if ($this->isLocalType($this->getTypeName($data['return']['type']))) {
                    $return = array('name' => $method->name.$this->responseSuffix,
                                 'sequence' => array(array('name' => 'Response', 'type' => $this->getTypeName($data['return']['type']))));
                } else {
                    $return = array('name' => $method->name.$this->responseSuffix,
                                 'sequence' => array(array('ref' => $this->getTypeName($data['return']['type']))));
                }

                $this->wsdl->addElement($return);
            }
        }
    }

    /**
     * Add operations to binding
     *
     * @return void
     */
    public function addBindingOperations()
    {
        $methods = $this->getClassMethods();

        //@todo Check if binding is instantiated

        foreach ($methods as $method) {
            $data = $this->getMethodIO($method->name);
            $bindingInput = false;
            $bindingOutput = false;
            if (array_key_exists("params", $data)) {
                $bindingInput  = array('use' => 'literal');
            }

            if (array_key_exists("return", $data)) {
                $bindingOutput = array('use' => 'literal');
            }

            $operation     = $this->wsdl->addBindingOperation($this->binding, $method->name, $bindingInput, $bindingOutput);
            $soapOperation = $this->wsdl->addSoapOperation($operation, $this->namespaceToUrn($this->refl->getNamespaceName())."/".$method->name);
        }
    }

    /**
     * Add Port operations
     *
     * @return void
     */
    private function addPortOperations()
    {
        $methods = $this->getClassMethods();

        foreach ($methods as $method) {
            $data = $this->getMethodIO($method->name);

            $input         = false;
            $output        = false;
            $bindingInput  = false;
            $bindingOutput = false;

            if (array_key_exists("params", $data)) {
                $input = $this->targetNsPrefix.":".$method->name.$this->requestSuffix;
            }

            if (array_key_exists("return", $data)) {
                $output = $this->targetNsPrefix.":".$method->name.$this->responseSuffix;
            }

            $this->wsdl->addPortOperation($this->portType,
                                          $method->name,
                                          $input,
                                          $output);
        }
    }

    /**
     * Return true if given type is local, false in case the type is not from xsd or tns namespaces
     *
     * @param string $type QName type name
     *
     * @return boolean
     */
    public function isLocalType($type)
    {
        if (preg_match('/:/', $type)) {
            list($ns, $typeName) = explode(":", $type);
            if ($ns == $this->targetNsPrefix || $ns == $this->xmlSchemaPreffix) {
                return true; // Local type
            } else {
                return false;
            }
        } else {
            // @todo - No namespace - local type
            return true;
        }
    }

    /**
     * Add SOAP messages
     *
     * @param array $messages Messages to add
     *
     * @return void
     * @throws RuntimeException If method does not have any inputs/outputs
     * @throws RuntimeException If
     */
    public function addMessages()
    {
        $methods = $this->getClassMethods();
        $messages = array();
        foreach ($methods as $method) {
            $data = $this->getMethodIO($method->name);

            $input = false;
            $output = false;
            // Add inputs if any
            if (array_key_exists("params", $data)) {
                array_push($messages, array('name' => $method->name.$this->requestSuffix, 'refType' => 'element',
                 'content' => array($method->name.$this->requestSuffix => $this->targetNsPrefix.":".$method->name)));
            }

            // Add outputs if any
            if (array_key_exists("return", $data)) {
                array_push($messages, array('name' => $method->name.$this->responseSuffix, 'refType' => 'element',
                 'content' => array($method->name.$this->responseSuffix => $this->targetNsPrefix.":".$method->name.$this->responseSuffix)));
            }
        }

        if (empty($messages)) {
            throw new \RuntimeException($method->name." does not have any input or output");
        }

        if (!is_array($messages)) {
            throw new \RuntimeException("Argument is not array");
        }

        foreach ($messages as $msg) {
            if ($msg['refType'] == 'type') {
                $this->wsdl->addMessage($msg['name'], $msg['content']);
            } elseif ($msg['refType'] == 'element') {
                $dom = $this->wsdl->toDomDocument();
                $msgEl = $dom->createElement('message');
                $msgNameAt = $dom->createAttribute('name');
                $msgNameTxtNode = $dom->createTextNode($msg['name']);
                $msgNameAt->appendChild($msgNameTxtNode);
                $msgEl->appendChild($msgNameAt);

                $partEl = $dom->createElement("part");
                $partNameAt = $dom->createAttribute("name");
                $partElAt = $dom->createAttribute('element');
                foreach ($msg['content'] as $name => $element) {
                    $partNameTxtNode = $dom->createTextNode($name);
                    $partNameAt->appendChild($partNameTxtNode);

                    $partElTxtNode = $dom->createTextNode($element);
                    $partElAt->appendChild($partElTxtNode);
                }
                $partEl->appendChild($partNameAt);
                $partEl->appendChild($partElAt);

                $msgEl->appendChild($partEl);

                $xpath = new \DOMXPath($dom);
                $query = "//*[local-name()='definitions']";
                $childs = $xpath->query($query);
                $childs->item(0)->appendChild($msgEl);
            } else {
                throw new \RuntimeException("refType only can be type or element");
            }
        }
    }

    /**
     * Get service class public methods
     *
     * @return array<ReflectionMethod>
     */
    private function getClassMethods()
    {
        return $this->refl->getMethods(\ReflectionMethod::IS_PUBLIC);
    }

    /**
     * Return method input/output
     *
     * @param string $method Method name
     *
     * @return array
     */
    private function getMethodIO($method)
    {
        $methodDocs = $this->refl->getMethod($method)->getDocComment();
        return $this->parseDocComments($methodDocs);
    }

    /**
     * Get port name
     *
     * @return string
     */
    private function getPortName()
    {
        return $this->refl->getShortName().$this->portNameSuffix;
    }

    private function getNamespaceName($class)
    {
        $this->refl->getNamespaceName();
    }

    private function namespaceToUrn($namespace)
    {
        $namespace = "urn:".preg_replace('/\\\/', ':', $namespace);
        return $namespace;
    }

    /**
     * Add SOAP Service
     *
     * @return void
     */
    private function addServices()
    {
        $this->wsdl->addService($this->getServiceName(), $this->getPortName(), $this->getBindingName(), $this->getLocation());
    }

    /**
     * Get QName style for given type
     * @param string $type Type name
     *
     * @return string
     * @throws RuntimeException If class of type cannot be found
     */
    private function getTypeName($type)
    {

        //@todo Check must be performed against PHP data types not XMLSchema
        if (!in_array($type, $this->basicTypes)) {
            if (!class_exists($type, CoreConstants::USE_AUTOLOADER)) {
                throw new \RuntimeException("Cannot find class ".$type);
            }

            $typeNamespace = $this->getClassNamespace($type);
            $typeName      = $this->getShortName($type);
            $nsCode        = $this->getNsCode($typeNamespace, true);
            $this->addImportToSchema($typeNamespace, $nsCode);
            return $nsCode.":".$typeName;
        } else {
            return $this->xmlSchemaPreffix.":".$type;
        }
    }

    /**
     * Add xsd:import tag to XML schema before any childs added
     *
     * @param string $namespace Namespace URI
     * @param string $code      Shortcut for namespace, fx, ns0. Returned by getNsCode()
     *
     * @return void
     */
    private function addImportToSchema($namespace, $code)
    {
        if (array_key_exists($namespace, $this->namespaces)) {
            if (in_array($namespace, $this->importedNamespaces)) {
                return;
            }

            $dom = $this->wsdl->toDomDocument();
            $dom->createAttributeNs($namespace, $code.":definitions");
            $importEl = $dom->createElement($this->xmlSchemaPreffix.":import");
            $nsAttr   = $dom->createAttribute("namespace");
            $txtNode  = $dom->createTextNode($namespace);
            $nsAttr->appendChild($txtNode);

            $nsAttr2   = $dom->createAttribute("schemaLocation");
            $schemaLocation = $this->getSchemaLocation($namespace);
            $publicSchema = $this->copyToPublic($schemaLocation, true);
            $publicSchema = $this->copyToPublic($schemaLocation, true);
            $schemaUrl = $this->importsToAbsUrl($publicSchema, $this->getSchemasPath());

            $txtNode2  = $dom->createTextNode($schemaUrl);
            $nsAttr2->appendChild($txtNode2);

            $importEl->appendChild($nsAttr);
            $importEl->appendChild($nsAttr2);
            $this->wsdl->getSchema();
            $xpath = new \DOMXPath($dom);
            $query = "//*[local-name()='types']/child::*/*";
            $firstElement = $xpath->query($query);

            if (!is_object($firstElement->item(0))) {
                $query = "//*[local-name()='types']/child::*";
                $schema = $xpath->query($query);
                $schema->item(0)->appendChild($importEl);
            } else {
                $this->wsdl->getSchema()->insertBefore($importEl, $firstElement->item(0));
            }

            array_push($this->importedNamespaces, $namespace);
        }
    }

    /**
     * Convert relative paths in XMLSchema's imports and includes to URLs using $location
     *
     * @param string $schemaPath Absolute path to XML Schema file
     *
     * @return string URL of the schema
     */
    public function importsToAbsUrl($schemaPath, $relPath = '')
    {
        $dom = new \DOMDocument();
        $dom->load($schemaPath);

        $xpath = new \DOMXPath($dom);
        $query = "//*[local-name()='schema']/*[local-name()='import']";
        $imports = $xpath->query($query);
        $urlParts = parse_url($this->getLocation());
        $rootUrl = $this->composeUrl($urlParts);
        $url = $rootUrl.$this->getPublicUrl()."/".basename($schemaPath);
        if (!is_object($imports->item(0))) {
            return $url;
        }

        $urlParts = parse_url($this->getLocation());
        $rootUrl = $this->composeUrl($urlParts);

        foreach ($imports as $import) {
            $scPath = realpath($relPath.DIRECTORY_SEPARATOR.$import->getAttribute('schemaLocation'));
            $copiedSchema = $this->copyToPublic($scPath, true);

            $schemaUrl = $rootUrl.$this->getPublicUrl()."/".basename($copiedSchema);
            $import->setAttribute('schemaLocation', $schemaUrl);

            $this->importsToAbsUrl($copiedSchema, dirname($scPath));
        }
        $dom->save($schemaPath);
        return $url;
    }

    /**
     * Compose URL of kind schema://host:port
     *
     * @param array $parts
     *
     *  @return string URL
     */
    public function composeUrl($parts)
    {
        $url = "";
        if (array_key_exists('scheme', $parts)) {
            $url .= $parts['scheme']."://";
        }

        if (array_key_exists('host', $parts)) {
            $url .= $parts['host'];
        }

        if (array_key_exists('port', $parts)) {
            $url .= ":".$parts['port'];
        }

        if (array_key_exists('path', $parts)) {
        }

        return $url;
    }

    /**
     * Copy given file to publicSchemaPath
     *
     * @param string  $path      Path to file
     * @param boolean $overwrite Overwrite file if it exists? default false
     *
     * @return string Path to new file
     * @throws RuntimeException If file cannot be copied
     */
    public function copyToPublic($path, $overwrite = false)
    {
        $publicPath = realpath($this->getPublicPath());
        $targetFilePath   = $publicPath.DIRECTORY_SEPARATOR.basename($path);

        if (($overwrite === true && file_exists($targetFilePath)) || !file_exists($targetFilePath)) {
            if (!copy($path, $targetFilePath)) {
                throw new \RuntimeException("Cannot copy ".basename($path)." to ".$publicPath);
            }
        }

        return $targetFilePath;
    }

    /**
     * Get path to schema in schemasPath with targetNamespace = $ns
     *
     * @param string $ns Namespace to match
     *
     * @return string absolut path to schema file
     * @throws RuntimeException If SchemaPath is not specified
     * @throws RuntimeException If Public folder is not specified
     * @throws RuntimeException If SchemaPath is not readable
     * @throws RuntimeException If PublicPath is not writable
     * @throws RuntimeException If namespace cannot be found in SchemaPath
     */
    public function getSchemaLocation($ns)
    {
        if ($this->getSchemasPath() == null || !file_exists($this->getSchemasPath())) {
            throw new \RuntimeException("Schemas path is not specified or is not exist - cannot start search for imports");
        }

        if ($this->getPublicPath() == null || !file_exists($this->getPublicPath())) {
            throw new \RuntimeException("Public folder for imported schemas is not specified or is not exist - cannot save imports");
        }

        if (!is_readable($this->getSchemasPath())) {
            throw new \RuntimeException($this->getSchemasPath()." is not readable");
        }

        if (!is_writeable($this->getPublicPath())) {
            throw new \RuntimeException($this->getPublicPath()." is not writable");
        }
        // Scan directory for xsd files

        $dir   = new \RecursiveDirectoryIterator(realpath($this->getSchemasPath()));
        $iter  = new \RecursiveIteratorIterator($dir);
        $regex = new \RegexIterator($iter, '/\.xsd$/', \RecursiveRegexIterator::MATCH);
        $files = array();
        foreach ($regex as $key => $value) {
            array_push($files, $key);
        }
        $schema = "";
        foreach ($files as $file) {
            if ($this->isTargetNamespaceEqualsTo($file, $ns)) {
                $schema = $file;
            }
        }

        if ($schema == "") {
            throw new \RuntimeException("Cannot find schema for namespace ".$ns." in ".realpath($this->getSchemasPath()));
        }

        return $schema;
    }

    /**
     * Check if $schema XML Schema file belongs to $ns
     *
     * @param string $schema Path to XML Schema file
     * @param string $ns     Target namespace to match
     *
     * @return boolean
     */
    public function isTargetNamespaceEqualsTo($schema, $ns)
    {
        $dom = new \DOMDocument();
        $dom->load($schema);

        $xpath = new \DOMXPath($dom);
        $query = "//*[local-name()='schema']/@targetNamespace";
        $tNs = $xpath->query($query);

        if (!is_object($tNs->item(0))) {
            return false;
        }
        if ($tNs->item(0)->nodeValue === $ns) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Get base name from URI
     *
     * @param string $ns URI
     *
     * @return string Base name
     */
    public function getBaseNsName($ns)
    {
        $ns = array_reverse(explode(":", $ns));
        return $ns[0];
    }

    /**
     * Return class XML namespace taken from @xmlNamespace doc
     *
     * @param string $class Class name
     *
     * @return string XML namespace
     * @throws RuntimeException If namespace cannot be found in class DocBlock
     */
    public function getClassNamespace($class)
    {
        $refl = new \ReflectionClass($class);

        $docs = $this->parseDocComments($refl->getDocComment());
        if (!array_key_exists("xmlNamespace", $docs)) {
            throw new RuntimeException('Cannot find namespace in class description in '.$class);
        }
        return $docs['xmlNamespace'];
    }

    /**
     * Get class name without namespace
     *
     * @param string $longName Class full name
     *
     * @return string Class base name
     */
    private function getShortName($longName)
    {
        $arr = array_reverse(explode('\\', $longName));
        $shortName = $arr[0];
        return $shortName;
    }

    /**
     * @todo Check this method
     *
     * @param string $type Data type
     *
     * @return string
     */
    private function addType($type)
    {
        if (!in_array($type, $this->basicTypes)) {
            $typeName      = $this->getShortName($type);
            $typeNamespace = $this->getClassNamespace($type);
            $nsCode        = $this->getNsCode($typeNamespace, true);
        } else {
            return $type;
        }
    }

    /**
     * Get service element name
     *
     * @return string
     */
    private function getServiceName()
    {
        return $this->refl->getShortName().$this->serviceNameSuffix;
    }

    /**
     * Get binding element name
     *
     * @return void
     */
    private function getBindingName()
    {
        return $this->targetNsPrefix.":".$this->refl->getShortName().$this->bindingNameSuffix;
    }

    /**
     * Get SOAP service location (service->port->address->location)
     *
     * @return string $location
     */
    public function getLocation()
    {
        return $this->location;
    }

    /**
     * @param $location the $location to set
     */
    public function setLocation($location)
    {
        $this->location = $location;
    }
    /**
     * Get SOAP binding style
     *
     * @return string $soapBindingStyle
     */
    public function getSoapBindingStyle()
    {
        return $this->soapBindingStyle;
    }

    /**
     * @param $soapBindingStyle the $soapBindingStyle to set
     */
    public function setSoapBindingStyle($soapBindingStyle)
    {
        $this->soapBindingStyle = $soapBindingStyle;
    }
    /**
     * Get SOAP binding transport
     *
     * @return string the $soapBindingTransport
     */
    public function getSoapBindingTransport()
    {
        return $this->soapBindingTransport;
    }

    /**
     * @param $soapBindingTransport the $soapBindingTransport to set
     */
    public function setSoapBindingTransport(
    $soapBindingTransport)
    {
        $this->soapBindingTransport = $soapBindingTransport;
    }
    /**
     * @return the $schemasPath
     */
    public function getSchemasPath()
    {
        return $this->schemasPath;
    }

    /**
     * @param $schemasPath the $schemasPath to set
     */
    public function setSchemasPath($schemasPath)
    {
        $this->schemasPath = $schemasPath;
    }
    /**
     * @return the $debug
     */
    public function getDebug()
    {
        return $this->debug;
    }

    /**
     * @param $debug the $debug to set
     */
    public function setDebug($debug)
    {
        $this->debug = $debug;
    }
    /**
     * @return the $publicPath
     */
    public function getPublicPath()
    {
        return $this->publicPath;
    }

    /**
     * @param $publicPath the $publicPath to set
     */
    public function setPublicPath($publicPath)
    {
        $this->publicPath = $publicPath;
    }
    /**
     * @return the $publicUrl
     */
    public function getPublicUrl()
    {
        return $this->publicUrl;
    }

    /**
     * @param $publicUrl the $publicUrl to set
     */
    public function setPublicUrl($publicUrl)
    {
        $this->publicUrl = $publicUrl;
    }
}


<?php
//namespace com\mikebevz\xsd2php;
namespace QuickBooksOnline\API\XSD2PHP\src\com\mikebevz\xsd2php;

/**
 * Copyright 2010 Mike Bevz <myb@mikebevz.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//require_once dirname(__FILE__) . '/PHPClass.php';
//require_once dirname(__FILE__).'/Common.php';

/**
 * Generate PHP classes based on XSD schema
 *
 * @author Mike Bevz <myb@mikebevz.com>
 * @version 0.0.1
 *
 */
class Xsd2Php extends Common
{
    /**
     * XSD schema to convert from
     * @var String
     */
    private $xsdFile;

    /**
     *
     * @var DOMXPath
     */
    private $xpath;

    /**
     * Namespaces in the current xsd schema
     * @var array
     */
    private $nspace;

    /**
     * XML file suitable for PHP code generation
     * @var string
     */
    private $xmlForPhp;

    /**
     * Show debug info
     * @var boolean
     */
    public $debug = false;

    /**
     * Namespaces = array (className => namespace ), used in dirs/files generation
     * @var array
     */
    //private $namespaces;

    /**
     * Short namespaces
     *
     * @var array
     */
    private $shortNamespaces;

    /**
     * XML Source
     *
     * @var string
     */
    private $xmlSource;

    /**
     * Target namespace
     *
     * @var string
     */
    private $targetNamespace;

    /**
     * XSD root namespace alias (fx, xsd = http://www.w3.org/2001/XMLSchema)
     *
     * @var string
     */
    private $xsdNs;

    /**
     * Already processed imports
     *
     * @var array
     */
    private $loadedImportFiles = array();

    /**
     * Processed namespaces
     *
     * @var array
     */
    private $importHeadNS = array();

    /**
     * XML Schema converted to XML
     *
     * @return string $xmlSource
     */
    public function getXmlSource()
    {
        return $this->xmlSource;
    }

    /**
     * Set XML representation of the XML Schema
     *
     * @param string $xmlSource XML Source
     *
     * @return void
     */
    public function setXmlSource($xmlSource)
    {
        $this->xmlSource = $xmlSource;
    }

    /**
     *
     * __construct
     *
     * @param string  $classPrefix Prefix for generated class names
     * @param string  $xsdFile Xsd file to convert
     * @param boolean $debug   Show debug messages[optional, default = false]
     *
     * @return void
     */
    public function __construct($classPrefix, $xsdFile, $debug = false)
    {
        parent::__construct($classPrefix);

        if ($debug != false) {
            $this->debug = $debug;
        }

        $this->xsdFile = $xsdFile;

        $this->dom = new \DOMDocument();
        $this->dom->load($this->xsdFile,
        LIBXML_DTDLOAD |
        LIBXML_DTDATTR |
        LIBXML_NOENT |
        LIBXML_XINCLUDE);

        $this->xpath = new \DOMXPath($this->dom);
        $this->targetNamespace = $this->getTargetNS($this->xpath);
        $this->shortNamespaces = $this->getNamespaces($this->xpath);

        $this->dom = $xsd = $this->loadIncludes($this->dom, dirname($this->xsdFile), $this->targetNamespace);
        $this->dom = $this->loadImports($this->dom, $this->xsdFile);

        if ($this->debug) {
            print_r($this->shortNamespaces);
        }
    }

    /**
     * Return target namespace for given DOMXPath object
     *
     * @param DOMXPath $xpath DOMXPath Object
     *
     * @return string
     */
    private function getTargetNS($xpath)
    {
        $query   = "//*[local-name()='schema' and namespace-uri()='http://www.w3.org/2001/XMLSchema']/@targetNamespace";
        $targetNs =  $xpath->query($query);

        if ($targetNs) {
            foreach ($targetNs as $entry) {
                return $entry->nodeValue;
            }
        }
    }

    /**
     * Return array of namespaces of the document
     *
     * @param DOMXPath $xpath
     *
     * @return array
     */
    public function getNamespaces($xpath)
    {
        $query   = "//namespace::*";
        $entries =  $xpath->query($query);
        $nspaces = array();

        foreach ($entries as $entry) {
            if ($entry->nodeValue == "http://www.w3.org/2001/XMLSchema") {
                $this->xsdNs = preg_replace('/xmlns:(.*)/', "$1", $entry->nodeName);
            }
            if (//$entry->nodeName != $this->xsdNs
            //&&
            $entry->nodeName != 'xmlns:xml') {
                if (preg_match('/:/', $entry->nodeName)) {
                    $nodeName = explode(':', $entry->nodeName);
                    $nspaces[$nodeName[1]] = $entry->nodeValue;
                } else {
                    $nspaces[$entry->nodeName] = $entry->nodeValue;
                }
            }
        }
        return $nspaces;
    }

    /**
     * Save generated classes to directory
     *
     * @param string  $dir             Directory to save classes to
     * @param boolean $createDirectory [optional] Create directory, false by default
     *
     * @return void
     */
    public function saveClasses($dir, $createDirectory = false)
    {
        $this->setXmlSource($this->getXML()->saveXML());
        $this->savePhpFiles($dir, $createDirectory);
    }

    /**
     * Load imports
     *
     * @param DOMDocument $dom     DOM model of the schema
     * @param string      $xsdFile Full path to first XSD Schema
     *
     * @return void
     */
    public function loadImports($dom, $xsdFile = '')
    {
        $xpath = new \DOMXPath($dom);
        $query = "//*[local-name()='import' and namespace-uri()='http://www.w3.org/2001/XMLSchema']";
        $entries = $xpath->query($query);
        if ($entries->length == 0) {
            return $dom;
        }
        foreach ($entries as $entry) {
            // load XSD file
            $namespace = $entry->getAttribute('namespace');
            $parent = $entry->parentNode;
            $xsd = new \DOMDocument();
            $xsdFileName = realpath(dirname($xsdFile).DIRECTORY_SEPARATOR.$entry->getAttribute("schemaLocation"));
            if ($this->debug) {
                print('Importing '.$xsdFileName."\n");
            }

            if (!file_exists($xsdFileName)) {
                if ($this->debug) {
                    print $xsdFileName. " is not found \n";
                }
                continue;
            }
            if (in_array($xsdFileName, $this->loadedImportFiles)) {
                if ($this->debug) {
                    print("Schema ".$xsdFileName." has been already imported");
                }
                $parent->removeChild($entry);
                continue;
            }
            $filepath = dirname($xsdFileName);
            $result = $xsd->load($xsdFileName,
            LIBXML_DTDLOAD|LIBXML_DTDATTR|LIBXML_NOENT|LIBXML_XINCLUDE);
            if ($result) {
                $mxpath = new \DOMXPath($xsd);
                $this->shortNamespaces = array_merge($this->shortNamespaces, $this->getNamespaces($mxpath));

                $xsd = $this->loadIncludes($xsd, $filepath, $namespace);

                $this->loadedImportFiles[] = $xsdFileName;
                $this->loadedImportFiles = array_unique($this->loadedImportFiles);
            }
            foreach ($xsd->documentElement->childNodes as $node) {
                if ($node->nodeName == $this->xsdNs.":import") {
                    // Do not change Namespace for import and include tags
                    //if ($this->debug) print("Insert Import ".$node->nodeName." NS=". $node->getAttribute('namespace'). "\n");

                    $loc = realpath($filepath.DIRECTORY_SEPARATOR.$node->getAttribute('schemaLocation'));
                    $node->setAttribute('schemaLocation', $loc);
                    if ($this->debug) {
                        print('Change imported schema location to '.$loc." \n");
                    }
                    $newNode = $dom->importNode($node, true);
                    $parent->insertBefore($newNode, $entry);

                    continue;
                } else {
                    //if ($this->debug) print($node->nodeName." \n". $namespace. "\n");
                    $newNodeNs = $xsd->createAttribute("namespace");
                    $textEl = $xsd->createTextNode($namespace);
                    $newNodeNs->appendChild($textEl);
                    $node->appendChild($newNodeNs);

                    $newNode = $dom->importNode($node, true);
                    $parent->insertBefore($newNode, $entry);
                }
            }
            // add to $dom
            $parent->removeChild($entry);
        }

        $xpath = new \DOMXPath($dom);
        $query = "//*[local-name()='import' and namespace-uri()='http://www.w3.org/2001/XMLSchema']";
        $imports = $xpath->query($query);
        if ($imports->length != 0) {
            $dom = $this->loadImports($dom);
        }

        if ($this->debug) {
            print_r("\n------------------------------------\n");
        }
        return $dom;
    }

    /**
     * Load includes in XML Schema
     *
     * @param DOMDocument $dom       Instance of DOMDocument
     * @param string      $filepath
     * @param string	  $namespace
     *
     * @return void
     */
    public function loadIncludes($dom, $filepath = '', $namespace = '')
    {
        $xpath = new \DOMXPath($dom);
        $query = "//*[local-name()='include' and namespace-uri()='http://www.w3.org/2001/XMLSchema']";
        $includes = $xpath->query($query);

        foreach ($includes as $entry) {
            $parent = $entry->parentNode;
            $xsd = new \DOMDocument();
            $xsdFileName = realpath($filepath.DIRECTORY_SEPARATOR.$entry->getAttribute("schemaLocation"));
            if ($this->debug) {
                print('Including '.$xsdFileName."\n");
            }

            if (!file_exists($xsdFileName)) {
                if ($this->debug) {
                    print $xsdFileName. " is not found \n";
                }
                continue;
            }

            $result = $xsd->load($xsdFileName,
            LIBXML_DTDLOAD|LIBXML_DTDATTR|LIBXML_NOENT|LIBXML_XINCLUDE);
            if ($result) {
                $mxpath = new \DOMXPath($xsd);
                $this->shortNamespaces = array_merge($this->shortNamespaces, $this->getNamespaces($mxpath));
            }
            foreach ($xsd->documentElement->childNodes as $node) {
                if ($node->nodeName == $this->xsdNs.":include") {
                    $loc = realpath($filepath.DIRECTORY_SEPARATOR.$node->getAttribute('schemaLocation'));
                    $node->setAttribute('schemaLocation', $loc);
                    if ($this->debug) {
                        print('Change included schema location to '.$loc." \n");
                    }
                    $newNode = $dom->importNode($node, true);
                    $parent->insertBefore($newNode, $entry);
                } else {
                    if ($namespace != '') {
                        $newNodeNs = $xsd->createAttribute("namespace");
                        $textEl = $xsd->createTextNode($namespace);
                        $newNodeNs->appendChild($textEl);
                        $node->appendChild($newNodeNs);
                    }

                    $newNode = $dom->importNode($node, true);
                    $parent->insertBefore($newNode, $entry);
                }
            }
            $parent->removeChild($entry);
        }

/*         $xpath = new \DOMXPath($dom);
        $query = "//*[local-name()='include' and namespace-uri()='http://www.w3.org/2001/XMLSchema']";
        $includes = $xpath->query($query);
        if ($includes->length != 0) {
            $dom = $this->loadIncludes($dom);
        } */

        if ($this->debug) {
            print_r("\n------------------------------------\n");
        }

        return $dom;
    }

    /**
     * Convert XSD to XML suitable for PHP code generation
     *
     * @return string
     */
    public function getXmlForPhp()
    {
        return $this->xmlForPhp;
    }

    /**
     * @param string $xmlForPhp XML
     *
     * @return void
     */
    public function setXmlForPhp($xmlForPhp)
    {
        $this->xmlForPhp = $xmlForPhp;
    }

    /**
     * Convert XSD to XML suitable for further processing
     *
     * @return string XML string
     *
     * @return DOMDocument
     */
    public function getXML()
    {
        try {
            $xsl    = new \XSLTProcessor();
            $xslDom = new \DOMDocument();
            $xslDom->load(dirname(__FILE__) . "/xsd2php2.xsl");
            $xsl->registerPHPFunctions();
            $xsl->importStyleSheet($xslDom);
            $dom = $xsl->transformToDoc($this->dom);
            $dom->formatOutput = true;

            return $dom;
        } catch (\Exception $e) {
            throw new \Exception(
                    "Error interpreting XSD document (".$e->getMessage().")");
        }
    }

    /**
     * Save PHP files to directory structure
     *
     * @param string  $dir             Directory to save files to
     * @param boolean $createDirectory Create $dir directory if it doesn't exist
     *
     * @throws RuntimeException if given directory does not exist
     *
     * @return void
     */
    private function savePhpFiles($dir, $createDirectory = false)
    {
        if (!file_exists($dir) && $createDirectory === false) {
            throw new \RuntimeException($dir." does not exist");
        }

        if (!file_exists($dir) && $createDirectory === true) {
            //@todo Implement Recursive mkdir
            mkdir($dir, 0777, true);
        }

        $classes = $this->getPHP();

        foreach ($classes as $fullkey => $value) {
            $keys = explode("|", $fullkey);
            $key = $keys[0];
            $namespace = $this->namespaceToPath($keys[1]);
            if ($this->overrideAsSingleNamespace) {
                $targetDir = $dir;
            } else {
                $targetDir = $dir.DIRECTORY_SEPARATOR.$namespace;
            }
            if (!file_exists($targetDir)) {
                mkdir($targetDir, 0777, true);
            }
            file_put_contents($targetDir.DIRECTORY_SEPARATOR.$key.'.php', $value);
        }
        if ($this->debug) {
            echo "Generated classes saved to ".$dir;
        }
    }

    /**
     * Return generated PHP source code. That's where we generate bindings code
     *
     * @return string
     */
    private function getPHP()
    {
        $phpfile = $this->getXmlForPhp();
        if ($phpfile == '' && $this->getXmlSource() == '') {
            throw new \RuntimeException('There is no XML generated');
        }

        $dom = new \DOMDocument();
       // print_r($this->getXmlSource());
        if ($this->getXmlSource() != '') {
            $dom->loadXML($this->getXmlSource(), LIBXML_DTDLOAD | LIBXML_DTDATTR |
            LIBXML_NOENT | LIBXML_XINCLUDE);
        } else {
            $dom->load($phpfile, LIBXML_DTDLOAD | LIBXML_DTDATTR |
            LIBXML_NOENT | LIBXML_XINCLUDE);
        }

        $xPath = new \DOMXPath($dom);

        $classes = $xPath->query('//classes/class');

        $sourceCode = array();
        foreach ($classes as $class) {
            $phpClass = new PHPClass($this->classPrefix);
            $phpClass->overrideAsSingleNamespace = $this->overrideAsSingleNamespace;
            $phpClass->name = $this->classPrefix.$class->getAttribute('name');

            if ($class->getAttribute('type') != '') {
                $phpClass->type = $class->getAttribute('type');
            }

            if ($class->getAttribute('simpleType') != '') {
                $phpClass->type = $class->getAttribute('simpleType');
            }
            if ($class->getAttribute('namespace') != '') {
                $phpClass->namespace = $class->getAttribute('namespace');
            }
            if ($class->getElementsByTagName('const')->length > 0) {
                $array =  array();
                foreach ($class->getElementsByTagName('const') as $item) {
                    $array[] = (string)$item->getAttribute('value');
                }
                $phpClass->constants = $array;
            }

            if ($class->getElementsByTagName('extends')->length > 0) {
                if (!in_array($class->getElementsByTagName('extends')->item(0)->getAttribute('name'), $this->basicTypes)) {
                    $phpClass->extends = $class->getElementsByTagName('extends')->item(0)->getAttribute('name');
                    $phpClass->type    = $class->getElementsByTagName('extends')->item(0)->getAttribute('name');
                    $phpClass->extendsNamespace = $this->namespaceToPhp($class->getElementsByTagName('extends')->item(0)->getAttribute('namespace'));
                }
            }

            $docs = $xPath->query('docs/doc', $class);
            $docBlock = array();
            //if ($phpClass->namespace != $this->xsdNs) {
            if ($this->overrideAsSingleNamespace) {
                $docBlock['xmlNamespace'] = $this->overrideAsSingleNamespace;
            } else {
                $docBlock['xmlNamespace'] = $this->expandNS($phpClass->namespace);
            }
            $docBlock['xmlType']      = $phpClass->type;
            $docBlock['xmlName']      = $phpClass->name;

            if ($this->overrideAsSingleNamespace) {
                $docBlock['var'] = $phpClass->name;
            } elseif ($phpClass->namespace != '') {
                $docBlock['var'] = $this->namespaceToPhp($this->expandNS($phpClass->namespace))."\\".$phpClass->name;
            } else {
                $docBlock['var'] = $phpClass->name;
            }

            foreach ($docs as $doc) {
                if ($doc->nodeValue != '') {
                    $docBlock["xml".$doc->getAttribute('name')] = $doc->nodeValue;
                } elseif ($doc->getAttribute('value') != '') {
                    $docBlock["xml".$doc->getAttribute('name')] = $doc->getAttribute('value');
                }
            }

            $phpClass->classDocBlock = $docBlock;

            $props      = $xPath->query('property', $class);
            $properties = array();
            $i = 0;
            $isArray = false;
            foreach ($props as $prop) {
                $properties[$i]['name'] = $prop->getAttribute('name');
                $docs                   = $xPath->query('docs/doc', $prop);
                foreach ($docs as $doc) {
                    $properties[$i]["docs"][$doc->getAttribute('name')] = $doc->nodeValue;
                }
                if ($prop->getAttribute('xmlType') != '') {
                    $properties[$i]["docs"]['xmlType']      = $prop->getAttribute('xmlType');
                }
                if ($prop->getAttribute('namespace') != '') {
                    $properties[$i]["docs"]['xmlNamespace'] = $this->expandNS($prop->getAttribute('namespace'));
                }
                if ($prop->getAttribute('minOccurs') != '') {
                    $properties[$i]["docs"]['xmlMinOccurs'] = $prop->getAttribute('minOccurs');
                }
                if ($prop->getAttribute('maxOccurs') != '') {
                    $properties[$i]["docs"]['xmlMaxOccurs'] = $prop->getAttribute('maxOccurs');
                    // If maxOccurs > 1, mark type as an array
                    if ($prop->getAttribute('maxOccurs') > 1) {
                        $isArray = $prop->getAttribute('maxOccurs');
                    } elseif ($prop->getAttribute('maxOccurs')=='unbounded') {
                        $isArray = true;
                    }
                }
                if ($prop->getAttribute('name') != '') {
                    $properties[$i]["docs"]['xmlName']      = $prop->getAttribute('name');
                }

                //@todo if $prop->getAttribute('maxOccurs') > 1 - var can be an array - in future special accessor cane be implemented
                if ($prop->getAttribute('type') != '' && $prop->getAttribute('typeNamespace') == '') {
                    /**
                     * In general it's stange to give to Type name's namespace. Reconsider this part
                     */
                    if ($prop->getAttribute('namespace') != '' && $prop->getAttribute('namespace') != $this->xsdNs) {
                        $ns = "";
                        if ($prop->getAttribute('namespace') == "#default#") {
                            $ns = $this->namespaceToPhp($this->targetNamespace);
                        } else {
                            $ns = $this->namespaceToPhp($this->expandNS($prop->getAttribute('namespace')));
                        }
                        $properties[$i]["docs"]['var'] = $ns.'\\'.$this->classPrefix.$prop->getAttribute('type');
                    } else {
                        $properties[$i]["docs"]['var'] = $prop->getAttribute('type');
                    }
                    // Is it unbounded array?
                    if ($isArray === true) {
                        $properties[$i]["docs"]['var'] = $properties[$i]["docs"]['var']."[]";
                        $isArray = false;
                    }
                    // Is it array with defined maximum amount of elements?
                    if ($isArray > 1) {
                        $properties[$i]["docs"]['var'] = $properties[$i]["docs"]['var']."[".$isArray."]";
                        $isArray = false;
                    }
                }

                if ($prop->getAttribute('type') != '' && $prop->getAttribute('typeNamespace') != '') {
                    $ns = "";
                    if ($prop->getAttribute('typeNamespace') == "#default#") {
                        $ns = $this->namespaceToPhp($this->targetNamespace);
                    } else {
                        $ns = $this->namespaceToPhp($this->expandNS($prop->getAttribute('typeNamespace')));
                    }

                    if ($prop->getAttribute('typeNamespace') == $this->xsdNs) {
                        $properties[$i]["docs"]['var'] = $this->normalizeType($prop->getAttribute('type'));
                    } else {
                        $properties[$i]["docs"]['var'] = $ns.'\\'.$this->classPrefix.$prop->getAttribute('type');
                    }
                }

                $i++;
            }

            $phpClass->classProperties = $properties;
            $namespaceClause = '';
            if ((!$this->overrideAsSingleNamespace) && ($docBlock['xmlNamespace'] != '')) {
                $namespaceClause           = "namespace ".$this->namespaceToPhp($docBlock['xmlNamespace']).";\n";
            }

            if ($this->overrideAsSingleNamespace) {
                $namespaceClause = "namespace QuickBooksOnline\API\Data;"."\n";
                $phpClass->namespace = $this->overrideAsSingleNamespace;
                $docBlock['xmlNamespace'] = $this->overrideAsSingleNamespace;
            }

            $sourceCode[$docBlock['xmlName']."|".$phpClass->namespace] = "<?php\n".
            $namespaceClause.
            $phpClass->getPhpCode();
        }
        return $sourceCode;
    }

    /**
     * Resolve short namespace
     * @param string $ns Short namespace
     *
     * @return string
     */
    private function expandNS($ns)
    {
        if ($ns == "#default#") {
            $ns = $this->targetNamespace;
        }
        foreach ($this->shortNamespaces as $shortNs => $longNs) {
            if ($ns == $shortNs) {
                $ns = $longNs;
            }
        }
        return $ns;
    }

    /**
     * Convert XML URI to PHP complient namespace
     *
     * @param string $xmlNS XML URI
     *
     * @return string
     */
    public function namespaceToPhp($xmlNS)
    {
        $ns = $xmlNS;
        $ns = $this->expandNS($ns);
        if (preg_match('/urn:/', $ns)) {
            //@todo check if there are any components of namespace which are
            $ns = preg_replace('/-/', '_', $ns);
            $ns = preg_replace('/urn:/', '', $ns);
            $ns = preg_replace('/:/', '\\', $ns);
        }

        /**
         if (preg_match('/http:\/\//', $ns)) {
         $ns = preg_replace('/http:\/\//', '', $ns);
         $ns = preg_replace('/\//','\\', $ns);
         $ns = preg_replace('/\./', '\\',$ns);
         }*/

        $matches = array();
        if (preg_match("#((http|https|ftp)://(\S*?\.\S*?))(\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)#", $ns, $matches)) {
            $elements = explode("/", $matches[3]);
            $domain = $elements[0];
            array_shift($elements);
            //print_r($domain."\n");
            $ns = implode("\\", array_reverse(explode(".", $domain)));
            //$ns = preg_replace('/\./', '\\', );
            //print $ns."\n";
            foreach ($elements as $key => $value) {
                if ($value != '') {
                    $value = preg_replace('/\./', '_', $value);
                    $ns .= "\\" . $value;
                }
            }
        }

        $ns = explode('\\', $ns);
        $i = 0;
        foreach ($ns as $elem) {
            if (preg_match('/^([0-9]+)(.*)$/', $elem)) {
                $ns[$i] = "_".$elem;
            }

            if (in_array($elem, $this->reservedWords)) {
                $ns[$i] = "_".$elem;
            }
            $i++;
        }

        $ns = implode('\\', $ns);

        return $ns;
    }

    /**
     * Convert XML URI to Path
     * @param string $xmlNS XML URI
     *
     * @return string
     */
    private function namespaceToPath($xmlNS)
    {
        $ns = $xmlNS;
        $ns = $this->expandNS($ns);

        if (preg_match('/urn:/', $ns)) {
            $ns = preg_replace('/-/', '_', $ns);
            $ns = preg_replace('/urn:/', '', $ns);
            $ns = preg_replace('/:/', DIRECTORY_SEPARATOR, $ns);
        }

        $matches = array();
        if (preg_match("#((http|https|ftp)://(\S*?\.\S*?))(\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)#", $ns, $matches)) {
            $elements = explode("/", $matches[3]);
            $domain = $elements[0];
            array_shift($elements);
            //print_r($domain."\n");
            $ns = implode(DIRECTORY_SEPARATOR, array_reverse(explode(".", $domain)));
            //$ns = preg_replace('/\./', '\\', );
            //print $ns."\n";
            foreach ($elements as $key => $value) {
                if ($value != '') {
                    $value = preg_replace('/[\.|-]/', '_', $value);
                    $ns .= DIRECTORY_SEPARATOR . $value;
                }
            }
        }

        $ns = explode(DIRECTORY_SEPARATOR, $ns);
        $i = 0;
        foreach ($ns as $elem) {
            if (preg_match('/^([0-9]+)(.*)$/', $elem)) {
                $ns[$i] = "_".$elem;
            }
            if (in_array($elem, $this->reservedWords)) {
                $ns[$i] = "_".$elem;
            }
            $i++;
        }
        $ns = implode(DIRECTORY_SEPARATOR, $ns);
        return $ns;
    }
}


<?php
/*******************************************************************************
 * Copyright (c) 2017 Intuit
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
namespace QuickBooksOnline\API\Core\OAuth\OAuth2;

use QuickBooksOnline\API\Exception\SdkException;
use QuickBooksOnline\API\Core\CoreConstants;

/**
 * Class OAuth2AccessToken
 *
 * A helper class to store the OAuth 2 Token related information
 *
 * @package QuickBooksOnline
 *
 */
class OAuth2AccessToken{

   /**
    * OAuth 2 Access Token key. Final product generated from OAuth 2 protocol.
    * @var String $accessTokenKey         A long String for representing access token key
    */
    private $accessTokenKey;

    /**
     * OAuth 2 Access Token Type. Always be bearer
     * @var String $tokenType             String Token Type
     */
    private $tokenType;

    /**
     * OAuth 2 Refresh Token key. A token used to generate new Access Token
     * @var String $refresh_token         A String for representing refresh token key
     */
    private $refresh_token;

    /**
     * OAuth 2 access Token key expire time. It is a date representation. Use current time plus one hour
     * @var String $accessTokenExpiresAt         A String representation of OAuth 2 access token expiration time
     */
    private $accessTokenExpiresAt;

    /**
     * OAuth 2 Refresh Token key expire time. It is a date representation. Use current time plus 101 days
     * @var String $refreshTokenExpiresAt         A String representation of OAuth 2 refresh token expiration time
     */
    private $refreshTokenExpiresAt;

    /**
     * OAuth 2 access Token key expire time in seconds. Always 3600 seconds.
     * @var String $accessTokenValidationPeriod          OAuth 2 access token validation time
     */
    private $accessTokenValidationPeriod;

    /**
     * OAuth 2 refresh Token key expire time in seconds. Always 8726400 seconds.
     * NOTE Since QBO OAuth 2 Access Token is only valid for 1 hour. You always need to use refresh token to make a refresh token API call to get a new Access Token. However,
     *      every 24 hours the refresh token returned from QBO will be changed. In practice, the Refresh Token is only valid for 24 hours. Please ALWAYS RECORD THE LATEST REFRESH TOKEN
     *      RETURN FROM LAST REFRESH TOKEN API CALL.
     * @var String $refreshTokenValidationPeriod         OAuth 2 refresh token validation time
     */
    private $refreshTokenValidationPeriod;

    /**
     * OAuth 2 Client ID, can be found on your apps "keys" tab.
     * @var String $clientID         the Client ID asscoiated with the app
     */
    private $clientID;

    /**
     * OAuth 2 Client Secret, can be found on your apps "keys" tab.
     * @var String $clientSecret     the Client ID asscoiated with the app
     */
    private $clientSecret;

    /**
     * The company ID associated with the OAuth 2 token.
     * @var String $realmID          A value will returned during OAuth 2 access token call
     */
    private $realmID;

    /**
     * The URL that is going be used for this Access Token
     * @var String $baseURL         The BaseURL for this Access Token
     */
    private $baseURL;

    /**
     * The constructor for passing the OAuth 2 Token related information
     * @param String $cID                      The Client ID
     * @param String $cS                       The Client Secret
     * @param String $atk                      The OAuth 2 Access Token generated from Client ID, Client Secret.
     * @param String $refreshtk                The OAuth 2 Refresh Token generated from Client ID, Client Secret.
     * @param int    $atei                     The number of seconds that access token expired. Always 3600 seconds.
     * @param int    $refreshtei               The number of seconds that refresh token expired. Always 8726400 seconds.
     * @param String $tk                       The token type. Always bearer, unless specified.
     */
    public function __construct($cID, $cS, $atk = null, $refreshtk = null, $atei = null, $refreshtei = null, $tk = "bearer"){
        $this->clientID = $cID;
        $this->clientSecret = $cS;
        $this->accessTokenKey = $atk;
        $this->refresh_token = $refreshtk;
        $this->accessTokenExpiresAt = $atei;
        $this->refreshTokenExpiresAt = $refreshtei;
        $this->tokenType = $tk;
    }

    /**
     * Set the OAuth 2 Access Token.
     * @param String $accessToken      The access token.
     */
    public function setAccessToken($accessToken){
       $this->accessTokenKey = $accessToken;
    }

    /**
     * Set the OAuth 2 refresh Token.
     * @param String $refreshToken      The refresh token.
     */
    public function setRefreshToken($refreshToken){
      $this->refresh_token = $refreshToken;
    }

    /**
     * Set the realmID associated with this Acess Token
     * @param string $realmID            The realmID for the access token
     */
    public function setRealmID($realmID){
      $this->realmID = $realmID;
    }

    /**
     * Set the baseURL associated with this Acess Token
     * @param string $baseUrl            The base URL
     */
    public function setBaseURL($baseURL){
      if(strcasecmp($baseURL, CoreConstants::DEVELOPMENT_SANDBOX) == 0){
         $this->baseURL = CoreConstants::SANDBOX_DEVELOPMENT;
      }else if(strcasecmp($baseURL, CoreConstants::PRODUCTION_QBO) == 0){
         $this->baseURL = CoreConstants::QBO_BASEURL;
      }else{
         $this->baseURL = $baseURL;
      }

    }

    /**
     * Set the number of seconds for access token.
     * The implementation of Quickbooks Online OAuth 2 is short access token, short refresh token. So this method is not useful.
     * @param int $seconds             The duration period of Access token
     */
    public function setAccessTokenValidationPeriodInSeconds($seconds){
      $this->accessTokenValidationPeriod = $seconds;
    }

    /**
     * Set the number of seconds for refresh token.
     * The implementation of Quickbooks Online OAuth 2 is short access token, short refresh token. So this method is not useful.
     * @param int $seconds             The duration period of refresh token
     */
    public function setRefreshTokenValidationPeriodInSeconds($seconds){
      $this->refreshTokenValidationPeriod = $seconds;
    }

    /**
     * Set the expiration date of access token.
     * The implementation of Quickbooks Online OAuth 2 is short access token, short refresh token. So this method is not useful.
     * @param string $date            The acutal expiration date of Access Token.
     */
    public function setAccessTokenExpiresAt($date){
      $this->accessTokenExpiresAt = $date;
    }

    /**
     * Set the expiration date of refresh token.
     * The implementation of Quickbooks Online OAuth 2 is short access token, short refresh token. So this method is not useful.
     * @param string $date            The acutal expiration date of refresh Token.
     */
    public function setRefreshTokenExpiresAt($date){
      $this->refreshTokenExpiresAt = $date;
    }

    /**
     * Check if the access token is set.
     * @return bool
     */
    public function isAccessTokenSet(){
       if(isset($this->accessTokenKey) && !empty($this->accessTokenKey)){
          return true;
       }else{
          return false;
       }
    }

    /**
     * Check if the refresh token is set.
     * @return bool
     */
    public function isFreshTokenSet(){
      if(isset($this->refresh_token) && !empty($this->refresh_token)){
         return true;
      }else{
         return false;
      }
    }

    /**
     * Return the number of seconds for refresh token
     * The implementation of Quickbooks Online OAuth 2 always return an 8726400 seconds refresh token. This method is not useful now
     * @return int
     */
    public function getRefreshTokenValidationPeriodInSeconds(){
      if(isset($this->refreshTokenValidationPeriod) && !empty($this->refreshTokenValidationPeriod))
      {
          return $this->refreshTokenValidationPeriod;
      }else{
        throw new SdkException("The validation period for OAuth 2 refresh Token is not set.");
      }
    }

    /**
     * Return the number of seconds for access token
     * The implementation of Quickbooks Online OAuth 2 always return an one hour access token. This method is not useful
     * @return int
     */
    public function getAccessTokenValidationPeriodInSeconds(){
      if(isset($this->accessTokenValidationPeriod) && !empty($this->accessTokenValidationPeriod))
      {
          return $this->accessTokenValidationPeriod;
      }else{
        throw new SdkException("The validation period for OAuth 2 access Token is not set.");
      }
    }

    /**
     * Return the expiration date of Access Token
     * @return Date
     */
    public function getAccessTokenExpiresAt(){
        if(!empty($this->accessTokenExpiresAt))
        {
          return $this->getDateFromSeconds($this->accessTokenExpiresAt);
        }else{
          throw new SdkException("The Expiration Time for OAuth 2 Access Token is not set.");
        }
    }

    /**
     * Return the expiration date of refresh Token
     * @return int
     */
    public function getRefreshTokenExpiresAt(){
      if(!empty($this->refreshTokenExpiresAt))
      {
        return $this->getDateFromSeconds($this->refreshTokenExpiresAt);
      }else{
        throw new SdkException("The Expiration Time for OAuth 2 refresh Token is not set.");
      }
    }

    /**
     * Return the refresh Token
     * @return String
     */
    public function getRefreshToken(){
        if(isset($this->refresh_token) && !empty($this->refresh_token))  return $this->refresh_token;
        else throw new SdkException("The OAuth 2 Refresh Token is not set in the Access Token Object.");
    }

    /**
     * Return the access Token
     * @return String
     */
    public function getAccessToken(){
      if(isset($this->accessTokenKey) && !empty($this->accessTokenKey))  return $this->accessTokenKey;
      else throw new SdkException("The OAuth 2 Access Token is not set in the Access Token Object.");
    }

    /**
     * Return the Client ID
     * @return String
     */
    public function getClientID(){
        if(isset($this->clientID) && !empty($this->clientID)){
            return $this->clientID;
        }else{
          throw new SdkException("Can't get OAuth 2 Client ID from Access Token Object. It is not set.");
        }
    }

    /**
     * Return the Client Secret
     * @return String
     */
    public function getClientSecret(){
        if(isset($this->clientSecret) && !empty($this->clientSecret)){
            return $this->clientSecret;
        }else{
          throw new SdkException("Can't get OAuth 2 Client Secret from Access Token Object. It is not set.");
        }
    }

    /**
     * Return the Company ID.
     * @return String
     */
    public function getRealmID(){
        if(isset($this->realmID) && !empty($this->realmID)){
            return $this->realmID;
        }else{
            return "";
        }
    }

    /**
     * Return the BaseURL for this access token generated for.
     * @return String
     */
    public function getBaseURL(){
        if(isset($this->baseURL) && !empty($this->baseURL)){
            return $this->baseURL;
        }else{
            return "";
        }
    }

    /**
     * Update an access token after creating a new access token or making a refresh tokan API call
     * @param int      $tokenExpiresTime          The number of seconds that access token expired. Always 3600 seconds.
     * @param String   $refreshToken              The OAuth 2 Refresh Token returned from refresh token API call or generated from a new request.
     * @param int      $refreshTokenExpiresTime   The number of seconds that refresh token expired. Always 8726400 seconds.
     * @param String   $accessToken               The OAuth 2 Access Token returned from refresh token API call or generated from a new request.
     */
    public function updateAccessToken($tokenExpiresTime, $refreshToken, $refreshTokenExpiresTime, $accessToken){
       $this->setAccessToken($accessToken);
       $this->setRefreshToken($refreshToken);
       $this->setAccessTokenValidationPeriodInSeconds($tokenExpiresTime);
       $this->setRefreshTokenValidationPeriodInSeconds($refreshTokenExpiresTime);
       $this->setAccessTokenExpiresAt(time() + $tokenExpiresTime);
       $this->setRefreshTokenExpiresAt(time() + $refreshTokenExpiresTime);
    }

    /**
     * A helper function to convert Seconds to date
     * @param  int  $second
     * @return string
     */
    private function getDateFromSeconds($seconds){
      return date('Y/m/d H:i:s', $seconds);
    }
}


<?php
namespace QuickBooksOnline\API\Facades;

class CreditMemo{

    public static function create(array $data, $throwException = TRUE){
        if(!isset($data) || empty($data)) throw new \Exception("Passed array for creating CreditMemo is Empty");
        $CreditMemoObject = FacadeHelper::reflectArrayToObject("CreditMemo", $data, $throwException );
        return $CreditMemoObject;
    }

    /**
     * This is an immutable function
     */
    public static function update($objToUpdate, array $data){
        $classOfObj = get_class($objToUpdate);
        if(strcmp($classOfObj, FacadeHelper::simpleAppendClassNameSpace("CreditMemo")) != 0){
            throw new \Exception("Target object class:{" .  $classOfObj . "} is not an instance of CreditMemo.");
        }
        $newCreditMemoObj = CreditMemo::create($data);
        $clonedOfObj = FacadeHelper::cloneObj($objToUpdate);
        FacadeHelper::mergeObj($clonedOfObj, $newCreditMemoObj);
        return $clonedOfObj;
    }

}


<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Soap
 * @subpackage Wsdl
 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: ArrayOfTypeComplex.php 20784 2010-01-31 08:22:51Z yoshida@zend.co.jp $
 */

/**
 * @see Zend_Soap_Wsdl_Strategy_DefaultComplexType
 */
require_once "Zend/Soap/Wsdl/Strategy/DefaultComplexType.php";

/**
 * Zend_Soap_Wsdl_Strategy_ArrayOfTypeComplex
 *
 * @category   Zend
 * @package    Zend_Soap
 * @subpackage Wsdl
 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Soap_Wsdl_Strategy_ArrayOfTypeComplex extends Zend_Soap_Wsdl_Strategy_DefaultComplexType
{
    protected $_inProcess = array();

    /**
     * Add an ArrayOfType based on the xsd:complexType syntax if type[] is detected in return value doc comment.
     *
     * @param string $type
     * @return string tns:xsd-type
     */
    public function addComplexType($type)
    {
        if (in_array($type, $this->_inProcess)) {
            require_once "Zend/Soap/Wsdl/Exception.php";
            throw new Zend_Soap_Wsdl_Exception("Infinite recursion, cannot nest '".$type."' into itself.");
        }
        $this->_inProcess[$type] = $type;

        $nestingLevel = $this->_getNestedCount($type);

        if ($nestingLevel > 1) {
            require_once "Zend/Soap/Wsdl/Exception.php";
            throw new Zend_Soap_Wsdl_Exception(
                "ArrayOfTypeComplex cannot return nested ArrayOfObject deeper than ".
                "one level. Use array object properties to return deep nested data.
            ");
        }

        $singularType = $this->_getSingularPhpType($type);

        if (!class_exists($singularType)) {
            require_once "Zend/Soap/Wsdl/Exception.php";
            throw new Zend_Soap_Wsdl_Exception(sprintf(
                "Cannot add a complex type %s that is not an object or where ".
                "class could not be found in 'DefaultComplexType' strategy.", $type
            ));
        }

        if ($nestingLevel == 1) {
            // The following blocks define the Array of Object structure
            $xsdComplexTypeName = $this->_addArrayOfComplexType($singularType, $type);
        } else {
            $xsdComplexTypeName = $singularType;
        }

        // The array for the objects has been created, now build the object definition:
        if (!in_array($singularType, $this->getContext()->getTypes())) {
            parent::addComplexType($singularType);
        }

        unset($this->_inProcess[$type]);
        return "tns:".$xsdComplexTypeName;
    }

    protected function _addArrayOfComplexType($singularType, $type)
    {
        $dom = $this->getContext()->toDomDocument();

        $xsdComplexTypeName = $this->_getXsdComplexTypeName($singularType);

        if (!in_array($xsdComplexTypeName, $this->getContext()->getTypes())) {
            $complexType = $dom->createElement('xsd:complexType');
            $complexType->setAttribute('name', $xsdComplexTypeName);

            $complexContent = $dom->createElement("xsd:complexContent");
            $complexType->appendChild($complexContent);

            $xsdRestriction = $dom->createElement("xsd:restriction");
            $xsdRestriction->setAttribute('base', 'soap-enc:Array');
            $complexContent->appendChild($xsdRestriction);

            $xsdAttribute = $dom->createElement("xsd:attribute");
            $xsdAttribute->setAttribute("ref", "soap-enc:arrayType");
            $xsdAttribute->setAttribute("wsdl:arrayType", sprintf("tns:%s[]", $singularType));
            $xsdRestriction->appendChild($xsdAttribute);

            $this->getContext()->getSchema()->appendChild($complexType);
            $this->getContext()->addType($xsdComplexTypeName);
        }

        return $xsdComplexTypeName;
    }

    protected function _getXsdComplexTypeName($type)
    {
        return sprintf('ArrayOf%s', $type);
    }

    /**
     * From a nested definition with type[], get the singular PHP Type
     *
     * @param  string $type
     * @return string
     */
    protected function _getSingularPhpType($type)
    {
        return str_replace("[]", "", $type);
    }

    /**
     * Return the array nesting level based on the type name
     *
     * @param  string $type
     * @return integer
     */
    protected function _getNestedCount($type)
    {
        return substr_count($type, "[]");
    }
}


<?php
namespace QuickBooksOnline\API\Facades;

class Line{

    public static function create(array $data, $throwException = TRUE){
        if(!isset($data) || empty($data)) throw new \Exception("Passed array for creating Line is Empty");
        $LineObject = FacadeHelper::reflectArrayToObject("Line", $data, $throwException );
        return $LineObject;
    }

    /**
     * This is an immutable function
     */
    public static function update($objToUpdate, array $data){
        $classOfObj = get_class($objToUpdate);
        if(strcmp($classOfObj, FacadeHelper::simpleAppendClassNameSpace("Line")) != 0){
            throw new \Exception("Target object class:{" .  $classOfObj . "} is not an instance of Line.");
        }
        $newLineObj = Line::create($data);
        $clonedOfObj = FacadeHelper::cloneObj($objToUpdate);
        FacadeHelper::mergeObj($clonedOfObj, $newLineObj);
        return $clonedOfObj;
    }

}


<?php

namespace QuickBooksOnline\API\Core\Http\Serialization;

use QuickBooksOnline\API\Core\CoreConstants;
use QuickBooksOnline\API\Data\IPPIntuitEntity;
use QuickBooksOnline\API\XSD2PHP\src\com\mikebevz\xsd2php\Php2Xml;
use QuickBooksOnline\API\XSD2PHP\src\com\mikebevz\xsd2php\Bind;
use QuickBooksOnline\API\Diagnostics\Logger;

/**
 * Xml Serialize(r) to serialize and de serialize.
 */
class XmlObjectSerializer extends IEntitySerializer
{

    /**
     * IDS Logger
     * @var Logger
     */
    public $IDSLogger;

  /**
   * Keeps last used object name
   * @var String
  */
  private $resourceURL =  null;

    /**
     * Marshall a POPO object to XML, presumably for inclusion on an IPP v3 API call
     *
     * @param mixed POPOObject $phpObj inbound POPO object
     * @return string XML output derived from POPO object
     */
    private static function getXmlFromObj($phpObj)
    {
        if (!$phpObj) {
            $this->IDSLogger->CustomLogger->Log(TraceLevel::Error, "Encountered an error parsing the xmlFromObj.");
            $this->IDSLogger->CustomLogger->Log(TraceLevel::Error, "Stack Trace: " . implode("\n", debug_backtrace()));
            return false;
        }

        $php2xml = new Php2Xml(CoreConstants::PHP_CLASS_PREFIX);
        $php2xml->overrideAsSingleNamespace='http://schema.intuit.com/finance/v3';

        try {
            return $php2xml->getXml($phpObj);
        } catch (\Exception $e) {
            $this->IDSLogger->CustomLogger->Log(TraceLevel::Error, "Encountered an error parsing the batch response." . $e->getMessage());
            $this->IDSLogger->CustomLogger->Log(TraceLevel::Error, "Object: " . var_export($phpObj, true));
            $this->IDSLogger->CustomLogger->Log(TraceLevel::Error, "Stack Trace: " . $e->getTraceAsString());
            return false;
        }
    }

    /**
     * Marshall a POPO object to be XML
     *
     * @param IPPIntuitEntity $entity The POPO object
     * @param string $urlResource the type of the POPO object
     * @return string the XML of the POPO object
     */
    public static function getPostXmlFromArbitraryEntity($entity, &$urlResource)
    {
        if (null==$entity) {
            return false;
        }

        $xmlElementName = XmlObjectSerializer::cleanPhpClassNameToIntuitEntityName(get_class($entity));
        $xmlElementName = trim($xmlElementName);
        $urlResource = strtolower($xmlElementName);
        $httpsPostBody = XmlObjectSerializer::getXmlFromObj($entity);
        return $httpsPostBody;
    }

    /**
     * Unmarshall XML into a POPO object, presumably the XML came from an IPP v3 API call
     *
     * @param string XML that conforms to IPP v3 XSDs
     * @return object POPOObject $phpObj resulting POPO object
     */
    private static function PhpObjFromXml($className, $xmlStr)
    {
        $className = trim($className);
        if (class_exists($className, CoreConstants::USE_AUTOLOADER)) {
            $phpObj = new $className;
        } elseif (class_exists(CoreConstants::NAMEPSACE_DATA_PREFIX . $className, CoreConstants::USE_AUTOLOADER)) {
            $className = CoreConstants::NAMEPSACE_DATA_PREFIX . $className;
            $phpObj = new $className;
        } else {
            throw new \Exception("Can't find corresponding CLASS for className" . $className . "during unmarshall XML into POPO Object");
        }
        $bind = new Bind(CoreConstants::PHP_CLASS_PREFIX);
        $bind->overrideAsSingleNamespace='http://schema.intuit.com/finance/v3';
        $bind->bindXml($xmlStr, $phpObj);
        return $phpObj;
    }

    /**
     * Parse an XML string into an array of IPPIntuitEntity objects
     *
     * @param string $responseXml XML string to parse
     * @param bool $bLimitToOne Signals to only parse the first element
     * @return array of IPPIntuitEntity objects
     */
    private static function ParseArbitraryResultObjects($responseXml, $bLimitToOne)
    {
        if (!$responseXml) {
            return null;
        }

        $resultObject = null;
        $resultObjects = null;

        $responseXmlObj = simplexml_load_string($responseXml);
        foreach ($responseXmlObj as $oneXmlObj) {
            $oneXmlElementName = (string)$oneXmlObj->getName();

            //The handling falut here is a little too simple. add more support for future
            //@hao
            if ('Fault'==$oneXmlElementName) {
                return null;
            }
            $phpClassName = XmlObjectSerializer::decorateIntuitEntityToPhpClassName($oneXmlElementName);
            $onePhpObj = XmlObjectSerializer::PhpObjFromXml($phpClassName, $oneXmlObj->asXML());
            $resultObject = $onePhpObj;
            $resultObjects[] = $onePhpObj;

            // Caller may be anticipating ONLY one object in result
            if ($bLimitToOne) {
                break;
            }
        }

        if ($bLimitToOne) {
            return $resultObject;
        } else {
            return $resultObjects;
        }
    }

    /**
     * Decorate an IPP v3 Entity name (like 'Class') to be a POPO class name (like 'IPPClass')
     *
     * @param string Intuit Entity name
     * @return string POPO class name
     */
    private static function decorateIntuitEntityToPhpClassName($intuitEntityName)
    {
        $intuitEntityName = trim($intuitEntityName);
        return CoreConstants::PHP_CLASS_PREFIX . $intuitEntityName;
    }

    /**
     * Clean a POPO class name (like 'IPPClass') to be an IPP v3 Entity name (like 'Class')
     *
     * @param string $phpClassName POPO class name
     * @return string Intuit Entity name
     */
    public static function cleanPhpClassNameToIntuitEntityName($phpClassName)
    {
        $phpClassName = trim($phpClassName);
        //if the className has delimiters, get the last part
        $separetes = explode('\\', $phpClassName);
        $phpClassName = end($separetes);
        if (0==strpos($phpClassName, CoreConstants::PHP_CLASS_PREFIX)) {
            return substr($phpClassName, strlen(CoreConstants::PHP_CLASS_PREFIX));
        }

        return null;
    }

    /**
     * Initializes a new instance of the XmlObjectSerializer class.
     * @param Logger idsLogger The ids logger.
     */
    public function __construct($idsLogger = null)
    {
        if ($idsLogger) {
            $this->IDSLogger = $idsLogger;
        } else {
            $this->IDSLogger = null;
        } // new TraceLogger();
    }

    /**
     * Serializes the specified entity and updates last used entity name @see resourceURL
     * @param object entity The entity.
     * @return string Returns the serialize entity in string format.
     */
    public function Serialize($entity)
    {
        $this->resetResourceURL();
        return XmlObjectSerializer::getPostXmlFromArbitraryEntity($entity, $this->resourceURL);
    }

        /**
         * Reset value for resourceURL to null
         *
         */
        public function resetResourceURL()
        {
            $this->resourceURL = null;
        }

        /**
         * Returns last used resource URL (which entity name)
         * @return string
         */
        public function getResourceURL()
        {
            return $this->resourceURL;
        }

        /**
     * DeSerializes the specified action entity type.
     * @param message The type to be  serialize to
     * @param bLimitToOne Limit to parsing just one response element
     * @return object Returns the de serialized object.
     */
    public function Deserialize($message, $bLimitToOne = false)
    {
        if (!$message) {
            return null;
        }

        $resultObject = null;
        $resultObjects = null;

        $responseXmlObj = simplexml_load_string($message);

        //handle count(*) case, for example Select count(*) from Invoice, and also handle the CDC case
        if(isset($responseXmlObj->attributes()['totalCount']) && !isset($responseXmlObj->attributes()['startPosition']) && !isset($responseXmlObj->attributes()['maxResults'])){
            return (int) $responseXmlObj->attributes()['totalCount'];
        }

        foreach ($responseXmlObj as $oneXmlObj) {
            $oneXmlElementName = (string)$oneXmlObj->getName();

            if ('Fault'==$oneXmlElementName) {
                return null;
            }

            $phpClassName = XmlObjectSerializer::decorateIntuitEntityToPhpClassName($oneXmlElementName);
            $onePhpObj = XmlObjectSerializer::PhpObjFromXml($phpClassName, $oneXmlObj->asXML());
            $resultObject = $onePhpObj;
            $resultObjects[] = $onePhpObj;

            // Caller may be anticipating ONLY one object in result
            if ($bLimitToOne) {
                break;
            }
        }

        if ($bLimitToOne) {
            return $resultObject;
        } else {
            return $resultObjects;
        }
    }
}


<?php
namespace QuickBooksOnline\API\Core\HttpClients;

use QuickBooksOnline\API\Exception\SdkException;

/**
 * Class BaseCurl
 *
 * A Http Client using PHP cURL extension to send HTTP/HTTPS request to QuickBooks Online
 * @package QuickBooksOnline
 *
 */
class BaseCurl{

  /* The cURL extension of PHP */
  private $curl = null;

  /* constructor of BaseCurl */
  public function __construct(){
    $this->init();
  }

  /**
   * Intialize a new PHP cURL extension instance for handling HTTP/HTTPS request
   */
  public function init(){
    if (!extension_loaded('curl')) {
        throw new SdkException('The PHP exention curl must be installed to use this PDK.');
    }
    $this->curl = curl_init();
  }

  /**
   * Check if cURL instance is set or Not
   * @return True or False
   */
  public function isCurlSet(){
      if(!isset($this->curl) || $this->curl === 0) return false;
      return true;
  }

  /**
   * set options for curl
   * @param String $k The key
   * @param String $v The value
   */
  public function setupOption($k, $v){
      if($this->isCurlSet()){
        curl_setopt($this->curl, $k, $v);
      } else {
        throw new SdkException("cURL instance is not set when calling setup Option.");
      }
  }

  /**
   * set options for curl by passing an array
   * @param array $k The options set for the curl
   */
  public function setupCurlOptArray(array $ary){
    if($this->isCurlSet()){
      curl_setopt_array($this->curl, $ary);
    } else {
      throw new SdkException("cURL instance is not set when calling setup curl Option from array.");
    }
  }

  /**
   * Send the request
   * @return mixed TRUE on success or FALSE on failure. However, if the CURLOPT_RETURNTRANSFER
   */
  public function execute(){
    if($this->isCurlSet()){
      return curl_exec($this->curl);
    } else {
      throw new SdkException("cURL instance is not set when trying to execute it.");
    }
  }

  /**
   * retrun the error number
   * @return int Error Number during sending cURL request
   */
  public function errno(){
    if($this->isCurlSet()){
        return curl_errno($this->curl);
    } else {
      throw new SdkException("cURL instance is not set when trying to get errno code from the execution.");
    }
  }

  /**
   * retrun the error message
   * @return string Error nessage during sending cURL request
   */
  public function error(){
    if($this->isCurlSet()){
        return curl_error($this->curl);
    } else {
      throw new SdkException("cURL instance is not set when trying to get error from the execution.");
    }
  }

  /**
   * Set the curl TLS Version and check if TLS 1.2 is supported
   * @return int TLSversion
   */
  public function versionOfTLS(){
      $tlsVersion = "";
      $curl = curl_init();
      curl_setopt($curl, CURLOPT_URL, "https://www.howsmyssl.com/a/check");
      curl_setopt($curl, CURLOPT_SSLVERSION, 6);
      curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
      $curlVersionResponse = curl_exec($curl);
      if($curlVersionResponse === false){
         throw new SdkException("Error in checking cURL version for TLS 1.2. Error Num:[" . curl_errno($curl) . "] Error message:[" . curl_error($curl) . "]");
      }else{
         $tlsVersion = json_decode($curlVersionResponse)->tls_version;
      }
      curl_close($curl);
      return $tlsVersion;
  }

  /**
   * Get info from a curl reference from the type
   * Mainly used after execute()
   * use $type=CURLINFO_HEADER_OUT for header, and $type=CURLINFO_HTTP_CODE for http_code
   * @param String The information you want to retrieve
   * @return mixed
   */
  public function getInfo($type)
  {
    if($this->isCurlSet()){
      return curl_getinfo($this->curl, $type);
    }else {
      throw new SdkException("cURL instance is not set when trying to get info from the type:" . $type);
    }
  }

  /**
   * Close the resource connection to curl
   * Remove the pointer after closed
  */
  public function close()
  {
      curl_close($this->curl);
      $this->curl = null;
  }
}


<?php

namespace QuickBooksOnline\API\Exception;

/**
 * Manages all the exceptions thrown to the user.
 */
class IdsExceptionManager
{
    /**
     * Handles exception thrown to the user.
     * @param string errorMessage Error Message
     */
    public static function HandleExceptionMessage($errorMessage)
    {
        throw new IdsException($errorMessage);
    }

    /**
     * Handles Exception thrown to the user.
     * @param IdsException idsException Ids Exception
     */
    public static function HandleExceptionObject($idsException)
    {
        throw $idsException;
    }

    /**
     * Handles Exception thrown to the user.
     * @param string errorMessage Error Message
     * @param string errorCode Error Code.
     * @param string source Source of the exception.
     * @param IdsException innerException Ids Exception
     */
    public static function HandleException($errorMessage=null, $errorCode=null, $source=null, $innerException=null)
    {
        $message = implode(", ", array($errorMessage, $errorCode, $source));
        throw new IdsException($message);
    }
}


<?php
namespace QuickBooksOnline\API\Utility;

use ReflectionProperty;
use RuntimeException;
use UnexpectedValueException;
use InvalidArgumentException;
use QuickBooksOnline\API\Core\CoreConstants;
use QuickBooksOnline\API\Core\Http\Serialization\ObjectEntity;
use QuickBooksOnline\API\Core\Http\Serialization\SimpleEntity;
use QuickBooksOnline\API\Core\Http\Serialization\UnknownEntity;
use QuickBooksOnline\API\Core\Http\Serialization\AbstractEntity;

/**
 * Extracts metadata for properties and decides which type is associated with this property
 *
 * @author amatiushkin
 */
class MetadataExtractor
{
    const REGULAR_GET_VAR = "/@var\s+(.*)/";

    public function processComments(array $properties)
    {
        $result = array();
        foreach ($properties as $key=>$value) {
            // skip none-properties
            if (!$value instanceof ReflectionProperty) {
                continue;
            }
            // extract content of @var ...
            // use it for object mapping
            $varCommentValue = $this->extractVarValueFromComment($value->getDocComment());
            if (is_null($varCommentValue)) {
                continue;
            }
            $entity = $this->verifyVariableType($varCommentValue);
            $this->completeProperty($entity, $value);
            $result[$key] = $entity;
        }
        return $result;
    }

    /**
     * Returns value of @var from text. It also returns last part (class name) of path-like value
     *
     * TODO Move this function into separate object outside of this class.
     * Domain Entity builder doesn't care about how values are parsed.
     * It's better to apply dependecy-injection pattern here.
     *
     * The only reason this function is here is for initial implementation.
     *
     * @param string $text
     */
    private function extractVarValueFromComment($text)
    {
        $matches = array();
        $result = preg_match_all(self::REGULAR_GET_VAR, $text, $matches);
         //handle errors
         if (false === $result) {
             // get one of PREG_INTERNAL_ERROR, PREG_BACKTRACK_LIMIT_ERROR etc
            $constants = get_defined_constants(true);
             $text = array_search(preg_last_error(), $constants['pcre']);
             throw new RuntimeException("Regular expression failed on $text with error code: "
                    . (false === $text ? preg_last_error() : $text)); //push text or error code
         }
         //handle multiple entries in a hard way
         if ($result > 1) {
             throw new UnexpectedValueException("Following comment: $text \ncontains more than one @var definition, which is unexpected");
         }

         // no result, stop here if no group match
         if (empty($result) || empty($matches[1]) || empty($matches[1][0])) {
             return null;
         }

         // return first result (0-index) of the first group match
         return $matches[1][0];
    }

    /**
     *  Verifies type and creates entity object
     * @param string $value
     * @return AbstractEntity
     */
    private function verifyVariableType($value)
    {
        // if value can be mapped to simple type
       if (in_array(strtolower($value), array("string","float","double","boolean", "integer"))) {
           return new SimpleEntity(strtolower($value));
       }

       // generate names
       // try it
       foreach ($this->generateObjectNames($value) as $name) {
           $name = $this->addNameSpaceToPotentialClassName($name);
           if (class_exists($name)) {
               return new ObjectEntity($name);
           }
       }

        return new UnknownEntity($value);
    }

    private function addNameSpaceToPotentialClassName($name)
    {
        $name = trim($name);
        $lists = explode('\\', $name);
        $ippEntityName = end($lists);
        $ippEntityName = CoreConstants::NAMEPSACE_DATA_PREFIX . $ippEntityName;
        return $ippEntityName;
    }

    /**
     * Returns order list of possible valid names of object type
     * @param string $value
     * @return array
     */
    private function generateObjectNames($value)
    {
        $reversiveStack = array();
        $reversiveStack[] = $value; // add original value. It will be called last
        $reversiveStack[] = $this->removeArrayBrackets($value); //
        $reversiveStack[] = $this->getIntuitName($this->removeArrayBrackets($value));
        $reversiveStack[] = $this->getIntuitName($this->removeArrayBrackets($this->getClassNameFromPackagePath($value)));
        $reversiveStack[] = $this->removeArrayBrackets($this->getClassNameFromPackagePath($value));
        $reversiveStack[] = $this->getClassNameFromPackagePath($value);
        return array_reverse($reversiveStack);
    }

    /**
     * Removes brackets from comments. In other words
     * it cleans up collection-like definitions (e.g. MyCollectionOfType[])
     * @param string $string
     * @return string
     */
    private function removeArrayBrackets($string)
    {
        return str_replace(array('[',']'), array('',''), $string);
    }

    /**
     * Returns last part from package-like names (e.g. my\package\MyClass).
     * Expected result is MyClass
     *
     * @param string $string
     * @return string
     */
    private function getClassNameFromPackagePath($string)
    {
        $array = explode('\\', $string);
        if (is_array($array) && count($array)) {
            return array_pop($array);
        }
        return '';
    }

    /**
     * Returns class name with prefix
     * TODO Move it out from this object and make this class generic
     * @param string $string
     * @return string
     */
    private function getIntuitName($string)
    {
        $string = trim($string);
        return $string;
    }

    /**
     * Finilizes created metadata description with few more info
     * @param AbstractEntity $a
     * @param ReflectionProperty $p
     * @throws InvalidArgumentException
     */
    private function completeProperty($a, ReflectionProperty $p)
    {
        if (!$a instanceof AbstractEntity) {
            throw new InvalidArgumentException("Expected instance of AbstractEntity here");
        }
        $a->setName($p->getName());
        $a->setClass($p->getDeclaringClass());
    }
}