Last active
December 2, 2019 03:01
-
-
Save lancegliser/6b935978d5d658f6ad26c119c645ea21 to your computer and use it in GitHub Desktop.
An base example, without real end points of creating a reusable trait for PHP that allows for API clients to quickly wrap and dispatch standard Guzzle HTTP client behaviors.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Traits\ApiLibrary; | |
use DateTimeImmutable; | |
use DateTimeInterface; | |
use DateTimeZone; | |
use Exception; | |
use GuzzleHttp\Middleware; | |
use Psr\Http\Message\RequestInterface; | |
use RuntimeException; | |
use Psr\Http\Message\ResponseInterface; | |
use GuzzleHttp\Exception\ConnectException; | |
use GuzzleHttp\Exception\RequestException; | |
use GuzzleHttp\Psr7\Request; | |
use GuzzleHttp\Psr7\Response; | |
use function GuzzleHttp\choose_handler; | |
use GuzzleHttp\RequestOptions; | |
use GuzzleHttp\Client as GuzzleClient; | |
use GuzzleHttp\HandlerStack; | |
use GuzzleHttp\TransferStats; | |
use Common_Profiler; | |
use App\Traits\ApiLibrary\Models\Call; | |
/** | |
* Trait ApiLibrary | |
* Provides wrapping functionality and utility interface methods of Guzzler HTTP | |
* | |
* @package App\Traits\ApiLibrary | |
*/ | |
Trait Client | |
{ | |
/** @var GuzzleClient */ | |
public $client; | |
/** @var \App\Traits\ApiLibrary\Models\Call[] */ | |
public $calls = []; | |
/** @var int Defines the number of attempts Guzzle will use when the retry middleware is used */ | |
protected $retriesDefault = 2; | |
/** @var int Defines the number of milliseconds until an attempt times out */ | |
protected $retryDelayDefault = 1000; | |
/** | |
* Defines if the Call array should include the request and response objects. | |
* This can take considerable memory for large responses. | |
* | |
* @var bool | |
*/ | |
protected $isExtendedLoggingEnabled = FALSE; | |
/** @var callable[] */ | |
protected $callSubscribers = []; | |
/** @var callable[] */ | |
protected $middleware = []; | |
/** | |
* Provider constructor. | |
* | |
* @param array $config | |
*/ | |
//public function __construct(){ | |
// $this->initializeClient($baseUrl, $options); | |
// $this->addCallSubscriber($CI->profiler->addApiLibraryCall); | |
//} | |
/** | |
* To be run when the service is constructed | |
* | |
* @param string $baseUrl | |
* @param array $options See: http://docs.guzzlephp.org/en/stable/request-options.html | |
* @return self | |
* @throws \RuntimeException | |
*/ | |
public function initializeClient($baseUrl, array $options = []) | |
{ | |
// Construct the internal client using Guzzler against a base uri and merged options | |
$options += [ | |
'base_uri' => $baseUrl, | |
] + $this->getDefaultClientOptions(); | |
$this->client = new GuzzleClient($options); | |
$this->addProfilerCallSubscription(); | |
return $this; | |
} | |
/** | |
* @return array | |
* @throws \RuntimeException | |
*/ | |
protected function getDefaultClientOptions() | |
{ | |
return [ | |
RequestOptions::CONNECT_TIMEOUT => 5, | |
RequestOptions::TIMEOUT => 60, | |
RequestOptions::VERIFY => FALSE, | |
// Shouldn't do this! Instead install https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt | |
RequestOptions::ON_STATS => function (TransferStats $stats) { | |
$this->onCallStats($stats, TRUE); | |
}, | |
'handler' => $this->getMiddlewareHandlerStack(), | |
]; | |
} | |
/** | |
* Returns the base handler stack, without any client middleware. | |
* | |
* @return \GuzzleHttp\HandlerStack | |
* @see http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html | |
* @throws \RuntimeException | |
*/ | |
final protected function getBaseHandlerStack() | |
{ | |
$handler = choose_handler(); | |
$stack = HandlerStack::create($handler); | |
$stack->push(Middleware::mapRequest($this->addRequestStartTime())); | |
return $stack; | |
} | |
/** | |
* @return \Closure | |
*/ | |
final protected function addRequestStartTime() | |
{ | |
/** | |
* @param Request $request | |
* @return Request | |
*/ | |
return function (Request $request) { | |
$request->startTime = $this->getCurrentDateTimeImmutable(); | |
return $request; | |
}; | |
} | |
/** | |
* Returns a handler stack with all of the defined middleware for the client included. | |
* | |
* @return \GuzzleHttp\HandlerStack | |
* @see http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html | |
* @throws \RuntimeException | |
*/ | |
final protected function getMiddlewareHandlerStack() | |
{ | |
$stack = $this->getBaseHandlerStack(); | |
foreach ($this->middleware as $callback) | |
{ | |
$stack->push($callback); | |
} | |
return $stack; | |
} | |
/** | |
* @param callable $callback | |
* @see http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html | |
*/ | |
public function addMiddleware(callable $callback) | |
{ | |
$this->middleware[] = $callback; | |
} | |
/** | |
* @return \Closure | |
*/ | |
protected function retryDecider() | |
{ | |
/** | |
* @param int $retries | |
* @param Request $request | |
* @param Response $response | |
* @param RequestException $exception | |
* @return boolean | |
*/ | |
return function ( | |
$retries, | |
Request $request, | |
Response $response = NULL, | |
RequestException $exception = NULL | |
){ | |
unset($request); | |
if ($retries >= $this->retriesDefault) | |
{ | |
return FALSE; | |
} | |
// Retry connection exceptions | |
if ($exception instanceof ConnectException) | |
{ | |
return TRUE; | |
} | |
if ($response) | |
{ | |
// Retry on server errors | |
if ($response->getStatusCode() >= 500) | |
{ | |
return TRUE; | |
} | |
} | |
return FALSE; | |
}; | |
} | |
/** | |
* Provides a delay calculator for when to reattempt a call. | |
* | |
* @return \Closure | |
*/ | |
function retryDelay() | |
{ | |
/** | |
* @param int $numberOfRetries | |
* @return int | |
*/ | |
return function ($numberOfRetries) { | |
return $this->retryDelayDefault * $numberOfRetries; | |
}; | |
} | |
/** | |
* Provides an automatic subscription to the profiler to push Api Call | |
* | |
* @throws \RuntimeException | |
*/ | |
protected function addProfilerCallSubscription() | |
{ | |
if(!function_exists('get_instance') || !class_exists('CI_Controller')){ | |
throw new RuntimeException('CodeIgniter is not yet registered to handle subscriptions.'); | |
} | |
$CI = get_instance(); | |
$CI->load->library('profiler'); | |
/** @var \Common_Profiler $profiler */ | |
$profiler = $CI->profiler; | |
if (!is_a($profiler, Common_Profiler::class)) | |
{ | |
throw new RuntimeException(get_class($profiler) . ' is not an instance of Common_Profiler.'); | |
} | |
$this->addCallSubscriber([$profiler, 'add_api_call']); | |
} | |
/** | |
* @param callable $callable | |
*/ | |
public function addCallSubscriber(callable $callable) | |
{ | |
$this->callSubscribers[] = $callable; | |
} | |
/** | |
* @param TransferStats $stats | |
* @param boolean $sendNotifications | |
*/ | |
public function onCallStats(TransferStats $stats, $sendNotifications = TRUE) | |
{ | |
try | |
{ | |
$endTime = $this->getCurrentDateTimeImmutable(); | |
$handlerStats = $stats->getHandlerStats(); | |
$request = $stats->getRequest(); | |
// The TransferStats object can be unreliable for total time statistics. | |
// Calculate our own instead using our middleware hooks if possible. | |
if(!empty($request->startTime)){ | |
/** @var DateTimeImmutable $startTime */ | |
$startTime = $request->startTime; | |
$totalTime = $this->getMicrotimeDiff($endTime, $startTime); | |
} else { | |
$startTime = NULL; | |
$totalTime = $stats->getTransferTime(); | |
} | |
$properties = [ | |
'method' => $request->getMethod(), | |
'uri' => $stats->getEffectiveUri(), | |
'startTime' => $startTime, | |
'endTime' => $endTime, | |
'totalTime' => $totalTime, | |
'contentType' => $handlerStats['content_type'], | |
'httpCode' => $handlerStats['http_code'], | |
'headerSize' => $handlerStats['header_size'], | |
'requestSize' => $handlerStats['request_size'], | |
'filetime' => $handlerStats['filetime'], | |
'sslVerifyResult' => $handlerStats['ssl_verify_result'], | |
'redirectCount' => $handlerStats['redirect_count'], | |
'namelookupTime' => $handlerStats['namelookup_time'], | |
'connectTime' => $handlerStats['connect_time'], | |
'pretransferTime' => $handlerStats['pretransfer_time'], | |
'sizeUpload' => $handlerStats['size_upload'], | |
'sizeDownload' => $handlerStats['size_download'], | |
'speedDownload' => $handlerStats['speed_download'], | |
'speedUpload' => $handlerStats['speed_upload'], | |
'downloadContentLength' => $handlerStats['download_content_length'], | |
'uploadContentLength' => $handlerStats['upload_content_length'], | |
'starttransferTime' => $handlerStats['starttransfer_time'], | |
'redirectTime' => $handlerStats['redirect_time'], | |
'redirectUrl' => $handlerStats['redirect_url'], | |
'primaryIp' => $handlerStats['primary_ip'], | |
'certinfo' => $handlerStats['certinfo'], | |
'primaryPort' => $handlerStats['primary_port'], | |
'localIp' => $handlerStats['local_ip'], | |
'localPort' => $handlerStats['local_port'], | |
]; | |
if ($this->isExtendedLoggingEnabled) | |
{ | |
$properties += [ | |
'request' => $request, | |
'response' => $stats->hasResponse() ? $stats->getResponse() : NULL, | |
]; | |
} | |
$call = new Call($properties); | |
$this->calls[] = $call; | |
if ($sendNotifications) | |
{ | |
$this->notifySubscribers($call); | |
} | |
// Log the really bad items for processing | |
if($call->getTotalTime() >= 3){ | |
log_warning("Long api call: " . $call->getTotalTime() . ' ' . $call->getProfilerContent() . "\n" . print_r($call, TRUE)); | |
} | |
} catch (Exception $exception) | |
{ | |
log_exception($exception); | |
} | |
} | |
/** | |
* Call this handler to avoid notifying the clients initially, then notify them if everything goes well in your own code: | |
* $this->notifySubscribers($this->getLastCall()); | |
* | |
* Disable call stats default handler. We're going to have to go sync. | |
* If we don't, the performance and other logging systems will fire causing a loop of log in attempts. | |
* | |
* @param TransferStats $stats | |
*/ | |
public function onCallStatsWithoutNotifications(TransferStats $stats) | |
{ | |
$this->onCallStats($stats, FALSE); | |
} | |
/** | |
* @param \App\Traits\ApiLibrary\Models\Call $call | |
*/ | |
protected function notifySubscribers(Call $call) | |
{ | |
foreach ($this->callSubscribers as $subscriber) | |
{ | |
$subscriber($call); | |
} | |
} | |
/** | |
* @return \App\Traits\ApiLibrary\Models\Call|NULL | |
*/ | |
public function getLastCall() | |
{ | |
if (empty($this->calls)) | |
{ | |
return NULL; | |
} | |
return $this->calls[ count($this->calls) - 1 ]; | |
} | |
/** | |
* @return \App\Traits\ApiLibrary\Models\Call[] | |
*/ | |
public function getAllCalls() | |
{ | |
return $this->calls; | |
} | |
/** | |
* Provides the current Date Time properly including microseconds in UTC | |
* | |
* @return \DateTimeImmutable | |
* @throws \Exception | |
*/ | |
protected function getCurrentDateTimeImmutable() | |
{ | |
if (is_php('7.1')) | |
{ | |
return new DateTimeImmutable('now', new DateTimeZone('UTC')); | |
} | |
list($usec) = explode(' ', microtime()); | |
$micro = sprintf("%06d", $usec * 1000000); | |
return new DateTimeImmutable(date('Y-m-d H:i:s.' . $micro), new DateTimeZone('UTC')); | |
} | |
/** | |
* Returns the absolute difference between dates with microsecond precision | |
* | |
* @param \DateTimeInterface|\DateTime|DateTimeImmutable $date1 | |
* @param \DateTimeInterface|\DateTime|DateTimeImmutable $date2 | |
* @return float | |
*/ | |
protected function getMicrotimeDiff(DateTimeInterface $date1, DateTimeInterface $date2) | |
{ | |
//Absolute val of Date 1 in seconds from (EPOCH Time) - Date 2 in seconds from (EPOCH Time) | |
$diff = abs(strtotime($date1->format('d-m-Y H:i:s.u')) - strtotime($date2->format('d-m-Y H:i:s.u'))); | |
//Creates variables for the microseconds of date1 and date2 | |
$micro1 = $date1->format("u"); | |
$micro2 = $date2->format("u"); | |
//Absolute difference between these micro seconds: | |
$micro = abs($micro1 - $micro2); | |
//Creates the variable that will hold the seconds (?): | |
$difference = $diff . "." . $micro; | |
return floatval($difference); | |
} | |
/** | |
* @param \Psr\Http\Message\ResponseInterface $response | |
* @return mixed | |
* @throws \Exception | |
*/ | |
public function getJsonResponseData(ResponseInterface $response) | |
{ | |
$response->getBody()->rewind(); | |
$bodyContents = $response->getBody()->getContents(); | |
// A bit of header checking - Don't attempt to decode anything that declares it's not JSON. | |
$contentTypeHeader = $response->getHeaderLine('Content-Type'); | |
if(!empty($contentTypeHeader) && !$this->isContentTypeJson($contentTypeHeader)) | |
{ | |
$bodySnippet = $this->getResponseBodySnippet($response); | |
$message = "Response Content-Type is invalid for json decoding: {$contentTypeHeader} '{$bodySnippet}'"; | |
throw new Exception($message); | |
} | |
$data = json_decode($bodyContents); | |
if ($data === NULL) | |
{ | |
$bodySnippet = $this->getResponseBodySnippet($response); | |
throw new Exception("Failed to decode service response body: {$bodySnippet}"); | |
} | |
return $data; | |
} | |
/** | |
* @param \Psr\Http\Message\ResponseInterface $response | |
* @return boolean | |
*/ | |
protected function isResponseContentTypeJson(ResponseInterface $response) | |
{ | |
$contentTypeHeader = $response->getHeaderLine('Content-Type'); | |
return $this->isContentTypeJson($contentTypeHeader); | |
} | |
/** | |
* @param string $contentType | |
* @return boolean | |
*/ | |
protected function isContentTypeJson($contentType) | |
{ | |
if(empty($contentType)){ | |
return FALSE; | |
} | |
list($contentType) = explode(';', $contentType); | |
return in_array( | |
$contentType, | |
[ | |
'application/json', // Standard JSON | |
'application/javascript', // JSON P for executable javascript required in some cross-domain fun | |
'application/geo+json', | |
] | |
); | |
} | |
/** | |
* @param \Psr\Http\Message\ResponseInterface $response | |
* @return bool|string | |
* @throws \RuntimeException | |
*/ | |
protected function getResponseBodySnippet(ResponseInterface $response) | |
{ | |
$response->getBody()->rewind(); | |
$bodyContents = $response->getBody()->getContents(); | |
return !empty($bodyContents) ? substr($bodyContents, 0, 200) : '{No contents}'; | |
} | |
/** | |
* @param \Psr\Http\Message\RequestInterface $request | |
* @param \Psr\Http\Message\ResponseInterface $response | |
* @return string[] | |
* @throws \RuntimeException | |
* @throws \Exception | |
*/ | |
protected function getRequestAndResponseDetailStrings(RequestInterface $request, ResponseInterface $response) | |
{ | |
$requestBody = $request->getBody(); | |
$requestBody->rewind(); | |
$requestDetails = json_encode([ | |
'method' => $request->getMethod(), | |
'url' => (string)$request->getUri(), | |
'headers' => $request->getHeaders(), | |
'contents' => json_decode($requestBody->getContents()), | |
] | |
); | |
$responseBody = $request->getBody(); | |
$responseBody->rewind(); | |
$responseContents = $responseBody->getContents(); | |
if($this->isResponseContentTypeJson($response)) | |
{ | |
$responseContents = $this->getJsonResponseData($response); | |
} | |
$responseDetails = json_encode([ | |
'headers' => $response->getHeaders(), | |
'contents' => $responseContents, | |
] | |
); | |
$messages = [ | |
$response->getStatusCode() . ': ' . $response->getReasonPhrase(), | |
"Request:", | |
$requestDetails, | |
"Response:", | |
$responseDetails, | |
] ; | |
return $messages; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\GuzzleApiClient; | |
use App\GuzzleApiClient\Exceptions\NotFoundException; | |
use DateTime; | |
use Exception; | |
use GuzzleHttp\Exception\ConnectException; | |
use GuzzleHttp\Exception\RequestException; | |
use GuzzleHttp\Psr7\Request; | |
use GuzzleHttp\Psr7\Response; | |
use Psr\SimpleCache\InvalidArgumentException; | |
use Symfony\Component\Cache\Simple\FilesystemCache; | |
use GuzzleHttp\Middleware; | |
use GuzzleHttp\RequestOptions; | |
use Psr\Http\Message\RequestInterface; | |
use App\Traits\Singleton; | |
use App\ApiLibrary\Client; | |
/** | |
* Class AbstractProvider | |
* | |
* @package App\GuzzleApiClient | |
* @link API documentation https://api.example.com/ | |
*/ | |
class GuzzleApiClient | |
{ | |
use Singleton; | |
use Client | |
{ | |
initializeClient as protected traitInitializeClient; | |
} | |
const TEST_API_HOST = 'https://dev-api.example.com/'; | |
const PROD_API_HOST = 'https://api.example.com/'; | |
const ENDPOINT_LOGIN = 'user/login'; | |
const ENDPOINT_LOGOUT = 'user/logout'; | |
const LOGIN_CACHE_KEY = 'login.passportKey'; | |
/** @var string */ | |
protected $username; | |
/** @var string */ | |
protected $password; | |
/** @var Models\Login */ | |
protected $login; | |
/** @var FilesystemCache */ | |
protected $cache; | |
/** | |
* @param string $username | |
* @param string $password | |
* @param string $cachePath | |
* @return \App\GuzzleApiClient\Provider | |
*/ | |
public static function getInstance($username, $password, $cachePath) | |
{ | |
return self::getStaticInstance($username, $password, $cachePath); | |
} | |
/** | |
* @param string|\DateTimeInterface $date A date-like string, or a real date time | |
* @return string|mixed | |
*/ | |
public static function formatDate($date) | |
{ | |
if(empty($date)){ | |
return ''; | |
} | |
// Get a valid DateTime object | |
if(is_string($date)) | |
{ | |
try | |
{ | |
$date = empty($timezone) ? new DateTime($date) : new DateTime($date, $timezone); | |
} catch (Exception $e) | |
{ | |
return $date; | |
} | |
} | |
// Format the string | |
$string = $date->format(self::DATE_FORMAT); | |
return $string; | |
} | |
/** | |
* Provider constructor. | |
* | |
* @param string $username | |
* @param string $password | |
* @param string $cachePath | |
*/ | |
protected function __construct($username, $password, $cachePath) | |
{ | |
$this->username = $username; | |
$this->password = $password; | |
//$this->isExtendedLoggingEnabled = TRUE; | |
$cacheNamespace = preg_replace('/[^\w\d]/', '.', __CLASS__); | |
$this->cache = new FilesystemCache($cacheNamespace, 0, $cachePath); | |
// You'll want to do something similar to this | |
$this->initializeClient(self::PROD_API_HOST); | |
} | |
/** | |
* To be run when the service is constructed | |
* | |
* @param string $baseUrl | |
* @param array $options See: http://docs.guzzlephp.org/en/stable/request-options.html | |
* @return self | |
* @throws \RuntimeException | |
*/ | |
public function initializeClient($baseUrl, array $options = []) | |
{ | |
// Retry middleware | |
$this->addMiddleware(Middleware::retry( | |
$this->retryDecider(), | |
$this->retryDelay() | |
) | |
); | |
// Authorization middleware | |
$this->addMiddleware(Middleware::mapRequest(function (RequestInterface $request){ | |
return $this->addAuthorization($request); | |
} | |
) | |
); | |
//$options[ RequestOptions::TIMEOUT ] = 15; // In case an api is really painfully slow | |
return $this->traitInitializeClient($baseUrl, $options); | |
} | |
/** | |
* @return self | |
*/ | |
public function clearCache() | |
{ | |
$this->login = NULL; | |
try | |
{ | |
$this->cache->deleteMultiple([self::LOGIN_CACHE_KEY]); | |
} catch (InvalidArgumentException $exception) | |
{ | |
} | |
return $this; | |
} | |
/** | |
* @param \Psr\Http\Message\RequestInterface $request | |
* @return \Psr\Http\Message\RequestInterface | |
* @throws \Exception | |
*/ | |
protected function addAuthorization(RequestInterface $request) | |
{ | |
$login = $this->getLogin(); | |
// Add the header to the request | |
$request = $request->withHeader('Authentication', $login->accessToken); | |
return $request; | |
} | |
/** | |
* @return Models\Login | |
* @throws \Exception | |
*/ | |
public function getLogin() | |
{ | |
// Check the cache | |
if (empty($this->login)) | |
{ | |
try | |
{ | |
$this->login = $this->getLoginFromCache(); | |
} catch (InvalidArgumentException $exception) | |
{ | |
log_exception($exception); | |
} | |
} | |
if (empty($this->login)) | |
{ | |
$this->login = $this->getLoginFromClient(); | |
try | |
{ | |
$this->setLoginCache($this->login); | |
} catch (InvalidArgumentException $exception) | |
{ | |
log_exception($exception); | |
} | |
} | |
return $this->login; | |
} | |
/** | |
* @return Models\Login|null | |
* @throws \Psr\SimpleCache\InvalidArgumentException | |
*/ | |
protected function getLoginFromCache() | |
{ | |
return $this->cache->get(self::LOGIN_CACHE_KEY); | |
} | |
/** | |
* @param Models\Login $login | |
* @return mixed|null | |
* @throws \Psr\SimpleCache\InvalidArgumentException | |
*/ | |
protected function setLoginCache(Models\Login $login) | |
{ | |
return $this->cache->set(self::LOGIN_CACHE_KEY, $login); | |
} | |
/** | |
* @return \App\GuzzleApiClient\Models\Login | |
* @throws \Exception | |
*/ | |
protected function getLoginFromClient() | |
{ | |
$data = [ | |
'Username' => $this->username, | |
'Password' => $this->password, | |
]; | |
$response = $this->client->get( | |
self::ENDPOINT_LOGIN, | |
[ | |
RequestOptions::QUERY => $data, | |
// We'll use the default handler so we don't rerun our middleware | |
'handler' => $this->getBaseHandlerStack() | |
] | |
+ $this->getDefaultClientOptions() | |
); | |
$data = $this->getJsonResponseData($response); | |
return new Models\Login($data); | |
} | |
/** | |
* @return self | |
* @throws \Exception | |
*/ | |
public function logout() | |
{ | |
$this->client->get(self::ENDPOINT_LOGOUT); | |
$this->clearCache(); | |
return $this; | |
} | |
/** | |
* @return \Closure | |
*/ | |
protected function retryDecider() | |
{ | |
/** | |
* @param int $retries | |
* @param Request $request | |
* @param Response $response | |
* @param RequestException $exception | |
* @return boolean | |
*/ | |
return function ( | |
$retries, | |
Request $request, | |
Response $response = NULL, | |
RequestException $exception = NULL | |
){ | |
unset($request); | |
// Limit the number of retries to 5 | |
if ($retries >= $this->retriesDefault) | |
{ | |
return FALSE; | |
} | |
// Retry connection exceptions | |
if ($exception instanceof ConnectException) | |
{ | |
return TRUE; | |
} | |
if ($response) | |
{ | |
// Retry on server errors | |
if ($response->getStatusCode() >= 500) | |
{ | |
return TRUE; | |
} | |
// Retry on authentication errors after clearing the login | |
if ($response->getStatusCode() === HTTP_UNAUTHORIZED) | |
{ | |
$this->clearCache(); | |
return TRUE; | |
} | |
} | |
return FALSE; | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment