<?php
namespace DTS\eBaySDK\Services;

use DTS\eBaySDK\Parser\XmlParser;
use DTS\eBaySDK\ConfigurationResolver;
use DTS\eBaySDK\Credentials\CredentialsProvider;
use \DTS\eBaySDK as Functions;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\ResponseInterface;

/**
 * The base class for every eBay service class.
 */
abstract class BaseService
{
    /**
     * Helper constent when build requests that contain attachments.
     */
    const CRLF = "\r\n";

    /**
     * HTTP header constant. Tells the server the encoding in which the client desires the response.
     */
    const HDR_RESPONSE_ENCODING = 'Accept-Encoding';

    /**
     * @var \DTS\eBaySDK\ConfigurationResolver Resolves configuration options.
     */
    private $resolver;

    /**
     * @var array Associative array storing the current configuration option values.
     */
    private $config;

    /**
     * @var string The production URL for the service.
     */
    private $productionUrl;

    /**
     * @var string The sandbox URL for the service.
     */
    private $sandboxUrl;

    /**
     * @param string $productionUrl The production URL.
     * @param string $sandboxUrl The sandbox URL.
     * @param array $config Configuration option values.
     */
    public function __construct(
        $productionUrl,
        $sandboxUrl,
        array $config
    ) {
        $this->resolver = new ConfigurationResolver(static::getConfigDefinitions());
        $this->config = $this->resolver->resolve($config);
        $this->productionUrl = $productionUrl;
        $this->sandboxUrl = $sandboxUrl;
    }

    /**
     * Returns definitions for each configuration option that is supported.
     *
     * @return array An associative array of configuration definitions.
     */
    public static function getConfigDefinitions()
    {
        return [
            'profile' => [
                'valid' => ['string'],
                'fn'    => 'DTS\eBaySDK\applyProfile',
            ],
            'compressResponse' => [
                'valid'   => ['bool'],
                'default' => false
            ],
            'credentials' => [
                'valid'   => ['DTS\eBaySDK\Credentials\CredentialsInterface', 'array', 'callable'],
                'fn'      => 'DTS\eBaySDK\applyCredentials',
                'default' => [CredentialsProvider::class, 'defaultProvider']
            ],
            'debug' => [
                'valid'   => ['bool', 'array'],
                'fn'      => 'DTS\eBaySDK\applyDebug',
                'default' => false
            ],
            'httpHandler' => [
                'valid'   => ['callable'],
                'default' => 'DTS\eBaySDK\defaultHttpHandler'
            ],
            'httpOptions' => [
                'valid'   => ['array'],
                'default' => []
            ],
            'sandbox' => [
                'valid'   => ['bool'],
                'default' => false
            ]
        ];
    }

    /**
     * Method to get the service's configuration.
     *
     * @param string|null $option The name of the option whos value will be returned.
     *
     * @return mixed Returns an associative array of configuration options if no parameters are passed,
     * otherwise returns the value for the specified configuration option.
     */
    public function getConfig($option = null)
    {
        return $option === null
            ? $this->config
            : (isset($this->config[$option])
                ? $this->config[$option]
                : null);
    }

    /**
     * Set multiple configuration options.
     *
     * @param array $configuration Associative array of configuration options and their values.
     */
    public function setConfig(array $configuration)
    {
        $this->config = Functions\arrayMergeDeep(
            $this->config,
            $this->resolver->resolveOptions($configuration)
        );
    }

    /**
     * Helper method to return the value of the credentials configuration option.
     *
     * @return \DTS\eBaySDK\Credentials\CredentialsInterface
     */
    public function getCredentials()
    {
        return $this->getConfig('credentials');
    }

    /**
     * Sends an asynchronous API request.
     *
     * @param string $name The name of the operation.
     * @param \DTS\eBaySDK\Types\BaseType $request Request object containing the request information.
     * @param string $responseClass The name of the PHP class that will be created from the XML response.
     *
     * @return \GuzzleHttp\Promise\PromiseInterface A promise that will be resolved with an object created from the XML response.
     */
    protected function callOperationAsync($name, \DTS\eBaySDK\Types\BaseType $request, $responseClass)
    {
        $url = $this->getUrl();
        $body = $this->buildRequestBody($request);
        $headers = $this->buildRequestHeaders($name, $request, $body);
        $debug = $this->getConfig('debug');
        $httpHandler = $this->getConfig('httpHandler');
        $httpOptions = $this->getConfig('httpOptions');

        if ($debug !== false) {
            $this->debugRequest($url, $headers, $body);
        }

        $request = new Request('POST', $url, $headers, $body);

        return $httpHandler($request, $httpOptions)->then(
            function (ResponseInterface $res) use ($debug, $responseClass) {
                list($xmlResponse, $attachment) = $this->extractXml($res->getBody()->getContents());

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

                $xmlParser = new XmlParser($responseClass);

                $response = $xmlParser->parse($xmlResponse);
                $response->attachment($attachment);

                return $response;
            }
        );
    }

    /**
     * Helper function to return the URL as determined by the sandbox configuration option.
     *
     * @return string Either the production or sandbox URL.
     */
    private function getUrl()
    {
        return $this->getConfig('sandbox') ? $this->sandboxUrl : $this->productionUrl;
    }

    /**
     * Builds the request body string.
     *
     * @param \DTS\eBaySDK\Types\BaseType $request Request object containing the request information.
     *
     * @return string The request body.
     */
    protected function buildRequestBody(\DTS\eBaySDK\Types\BaseType $request)
    {
        if (!$request->hasAttachment()) {
            return $request->toRequestXml();
        } else {
            return $this->buildXopDocument($request).$this->buildAttachmentBody($request->attachment());
        }
    }

    /**
     * Builds the XOP document part of the request body string.
     *
     * @param \DTS\eBaySDK\Types\BaseType $request Request object containing the request information.
     *
     * @return string The XOP document part of request body.
     */
    private function buildXopDocument(\DTS\eBaySDK\Types\BaseType $request)
    {
        return sprintf(
            '%s%s%s%s%s',
            '--MIME_boundary'.self::CRLF,
            'Content-Type: application/xop+xml;charset=UTF-8;type="text/xml"'.self::CRLF,
            'Content-Transfer-Encoding: 8bit'.self::CRLF,
            'Content-ID: <request.xml@devbay.net>'.self::CRLF.self::CRLF,
            $request->toRequestXml().self::CRLF
        );
    }

    /**
     * Builds the attachment part of the request body string.
     *
     * @param array $attachment The attachment
     *
     * @return string The attachment part of request body.
     */
    private function buildAttachmentBody(array $attachment)
    {
        return sprintf(
            '%s%s%s%s%s%s',
            '--MIME_boundary'.self::CRLF,
            'Content-Type: '.$attachment['mimeType'].self::CRLF,
            'Content-Transfer-Encoding: binary'.self::CRLF,
            'Content-ID: <attachment.bin@devbay.net>'.self::CRLF.self::CRLF,
            $attachment['data'].self::CRLF,
            '--MIME_boundary--'
        );
    }

    /**
     * Builds the XML payload part of a multipart/form-data request body.
     *
     * @param \DTS\eBaySDK\Types\BaseType $request Request object containing the request information.
     *
     * @return string The XML payload part of a multipart/form-data request body.
     */
    protected function buildMultipartFormDataXMLPayload(\DTS\eBaySDK\Types\BaseType $request)
    {
        return sprintf(
            '%s%s%s',
            '--boundary'.self::CRLF,
            'Content-Disposition: form-data; name="XML Payload"'.self::CRLF.self::CRLF,
            $request->toRequestXml().self::CRLF
        );
    }

    /**
     * Builds the file part of a multipart/form-data request body.
     *
     * @param string $name
     * @param array $attachment
     *
     * @return string The file part of a multipart/form-data request body.
     */
    protected function buildMultipartFormDataFilePayload($name, $attachment)
    {
        return sprintf(
            '%s%s%s%s%s',
            '--boundary'.self::CRLF,
            'Content-Disposition: form-data; name="'.$name.'"; filename="picture"'.self::CRLF,
            'Content-Type: '.$attachment['mimeType'].self::CRLF.self::CRLF,
            $attachment['data'].self::CRLF,
            '--boundary--'
        );
    }

    /**
     * Helper function that builds the HTTP request headers.
     *
     * @param string $name The name of the operation.
     * @param \DTS\eBaySDK\Types\BaseType $request Request object containing the request information.
     * @param string $body The request body.
     *
     * @return array An associative array of HTTP headers.
     */
    private function buildRequestHeaders($name, $request, $body)
    {
        $headers = [];

        if ($request->hasAttachment()) {
            $headers['Content-Type'] = 'multipart/related;boundary=MIME_boundary;type="application/xop+xml";start="<request.xml@devbay.net>";start-info="text/xml"';
        } else {
            $headers['Content-Type'] = 'text/xml';
        }

        if ($this->getConfig('compressResponse')) {
            $headers[self::HDR_RESPONSE_ENCODING] = 'application/gzip';
        }

        $headers['Content-Length'] = strlen($body);

        return array_merge($headers, $this->getEbayHeaders($name));
    }

    /**
     * Extracts the XML from the response if it contains an attachment.
     *
     * @param string $response The XML response body.
     *
     * @return array first item is the XML part of response body and the second
     * is an attachement if one was present in the API response.
     */
    private function extractXml($response)
    {
        /**
         * Ugly way of seeing if an attachment is present in the response.
         */
        if (strpos($response, 'application/xop+xml') === false) {
            return [$response, ['data' => null, 'mimeType' => null]];
        } else {
            return $this->extractXmlAndAttachment($response);
        }
    }

    /**
     * Extracts the XML and the attachment from the response if it contains an attachment.
     *
     * @param string $response The XML response body.
     *
     * @return string The XML part of response body.
     */
    private function extractXmlAndAttachment($response)
    {
        $attachment = ['data' => null, 'mimeType' => null];

        preg_match('/\r\n/', $response, $matches, PREG_OFFSET_CAPTURE);
        $boundary = substr($response, 0, $matches[0][1]);

        $xmlStartPos = strpos($response, '<?xml ');
        $xmlEndPos = strpos($response, $boundary, $xmlStartPos) - 2;
        $xml = substr($response, $xmlStartPos, $xmlEndPos - $xmlStartPos);

        preg_match('/\r\n\r\n/', $response, $matches, PREG_OFFSET_CAPTURE, $xmlEndPos);
        $attachmentStartPos = $matches[0][1] + 4;
        $attachmentEndPos = strpos($response, $boundary, $attachmentStartPos) - 2;
        $attachment['data'] = substr($response, $attachmentStartPos, $attachmentEndPos - $attachmentStartPos);

        $mimeTypeStartPos = strpos($response, 'Content-Type: ', $xmlEndPos) + 14;
        preg_match('/\r\n/', $response, $matches, PREG_OFFSET_CAPTURE, $mimeTypeStartPos);
        $mimeTypeEndPos = $matches[0][1];
        $attachment['mimeType'] = substr($response, $mimeTypeStartPos, $mimeTypeEndPos - $mimeTypeStartPos);

        return [$xml, $attachment];
    }

