From 0922c4dea8de33204628f2f6b3fa059bd3b575d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Lorente?= Date: Tue, 29 May 2018 19:13:14 +0200 Subject: [PATCH] Initial commit with InAppEvent endpoint. --- LICENSE | 13 + composer.json | 28 +++ src/AmountConverter.php | 44 ++++ src/Api/InAppEvent.php | 52 ++++ src/Appsflyer.php | 250 ++++++++++++++++++++ src/Config.php | 133 +++++++++++ src/ConfigInterface.php | 69 ++++++ src/Core/Api.php | 203 ++++++++++++++++ src/Core/ApiInterface.php | 119 ++++++++++ src/Exception/ApiLimitExceededException.php | 33 +++ src/Exception/AppsflyerException.php | 145 ++++++++++++ src/Exception/BadRequestException.php | 25 ++ src/Exception/Handler.php | 125 ++++++++++ src/Exception/InvalidRequestException.php | 25 ++ src/Exception/MissingParameterException.php | 25 ++ src/Exception/NotFoundException.php | 25 ++ src/Exception/ServerErrorException.php | 25 ++ src/Exception/UnauthorizedException.php | 25 ++ src/Pager.php | 106 +++++++++ src/Utility.php | 65 +++++ 20 files changed, 1535 insertions(+) create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 src/AmountConverter.php create mode 100644 src/Api/InAppEvent.php create mode 100644 src/Appsflyer.php create mode 100644 src/Config.php create mode 100644 src/ConfigInterface.php create mode 100644 src/Core/Api.php create mode 100644 src/Core/ApiInterface.php create mode 100644 src/Exception/ApiLimitExceededException.php create mode 100644 src/Exception/AppsflyerException.php create mode 100644 src/Exception/BadRequestException.php create mode 100644 src/Exception/Handler.php create mode 100644 src/Exception/InvalidRequestException.php create mode 100644 src/Exception/MissingParameterException.php create mode 100644 src/Exception/NotFoundException.php create mode 100644 src/Exception/ServerErrorException.php create mode 100644 src/Exception/UnauthorizedException.php create mode 100644 src/Pager.php create mode 100644 src/Utility.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e3ba52e --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +The BSD 3-Clause License +Copyright (c) 2011-2018, Cartalyst LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of Cartalyst LLC and its libraries nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e7985f8 --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "jlorente/appsflyer", + "description": "An Appsflyer API package.", + "keywords": [ + "php", + "jlorente", + "appsflyer" + ], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "José Lorente", + "email": "jose.lorente.martin@gmail.com" + } + ], + "require": { + "php": ">=5.6", + "guzzlehttp/guzzle": "~6.0" + }, + "require-dev": { + "mockery/mockery": "~0.9", + "phpunit/phpunit": "~4.8" + }, + "autoload": { + "psr-4": { "Jlorente\\Appsflyer\\": "src/" } + }, + "minimum-stability": "stable" +} diff --git a/src/AmountConverter.php b/src/AmountConverter.php new file mode 100644 index 0000000..692fc68 --- /dev/null +++ b/src/AmountConverter.php @@ -0,0 +1,44 @@ + + */ +class InAppEvent extends Api +{ + + /** + * {@inheritdoc} + */ + public function baseUrl() + { + return 'https://api2.appsflyer.com'; + } + + /** + * Creates a new in-app event. + * + * @param string $appId + * @param array $parameters + * @return array + */ + public function create($appId, array $parameters = []) + { + return $this->_post("inappevent/$appId", $parameters); + } + +} diff --git a/src/Appsflyer.php b/src/Appsflyer.php new file mode 100644 index 0000000..01dd099 --- /dev/null +++ b/src/Appsflyer.php @@ -0,0 +1,250 @@ +config = new Config(self::VERSION, $devKey, $apiToken); + } + + /** + * Create a new Appsflyer API instance. + * + * @param string $devKey + * @param string $apiToken + * @return \Jlorente\Appsflyer\Appsflyer + */ + public static function make($devKey = null, $apiToken = null) + { + return new static($devKey, $apiToken); + } + + /** + * Returns the current package version. + * + * @return string + */ + public static function getVersion() + { + return self::VERSION; + } + + /** + * Returns the Config repository instance. + * + * @return \Jlorente\Appsflyer\ConfigInterface + */ + public function getConfig() + { + return $this->config; + } + + /** + * Sets the Config repository instance. + * + * @param \Jlorente\Appsflyer\ConfigInterface $config + * @return $this + */ + public function setConfig(ConfigInterface $config) + { + $this->config = $config; + + return $this; + } + + /** + * Returns the Appsflyer API key. + * + * @return string + */ + public function getApiKey() + { + return $this->config->getApiKey(); + } + + /** + * Sets the Appsflyer API key. + * + * @param string $devKey + * @return $this + */ + public function setApiKey($devKey) + { + $this->config->setApiKey($devKey); + + return $this; + } + + /** + * Returns the Appsflyer API version. + * + * @return string + */ + public function getApiToken() + { + return $this->config->getApiToken(); + } + + /** + * Sets the Appsflyer API version. + * + * @param string $apiToken + * @return $this + */ + public function setApiToken($apiToken) + { + $this->config->setApiToken($apiToken); + + return $this; + } + + /** + * Returns the amount converter class and method name. + * + * @return string + */ + public static function getAmountConverter() + { + return static::$amountConverter; + } + + /** + * Sets the amount converter class and method name. + * + * @param $amountConverter string + * @return void + */ + public static function setAmountConverter($amountConverter) + { + static::$amountConverter = $amountConverter; + } + + /** + * Disables the amount converter. + * + * @return void + */ + public static function disableAmountConverter() + { + static::setAmountConverter(null); + } + + /** + * Returns the default amount converter. + * + * @return string + */ + public static function getDefaultAmountConverter() + { + return '\\Jlorente\\Appsflyer\\AmountConverter::convert'; + } + + /** + * Sets the default amount converter; + * + * @return void + */ + public static function setDefaultAmountConverter() + { + static::setAmountConverter( + static::getDefaultAmountConverter() + ); + } + + /** + * Dynamically handle missing methods. + * + * @param string $method + * @param array $parameters + * @return \Jlorente\Appsflyer\Api\ApiInterface + */ + public function __call($method, array $parameters) + { + if ($this->isIteratorRequest($method)) { + $apiInstance = $this->getApiInstance(substr($method, 0, -11)); + + return (new Pager($apiInstance))->fetch($parameters); + } + + return $this->getApiInstance($method); + } + + /** + * Determines if the request is an iterator request. + * + * @return bool + */ + protected function isIteratorRequest($method) + { + return substr($method, -8) === 'Iterator'; + } + + /** + * Returns the Api class instance for the given method. + * + * @param string $method + * @return \Jlorente\Appsflyer\Api\ApiInterface + * @throws \BadMethodCallException + */ + protected function getApiInstance($method) + { + $class = "\\Jlorente\\Appsflyer\\Api\\" . ucwords($method); + + if (class_exists($class) && !(new ReflectionClass($class))->isAbstract()) { + return new $class($this->config); + } + + throw new \BadMethodCallException("Undefined method [{$method}] called."); + } + +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..3d54229 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,133 @@ +setVersion($version); + + $this->setDevKey($devKey ?: getenv('APPSFLYER_DEV_KEY')); + + $this->setApiToken($apiToken ?: getenv('APPSFLYER_API_TOKEN')); + + if (!$this->devKey) { + throw new \RuntimeException('The Appsflyer dev_key is not defined!'); + } + + if (!$this->devKey) { + throw new \RuntimeException('The Appsflyer dev_key is not defined!'); + } + } + + /** + * {@inheritdoc} + */ + public function getVersion() + { + return $this->version; + } + + /** + * {@inheritdoc} + */ + public function setVersion($version) + { + $this->version = $version; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getDevKey() + { + return $this->devKey; + } + + /** + * {@inheritdoc} + */ + public function setDevKey($devKey) + { + $this->devKey = $devKey; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getApiToken() + { + return $this->apiVersion; + } + + /** + * {@inheritdoc} + */ + public function setApiToken($apiToken) + { + $this->apiVersion = $apiToken; + + return $this; + } + +} diff --git a/src/ConfigInterface.php b/src/ConfigInterface.php new file mode 100644 index 0000000..1b162e9 --- /dev/null +++ b/src/ConfigInterface.php @@ -0,0 +1,69 @@ +config = $config; + } + + /** + * {@inheritdoc} + */ + public function baseUrl() + { + return 'https://hq.appsflyer.com'; + } + + /** + * {@inheritdoc} + */ + public function getPerPage() + { + return $this->perPage; + } + + /** + * {@inheritdoc} + */ + public function setPerPage($perPage) + { + $this->perPage = (int) $perPage; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function _get($url = null, $parameters = []) + { + if ($perPage = $this->getPerPage()) { + $parameters['limit'] = $perPage; + } + + return $this->execute('get', $url, $parameters); + } + + /** + * {@inheritdoc} + */ + public function _head($url = null, array $parameters = []) + { + return $this->execute('head', $url, $parameters); + } + + /** + * {@inheritdoc} + */ + public function _delete($url = null, array $parameters = []) + { + return $this->execute('delete', $url, $parameters); + } + + /** + * {@inheritdoc} + */ + public function _put($url = null, array $parameters = []) + { + return $this->execute('put', $url, $parameters); + } + + /** + * {@inheritdoc} + */ + public function _patch($url = null, array $parameters = []) + { + return $this->execute('patch', $url, $parameters); + } + + /** + * {@inheritdoc} + */ + public function _post($url = null, array $parameters = []) + { + return $this->execute('post', $url, $parameters); + } + + /** + * {@inheritdoc} + */ + public function _options($url = null, array $parameters = []) + { + return $this->execute('options', $url, $parameters); + } + + /** + * {@inheritdoc} + */ + public function execute($httpMethod, $url, array $parameters = []) + { + try { + $parameters = Utility::prepareParameters($parameters); + + $response = $this->getClient()->{$httpMethod}('/' . $url, ['query' => $parameters]); + + return json_decode((string) $response->getBody(), true); + } catch (ClientException $e) { + new Handler($e); + } + } + + /** + * Returns an Http client instance. + * + * @return \GuzzleHttp\Client + */ + protected function getClient() + { + return new Client([ + 'base_uri' => $this->baseUrl(), 'handler' => $this->createHandler() + ]); + } + + /** + * Create the client handler. + * + * @return \GuzzleHttp\HandlerStack + */ + protected function createHandler() + { + $stack = HandlerStack::create(); + + $stack->push(Middleware::mapRequest(function (RequestInterface $request) { + $config = $this->config; + + $request = $request->withHeader('authentication', base64_encode($config->getApiKey())); + + return $request; + })); + + $stack->push(Middleware::retry(function ($retries, RequestInterface $request, ResponseInterface $response = null, TransferException $exception = null) { + return $retries < 3 && ($exception instanceof ConnectException || ($response && $response->getStatusCode() >= 500)); + }, function ($retries) { + return (int) pow(2, $retries) * 1000; + })); + + return $stack; + } + +} diff --git a/src/Core/ApiInterface.php b/src/Core/ApiInterface.php new file mode 100644 index 0000000..b6e59fa --- /dev/null +++ b/src/Core/ApiInterface.php @@ -0,0 +1,119 @@ +errorCode; + } + + /** + * Sets the error type returned by Appsflyer. + * + * @param string $errorCode + * @return $this + */ + public function setErrorCode($errorCode) + { + $this->errorCode = $errorCode; + + return $this; + } + + /** + * Returns the error type returned by Appsflyer. + * + * @return string + */ + public function getErrorType() + { + return $this->errorType; + } + + /** + * Sets the error type returned by Appsflyer. + * + * @param string $errorType + * @return $this + */ + public function setErrorType($errorType) + { + $this->errorType = $errorType; + + return $this; + } + + /** + * Returns missing parameter returned by Appsflyer with the error. + * + * @return string + */ + public function getMissingParameter() + { + return $this->missingParameter; + } + + /** + * Sets the missing parameter returned by Appsflyer with the error. + * + * @param string $missingParameter + * @return $this + */ + public function setMissingParameter($missingParameter) + { + $this->missingParameter = $missingParameter; + + return $this; + } + + /** + * Returns raw output returned by Appsflyer in case of exception. + * + * @return string + */ + public function getRawOutput() + { + return $this->rawOutput; + } + + /** + * Sets the raw output parameter returned by Appsflyer in case of exception. + * + * @param string $rawOutput + * @return $this + */ + public function setRawOutput($rawOutput) + { + $this->rawOutput = $rawOutput; + + return $this; + } + +} diff --git a/src/Exception/BadRequestException.php b/src/Exception/BadRequestException.php new file mode 100644 index 0000000..042433c --- /dev/null +++ b/src/Exception/BadRequestException.php @@ -0,0 +1,25 @@ + 'CardError', + ]; + + /** + * List of mapped exceptions and their corresponding status codes. + * + * @var array + */ + protected $exceptionsByStatusCode = [ + // Often missing a required parameter + 400 => 'BadRequest', + // Invalid Appsflyer API key provided + 401 => 'Unauthorized', + // Parameters were valid but request failed + 402 => 'InvalidRequest', + // The requested item doesn't exist + 404 => 'NotFound', + // Something went wrong on Appsflyer's end + 500 => 'ServerError', + 502 => 'ServerError', + 503 => 'ServerError', + 504 => 'ServerError', + ]; + + /** + * Constructor. + * + * @param \GuzzleHttp\Exception\ClientException $exception + * @return void + * @throws \Jlorente\Appsflyer\Exception\AppsflyerException + */ + public function __construct(ClientException $exception) + { + $response = $exception->getResponse(); + + $statusCode = $response->getStatusCode(); + + $rawOutput = json_decode($response->getBody(true), true); + + $error = isset($rawOutput['error']) ? $rawOutput['error'] : []; + + $errorCode = isset($error['code']) ? $error['code'] : null; + + $errorType = isset($error['type']) ? $error['type'] : null; + + $message = isset($error['message']) ? $error['message'] : null; + + $missingParameter = isset($error['param']) ? $error['param'] : null; + + $this->handleException( + $message, $statusCode, $errorType, $errorCode, $missingParameter, $rawOutput + ); + } + + /** + * Guesses the FQN of the exception to be thrown. + * + * @param string $message + * @param int $statusCode + * @param string $errorType + * @param string $errorCode + * @param string $missingParameter + * @return void + * @throws \Jlorente\Appsflyer\Exception\AppsflyerException + */ + protected function handleException($message, $statusCode, $errorType, $errorCode, $missingParameter, $rawOutput) + { + if ($statusCode === 400 && $errorCode === 'rate_limit') { + $class = 'ApiLimitExceeded'; + } elseif ($statusCode === 400 && $errorType === 'invalid_request_error') { + $class = 'MissingParameter'; + } elseif (array_key_exists($errorType, $this->exceptionsByErrorType)) { + $class = $this->exceptionsByErrorType[$errorType]; + } elseif (array_key_exists($statusCode, $this->exceptionsByStatusCode)) { + $class = $this->exceptionsByStatusCode[$statusCode]; + } else { + $class = 'Appsflyer'; + } + + $class = "\\Jlorente\\Appsflyer\\Exception\\{$class}Exception"; + + $instance = new $class($message, $statusCode); + + $instance->setErrorCode($errorCode); + $instance->setErrorType($errorType); + $instance->setMissingParameter($missingParameter); + $instance->setRawOutput($rawOutput); + + throw $instance; + } + +} diff --git a/src/Exception/InvalidRequestException.php b/src/Exception/InvalidRequestException.php new file mode 100644 index 0000000..54f88ea --- /dev/null +++ b/src/Exception/InvalidRequestException.php @@ -0,0 +1,25 @@ +api = $api; + } + + /** + * Fetches all the objects of the given api. + * + * @param array $parameters + * @return array + */ + public function fetch(array $parameters = []) + { + $this->api->setPerPage(100); + + $results = $this->processRequest($parameters); + + while ($this->nextToken) { + $results = array_merge($results, $this->processRequest($parameters)); + } + + return $results; + } + + /** + * Processes the api request. + * + * @param array $parameters + * @return array + */ + protected function processRequest(array $parameters = []) + { + if ($this->nextToken) { + $parameters['starting_after'] = $this->nextToken; + } + + if (isset($parameters[0])) { + $id = $parameters[0]; + + unset($parameters[0]); + + if (isset($parameters[1])) { + $parameters = $parameters[1]; + + unset($parameters[1]); + } + + $parameters = [$id, $parameters]; + } else { + $parameters = [$parameters]; + } + + $result = call_user_func_array([$this->api, 'all'], $parameters); + + $this->nextToken = $result['has_more'] ? end($result['data'])['id'] : false; + + return $result['data']; + } + +} diff --git a/src/Utility.php b/src/Utility.php new file mode 100644 index 0000000..bcd2b46 --- /dev/null +++ b/src/Utility.php @@ -0,0 +1,65 @@ +