diff --git a/.gitignore b/.gitignore index f13d388b..59f07e53 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ composer.phar vendor/ .env index.php -storage.txt \ No newline at end of file +storage.txt +composer.lock \ No newline at end of file diff --git a/composer.json b/composer.json index a72c56ee..2a48c31b 100644 --- a/composer.json +++ b/composer.json @@ -15,15 +15,9 @@ "email": "info@picqer.com" } ], - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/picqer/guzzle-oauth2-plugin" - } - ], "require": { - "php": ">=5.4.0", - "commerceguys/guzzle-oauth2-plugin": "~1.0" + "php": ">=5.5.0", + "guzzlehttp/guzzle": "~6.0" }, "autoload": { "psr-4": { diff --git a/example.php b/example.php new file mode 100644 index 00000000..7de1eafb --- /dev/null +++ b/example.php @@ -0,0 +1,88 @@ +setRedirectUrl('YOUR_REDIRECT_URL'); + $connection->setExactClientId('YOUR_CLIENT_ID'); + $connection->setExactClientSecret('YOUR_CLIENT_SECRET'); + $connection->redirectForAuthorization(); +} + +function connect() +{ + $connection = new \Picqer\Financials\Exact\Connection(); + $connection->setRedirectUrl('YOUR_REDIRECT_URL'); + $connection->setExactClientId('YOUR_CLIENT_ID'); + $connection->setExactClientSecret('YOUR_CLIENT_SECRET'); + + if (getValue('authorizationcode')) // Retrieves authorizationcode from database + { + $connection->setAuthorizationCode(getValue('authorizationcode')); + } + + if (getValue('accesstoken')) // Retrieves accesstoken from database + { + $connection->setAccessToken(getValue('accesstoken')); + } + + if (getValue('refreshtoken')) // Retrieves refreshtoken from database + { + $connection->setRefreshToken(getValue('refreshtoken')); + } + + if (getValue('expires_in')) // Retrieves expires from database + { + $connection->setTokenExpires(getValue('expires_in')); + } + + // Make the client connect and exchange tokens + try { + $connection->connect(); + } catch (\Exception $e) { + throw new Exception('Could not connect to Exact: ' . $e->getMessage()); + } + + // Save the new tokens for next connections + setValue('accesstoken', $connection->getAccessToken()); + setValue('refreshtoken', $connection->getRefreshToken()); + setValue('expires_in', $connection->getTokenExpires()); + + return $connection; +} + +if (isset($_GET['code']) && is_null(getValue('authorizationcode'))) { + setValue('authorizationcode', $_GET['code']); +} + +$connection = connect(); + +try { + $journals = new \Picqer\Financials\Exact\Journal($connection); + $result = $journals->get(); + foreach ($result as $journal) { + echo 'journal: ' . $journal->Description . '
'; + } + + echo 'done'; +} catch (\Exception $e) { + echo get_class($e) . ' : ' . $e->getMessage(); +} diff --git a/example/example.php b/example/example.php index 448c3109..ed05b2b3 100644 --- a/example/example.php +++ b/example/example.php @@ -72,7 +72,7 @@ function connect() // Make the client connect and exchange tokens try { - $connection->client(); + $connection->connect(); } catch (\Exception $e) { throw new Exception('Could not connect to Exact: ' . $e->getMessage()); } diff --git a/src/Picqer/Financials/Exact/Connection.php b/src/Picqer/Financials/Exact/Connection.php index 4269a8c3..34193271 100644 --- a/src/Picqer/Financials/Exact/Connection.php +++ b/src/Picqer/Financials/Exact/Connection.php @@ -1,28 +1,62 @@ client) return $this->client; - - $this->client = new Client($this->apiUrl); - $this->client->setDefaultOption('headers/Accept', 'application/json'); - $this->client->setDefaultOption('headers/Content-Type', 'application/json'); + if ($this->client) { + return $this->client; + } - $this->authenticate(); + $this->client = new Client([ + 'http_errors' => true, + ]); return $this->client; } - public function get($url, array $params = []) + public function connect() { - if ($url !== 'current/Me') $this->addDivisionNumberToApiUrl(); + // Redirect for authorization if needed (no access token or refresh token given) + if ($this->needsAuthentication()) { + $this->redirectForAuthorization(); + } + + // If access token is not set or token has expired, acquire new token + if (empty($this->accessToken) || $this->tokenHasExpired()) { + $this->acquireAccessToken(); + } - $request = $this->client()->createRequest('GET', $url); + $client = $this->client(); - $query = $request->getQuery(); + return $client; + } - foreach ($params as $paramName => $paramValue) - { - $query->set($paramName, $paramValue); + /** + * @param string $method + * @param $endpoint + * @param null $body + * @param array $params + * @param array $headers + * @return Request + */ + private function createRequest($method = 'GET', $endpoint, $body = null, array $params = [], array $headers = []) + { + // Add default json headers to the request + $headers = array_merge($headers, [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json' + ]); + + // If access token is not set or token has expired, acquire new token + if (empty($this->accessToken) || $this->tokenHasExpired()) { + $this->acquireAccessToken(); } - $result = $this->client()->send($request); + // If we have a token, sign the request + if (!$this->needsAuthentication() && !empty($this->accessToken)) { + $headers['Authorization'] = 'Bearer ' . $this->accessToken; + } + + // Create param string + if (!empty($params)) { + $endpoint .= '?' . http_build_query($params); + } + + // Create the request + $request = new Request($method, $endpoint, $headers, $body); - return $this->parseResult($result); + return $request; } + /** + * @param $url + * @param array $params + * @return mixed + * @throws ApiException + */ + public function get($url, array $params = []) + { + $url = $this->formatUrl($url, $url !== 'current/Me'); + + $request = $this->createRequest('GET', $url, null, $params); + + $response = $this->client()->send($request); + + return $this->parseResponse($response); + } + + /** + * @param $url + * @param $body + * @return mixed + * @throws ApiException + */ public function post($url, $body) { - $this->addDivisionNumberToApiUrl(); - try - { - $result = $this->client()->post($url, null, $body)->send(); - } catch (ServerErrorResponseException $e) - { - throw new ApiException($e->getResponse()->getBody(true)); + $url = $this->formatUrl($url); + + try { + $request = $this->createRequest('POST', $url, $body); + $response = $this->client()->send($request); + } catch (\Exception $e) { + throw new ApiException($e->getMessage()); } - return $this->parseResult($result); + return $this->parseResponse($response); } + /** + * @param $url + * @param $body + * @return mixed + * @throws ApiException + */ public function put($url, $body) { - $this->addDivisionNumberToApiUrl(); - $result = $this->client()->put($url, null, $body)->send(); + $url = $this->formatUrl($url); + + $request = $this->createRequest('PUT', $url, $body); + $response = $this->client()->send($request); - return $this->parseResult($result); + return $this->parseResponse($response); } + /** + * @param $url + * @return mixed + * @throws ApiException + */ public function delete($url) { - $this->addDivisionNumberToApiUrl(); - $result = $this->client()->delete($url)->send(); + $url = $this->formatUrl($url); + + $request = $this->createRequest('DELETE', $url); + $response = $this->client()->send($request); - return $this->parseResult($result); + return $this->parseResponse($response); } - public function getAuthUrl() + /** + * @return string + */ + private function getAuthUrl() { return $this->authUrl . '?' . http_build_query(array( - 'client_id' => $this->exactClientId, - 'redirect_uri' => $this->redirectUrl, + 'client_id' => $this->exactClientId, + 'redirect_uri' => $this->redirectUrl, 'response_type' => 'code' )); } @@ -140,36 +254,14 @@ public function setRefreshToken($refreshToken) $this->refreshToken = $refreshToken; } - public function authenticate() - { - $params = array( - 'code' => $this->authorizationCode, - 'redirect_uri' => $this->redirectUrl, - 'client_id' => $this->exactClientId, - 'client_secret' => $this->exactClientSecret, - 'refresh_token' => $this->refreshToken - ); - - $oauth2Client = new Client($this->tokenUrl); - $grantType = new AuthorizationCode($oauth2Client, $params); - $refreshTokenGrantType = new RefreshToken($oauth2Client, $params); - $oauth2Plugin = new Oauth2Plugin($grantType, $refreshTokenGrantType); - if (! empty($this->refreshToken)) - $oauth2Plugin->setRefreshToken($this->refreshToken); - - $this->accessToken = $oauth2Plugin->getAccessToken(); - $this->refreshToken = $oauth2Plugin->getRefreshToken(); - - $this->client()->addSubscriber($oauth2Plugin); - } + /** + * + */ public function redirectForAuthorization() { - header('Location: ' . $this->getAuthUrl( - $this->exactClientId, - $this->redirectUrl - ) - ); + $authUrl = $this->getAuthUrl(); + header('Location: ' . $authUrl); exit; } @@ -181,22 +273,26 @@ public function setRedirectUrl($redirectUrl) $this->redirectUrl = $redirectUrl; } + /** + * @return bool + */ public function needsAuthentication() { return empty($this->refreshToken) && empty($this->authorizationCode); } - public function parseResult(Response $response) + /** + * @param Response $response + * @return mixed + * @throws ApiException + */ + private function parseResponse(Response $response) { - try - { - $json = $response->json(); - if (array_key_exists('d', $json)) - { - if (array_key_exists('results', $json['d'])) - { - if (count($json['d']['results']) == 1) - { + try { + $json = json_decode($response->getBody()->getContents(), true); + if (array_key_exists('d', $json)) { + if (array_key_exists('results', $json['d'])) { + if (count($json['d']['results']) == 1) { return $json['d']['results'][0]; } @@ -207,21 +303,22 @@ public function parseResult(Response $response) } return $json; - } catch (\RuntimeException $e) - { + } catch (\RuntimeException $e) { throw new ApiException($e->getMessage()); } } - public function addDivisionNumberToApiUrl() + /** + * @return mixed + */ + private function getCurrentDivisionNumber() { - if (empty($this->division)) - { - $me = new Me($this); + if (empty($this->division)) { + $me = new Me($this); $this->division = $me->find()->CurrentDivision; } - $this->client()->setBaseUrl($this->apiUrl . '/' . $this->division); + return $this->division; } /** @@ -240,6 +337,98 @@ public function getAccessToken() return $this->accessToken; } + private function acquireAccessToken() + { + // If refresh token not yet acquired, do token request + if (empty($this->refreshToken)) { + $body = [ + 'form_params' => [ + 'redirect_uri' => $this->redirectUrl, + 'grant_type' => 'authorization_code', + 'client_id' => $this->exactClientId, + 'client_secret' => $this->exactClientSecret, + 'code' => $this->authorizationCode + ] + ]; + } else { // else do refresh token request + $body = [ + 'form_params' => [ + 'refresh_token' => $this->refreshToken, + 'grant_type' => 'refresh_token', + 'client_id' => $this->exactClientId, + 'client_secret' => $this->exactClientSecret, + ] + ]; + } + + $response = $this->client()->post($this->tokenUrl, $body); + + if ($response->getStatusCode() == 200) { + $body = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() === JSON_ERROR_NONE) { + $this->accessToken = $body['access_token']; + $this->refreshToken = $body['refresh_token']; + $this->tokenExpires = $this->getDateTimeFromExpires($body['expires_in']); + } else { + throw new ApiException('Could not acquire tokens, json decode failed. Got response: ' . $response->getBody()->getContents()); + } + } else { + throw new ApiException('Could not acquire or refresh tokens'); + } + } + + + private function getDateTimeFromExpires($expires) + { + if (!is_numeric($expires)) { + throw new \InvalidArgumentException('Function requires a numeric expires value'); + } + + return time() + 600; + } + + /** + * @return mixed + */ + public function getTokenExpires() + { + return $this->tokenExpires; + } + + /** + * @param mixed $tokenExpires + */ + public function setTokenExpires($tokenExpires) + { + $this->tokenExpires = $tokenExpires; + } + + private function tokenHasExpired() + { + if (empty($this->tokenExpires)) { + return true; + } + + return $this->tokenExpires <= time(); + } + + private function formatUrl($endPoint, $includeDivision = true) + { + if ($includeDivision) { + return implode('/', [ + $this->apiUrl, + $this->getCurrentDivisionNumber(), + $endPoint + ]); + } + + return implode('/', [ + $this->apiUrl, + $endPoint + ]); + } + /** * @return mixed