    /**
     * Derived classes must implement this method that will build the needed eBay http headers.
     *
     * @param string $operationName The name of the operation been called.
     *
     * @return array An associative array of eBay http headers.
     */
    abstract protected function getEbayHeaders($operationName);

    /**
     * Sends a debug string of the request details.
     *
     * @param string $url API endpoint.
     * @param array  $headers Associative array of HTTP headers.
     * @param string $body The XML body of the POST request.
      */
    private function debugRequest($url, array $headers, $body)
    {
        $str = $url.PHP_EOL;

        $str .= array_reduce(array_keys($headers), function ($str, $key) use ($headers) {
            $str .= $key.': '.$headers[$key].PHP_EOL;
            return $str;
        }, '');

        $str .= $body;

        $this->debug($str);
    }

    /**
     * Sends a debug string of the response details.
     *
     * @param string $body The XML body of the response.
      */
    private function debugResponse($body)
    {
        $this->debug($body);
    }

    /**
     * Sends a debug string via the attach debugger.
     *
     * @param string $str
     */
    private function debug($str)
    {
        $debugger = $this->getConfig('debug');
        $debugger($str);
    }
}


<?php
namespace DTS\eBaySDK\Compliance\Services;

/**
 * Base class for the Compliance service.
 */
class ComplianceBaseService extends \DTS\eBaySDK\Services\BaseRestService
{
    /**
     * @var array $endPoints The API endpoints.
     */
    protected static $endPoints = [
        'sandbox'    => 'https://api.sandbox.ebay.com/sell/compliance',
        'production' => 'https://api.ebay.com/sell/compliance'
    ];

    /**
     * HTTP header constant. The Authentication Token that is used to validate the caller has permission to access the eBay servers.
     */
    const HDR_AUTHORIZATION = 'Authorization';

    /**
     * HTTP header constant. The global ID of the eBay site on which the transaction took place.
     */
    const HDR_MARKETPLACE_ID = 'X-EBAY-C-MARKETPLACE-ID';

    /**
     * @param array $config Configuration option values.
     */
    public function __construct(array $config)
    {
        parent::__construct($config);
    }

    /**
     * Returns definitions for each configuration option that is supported.
     *
     * @return array An associative array of configuration definitions.
     */
    public static function getConfigDefinitions()
    {
        $definitions = parent::getConfigDefinitions();

        return $definitions + [
            'apiVersion' => [
                'valid' => ['string'],
                'default' => \DTS\eBaySDK\Compliance\Services\ComplianceService::API_VERSION,
                'required' => true
            ],
            'authorization' => [
                'valid' => ['string'],
                'required' => true
            ],
            'marketplaceId' => [
                'valid' => ['string']
            ]
        ];
    }

    /**
     * Builds the needed eBay HTTP headers.
     *
     * @return array An associative array of eBay HTTP headers.
     */
    protected function getEbayHeaders()
    {
        $headers = [];

        // Add required headers first.
        $headers[self::HDR_AUTHORIZATION] = 'Bearer '.$this->getConfig('authorization');

        // Add optional headers.
        if ($this->getConfig('marketplaceId')) {
            $headers[self::HDR_MARKETPLACE_ID] = $this->getConfig('marketplaceId');
        }

        return $headers;
    }
}


<?php
/**
 * Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
namespace DTS\eBaySDK\JmesPath;

use DTS\eBaySDK\JmesPath\Lexer as T;

/**
 * JMESPath Pratt parser
 * @link http://hall.org.ua/halls/wizzard/pdf/Vaughan.Pratt.TDOP.pdf
 */
class Parser
{
    /** @var Lexer */
    private $lexer;
    private $tokens;
    private $token;
    private $tpos;
    private $expression;
    private static $nullToken = ['type' => T::T_EOF];
    private static $currentNode = ['type' => T::T_CURRENT];

    private static $bp = [
        T::T_EOF               => 0,
        T::T_QUOTED_IDENTIFIER => 0,
        T::T_IDENTIFIER        => 0,
        T::T_RBRACKET          => 0,
        T::T_RPAREN            => 0,
        T::T_COMMA             => 0,
        T::T_RBRACE            => 0,
        T::T_NUMBER            => 0,
        T::T_CURRENT           => 0,
        T::T_EXPREF            => 0,
        T::T_COLON             => 0,
        T::T_PIPE              => 1,
        T::T_OR                => 2,
        T::T_AND               => 3,
        T::T_COMPARATOR        => 5,
        T::T_FLATTEN           => 9,
        T::T_STAR              => 20,
        T::T_FILTER            => 21,
        T::T_DOT               => 40,
        T::T_NOT               => 45,
        T::T_LBRACE            => 50,
        T::T_LBRACKET          => 55,
        T::T_LPAREN            => 60,
    ];

    /** @var array Acceptable tokens after a dot token */
    private static $afterDot = [
        T::T_IDENTIFIER        => true, // foo.bar
        T::T_QUOTED_IDENTIFIER => true, // foo."bar"
        T::T_STAR              => true, // foo.*
        T::T_LBRACE            => true, // foo[1]
        T::T_LBRACKET          => true, // foo{a: 0}
        T::T_FILTER            => true, // foo.[?bar==10]
    ];

    /**
     * @param Lexer $lexer Lexer used to tokenize expressions
     */
    public function __construct(Lexer $lexer = null)
    {
        $this->lexer = $lexer ?: new Lexer();
    }

    /**
     * Parses a JMESPath expression into an AST
     *
     * @param string $expression JMESPath expression to compile
     *
     * @return array Returns an array based AST
     * @throws SyntaxErrorException
     */
    public function parse($expression)
    {
        $this->expression = $expression;
        $this->tokens = $this->lexer->tokenize($expression);
        $this->tpos = -1;
        $this->next();
        $result = $this->expr();

        if ($this->token['type'] === T::T_EOF) {
            return $result;
        }

        throw $this->syntax('Did not reach the end of the token stream');
    }

    /**
     * Parses an expression while rbp < lbp.
     *
     * @param int   $rbp  Right bound precedence
     *
     * @return array
     */
    private function expr($rbp = 0)
    {
        $left = $this->{"nud_{$this->token['type']}"}();
        while ($rbp < self::$bp[$this->token['type']]) {
            $left = $this->{"led_{$this->token['type']}"}($left);
        }

        return $left;
    }

    private function nud_identifier()
    {
        $token = $this->token;
        $this->next();
        return ['type' => 'field', 'value' => $token['value']];
    }

    private function nud_quoted_identifier()
    {
        $token = $this->token;
        $this->next();
        $this->assertNotToken(T::T_LPAREN);
        return ['type' => 'field', 'value' => $token['value']];
    }

    private function nud_current()
    {
        $this->next();
        return self::$currentNode;
    }

    private function nud_literal()
    {
        $token = $this->token;
        $this->next();
        return ['type' => 'literal', 'value' => $token['value']];
    }

    private function nud_expref()
    {
        $this->next();
        return ['type' => T::T_EXPREF, 'children' => [$this->expr(self::$bp[T::T_EXPREF])]];
    }

    private function nud_not()
    {
        $this->next();
        return ['type' => T::T_NOT, 'children' => [$this->expr(self::$bp[T::T_NOT])]];
    }

    private function nud_lparen() {
        $this->next();
        $result = $this->expr(0);
        if ($this->token['type'] !== T::T_RPAREN) {
            throw $this->syntax('Unclosed `(`');
        }
        $this->next();
        return $result;
    }

    private function nud_lbrace()
    {
        static $validKeys = [T::T_QUOTED_IDENTIFIER => true, T::T_IDENTIFIER => true];
        $this->next($validKeys);
        $pairs = [];

        do {
            $pairs[] = $this->parseKeyValuePair();
            if ($this->token['type'] == T::T_COMMA) {
                $this->next($validKeys);
            }
        } while ($this->token['type'] !== T::T_RBRACE);

        $this->next();

        return['type' => 'multi_select_hash', 'children' => $pairs];
    }

    private function nud_flatten()
    {
        return $this->led_flatten(self::$currentNode);
    }

    private function nud_filter()
    {
        return $this->led_filter(self::$currentNode);
    }

    private function nud_star()
    {
        return $this->parseWildcardObject(self::$currentNode);
    }

    private function nud_lbracket()
    {
        $this->next();
        $type = $this->token['type'];
        if ($type == T::T_NUMBER || $type == T::T_COLON) {
            return $this->parseArrayIndexExpression();
        } elseif ($type == T::T_STAR && $this->lookahead() == T::T_RBRACKET) {
            return $this->parseWildcardArray();
        } else {
            return $this->parseMultiSelectList();
        }
    }

    private function led_lbracket(array $left)
    {
        static $nextTypes = [T::T_NUMBER => true, T::T_COLON => true, T::T_STAR => true];
        $this->next($nextTypes);
        switch ($this->token['type']) {
            case T::T_NUMBER:
            case T::T_COLON:
                return [
                    'type' => 'subexpression',
                    'children' => [$left, $this->parseArrayIndexExpression()]
                ];
            default:
                return $this->parseWildcardArray($left);
        }
    }

    private function led_flatten(array $left)
    {
        $this->next();

        return [
            'type'     => 'projection',
            'from'     => 'array',
            'children' => [
                ['type' => T::T_FLATTEN, 'children' => [$left]],
                $this->parseProjection(self::$bp[T::T_FLATTEN])
            ]
        ];
    }

    private function led_dot(array $left)
    {
        $this->next(self::$afterDot);

        if ($this->token['type'] == T::T_STAR) {
            return $this->parseWildcardObject($left);
        }

        return [
            'type'     => 'subexpression',
            'children' => [$left, $this->parseDot(self::$bp[T::T_DOT])]
        ];
    }

    private function led_or(array $left)
    {
        $this->next();
        return [
            'type'     => T::T_OR,
            'children' => [$left, $this->expr(self::$bp[T::T_OR])]
        ];
    }

    private function led_and(array $left)
    {
        $this->next();
        return [
            'type'     => T::T_AND,
            'children' => [$left, $this->expr(self::$bp[T::T_AND])]
        ];
    }

    private function led_pipe(array $left)
    {
        $this->next();
        return [
            'type'     => T::T_PIPE,
            'children' => [$left, $this->expr(self::$bp[T::T_PIPE])]
        ];
    }

    private function led_lparen(array $left)
    {
        $args = [];
        $this->next();

        while ($this->token['type'] != T::T_RPAREN) {
            $args[] = $this->expr(0);
            if ($this->token['type'] == T::T_COMMA) {
                $this->next();
            }
        }

        $this->next();

        return [
            'type'     => 'function',
            'value'    => $left['value'],
            'children' => $args
        ];
    }

    private function led_filter(array $left)
    {
        $this->next();
        $expression = $this->expr();
        if ($this->token['type'] != T::T_RBRACKET) {
            throw $this->syntax('Expected a closing rbracket for the filter');
        }

        $this->next();
        $rhs = $this->parseProjection(self::$bp[T::T_FILTER]);

        return [
            'type'       => 'projection',
            'from'       => 'array',
            'children'   => [
                $left ?: self::$currentNode,
                [
                    'type' => 'condition',
                    'children' => [$expression, $rhs]
                ]
            ]
        ];
    }

    private function led_comparator(array $left)
    {
        $token = $this->token;
        $this->next();

        return [
            'type'     => T::T_COMPARATOR,
            'value'    => $token['value'],
            'children' => [$left, $this->expr(self::$bp[T::T_COMPARATOR])]
        ];
    }

    private function parseProjection($bp)
    {
        $type = $this->token['type'];
        if (self::$bp[$type] < 10) {
            return self::$currentNode;
        } elseif ($type == T::T_DOT) {
            $this->next(self::$afterDot);
            return $this->parseDot($bp);
        } elseif ($type == T::T_LBRACKET || $type == T::T_FILTER) {
            return $this->expr($bp);
        }

        throw $this->syntax('Syntax error after projection');
    }

    private function parseDot($bp)
    {
        if ($this->token['type'] == T::T_LBRACKET) {
            $this->next();
            return $this->parseMultiSelectList();
        }

        return $this->expr($bp);
    }

    private function parseKeyValuePair()
    {
        static $validColon = [T::T_COLON => true];
        $key = $this->token['value'];
        $this->next($validColon);
        $this->next();

        return [
            'type'     => 'key_val_pair',
            'value'    => $key,
            'children' => [$this->expr()]
        ];
    }

    private function parseWildcardObject(array $left = null)
    {
        $this->next();

        return [
            'type'     => 'projection',
            'from'     => 'object',
            'children' => [
                $left ?: self::$currentNode,
                $this->parseProjection(self::$bp[T::T_STAR])
            ]
        ];
    }

    private function parseWildcardArray(array $left = null)
    {
        static $getRbracket = [T::T_RBRACKET => true];
        $this->next($getRbracket);
        $this->next();

        return [
            'type'     => 'projection',
            'from'     => 'array',
            'children' => [
                $left ?: self::$currentNode,
                $this->parseProjection(self::$bp[T::T_STAR])
            ]
        ];
    }

    /**
     * Parses an array index expression (e.g., [0], [1:2:3]
     */
    private function parseArrayIndexExpression()
    {
        static $matchNext = [
            T::T_NUMBER   => true,
            T::T_COLON    => true,
            T::T_RBRACKET => true
        ];

        $pos = 0;
        $parts = [null, null, null];
        $expected = $matchNext;

        do {
            if ($this->token['type'] == T::T_COLON) {
                $pos++;
                $expected = $matchNext;
            } elseif ($this->token['type'] == T::T_NUMBER) {
                $parts[$pos] = $this->token['value'];
                $expected = [T::T_COLON => true, T::T_RBRACKET => true];
            }
            $this->next($expected);
        } while ($this->token['type'] != T::T_RBRACKET);

        // Consume the closing bracket
        $this->next();

        if ($pos === 0) {
            // No colons were found so this is a simple index extraction
            return ['type' => 'index', 'value' => $parts[0]];
        }

        if ($pos > 2) {
            throw $this->syntax('Invalid array slice syntax: too many colons');
        }

        // Sliced array from start (e.g., [2:])
        return [
            'type'     => 'projection',
            'from'     => 'array',
            'children' => [
                ['type' => 'slice', 'value' => $parts],
                $this->parseProjection(self::$bp[T::T_STAR])
            ]
        ];
    }

    private function parseMultiSelectList()
    {
        $nodes = [];

        do {
            $nodes[] = $this->expr();
            if ($this->token['type'] == T::T_COMMA) {
                $this->next();
                $this->assertNotToken(T::T_RBRACKET);
            }
        } while ($this->token['type'] !== T::T_RBRACKET);
        $this->next();

        return ['type' => 'multi_select_list', 'children' => $nodes];
    }

    private function syntax($msg)
    {
        return new SyntaxErrorException($msg, $this->token, $this->expression);
    }

    private function lookahead()
    {
        return (!isset($this->tokens[$this->tpos + 1]))
            ? T::T_EOF
            : $this->tokens[$this->tpos + 1]['type'];
    }

    private function next(array $match = null)
    {
        if (!isset($this->tokens[$this->tpos + 1])) {
            $this->token = self::$nullToken;
        } else {
            $this->token = $this->tokens[++$this->tpos];
        }

        if ($match && !isset($match[$this->token['type']])) {
            throw $this->syntax($match);
        }
    }

    private function assertNotToken($type)
    {
        if ($this->token['type'] == $type) {
            throw $this->syntax("Token {$this->tpos} not allowed to be $type");
        }
    }

    /**
     * @internal Handles undefined tokens without paying the cost of validation
     * @param $method
     * @param $args
     */
    public function __call($method, $args)
    {
        $prefix = substr($method, 0, 4);
        if ($prefix == 'nud_' || $prefix == 'led_') {
            $token = substr($method, 4);
            $message = "Unexpected \"$token\" token ($method). Expected one of"
                . " the following tokens: "
                . implode(', ', array_map(function ($i) {
                    return '"' . substr($i, 4) . '"';
                }, array_filter(
                    get_class_methods($this),
                    function ($i) use ($prefix) {
                        return strpos($i, $prefix) === 0;
                    }
                )));
            throw $this->syntax($message);
        }

        throw new \BadMethodCallException("Call to undefined method $method");
    }
}


<?php
namespace DTS\eBaySDK\Feedback\Services;

/**
 * Base class for the Return Management service.
 */
class FeedbackBaseService extends \DTS\eBaySDK\Services\BaseService
{
    /**
     * HTTP header constant. The API version your application supports.
     */
    const HDR_API_VERSION = 'X-EBAY-SOA-SERVICE-VERSION';

    /**
     * HTTP header constant. The Authentication Token that is used to validate the caller has permission to access the eBay servers.
     */
    const HDR_AUTH_TOKEN = 'X-EBAY-SOA-SECURITY-TOKEN';

    /**
     * HTTP header constant. The name of the operation you are calling.
     */
    const HDR_OPERATION_NAME = 'X-EBAY-SOA-OPERATION-NAME';

    /**
     * @param array $config Optional configuration option values.
     */
    public function __construct(array $config)
    {
        parent::__construct('https://svcs.ebay.com/FeedbackService', 'https://svcs.sandbox.ebay.com/FeedbackService', $config);
    }

    /**
     * Returns definitions for each configuration option that is supported.
     *
     * @return array An associative array of configuration definitions.
     */
    public static function getConfigDefinitions()
    {
        $definitions = parent::getConfigDefinitions();

        return $definitions + [
            'apiVersion' => [
                'valid' => ['string'],
                'default' => \DTS\eBaySDK\Feedback\Services\FeedbackService::API_VERSION
            ],
            'authToken' => [
                'valid' => ['string'],
                'required' => true
            ]
        ];
    }

    /**
     * Builds the needed eBay HTTP headers.
     *
     * @param string $operationName The name of the operation been called.
     *
     * @return array An associative array of eBay http headers.
     */
    protected function getEbayHeaders($operationName)
    {
        $headers = [];

        // Add required headers first.
        $headers[self::HDR_AUTH_TOKEN] = $this->getConfig('authToken');
        $headers[self::HDR_OPERATION_NAME] = $operationName;

        // Add optional headers.
        if ($this->getConfig('apiVersion')) {
            $headers[self::HDR_API_VERSION] = $this->getConfig('apiVersion');
        }

        return $headers;
    }
}


<?php
namespace DTS\eBaySDK;

/**
 * @internal Resolves and validates an associative array of configuration options.
 */
class ConfigurationResolver
{
    /**
     * @var array $definitions Definitions for each configuration option.
     */
    private $definitions;

    /**
     * @var array Map of type to function that confirms type.
     */
    private static $typeMap = [
        'array' => 'is_array',
        'bool' => 'is_bool',
        'callable' => 'is_callable',
        'int' => 'is_int',
        'string' => 'is_string'
    ];

    /**
     * @param array $definitions Definitions for each configuration option.
     */
    public function __construct(array $definitions)
    {
        $this->definitions = $definitions;
    }

    /**
     * Resolve and validate the passed configuration options.
     *
     * @param array $configuration Associative array of configuration options that will be resolved and validated.
     *
     * @return array Returns an associative array of the resolved and validated configuration options.
     * @throws \InvalidArgumentException.
     */
    public function resolve(array $configuration)
    {
        foreach ($this->definitions as $key => $def) {
            if (!isset($configuration[$key])) {
                if (isset($def['default'])) {
                    $configuration[$key] = is_callable($def['default'])
                        ? $def['default']($configuration)
                        : $def['default'];
                } elseif (empty($def['required'])) {
                    continue;
                } else {
                    $this->throwRequired($configuration);
                }
            }

            $this->checkType($def['valid'], $key, $configuration[$key]);

            if (isset($def['fn'])) {
                $def['fn']($configuration[$key], $configuration);
            }
        }

        return $configuration;
    }

    /**
     * Resolve and validate the passed configuration options.
     * The difference between this method and resolve is that
     * this will only resolve options in the passed configuration.
     * It will not use the definitions to resolve optional and required options.
     *
     * @param array $configuration Associative array of configuration options that will be resolved and validated.
     *
     * @return array Returns an associative array of the resolved and validated configuration options.
     * @throws \InvalidArgumentException.
     */
    public function resolveOptions(array $configuration)
    {
        foreach ($configuration as $key => $value) {
            if (isset($this->definitions[$key])) {
                $def = $this->definitions[$key];

                $this->checkType($def['valid'], $key, $value);

                if (isset($def['fn'])) {
                    $def['fn']($configuration[$key], $configuration);
                }
            }
        }

        return $configuration;
    }

