Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"scriptfusion/static-class": "^1",
"scriptfusion/retry": "^1.1",
"scriptfusion/retry-exception-handlers": "^1",
"eloquent/enumeration": "^5",
"psr/container": "^1",
"psr/cache": "^1"
},
Expand Down
15 changes: 0 additions & 15 deletions src/Cache/Cache.php

This file was deleted.

35 changes: 0 additions & 35 deletions src/Cache/CacheAdvice.php

This file was deleted.

2 changes: 2 additions & 0 deletions src/Cache/CacheKeyGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

interface CacheKeyGenerator
{
const RESERVED_CHARACTERS = '{}()/\@:';

/**
* @param string $source
* @param array $sortedOptions Options sorted by key.
Expand Down
9 changes: 2 additions & 7 deletions src/Cache/CacheUnavailableException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,10 @@
/**
* The exception that is thrown when cache is unavailable.
*/
class CacheUnavailableException extends \RuntimeException implements CacheException
final class CacheUnavailableException extends \RuntimeException implements CacheException
{
public static function unsupported()
public static function createUnsupported()
{
return new self('Cannot cache: connector does not support caching.');
}

public static function unavailable()
{
return new self('Cannot cache: connector reported cache currently unavailable.');
}
}
4 changes: 1 addition & 3 deletions src/Cache/JsonCacheKeyGenerator.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<?php
namespace ScriptFUSION\Porter\Cache;

use ScriptFUSION\Porter\Connector\CachingConnector;

class JsonCacheKeyGenerator implements CacheKeyGenerator
{
public function generateCacheKey($source, array $sortedOptions)
{
return str_replace(
str_split(CachingConnector::RESERVED_CHARACTERS),
str_split(self::RESERVED_CHARACTERS),
'.',
json_encode([$source, $sortedOptions], JSON_UNESCAPED_SLASHES)
);
Expand Down
61 changes: 19 additions & 42 deletions src/Connector/CachingConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
namespace ScriptFUSION\Porter\Connector;

use Psr\Cache\CacheItemPoolInterface;
use ScriptFUSION\Porter\Cache\Cache;
use ScriptFUSION\Porter\Cache\CacheKeyGenerator;
use ScriptFUSION\Porter\Cache\InvalidCacheKeyException;
use ScriptFUSION\Porter\Cache\JsonCacheKeyGenerator;
use ScriptFUSION\Porter\Cache\MemoryCache;
use ScriptFUSION\Porter\Options\EncapsulatedOptions;

/**
* Caches remote data using PSR-6-compliant objects.
* Wraps a connector to cache fetched data using PSR-6-compliant objects.
*/
abstract class CachingConnector implements Connector, Cache
class CachingConnector implements Connector
{
const RESERVED_CHARACTERS = '{}()/\@:';
/**
* @var Connector
*/
private $connector;

/**
* @var CacheItemPoolInterface
Expand All @@ -26,14 +28,17 @@ abstract class CachingConnector implements Connector, Cache
*/
private $cacheKeyGenerator;

public function __construct(CacheItemPoolInterface $cache = null, CacheKeyGenerator $cacheKeyGenerator = null)
{
public function __construct(
Connector $connector,
CacheItemPoolInterface $cache = null,
CacheKeyGenerator $cacheKeyGenerator = null
) {
$this->connector = $connector;
$this->cache = $cache ?: new MemoryCache;
$this->cacheKeyGenerator = $cacheKeyGenerator ?: new JsonCacheKeyGenerator;
}

/**
* @param ConnectionContext $context
* @param string $source
* @param EncapsulatedOptions|null $options
*
Expand All @@ -43,73 +48,45 @@ public function __construct(CacheItemPoolInterface $cache = null, CacheKeyGenera
*/
public function fetch(ConnectionContext $context, $source, EncapsulatedOptions $options = null)
{
if ($context->shouldCache()) {
if ($context->mustCache()) {
$optionsCopy = $options ? $options->copy() : [];

ksort($optionsCopy);

$key = $this->validateCacheKey($this->getCacheKeyGenerator()->generateCacheKey($source, $optionsCopy));
$this->validateCacheKey($key = $this->cacheKeyGenerator->generateCacheKey($source, $optionsCopy));

if ($this->cache->hasItem($key)) {
return $this->cache->getItem($key)->get();
}
}

$data = $this->fetchFreshData($source, $options);
$data = $this->connector->fetch($context, $source, $options);

isset($key) && $this->cache->save($this->cache->getItem($key)->set($data));

return $data;
}

abstract public function fetchFreshData($source, EncapsulatedOptions $options = null);

public function isCacheAvailable()
{
return true;
}

public function getCache()
{
return $this->cache;
}

public function setCache(CacheItemPoolInterface $cache)
{
$this->cache = $cache;
}

public function getCacheKeyGenerator()
{
return $this->cacheKeyGenerator;
}

public function setCacheKeyGenerator(CacheKeyGenerator $cacheKeyGenerator)
{
$this->cacheKeyGenerator = $cacheKeyGenerator;
}

/**
* @param mixed $key
*
* @return string
* @return void
*
* @throws InvalidCacheKeyException Cache key contains invalid data.
*/
private function validateCacheKey($key)
{
// TODO: Remove when PHP 5 support dropped and replace with string hint.
if (!is_string($key)) {
throw new InvalidCacheKeyException('Cache key must be a string.');
}

if (strpbrk($key, self::RESERVED_CHARACTERS) !== false) {
if (strpbrk($key, CacheKeyGenerator::RESERVED_CHARACTERS) !== false) {
throw new InvalidCacheKeyException(sprintf(
'Cache key "%s" contains one or more reserved characters: "%s".',
$key,
self::RESERVED_CHARACTERS
CacheKeyGenerator::RESERVED_CHARACTERS
));
}

return $key;
}
}
34 changes: 9 additions & 25 deletions src/Connector/ConnectionContext.php
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
<?php
namespace ScriptFUSION\Porter\Connector;

use ScriptFUSION\Porter\Cache\CacheAdvice;

final class ConnectionContext
{
private $cacheAdvice;
private $mustCache;

private $fetchExceptionHandler;

private $maxFetchAttempts;

public function __construct(CacheAdvice $cacheAdvice, callable $fetchExceptionHandler, $maxFetchAttempts)
public function __construct($mustCache, callable $fetchExceptionHandler, $maxFetchAttempts)
{
$this->cacheAdvice = $cacheAdvice;
$this->mustCache = (bool)$mustCache;
$this->fetchExceptionHandler = $fetchExceptionHandler;
$this->maxFetchAttempts = (int)$maxFetchAttempts;
}

public function mustCache()
{
return $this->mustCache;
}

public function retry(callable $callable)
{
return \ScriptFUSION\Retry\retry(
Expand All @@ -29,28 +32,9 @@ function (\Exception $exception) {
throw $exception;
}

// TODO Clone exception handler to avoid persisting state between calls.
call_user_func($this->fetchExceptionHandler, $exception);
}
);
}

/**
* Gets a value indicating whether a connector should cache data.
*
* @return bool True if the connector should cache data, otherwise false.
*/
public function shouldCache()
{
return $this->cacheAdvice->anyOf(CacheAdvice::SHOULD_CACHE(), CacheAdvice::MUST_CACHE());
}

/**
* Gets a value indicating whether a connector must support caching.
*
* @return bool True if connector must support caching, otherwise false.
*/
public function mustSupportCaching()
{
return $this->cacheAdvice->anyOf(CacheAdvice::MUST_CACHE(), CacheAdvice::MUST_NOT_CACHE());
}
}
2 changes: 1 addition & 1 deletion src/Connector/ConnectionContextFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class ConnectionContextFactory
public static function create(ImportSpecification $specification)
{
return new ConnectionContext(
$specification->getCacheAdvice(),
$specification->mustCache(),
$specification->getFetchExceptionHandler(),
$specification->getMaxFetchAttempts()
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,33 @@
<?php
namespace ScriptFUSION\Porter\Connector;

use ScriptFUSION\Porter\Cache\Cache;
use ScriptFUSION\Porter\Cache\CacheUnavailableException;
use ScriptFUSION\Porter\Options\EncapsulatedOptions;

final class SuperConnector
/**
* Connector whose lifecycle is synchronised with an import operation. Ensures correct ConnectionContext is delivered
* to each fetch() operation.
*
* Do not store references to this connector that would prevent it expiring when an import operation ends.
*/
final class ImportConnector
{
private $connector;

private $context;

public function __construct(Connector $connector, ConnectionContext $context)
{
if ($context->mustCache() && !$connector instanceof CachingConnector) {
throw CacheUnavailableException::createUnsupported();
}

$this->connector = $connector;
$this->context = $context;
}

public function fetch($source, EncapsulatedOptions $options = null)
{
$this->validateCacheState();

return $this->connector->fetch($this->context, $source, $options);
}

private function validateCacheState()
{
if ($this->context->mustSupportCaching()) {
if (!$this->connector instanceof Cache) {
throw CacheUnavailableException::unsupported();
}

if (!$this->connector->isCacheAvailable()) {
throw CacheUnavailableException::unavailable();
}
}
}
}
10 changes: 5 additions & 5 deletions src/Porter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use ScriptFUSION\Porter\Collection\RecordCollection;
use ScriptFUSION\Porter\Connector\ConnectionContext;
use ScriptFUSION\Porter\Connector\ConnectionContextFactory;
use ScriptFUSION\Porter\Connector\SuperConnector;
use ScriptFUSION\Porter\Connector\ImportConnector;
use ScriptFUSION\Porter\Provider\ForeignResourceException;
use ScriptFUSION\Porter\Provider\ObjectNotCreatedException;
use ScriptFUSION\Porter\Provider\Provider;
Expand All @@ -20,17 +20,17 @@
use ScriptFUSION\Porter\Transform\Transformer;

/**
* Imports data according to an ImportSpecification.
* Imports data according to an ImportSpecification from a provider in the container of providers or internal factory.
*/
class Porter
{
/**
* @var ContainerInterface
* @var ContainerInterface Container of user-defined providers.
*/
private $providers;

/**
* @var ProviderFactory
* @var ProviderFactory Internal factory of first-party providers.
*/
private $providerFactory;

Expand Down Expand Up @@ -110,7 +110,7 @@ private function fetch(ProviderResource $resource, $providerName, ConnectionCont
}

$records = $resource->fetch(
new SuperConnector($provider->getConnector(), $context),
new ImportConnector($provider->getConnector(), $context),
$provider instanceof ProviderOptions ? clone $provider->getOptions() : null
);

Expand Down
Loading