    /**
     * @param array $valid Array of valid types.
     * @param $name Name of the configuration option.
     * @param $provided Value to check the type of.
     *
     * @throws \InvalidArgumentException.
     */
    private function checkType(array $valid, $name, $provided)
    {
        foreach ($valid as $check) {
            if (isset(self::$typeMap[$check])) {
                $fn = self::$typeMap[$check];
                if ($fn($provided)) {
                    return;
                }
            } elseif ($provided instanceof $check) {
                return;
            }
        }

        $expected = implode('|', $valid);
        $msg = sprintf(
            'Invalid configuration value provided for "%s". Expected %s, but got %s',
            $name,
            $expected,
            describeType($provided)
        );
        throw new \InvalidArgumentException($msg);
    }

    /**
     * @param array $configuration Associative array of configuration options.
     *
     * @throws \InvalidArgumentException.
     */
    private function throwRequired(array $configuration)
    {
        $missing = [];

        foreach ($this->definitions as $key => $def) {
            if (empty($def['required'])
                || isset($def['default'])
                || array_key_exists($key, $configuration)
            ) {
                continue;
            }
            $missing[] = $key;
        }

        $msg = "Missing required configuration options: \n\n";
        $msg .= implode("\n\n", $missing);
        throw new \InvalidArgumentException($msg);
    }
}


<?php
namespace DTS\eBaySDK\Types;

use \DTS\eBaySDK\Exceptions;
use \DTS\eBaySDK\JmesPath\JmesPathableArrayInterface;

/**
 * Class to handle XML elements that are repeatable.
 *
 * Allows properties in an object to be treated as an array.
 */
class RepeatableType implements \ArrayAccess, \Countable, \Iterator, JmesPathableArrayInterface
{
    /**
     * @var array The data to store as an array.
     */
    private $data = [];

    /**
     * @var int The current position in the array.
     */
    private $position = 0;

    /**
     * @var string The name of the class that the property is a member of.
     */
    private $class;

    /**
     * @var string The name of the property that acts like an array.
     */
    private $property;

    /**
     * @var string The type that values assigned to the array should be.
     */
    private $expectedType;

    /**
     *
     * @param string $class The name of the class that the property is a member of.
     * @param string $property The name of the property that acts like an array.
     * @param string $expectedType The type that values assigned to the array should be.
     */
    public function __construct($class, $property, $expectedType)
    {
        $this->class = $class;
        $this->property = $property;
        $this->expectedType = $expectedType;
    }

    /**
     * Determines if the offset exists in the array.
     *
     * @param int $offset The array index to check.
     *
     * @return bool Returns if the offset exists in the array.
     */
    public function offsetExists($offset)
    {
        return isset($this->data[$offset]);
    }

    /**
     * Returns the value of the given offset.
     *
     * @param int $offset The array index.
     *
     * @return mixed Returns the value for the given offset or null if it doesn't exist.
     */
    public function offsetGet($offset)
    {
        return $this->offsetExists($offset) ? $this->data[$offset] : null;
    }

    /**
     * Sets a value for the given offset.
     *
     * @param mixed $offset The array index or null to add the value to the end of the array.
     * @param mixed $value The value to add.
     *
     * @throws \DTS\eBaySDK\Exceptions\InvalidPropertyTypeException If the value is the wrong type for the array.
     */
    public function offsetSet($offset, $value)
    {
        self::ensurePropertyType($value);

        if (is_null($offset)) {
            $this->data[] = $value;
        } else {
            $this->data[$offset] = $value;
        }
    }

    /**
     * Unsets the value of the given offset.
     *
     * @param int $offset The array index.
     */
    public function offsetUnset($offset)
    {
        unset($this->data[$offset]);
    }

    /**
     * @return int The number of array items.
     */
    public function count()
    {
        return count($this->data);
    }

    /**
     * @return mixed The value of the current array index.
     */
    public function current()
    {
        return $this->offsetGet($this->position);
    }

    /**
     * @return int The current array index.
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * Move onto the next array index.
     */
    public function next()
    {
        $this->position++;
    }

    /**
     * Reset the array index to the start of the array.
     */
    public function rewind()
    {
        $this->position = 0;
    }

    /**
     * @return bool Return if the current array index is valid.
     */
    public function valid()
    {
        return $this->offsetExists($this->position);
    }

    /**
     * Determines if the value is the correct type to assign to the array.
     *
     * @param mixed $value The value to check the type of.
     *
     * @throws \DTS\eBaySDK\Exceptions\InvalidPropertyTypeException If the value is the wrong type for the array.
     */
    private function ensurePropertyType($value)
    {
        $actualType = gettype($value);
        if ('object' === $actualType) {
            $actualType = get_class($value);
        }

        $valid = explode('|', $this->expectedType);
        $isValid = false;
        foreach ($valid as $check) {
            if ($check !== 'any' && \DTS\eBaySDK\checkPropertyType($check)) {
                if ($check === $actualType) {
                    return;
                }
                $isValid = false;
            } else {
                $isValid = true;
            }
        }

        if (!$isValid) {
            throw new Exceptions\InvalidPropertyTypeException($this->property, $this->expectedType, $actualType);
        }
    }
}


<?php
namespace DTS\eBaySDK\Types;

use \DTS\eBaySDK\Types;
use \DTS\eBaySDK\Exceptions;
use \DTS\eBaySDK\JmesPath\Env;
use \DTS\eBaySDK\JmesPath\JmesPathableObjectInterface;

/**
 * Base class for all API objects.
 *
 */
class BaseType implements JmesPathableObjectInterface
{
    /**
     * @var array Associative array containing meta data about each property.
     * The array key is the class name of the PHP object.
     * This way we only store a single copy of the meta data for each class.
     * For each class name the value will be an associative array.
     * The array key is the name that client code will use to access the property.
     * The value is an associative array which is the meta data for the property.
     *
     * 'subject' => [                  The name of the property.
     *     'type' => 'string',         The data type or class name.
     *     'repeatable' => false,         Indicates if the property is repeatable, I.e is an array.
     *     'attribute' => false,       Indicates if the proeprty is an attribute in the XML.
     *     'elementName' => 'Subject'  The corressponding element in the XML.
     * ]
     *
     */
    protected static $properties = [];

    /**
     * @var array Associative array. Key is the class name and the value is the xml namespace.
     */
    protected static $xmlNamespaces = [];

    /**
     * @var array Associative array. Key is the name of the XML root element used in the request.
     */
    protected static $requestXmlRootElementNames = [];

    /**
     * @var array When a property is set the value will be stored in this array.
     */
    private $values = [];

    /**
     * @var array Associative array storing an attachment.
     */
    private $attachment;

    /**
     * @param array $values Can pass an associative array that will set the objects properties.
     */
    public function __construct(array $values = [])
    {
        if (!array_key_exists(__CLASS__, self::$properties)) {
            self::$properties[__CLASS__] = [];
        }

        $this->setValues(__CLASS__, $values);

        $this->attachment = ['data' => null, 'mimeType' => null];
    }

    /**
     * PHP magic function that is called when getting a property.
     *
     * @param string $name The property name.
     *
     * @return mixed
     */
    public function __get($name)
    {
        return $this->get(get_class($this), $name);
    }

    /**
     * PHP magic function that is called when setting a property.
     *
     * @param string $name The property name.
     * @param mixed $value Value assigned to the property.
     */
    public function __set($name, $value)
    {
        $this->set(get_class($this), $name, $value);
    }

    /**
     * PHP magic function that is called to determine if a property is set.
     *
     * @param string $name The property name.
     *
     * @return bool
     */
    public function __isset($name)
    {
        return $this->isPropertySet(get_class($this), $name);
    }

    /**
     * PHP magic function that is called to unset a property.
     *
     * @param string $name The property name.
     */
    public function __unset($name)
    {
        $this->unSetProperty(get_class($this), $name);
    }

    /**
     * Converts the object to a XML request string.
     *
     * @return string The XML request string.
     */
    public function toRequestXml()
    {
        return $this->toXml(self::$requestXmlRootElementNames[get_class($this)], true);
    }

    /**
     * Converts the object to a XML string.
     *
     * @param string $elementName The XML element that the object's properties will be a children of.
     * @param bool $rootElement Indicates if the XML will be the root element.
     *
     * @return string The XML.
     */
    private function toXml($elementName, $rootElement = false)
    {
        return sprintf(
            '%s<%s%s%s>%s</%s>',
            $rootElement ? "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" : '',
            $elementName,
            $this->attributesToXml(),
            array_key_exists(get_class($this), self::$xmlNamespaces) ? sprintf(' %s', self::$xmlNamespaces[get_class($this)]) : '',
            $this->propertiesToXml(),
            $elementName
        );
    }

    /**
     * Returns the meta data for a property.
     *
     * This method is used when parsing the XML into a PHP object. The parser
     * needs the meta data for a property when the parser has only the element name.
     *
     * @param string $elementName The element name.
     *
     * @return mixed The meta for the property or null if not found.
     */
    public function elementMeta($elementName)
    {
        $class = get_class($this);
        if (array_key_exists($elementName, self::$properties[$class])) {
            $info = self::$properties[$class][$elementName];
            $nameKey = $info['attribute'] ? 'attributeName' : 'elementName';
            if (array_key_exists($nameKey, $info)) {
                if ($info[$nameKey] === $elementName) {
                    $meta = new \stdClass();
                    $meta->propertyName = $elementName;
                    $meta->phpType = $info['type'];
                    $meta->repeatable = $info['repeatable'];
                    $meta->attribute = $info['attribute'];
                    $meta->elementName = $info[$nameKey];
                    $meta->strData = '';

                    return $meta;
                }
            }
        }

        return null;
    }

    /**
     * Method to get or set the object's attachment. Overrides any existing attachment is setting.
     *
     * @param mixed $data If a string it is assumed to be the contents of the attachment. If an array copy its values across.
     * @param string $mimeType The MIME type of the attachment that will be used in the request. Defaults to application/octet-stream.
     *
     * @return mixed Returns the contents of the current atachment or null if none has been specified.
     */
    public function attachment($data = null, $mimeType = 'application/octet-stream')
    {
        if ($data !== null) {
            if (is_array($data)) {
                $this->attachment['data'] = array_key_exists('data', $data) ? $data['data'] : null;
                $this->attachment['mimeType'] = array_key_exists('mimeType', $data) ? $data['mimeType'] : 'application/octet-stream';
            } else {
                $this->attachment['data'] = $data;
                $this->attachment['mimeType'] = $mimeType;
            }
        }

        return $this->attachment;
    }

    /**
     * Helper method to check if an object has an attachment.
     *
     * @return bool Returns true if an object has an attachment.
     */
    public function hasAttachment()
    {
        return $this->attachment['data'] !== null;
    }

    /**
     * Helper method that returns an associative array of the object's properties and values.
     *
     * @return array Associative array where the keys are the object's properties.
     */
    public function toArray()
    {
        $array = [];

        foreach (self::$properties[get_class($this)] as $name => $info) {
            if (!array_key_exists($name, $this->values)) {
                continue;
            }

            $value = $this->values[$name];

            if ($info['repeatable']) {
                if (count($value)) {
                    $array[$name] = [];
                    foreach ($value as $property) {
                        $array[$name][] = self::propertyToArrayValue($property);
                    }
                }
            } else {
                $array[$name] = self::propertyToArrayValue($value);
            }
        }

        return $array;
    }

    /**
     * Assign multiple values to an object.
     *
     * @param string $expression A valid JMESPath expression
     *
     * @return mixed The results of the search.
     */
    public function search($expression)
    {
        return Env::search($expression, $this);
    }

    /**
     * @return string JSON string of the object's properties and values.
     */
    public function __toString()
    {
        return json_encode($this->toArray());
    }

    /**
     * Assign multiple values to an object.
     *
     * @param string $class The name of the class the properties belong to.
     * @param array $values. Associative array of property names and their values.
     *
     * @throws \DTS\eBaySDK\Exceptions\UnknownPropertyException If the property does not exist.
     * @throws \DTS\eBaySDK\Exceptions\InvalidPropertyTypeException If the value is the wrong type for the property.
     */
    protected function setValues($class, array $values = [])
    {
        foreach ($values as $property => $value) {
            $value = self::removeNull($value);
            if (!is_null($value)) {
                $actualValue = self::determineActualValueToAssign($class, $property, $value);
                $this->set($class, $property, $actualValue);
            }
        }
    }

    /**
     * Return a property's value.
     *
     * @param string $class The name of the class the property belongs to.
     * @param string $name The property name.
     *
     * @return mixed The property value.
     * @throws \DTS\eBaySDK\Exceptions\UnknownPropertyException If the property does not exist.
     */
    private function get($class, $name)
    {
        self::ensurePropertyExists($class, $name);

        return $this->getValue($class, $name);
    }

    /**
     * Assign a value to a property.
     *
     * @param string $class The name of the class the properties belong to.
     * @param string $name The property name.
     * @param mixed $value. The value to assign to the property.
     *
     * @throws \DTS\eBaySDK\Exceptions\UnknownPropertyException If the property does not exist.
     * @throws \DTS\eBaySDK\Exceptions\InvalidPropertyTypeException If the value is the wrong type for the property.
     */
    private function set($class, $name, $value)
    {
        self::ensurePropertyExists($class, $name);
        self::ensurePropertyType($class, $name, $value);

        $this->setValue($class, $name, $value);
    }

    /**
     * Determine if a property has been set.
     *
     * @param string $class The name of the class the properties belong to.
     * @param string $name The property name.
     *
     * @return bool Returns if the property has been set.
     * @throws \DTS\eBaySDK\Exceptions\UnknownPropertyException If the property does not exist.
     */
    private function isPropertySet($class, $name)
    {
        self::ensurePropertyExists($class, $name);

        return array_key_exists($name, $this->values);
    }

    /**
     *  Unset a property.
     *
     * @param string $class The name of the class the properties belong to.
     * @param string $name The property name.
     *
     * @throws \DTS\eBaySDK\Exceptions\UnknownPropertyException If the property does not exist.
     */
    private function unSetProperty($class, $name)
    {
        self::ensurePropertyExists($class, $name);

        unset($this->values[$name]);
    }

    /**
     * Get the value of a property. Assumes property exists.
     *
     * @param string $class The name of the class the properties belong to.
     * @param string $name The property name.
     *
     * @return mixed The property value.
     */
    private function getValue($class, $name)
    {
        $info = self::propertyInfo($class, $name);

        if ($info['repeatable'] && !array_key_exists($name, $this->values)) {
            $this->values[$name] = new Types\RepeatableType($class, $name, $info['type']);
        }

        return array_key_exists($name, $this->values) ? $this->values[$name] : null;
    }

    /**
     * Set the value of a property. Assumes property exists.
     *
     * @param string $class The name of the class the properties belong to.
     * @param string $name The property name.
     * @param mixed $value. The value to assign to the property.
     *
     * @throws \DTS\eBaySDK\Exceptions\InvalidPropertyTypeException If trying to assign a non array type to an repeatable property.
     */
    private function setValue($class, $name, $value)
    {
        $info = self::propertyInfo($class, $name);

        if (!$info['repeatable']) {
            $this->values[$name] = $value;
        } else {
            $actualType = self::getActualType($value);
            if ('array' !== $actualType) {
                throw new Exceptions\InvalidPropertyTypeException($name, 'DTS\eBaySDK\Types\RepeatableType', $actualType);
            } else {
                $this->values[$name] = new Types\RepeatableType(get_class($this), $name, $info['type']);
                foreach ($value as $item) {
                    $this->values[$name][] = $item;
                }
            }
        }
    }

    /**
     * Returns an XML string of the object's attributes.
     *
     * @return string The XML.
     */
    private function attributesToXml()
    {
        $attributes = [];

        foreach (self::$properties[get_class($this)] as $name => $info) {
            if (!$info['attribute']) {
                continue;
            }

            if (!array_key_exists($name, $this->values)) {
                continue;
            }

            $attributes[] = self::attributeToXml($info['attributeName'], $this->values[$name]);
        }

        return join('', $attributes);
    }

    /**
     * Returns an XML string of the object's properties.
     *
     * @return string The XML.
     */
    private function propertiesToXml()
    {
        $properties = [];

        foreach (self::$properties[get_class($this)] as $name => $info) {
            if ($info['attribute']) {
                continue;
            }

            if (!array_key_exists($name, $this->values)) {
                continue;
            }

            $value = $this->values[$name];

            if (!array_key_exists('elementName', $info) && !array_key_exists('attributeName', $info)) {
                $properties[] = self::encodeValueXml($value);
            } else {
                if ($info['repeatable']) {
                    foreach ($value as $property) {
                        $properties[] = self::propertyToXml($info['elementName'], $property);
                    }
                } else {
                    $properties[] = self::propertyToXml($info['elementName'], $value);
                }
            }
        }

        return join("\n", $properties);
    }

    /**
     * Determines if the property is a member of the class.
     *
     * @param string $class The name of the class that we are checking for.
     * @param string $name The property name.
     *
     * @throws \DTS\eBaySDK\Exceptions\UnknownPropertyException If the property does not exist.
     */
    private static function ensurePropertyExists($class, $name)
    {
        if (!array_key_exists($name, self::$properties[$class])) {
            throw new Exceptions\UnknownPropertyException($name);
        }
    }

    /**
     * Determines if the value is the correct type to assign to a property.
     *
     * @param string $class The name of the class that we are checking for.
     * @param mixed $name The property name.
     * @param mixed $value The value to check the type of.
     *
     * @throws \DTS\eBaySDK\Exceptions\InvalidPropertyTypeException If the value is the wrong type for the property.
     */
    private static function ensurePropertyType($class, $name, $value)
    {
        $isValid = false;
        $info = self::propertyInfo($class, $name);
        $actualType = self::getActualType($value);
        $valid = explode('|', $info['type']);

        foreach ($valid as $check) {
            if ($check !== 'any' && \DTS\eBaySDK\checkPropertyType($check)) {
                if ($check === $actualType || 'array' === $actualType) {
                    return;
                }
                $isValid = false;
            } else {
                $isValid = true;
            }
        }

        if (!$isValid) {
            $expectedType = $info['type'];
            throw new Exceptions\InvalidPropertyTypeException($name, $expectedType, $actualType);
        }
    }

    /**
     * Helper function to return the actual type of a value.
     *
     * @param mixed $value The value we want the type of.
     *
     * @return string The type name of the value.
     */
    private static function getActualType($value)
    {
        $actualType = gettype($value);

        if ('object' === $actualType) {
            $actualType = get_class($value);
        }

        return $actualType;
    }

    /**
     * Helper function to return the meta data of a property.
     *
     * @param string $class The name of the class the property belongs to.
     * @param string $name The property name.
     *
     * @return array The meta data for the property.
     */
    private static function propertyInfo($class, $name)
    {
        return self::$properties[$class][$name];
    }

    /**
     * Helper function to remove the properties and values that belong to a object's parent.
     *
     * @param array $properties
     * @param array $values
     *
     * @return array The first element is an array of parent properties and values.
     * The second element is an array of the object's properties and values.
     */
    protected static function getParentValues(array $properties, array $values)
    {
        return [
            array_diff_key($values, $properties),
            array_intersect_key($values, $properties)
        ];
    }

    /**
     * Helper function to convert an attribute property into XML
     *
     * @param string $name The attribute name.
     * @param mixed $value The attribute value.
     *
     * @return string The XML.
     */
    private static function attributeToXml($name, $value)
    {
        return sprintf(' %s="%s"', $name, self::encodeValueXml($value));
    }

    /**
     * Helper function to convert an property into XML
     *
     * @param string $name The property name.
     * @param mixed $value The property value.
     *
     * @return string The XML.
     */
    private static function propertyToXml($name, $value)
    {
        if (is_subclass_of($value, '\DTS\eBaySDK\Types\BaseType', false)) {
            return $value->toXml($name);
        } else {
            return sprintf('<%s>%s</%s>', $name, self::encodeValueXml($value), $name);
        }
    }

    /**
     * Helper function to convert a value into XML
     *
     * @param mixed $value The property value.
     *
     * @return string The XML.
     */
    private static function encodeValueXml($value)
    {
        if ($value instanceof \DateTime) {
            return $value->format('Y-m-d\TH:i:s.000\Z');
        } elseif (is_bool($value)) {
            return $value ? 'true' : 'false';
        } else {
            return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', true);
        }
    }

    /**
     * Helper function to convert a property in a value that we want in an array.
     *
     * @param mixed $value The property value.
     *
     * @return mixed A value to add to an array.
     */
    private static function propertyToArrayValue($value)
    {
        if (is_subclass_of($value, '\DTS\eBaySDK\Types\BaseType', false)) {
            return $value->toArray();
        } elseif ($value instanceof \DateTime) {
            return $value->format('Y-m-d\TH:i:s.000\Z');
        } else {
            return $value;
        }
    }

    /**
     * Helper function when assigning values via the ctor.
     * Determines the actual value to assign to a property.
     *
     * @param string $class The name of the class the property belong to.
     * @param string $property The property name.
     * @param mixed $value The property value.
     *
     * @return mixed
     */
    private static function determineActualValueToAssign($class, $property, $value)
    {
        if (!array_key_exists($property, self::$properties[$class])) {
            return $value;
        }

        $info = self::propertyInfo($class, $property);

        if ($info['repeatable'] && is_array($value)) {
            $values = [];
            foreach ($value as $val) {
                $values[] = self::actualValue($info, $val);
            }
            return $values;
        }

        return self::actualValue($info, $value);
    }

    /**
     * Helper function when assigning values via the ctor.
     *
     * @param array $info The metadata for the property.
     * @param mixed $value The property value.
     *
     * @return mixed
     */
    private static function actualValue(array $info, $value)
    {
        /**
         * Shortcut. Objects can be assigned as is.
         */
        if (is_object($value)) {
            return $value;
        }

        $types = explode('|', $info['type']);

        foreach ($types as $type) {
            switch ($type) {
                case 'integer':
                case 'string':
                case 'double':
                case 'boolean':
                case 'any':
                    return $value;
                case 'DateTime':
                    return new \DateTime($value, new \DateTimeZone('UTC'));
            }
        }

        return new $info['type']($value);
    }

    /**
     * @param mixed $value Remove null elements if an array.
     *
     * @return mixed Original value if not an array or array without null elements.
     */
    private static function removeNull($value)
    {
        if (!is_array($value)) {
            return $value;
        }

        return array_filter($value, function ($val) {
            return !is_null($val);
        });
    }
}


<?php
namespace DTS\eBaySDK\Credentials;

/**
 * Credentials providers are functions that accept no arguments and return an
 * instance that implements DTS\eBaySDK\Credentials\CredentialsInterface. If they
 * are unable to provide the credentials an instance of \InvalidArgumentException
 * must be returned. The function can supply a message via the returned exception
 * object to explain why no credentials are available.
 */
class CredentialsProvider
{
    const ENV_APP_ID = 'EBAY_SDK_APP_ID';
    const ENV_CERT_ID = 'EBAY_SDK_CERT_ID';
    const ENV_DEV_ID = 'EBAY_SDK_DEV_ID';
    const ENV_PROFILE = 'EBAY_SDK_PROFILE';

    /**
     * Create a default credentials provider that first checks for environment
     * variables, then checks for the "default" profile in ~/.ebay_sdk/credentials.
     *
     * This provider is automatically wrapped in a memoize function that caches
     * previously provided credentails.
     *
     * @return callable
     */
    public static function defaultProvider()
    {
        return self::memoize(
            self::chain(
                self::env(),
                self::ini()
            )
        );
    }

    /**
     * Wraps a credentials provider and caches previously provided credentials.
     *
     * @param callable $provider Credentials provider to wrap.
     *
     * @return callable Wrapped provider that returns cached credentials when called.
     */
    public static function memoize(callable $provider)
    {
        return function () use ($provider) {
            static $result;
            static $isConstant;

            if ($isConstant) {
                return $result;
            }

            $isConstant = true;

            return $result = $provider();
        };
    }

    /**
     * Creates an aggregate credentials provider that invokes the provided
     * variadic providers one after the other until a provider returns
     * credentials
     *
     * @return callable
     * @throws \InvalidArgumentException
     */
    public static function chain()
    {
        $providers = func_get_args();
        if (empty($providers)) {
            throw new \InvalidArgumentException('No providers in chain');
        }

        return function () use ($providers) {
            $provider = array_shift($providers);
            $credentials = $provider();

            while (($provider = array_shift($providers))
                && !($credentials instanceof Credentials)
            ) {
                $credentials = $provider();
            }
            return $credentials;
        };
    }

    /**
     * Provider that creates credentials from environment variables
     * EBAY_SDK_APP_ID, EBAY_SDK_CERT_ID, and EBAY_SDK_DEV_ID.
     *
     * @return callable
     */
    public static function env()
    {
        return function () {
            $appId = getenv(self::ENV_APP_ID);
            $certId = getenv(self::ENV_CERT_ID);
            $devId = getenv(self::ENV_DEV_ID);

            if ($appId && $certId && $devId) {
                return new Credentials($appId, $certId, $devId);
            } else {
                return new \InvalidArgumentException(
                    'Could not find environment variable '
                    . 'credentials in '. self::ENV_APP_ID . '/'
                    . self::ENV_CERT_ID . '/'
                    . self::ENV_DEV_ID
                );
            }
        };
    }

    /**
     * Provider that creates credentials using an ini file stored in the
     * current user's home directory.
     *
     * @param string|null $profile Profile to use. Defaults to "default".
     * @param string|null $filename If provided, uses a custom filename rather than
     * looking in the home directory for the current user.
     *
     * @return callable
     * @throws \InvalidArgumentException
     */
    public static function ini($profile = null, $filename = null)
    {
        $filename = $filename ?: (self::getHomeDir() . '/.ebay_sdk/credentials');
        $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default');

        return function () use ($filename, $profile) {
            if (!is_readable($filename)) {
                return new \InvalidArgumentException("Cannot read credentials from $filename");
            }
            $data = parse_ini_file($filename, true);
            if ($data === false) {
                return new \InvalidArgumentException("Invalid credentials file $filename");
            }
            if (!isset($data[$profile])) {
                return new \InvalidArgumentException("'$profile' not found in credentials file");
            }
            if (!isset($data[$profile]['ebay_app_id'])
                || !isset($data[$profile]['ebay_cert_id'])
                || !isset($data[$profile]['ebay_dev_id'])) {
                return new \InvalidArgumentException("No credentials present in INI profile '$profile' ($filename)");
            }

            return new Credentials(
                $data[$profile]['ebay_app_id'],
                $data[$profile]['ebay_cert_id'],
                $data[$profile]['ebay_dev_id']
            );
        };
    }

    /**
     * Gets the environment's HOME directory if available.
     *
     * @return string|null
     */
    private static function getHomeDir()
    {
        // Linux/Unix-like systems.
        if ($homeDir = getenv('HOME')) {
            return $homeDir;
        }

        $homeDrive = getenv('HOMEDRIVE');
        $homePath = getenv('HOMEPATH');

        return ($homeDrive && $homePath) ? $homeDrive . $homePath : null;
    }
}


<?php
namespace DTS\eBaySDK;

/**
 * Builds DTS\eBaySDK services based on passed configuration options.
 *
 * @method \DTS\eBaySDK\Account\Services\AccountService createAccount(array $args = [])
 * @method \DTS\eBaySDK\Analytics\Services\AnalyticsService createAnalytics(array $args = [])
 * @method \DTS\eBaySDK\Browse\Services\BrowseService createBrowse(array $args = [])
 * @method \DTS\eBaySDK\BulkDataExchange\Services\BulkDataExchangeService createBulkDataExchange(array $args = [])
 * @method \DTS\eBaySDK\BusinessPoliciesManagement\Services\BusinessPoliciesManagementService createBusinessPoliciesManagement(array $args = [])
 * @method \DTS\eBaySDK\Feedback\Services\FeedbackService createFeedback(array $args = [])
 * @method \DTS\eBaySDK\FileTransfer\Services\FileTransferService createFileTransfer(array $args = [])
 * @method \DTS\eBaySDK\Finding\Services\FindingService createFinding(array $args = [])
 * @method \DTS\eBaySDK\Fulfillment\Services\FulfillmentService createFulfillment(array $args = [])
 * @method \DTS\eBaySDK\HalfFinding\Services\HalfFindingService createHalfFinding(array $args = [])
 * @method \DTS\eBaySDK\Inventory\Services\InventoryService createInventory(array $args = [])
 * @method \DTS\eBaySDK\Marketing\Services\MarketingService createMarketing(array $args = [])
 * @method \DTS\eBaySDK\Merchandising\Services\MerchandisingService createMerchandising(array $args = [])
 * @method \DTS\eBaySDK\Metadata\Services\MetadataService createMetadata(array $args = [])
 * @method \DTS\eBaySDK\Order\Services\OrderService createOrder(array $args = [])
 * @method \DTS\eBaySDK\PostOrder\Services\PostOrderService createPostOrder(array $args = [])
 * @method \DTS\eBaySDK\Product\Services\ProductService createProduct(array $args = [])
 * @method \DTS\eBaySDK\ProductMetadata\Services\ProductMetadataService createProductMetadata(array $args = [])
 * @method \DTS\eBaySDK\RelatedItemsManagement\Services\RelatedItemsManagementService createRelatedItemsManagement(array $args = [])
 * @method \DTS\eBaySDK\ResolutionCaseManagement\Services\ResolutionCaseManagementService createResolutionCaseManagement(array $args = [])
 * @method \DTS\eBaySDK\ReturnManagement\Services\ReturnManagementService createReturnManagement(array $args = [])
 * @method \DTS\eBaySDK\Shopping\Services\ShoppingService createShopping(array $args = [])
 * @method \DTS\eBaySDK\Trading\Services\TradingService createTrading(array $args = [])
 */
class Sdk
{
    const VERSION = '18.0.0';

    /**
     * @var bool Controls if the SDK should enforce strict types
     * when values are assigned to property classes.
     */
    public static $STRICT_PROPERTY_TYPES = true;

    /**
     * @var array Configuration options for all services.
     */
    private $config;

    /**
     * @param array $config Configuration option values for all services.
     */
    public function __construct(array $config = [])
    {
        $this->config = $config;
    }

    /**
     * @param string $name The method name.
     * @param array $args Arguments that will be passed to the method.
     *
     * @return \DTS\eBaySDK\Services\BaseService
     * @throws \BadMethodCallException
     */
    public function __call($name, array $args)
    {
        if (strpos($name, 'create') === 0) {
            return $this->createService(
                substr($name, 6),
                isset($args[0]) ? $args[0] : []
            );
        }

        throw new \BadMethodCallException("Unknown method: {$name}.");
    }

    /**
     * Create a service object by namespace. Service is configured using array of options.
     *
     * @param string $namespace Service namespace (e.g. Finding, Trading).
     * @param array  $config Configuration options for the service.
     *
     * @return \DTS\eBaySDK\Services\BaseService
     **/
    public function createService($namespace, array $config = [])
    {
        $configuration = $this->config;

        if (isset($this->config[$namespace])) {
            $configuration = arrayMergeDeep($configuration, $this->config[$namespace]);
        }

        $configuration = arrayMergeDeep($configuration, $config);

        $service = "DTS\\eBaySDK\\{$namespace}\\Services\\{$namespace}Service";

        return new $service($configuration);
    }
}


<?php
namespace DTS\eBaySDK\Trading\Services;

use DTS\eBaySDK\Trading\Types;

/**
 * Base class for the Trading service.
 */
class TradingBaseService extends \DTS\eBaySDK\Services\BaseService
{
    /**
     * HTTP header constant. The API version your application supports.
     */
    const HDR_API_VERSION = 'X-EBAY-API-COMPATIBILITY-LEVEL';

    /**
     * HTTP header constant. Your application ID.
     */
    const HDR_APP_ID = 'X-EBAY-API-APP-NAME';

    /**
     * HTTP header constant. The OAUTH Authentication Token that is used to validate the caller has permission to access the eBay servers.
     */
    const HDR_AUTHORIZATION = 'X-EBAY-API-IAF-TOKEN';

    /**
     * HTTP header constant. Your certificate ID.
     */
    const HDR_CERT_ID = 'X-EBAY-API-CERT-NAME';

    /**
     * HTTP header constant. Your developer ID.
     */
    const HDR_DEV_ID = 'X-EBAY-API-DEV-NAME';

    /**
     * HTTP header constant. The name of the operation you are calling.
     */
    const HDR_OPERATION_NAME = 'X-EBAY-API-CALL-NAME';

    /**
     * HTTP header constant. The site ID of the eBay site the request is for.
     */
    const HDR_SITE_ID = 'X-EBAY-API-SITEID';

    /**
     * @param array $config Configuration option values.
     */
    public function __construct(array $config)
    {
        parent::__construct('https://api.ebay.com/ws/api.dll', 'https://api.sandbox.ebay.com/ws/api.dll', $config);
    }

    /**
     * Returns definitions for each configuration option that is supported.
     *
     * @return array An associative array of configuration definitions.
     */
    public static function getConfigDefinitions()
    {
        $definitions = parent::getConfigDefinitions();

        return $definitions + [
            'apiVersion' => [
                'valid' => ['string'],
                'default' => \DTS\eBaySDK\Trading\Services\TradingService::API_VERSION,
                'required' => true
            ],
            'authorization' => [
                'valid' => ['string']
            ],
            'authToken' => [
                'valid' => ['string']
            ],
            'siteId' => [
                'valid' => ['int', 'string'],
                'required' => true
            ]
        ];
    }

    /**
     * Sends an API request.
     *
     * This method overrides the parent so that it can modify
     * the request object before it is handled by the parent class.
     *
     * @param string $name The name of the operation.
     * @param \DTS\eBaySDK\Types\BaseType $request Request object containing the request information.
     * @param string $responseClass The name of the PHP class that will be created from the XML response.
     *
     * @return \GuzzleHttp\Promise\PromiseInterface A promise that will be resolved with an object created from the XML response.
     */
    protected function callOperationAsync($name, \DTS\eBaySDK\Types\BaseType $request, $responseClass)
    {
        /**
         * Modify the request object to include the auth token that was set up in the configuration.
         */
        if ($this->getConfig('authorization') !== null) {
            /**
             * Don't send requester credentials if oauth authentication needed.
             */
            if (isset($request->RequesterCredentials)) {
                unset($request->RequesterCredentials);
            }
        } elseif ($this->getConfig('authToken') !== null) {
            /**
             * Don't modify a request if the token already exists.
             */
            if (!isset($request->RequesterCredentials)) {
                $request->RequesterCredentials = new \DTS\eBaySDK\Trading\Types\CustomSecurityHeaderType();
            }
            if (!isset($request->RequesterCredentials->eBayAuthToken)) {
                $request->RequesterCredentials->eBayAuthToken = $this->getConfig('authToken');
            }
        }

        return parent::callOperationAsync($name, $request, $responseClass);
    }

    /**
     * Builds the needed eBay HTTP headers.
     *
     * @param string $operationName The name of the operation been called.
     *
     * @return array An associative array of eBay http headers.
     */
    protected function getEbayHeaders($operationName)
    {
        $credentials = $this->getConfig('credentials');
        $appId = $credentials->getAppId();
        $certId = $credentials->getCertId();
        $devId = $credentials->getDevId();

        $headers = [];

        // Add required headers first.
        $headers[self::HDR_API_VERSION] = $this->getConfig('apiVersion');
        $headers[self::HDR_OPERATION_NAME] = $operationName;
        $headers[self::HDR_SITE_ID] = $this->getConfig('siteId');

        // Add optional headers.
        if ($appId) {
            $headers[self::HDR_APP_ID] = $appId;
        }

        if ($certId) {
            $headers[self::HDR_CERT_ID] = $certId;
        }

        if ($devId) {
            $headers[self::HDR_DEV_ID] = $devId;
        }

        if ($this->getConfig('authorization')) {
            $headers[self::HDR_AUTHORIZATION] = $this->getConfig('authorization');
        }

        /**
         * Note that we have no way of detecting that the actual request has an attachment.
         */
        if ($operationName === 'UploadSiteHostedPictures') {
            $headers['Content-Type'] = 'multipart/form-data;boundary="boundary"';
        }

        return $headers;
    }

    /**
     * Builds the request body string.
     *
     * @param \DTS\eBaySDK\Types\BaseType $request Request object containing the request information.
     *
     * @return string The request body.
     */
    protected function buildRequestBody(\DTS\eBaySDK\Types\BaseType $request)
    {
        if ($request->hasAttachment() && $request instanceof Types\UploadSiteHostedPicturesRequestType) {
            return $this->buildMultipartFormDataXMLPayload($request).$this->buildMultipartFormDataFilePayload($request->PictureName, $request->attachment());
        } else {
            return parent::buildRequestBody($request);
        }
    }
}


<?php
namespace DTS\eBaySDK\Parser;

/**
 * Responsible for parsing XML and returning a PHP object.
 */
class XmlParser
{
    /**
     * @var string The name of the PHP class that will be created.
     */
    private $rootObjectClass;

    /**
     * @var mixed The PHP object created from the XML.
     */
    private $rootObject;

    /**
     * @var \SplStack
     */
    private $metaStack;

    /**
     * @param string $rootObjectClass The name of the PHP class that will be created.
     */
    public function __construct($rootObjectClass)
    {
        $this->rootObjectClass = $rootObjectClass;

        $this->metaStack = new \SplStack();
    }

    /**
     * Parse the passed XML to create a PHP object.
     *
     * @param string $xml The xml string to parse.
     *
     * @return mixed A PHP object derived from DTS\eBaySDK\Types\BaseType
     */
    public function parse($xml)
    {
        $parser = xml_parser_create_ns('UTF-8', '@');

        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
        xml_set_object($parser, $this);
        xml_set_element_handler($parser, 'startElement', 'endElement');
        xml_set_character_data_handler($parser, 'cdata');

        xml_parse($parser, $xml, true);

        xml_parser_free($parser);

        return $this->rootObject;
    }

    /**
     * Handler for the parser that is called at the start of each XML element.
     *
     * @param resource $parser Reference to the XML parser calling the handler.
     * @param string $name The name of the element.
     * @param array $attributes Associative array of the element's attributes.
     */
    private function startElement($parser, $name, array $attributes)
    {
        $this->metaStack->push($this->getPhpMeta($this->normalizeElementName($name), $attributes));
    }

    /**
     * Handler for the parser that is called for character data.
     *
     * @param resource $parser Reference to the XML parser calling the handler.
     * @param string $cdata The character data.
     */
    private function cdata($parser, $cdata)
    {
        $this->metaStack->top()->strData .= $cdata;
    }

    /**
     * Handler for the parser that is called at the end of each XML element.
     *
     * Creates the required PHP object that represents the element and assigns
     * it to the parent PHP object.
     *
     * @param resource $parser Reference to the XML parser calling the handler.
     * @param string $name The name of the element.
     */
    private function endElement($parser, $name)
    {
        $meta = $this->metaStack->pop();

        if (!$this->metaStack->isEmpty()) {
            // Element in the XML may not exist as a property name in the class.
            // This could happen if the SDK is out of date with what eBay return.
            // It could also happen if eBay return elements that are not mentioned in any WSDL.
            if ($meta->propertyName !== '') {
                $parentObject = $this->getParentObject();
                // Parent object may not have been created if it didn't exist as a property name.
                if ($parentObject) {
                    if (!$meta->repeatable) {
                        $parentObject->{$meta->propertyName} = $this->getValueToAssign($meta);
                    } else {
                        $parentObject->{$meta->propertyName}[] = $this->getValueToAssign($meta);
                    }
                }
            }
        } else {
            $this->rootObject = $meta->phpObject;
        }
    }

    /**
     * Handles element names that may have namespaces in them.
     *
     * @param string $name Element name.
     *
     * @return string The element name stripped of any namespaces.
     */
    private function normalizeElementName($name)
    {
        $nsElement = explode('@', $name);

        if (count($nsElement) > 1) {
            array_shift($nsElement);
            return $nsElement[0];
        } else {
            return $name;
        }
    }

    /**
     * Returns the parent PHP object.
     *
     * @return mixed The parent PHP object.
     */
    private function getParentObject()
    {
        return $this->metaStack->top()->phpObject;
    }

    /**
     * Looks up the PHP meta data for the element.
     *
     * Allow the parser to build the required PHP object for an element.
     *
     * @param string $elementName The element name.
     * @param array $attributes Associative array of the element's attributes.
     *
     * @return \stdClass
     */
    private function getPhpMeta($elementName, array $attributes)
    {
        $meta = new \stdClass();
        $meta->propertyName = '';
        $meta->phpType = '';
        $meta->repeatable = false;
        $meta->attribute = false;
        $meta->elementName = '';
        $meta->strData = '';

        if (!$this->metaStack->isEmpty()) {
            $parentObject = $this->getParentObject();
            if ($parentObject) {
                $elementMeta = $parentObject->elementMeta($elementName);
                if ($elementMeta) {
                    $meta = $elementMeta;
                }
            }
        } else {
            $meta->phpType = $this->rootObjectClass;
        }

        $meta->phpObject = $this->newPhpObject($meta);

        if ($meta->phpObject) {
            foreach ($attributes as $attribute => $value) {
                // These attribute will simply not exist in a PHP object.
                if ('xmlns' === $attribute) {
                    continue;
                }
                $attributeMeta = $meta->phpObject->elementMeta($attribute);
                // Attribute in the XML may not exist as a property name in the class.
                // This could happen if the SDK is out of date with what eBay return.
                // It could also happen if eBay return elements that are not mentioned in any WSDL.
                if ($attributeMeta) {
                    $attributeMeta->strData = $value;
                    $meta->phpObject->{$attributeMeta->propertyName} = $this->getValueToAssignToProperty($attributeMeta);
                }
            }
        }

        return $meta;
    }

    /**
     * Builds the required PHP object.
     *
     * @param \stdClass $meta The PHP meta data.
     *
     * @return mixed A new PHP object or null.
     */
    private function newPhpObject(\stdClass $meta)
    {
        $phpTypes = explode('|', $meta->phpType);

        foreach ($phpTypes as $phpType) {
            switch ($phpType) {
                case 'integer':
                case 'string':
                case 'double':
                case 'boolean':
                case 'DateTime':
                    continue;
                default:
                    return $meta->phpType !== '' ? new $phpType() : null;
            }
        }

        return null;
    }

    /**
     * Returns a value that will be assigned to an object's property.
     *
     * @param \stdClass $meta The PHP meta data.
     *
     * @return mixed The value to assign.
     */
    private function getValueToAssign(\stdClass $meta)
    {
        if ($this->isSimplePhpType($meta)) {
            return $this->getValueToAssignToProperty($meta);
        } else {
            if ($this->setByValue($meta)) {
                $meta->phpObject->value = $this->getValueToAssignToValue($meta);
            }
            return $meta->phpObject;
        }
    }

    /**
     * Determines if the type of the property is simple.
     *
     * @param \stdClass $meta The PHP meta data.
     *
     * @return bool True if the property type is simple.
     */
    private function isSimplePhpType(\stdClass $meta)
    {
        $phpTypes = explode('|', $meta->phpType);

        foreach ($phpTypes as $phpType) {
            switch ($phpType) {
                case 'integer':
                case 'string':
                case 'double':
                case 'boolean':
                case 'DateTime':
                    continue;
                default:
                    return false;
            }
        }

        return true;
    }

    /**
     * Determines if the the property of an object is set by a _value_ property.
     *
     * @param \stdClass $meta The PHP meta data.
     *
     * @return bool True if the property needs to be set by _value_.
     */
    private function setByValue(\stdClass $meta)
    {
        return (
            is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\Base64BinaryType', false) ||
            is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\BooleanType', false) ||
            is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\DecimalType', false) ||
            is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\DoubleType', false) ||
            is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\IntegerType', false) ||
            is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\StringType', false) ||
            is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\TokenType', false) ||
            is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\URIType', false)
        );
    }

    /**
     * Returns a value that will be assigned to an object's property.
     *
     * @param \stdClass $meta The PHP meta data.
     *
     * @return mixed The value to assign.
     */
    private function getValueToAssignToProperty(\stdClass $meta)
    {
        switch ($meta->phpType) {
            case 'integer':
                return (integer)$meta->strData;
            case 'double':
                return (double)$meta->strData;
            case 'boolean':
                return strtolower($meta->strData) === 'true';
            case 'DateTime':
                return new \DateTime($meta->strData, new \DateTimeZone('UTC'));
            case 'string':
            default:
                return $meta->strData;
        }
    }

    /**
     * Returns a value that will be assigned to an object's _value_ property.
     *
     * @param \stdClass $meta The PHP meta data.
     *
     * @return mixed The value to assign.
     */
    private function getValueToAssignToValue(\stdClass $meta)
    {
        if (is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\Base64BinaryType', false)) {
            return $meta->strData;
        } elseif (is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\BooleanType', false)) {
            return strtolower($meta->strData) === 'true';
        } elseif (is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\DecimalType', false)) {
            return is_int(0 + $meta->strData) ? (integer)$meta->strData : (double)$meta->strData;
        } elseif (is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\DoubleType', false)) {
            return (double)$meta->strData;
        } elseif (is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\IntegerType', false)) {
            return (integer)$meta->strData;
        } elseif (is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\StringType', false)) {
            return $meta->strData;
        } elseif (is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\TokenType', false)) {
            return $meta->strData;
        } elseif (is_subclass_of($meta->phpObject, '\DTS\eBaySDK\Types\URIType', false)) {
            return $meta->strData;
        }

        return $meta->strData;
    }
}


<?php
namespace DTS\eBaySDK\Analytics\Services;

/**
 * Base class for the Analytics service.
 */
class AnalyticsBaseService extends \DTS\eBaySDK\Services\BaseRestService
{
    /**
     * @var array $endPoints The API endpoints.
     */
    protected static $endPoints = [
        'sandbox'    => 'https://api.sandbox.ebay.com/sell/analytics',
        'production' => 'https://api.ebay.com/sell/analytics'
    ];

    /**
     * HTTP header constant. The Authentication Token that is used to validate the caller has permission to access the eBay servers.
     */
    const HDR_AUTHORIZATION = 'Authorization';

    /**
     * HTTP header constant. The global ID of the eBay site on which the transaction took place.
     */
    const HDR_MARKETPLACE_ID = 'X-EBAY-C-MARKETPLACE-ID';

    /**
     * @param array $config Configuration option values.
     */
    public function __construct(array $config)
    {
        parent::__construct($config);
    }

    /**
     * Returns definitions for each configuration option that is supported.
     *
     * @return array An associative array of configuration definitions.
     */
    public static function getConfigDefinitions()
    {
        $definitions = parent::getConfigDefinitions();

        return $definitions + [
            'apiVersion' => [
                'valid' => ['string'],
                'default' => \DTS\eBaySDK\Analytics\Services\AnalyticsService::API_VERSION,
                'required' => true
            ],
            'authorization' => [
                'valid' => ['string'],
                'required' => true
            ],
            'marketplaceId' => [
                'valid' => ['string']
            ]
        ];
    }

    /**
     * Builds the needed eBay HTTP headers.
     *
     * @return array An associative array of eBay HTTP headers.
     */
    protected function getEbayHeaders()
    {
        $headers = [];

        // Add required headers first.
        $headers[self::HDR_AUTHORIZATION] = 'Bearer '.$this->getConfig('authorization');

        // Add optional headers.
        if ($this->getConfig('marketplaceId')) {
            $headers[self::HDR_MARKETPLACE_ID] = $this->getConfig('marketplaceId');
        }

        return $headers;
    }
}


<?php
namespace DTS\eBaySDK\Parser;

class JsonParser
{
    /**
     * @param mixed A PHP object derived from DTS\eBaySDK\Types\BaseType
     * @param string $json The JSON string to parse.
     */
    public static function parseAndAssignProperties($object, $json)
    {
        $properties = $json !== '' ? json_decode($json, true) : [];
        self::assignProperties($object, $properties);
    }

    private static function assignProperties(\DTS\eBaySDK\Types\BaseType $object, array $properties)
    {
        foreach ($properties as $property => $value) {
            $propertyMeta = $object->elementMeta($property);
            // Property in the JSON may not exist as a property name in the class.
            // This could happen if the SDK is out of date with what eBay return.
            // It could also happen if eBay return elements that are not mentioned in any documentation!
            if ($propertyMeta) {
                $value = self::removeNull($value);
                if (!is_null($value)) {
                    $actualValue = self::determineActualValueToAssign($propertyMeta, $value);
                    $object->{$propertyMeta->propertyName} = $actualValue;
                }
            }
        }
    }

    /**
     * @param mixed $value Remove null elements if an array.
     *
     * @return mixed Original value if not an array or array without null elements.
     */
    private static function removeNull($value)
    {
        if (!is_array($value)) {
            return $value;
        }

        return array_filter($value, function ($val) {
            return !is_null($val);
        });
    }

    /**
     * @param \stdClass $propertyMeta The metadata for the property.
     * @param mixed $value The property value.
     *
     * @return mixed
     */
    private static function determineActualValueToAssign($propertyMeta, $value)
    {
        if ($propertyMeta->repeatable && is_array($value)) {
            $values = [];
            foreach ($value as $val) {
                $values[] = self::actualValue($propertyMeta, $val);
            }
            return $values;
        }

        return self::actualValue($propertyMeta, $value);
    }

    /**
     * @param \stdClass $propertyMeta The metadata for the property.
     * @param mixed $value The property value.
     *
     * @return mixed
     */
    private static function actualValue(\stdClass $propertyMeta, $value)
    {
        /**
         * Shortcut. Objects can be assigned as is.
         */
        if (is_object($value)) {
            return $value;
        }

        $types = explode('|', $propertyMeta->phpType);

        foreach ($types as $type) {
            switch ($type) {
                case 'integer':
                case 'string':
                case 'double':
                case 'boolean':
                case 'any':
                    return $value;
                case 'DateTime':
                    return new \DateTime($value, new \DateTimeZone('UTC'));
            }
        }

        $object = new $propertyMeta->phpType();
        self::assignProperties($object, $value);
        return $object;
    }
}


<?php
namespace DTS\eBaySDK\Inventory\Services;

/**
 * Base class for the Inventory service.
 */
class InventoryBaseService extends \DTS\eBaySDK\Services\BaseRestService
{
    /**
     * @var array $endPoints The API endpoints.
     */
    protected static $endPoints = [
        'sandbox'    => 'https://api.sandbox.ebay.com/sell/inventory',
        'production' => 'https://api.ebay.com/sell/inventory'
    ];

    /**
     * HTTP header constant. The Authentication Token that is used to validate the caller has permission to access the eBay servers.
     */
    const HDR_AUTHORIZATION = 'Authorization';

    /**
     * HTTP header constant. The global ID of the eBay site on which the transaction took place.
     */
    const HDR_MARKETPLACE_ID = 'X-EBAY-C-MARKETPLACE-ID';

    /**
     * @param array $config Configuration option values.
     */
    public function __construct(array $config)
    {
        parent::__construct($config);
    }

    /**
     * Returns definitions for each configuration option that is supported.
     *
     * @return array An associative array of configuration definitions.
     */
    public static function getConfigDefinitions()
    {
        $definitions = parent::getConfigDefinitions();

        return $definitions + [
            'apiVersion' => [
                'valid' => ['string'],
                'default' => \DTS\eBaySDK\Inventory\Services\InventoryService::API_VERSION,
                'required' => true
            ],
            'authorization' => [
                'valid' => ['string'],
                'required' => true
            ],
            'marketplaceId' => [
                'valid' => ['string']
            ]
        ];
    }

    /**
     * Builds the needed eBay HTTP headers.
     *
     * @return array An associative array of eBay HTTP headers.
     */
    protected function getEbayHeaders()
    {
        $headers = [];

        // Add required headers first.
        $headers[self::HDR_AUTHORIZATION] = 'Bearer '.$this->getConfig('authorization');

        // Add optional headers.
        if ($this->getConfig('marketplaceId')) {
            $headers[self::HDR_MARKETPLACE_ID] = $this->getConfig('marketplaceId');
        }

        return $headers;
    }
}