<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery;

use Contentful\Core\Api\Link;
use Contentful\Core\Api\LinkResolverInterface;
use Contentful\Core\Resource\ResourceInterface;
use Contentful\Core\Resource\ResourcePoolInterface;
use Contentful\Delivery\Client\ClientInterface;

class LinkResolver implements LinkResolverInterface
{
    /**
     * @var ClientInterface
     */
    private $client;

    /**
     * @var ResourcePoolInterface
     */
    private $resourcePool;

    /**
     * LinkResolver constructor.
     *
     * @param ClientInterface       $client
     * @param ResourcePoolInterface $resourcePool
     */
    public function __construct(ClientInterface $client, ResourcePoolInterface $resourcePool)
    {
        $this->client = $client;
        $this->resourcePool = $resourcePool;
    }

    /**
     * {@inheritdoc}
     */
    public function resolveLink(Link $link, array $parameters = []): ResourceInterface
    {
        $locale = $parameters['locale'] ?? null;

        switch ($link->getLinkType()) {
            case 'Asset':
                return $this->client->getAsset($link->getId(), $locale);
            case 'ContentType':
                return $this->client->getContentType($link->getId());
            case 'Entry':
                return $this->client->getEntry($link->getId(), $locale);
            case 'Environment':
                return $this->client->getEnvironment();
            case 'Space':
                return $this->client->getSpace();
            default:
                throw new \InvalidArgumentException(\sprintf(
                    'Trying to resolve link for unknown type "%s".',
                    $link->getLinkType()
                ));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function resolveLinkCollection(array $links, array $parameters = []): array
    {
        $locale = $parameters['locale'] ?? null;

        // We load all resources for the given resource types
        $types = \array_unique(\array_map(function (Link $link) {
            return $link->getLinkType();
        }, $links));

        $resources = [];
        foreach ($types as $type) {
            $resources = \array_merge($resources, $this->resolveLinksForResourceType($type, $links, $locale));
        }

        $collection = [];
        foreach ($links as $link) {
            $key = $link->getLinkType().'.'.$link->getId();

            if (isset($resources[$key])) {
                $collection[] = $resources[$key];
            }
        }

        return $collection;
    }

    /**
     * Loads resources for a certain type only.
     *
     * @param string      $type
     * @param Link[]      $links
     * @param string|null $locale
     *
     * @return ResourceInterface[]
     */
    private function resolveLinksForResourceType(string $type, array $links, string $locale = null): array
    {
        $resourceIds = \array_map(function (Link $link): string {
            return $link->getId();
        }, \array_filter($links, function (Link $link) use ($type): bool {
            return $link->getLinkType() === $type;
        }));

        $resources = [];
        $collection = $this->fetchResourcesForGivenIds($resourceIds, $type, $locale);
        foreach ($collection as $resource) {
            $resources[$type.'.'.$resource->getId()] = $resource;
        }

        return $resources;
    }

    /**
     * Loads resources in the current cache pool and fetches the missing ones from the API.
     *
     * @param array       $resourceIds
     * @param string      $type
     * @param string|null $locale
     *
     * @return ResourceInterface[]
     */
    private function fetchResourcesForGivenIds(array $resourceIds, string $type, string $locale = null): array
    {
        $resources = [];
        $resourcePoolOptions = ['locale' => $locale];
        foreach ($resourceIds as $index => $resourceId) {
            if ($this->resourcePool->has($type, $resourceId, $resourcePoolOptions)) {
                $resources[] = $this->resourcePool->get($type, $resourceId, $resourcePoolOptions);

                unset($resourceIds[$index]);
            }
        }

        foreach ($this->createIdChunks($resourceIds) as $chunk) {
            $resources += $this->fetchCollectionFromApi($chunk, $type, $locale);
        }

        return $resources;
    }

    /**
     * Each Contentful ID can be up to 64 characters long,
     * so if we have too many links in a single query it might create some queries
     * that are too long.
     *
     * For this reason, we split the IDs into smaller chunks with at most 30 elements each,
     * so we're sure we stay under the 2000 characters per URL.
     *
     * @param string[] $resourceIds
     *
     * @return string[][]
     */
    private function createIdChunks(array $resourceIds): array
    {
        $chunks = [];
        $chunkId = -1;
        $resourceIds = \array_values($resourceIds);
        foreach ($resourceIds as $index => $resourceId) {
            if (0 === $index % 30) {
                ++$chunkId;
                $chunks[$chunkId] = [];
            }

            $chunks[$chunkId][] = $resourceId;
        }

        return $chunks;
    }

    /**
     * Actually make the relevant API calls.
     *
     * @param string[]    $resourceIds
     * @param string      $type
     * @param string|null $locale
     *
     * @return ResourceInterface[]
     */
    private function fetchCollectionFromApi(array $resourceIds, string $type, string $locale = null): array
    {
        $query = (new Query())
            ->where('sys.id[in]', $resourceIds)
        ;

        if ('Asset' === $type || 'Entry' === $type) {
            $query->setLocale($locale);
        }

        switch ($type) {
            case 'Asset':
                return $this->client->getAssets($query)->getItems();
            case 'ContentType':
                return $this->client->getContentTypes($query)->getItems();
            case 'Entry':
                return $this->client->getEntries($query)->getItems();
            case 'Environment':
                return [$this->client->getEnvironment()];
            case 'Space':
                return [$this->client->getSpace()];
            default:
                throw new \InvalidArgumentException(\sprintf(
                    'Trying to resolve link for unknown type "%s".',
                    $type
                ));
        }
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery;

use Contentful\Core\Resource\ResourceInterface;
use Contentful\Core\Resource\ResourcePoolInterface;
use Contentful\Core\ResourceBuilder\BaseResourceBuilder;
use Contentful\Core\ResourceBuilder\MapperInterface;
use Contentful\Delivery\Client\ClientInterface;
use Contentful\RichText\ParserInterface;

/**
 * ResourceBuilder class.
 *
 * This class is responsible for turning responses from the API into instances of PHP classes.
 *
 * A ResourceBuilder will only work for one space,
 * when working with multiple spaces multiple instances have to be used.
 */
class ResourceBuilder extends BaseResourceBuilder
{
    /**
     * @var ClientInterface
     */
    private $client;

    /**
     * @var ResourcePoolInterface
     */
    private $resourcePool;

    /**
     * @var ParserInterface
     */
    private $richTextParser;

    /**
     * @var string[]
     */
    private static $availableTypes = [
        'Asset',
        'ContentType',
        'DeletedAsset',
        'DeletedContentType',
        'DeletedEntry',
        'Environment',
        'Entry',
        'Locale',
        'Space',
    ];

    /**
     * ResourceBuilder constructor.
     *
     * @param ClientInterface       $client
     * @param ResourcePoolInterface $resourcePool
     * @param ParserInterface       $richTextParser
     */
    public function __construct(
        ClientInterface $client,
        ResourcePoolInterface $resourcePool,
        ParserInterface $richTextParser
    ) {
        $this->client = $client;
        $this->resourcePool = $resourcePool;
        $this->richTextParser = $richTextParser;

        parent::__construct();
    }

    /**
     * {@inheritdoc}
     */
    protected function getMapperNamespace(): string
    {
        return __NAMESPACE__.'\\Mapper';
    }

    /**
     * {@inheritdoc}
     */
    protected function createMapper($fqcn): MapperInterface
    {
        return new $fqcn($this, $this->client, $this->richTextParser);
    }

    /**
     * {@inheritdoc}
     */
    protected function getSystemType(array $data): string
    {
        if ('Array' === $data['sys']['type']) {
            return 'ResourceArray';
        }

        if (\in_array($data['sys']['type'], self::$availableTypes, true)) {
            return $data['sys']['type'];
        }

        throw new \InvalidArgumentException(\sprintf(
            'Unexpected system type "%s" while trying to build a resource.',
            $data['sys']['type']
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function build(array $data, ResourceInterface $resource = null)
    {
        $type = $data['sys']['type'];

        if ('Array' === $type) {
            $this->buildContentTypeCollection($data);
            $this->buildIncludes($data);

            return parent::build($data);
        }

        $resourceId = $data['sys']['id'];

        // Assets and entries are stored in cache using their locales.
        $locale = null;
        if (\in_array($data['sys']['type'], ['Asset', 'Entry'], true)) {
            $locale = $data['sys']['locale'] ?? '*';
        }

        if ($this->resourcePool->has($type, $resourceId, ['locale' => $locale])) {
            $resource = $this->resourcePool->get($type, $resourceId, ['locale' => $locale]);

            // If it's an entry, we still proceed with the resource building,
            // as it might have fields that were not previously loaded
            // due to the use of the select query operator.
            // For any other resource, we skip the building because
            // we have the result cached already.
            if ('Entry' !== $data['sys']['type']) {
                return $resource;
            }
        }

        $resource = parent::build($data, $resource);

        if ($resource instanceof ResourceInterface) {
            $this->resourcePool->save($resource);
        }

        return $resource;
    }

    /**
     * We extract content types that need to be fetched from a response array.
     * This way we can use a collective query rather than making separate queries
     * for every content type.
     *
     * @param array $data
     */
    private function buildContentTypeCollection(array $data)
    {
        $items = \array_merge(
            $data['items'],
            $data['includes']['Entry'] ?? []
        );

        $ids = \array_map(function (array $item) {
            return 'Entry' === $item['sys']['type']
                ? $item['sys']['contentType']['sys']['id']
                : null;
        }, $items);

        $ids = \array_filter(\array_unique($ids), function ($id): bool {
            return $id && !$this->resourcePool->has('ContentType', $id);
        });

        if ($ids) {
            $query = (new Query())
                ->where('sys.id[in]', \implode(',', $ids))
            ;
            $this->client->getContentTypes($query);
        }
    }

    /**
     * @param array $data
     */
    private function buildIncludes(array $data)
    {
        $items = \array_merge(
            $data['includes']['Entry'] ?? [],
            $data['includes']['Asset'] ?? []
        );
        foreach ($items as $item) {
            $this->build($item);
        }
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Cache;

use Contentful\Core\Resource\SystemPropertiesInterface;
use Contentful\Delivery\SystemProperties\LocalizedResource as LocalizedResourceSystemProperties;

/**
 * CacheClearer class.
 *
 * Use this class to clear the needed cache information from a
 * PSR-6 compatible pool.
 */
class CacheClearer extends BaseCacheHandler
{
    /**
     * @param bool $cacheContent
     *
     * @return bool
     */
    public function clear($cacheContent = false): bool
    {
        $keys = [];
        foreach ($this->fetchResources($cacheContent) as $resource) {
            /** @var SystemPropertiesInterface $sys */
            $sys = $resource->getSystemProperties();

            $options = $sys instanceof LocalizedResourceSystemProperties
                ? ['locale' => $sys->getLocale()]
                : [];
            $keys[] = $this->resourcePool->generateKey($sys->getType(), $sys->getId(), $options);
        }

        return $this->cacheItemPool->deleteItems($keys);
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery;

use Cache\Adapter\Void\VoidCachePool;
use Contentful\Core\Log\NullLogger;
use GuzzleHttp\Client as HttpClient;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;

class ClientOptions
{
    /**
     * @var string
     */
    private $host = Client::URI_DELIVERY;

    /**
     * @var CacheItemPoolInterface
     */
    private $cacheItemPool;

    /**
     * @var bool
     */
    private $cacheAutoWarmup = false;

    /**
     * @var bool
     */
    private $cacheContent = false;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var string|null
     */
    private $defaultLocale;

    /**
     * @var HttpClient
     */
    private $httpClient;

    /**
     * @var bool
     */
    private $usesLowMemoryResourcePool = false;

    /**
     * ClientOptions constructor.
     */
    public function __construct()
    {
        $this->cacheItemPool = new VoidCachePool();
        $this->logger = new NullLogger();
        $this->httpClient = new HttpClient();
    }

    /**
     * @return self
     */
    public static function create(): self
    {
        return new self();
    }

    /**
     * @param string $locale
     *
     * @return self
     */
    public function withDefaultLocale(string $locale)
    {
        $this->defaultLocale = $locale;

        return $this;
    }

    /**
     * @return string|null
     */
    public function getDefaultLocale()
    {
        return $this->defaultLocale;
    }

    /**
     * @return self
     */
    public function usingDeliveryApi(): self
    {
        $this->host = Client::URI_DELIVERY;

        return $this;
    }

    /**
     * @return self
     */
    public function usingPreviewApi(): self
    {
        $this->host = Client::URI_PREVIEW;

        return $this;
    }

    /**
     * @param string $host
     *
     * @return self
     */
    public function withHost(string $host): self
    {
        if ('/' === \mb_substr($host, -1)) {
            $host = \mb_substr($host, 0, -1);
        }

        $this->host = $host;

        return $this;
    }

    /**
     * @return string
     */
    public function getHost(): string
    {
        return $this->host;
    }

    /**
     * @param CacheItemPoolInterface $cacheItemPool
     * @param bool                   $autoWarmup
     * @param bool                   $cacheContent
     *
     * @return self
     */
    public function withCache(
        CacheItemPoolInterface $cacheItemPool,
        bool $autoWarmup = false,
        bool $cacheContent = false
    ): self {
        $this->cacheItemPool = $cacheItemPool;
        $this->cacheAutoWarmup = $autoWarmup;
        $this->cacheContent = $cacheContent;

        return $this;
    }

    /**
     * @return CacheItemPoolInterface
     */
    public function getCacheItemPool(): CacheItemPoolInterface
    {
        return $this->cacheItemPool;
    }

    /**
     * @return bool
     */
    public function hasCacheAutoWarmup(): bool
    {
        return $this->cacheAutoWarmup;
    }

    /**
     * @return bool
     */
    public function hasCacheContent(): bool
    {
        return $this->cacheContent;
    }

    /**
     * Configure the Client to use any PSR-3 compatible logger.
     *
     * @param LoggerInterface $logger
     *
     * @return self
     */
    public function withLogger(LoggerInterface $logger): self
    {
        $this->logger = $logger;

        return $this;
    }

    /**
     * @return LoggerInterface
     */
    public function getLogger(): LoggerInterface
    {
        return $this->logger;
    }

    /**
     * @param HttpClient $client
     *
     * @return self
     */
    public function withHttpClient(HttpClient $client): self
    {
        $this->httpClient = $client;

        return $this;
    }

    /**
     * @return HttpClient
     */
    public function getHttpClient(): HttpClient
    {
        return $this->httpClient;
    }

    /**
     * Configures the client to use the default resource pool implementation,
     * which may use more memory in extreme scenarios (tens of thousands of resources).
     *
     * @return ClientOptions
     */
    public function withNormalResourcePool(): self
    {
        $this->usesLowMemoryResourcePool = false;

        return $this;
    }

    /**
     * Configures the client to use a resource pool which will not cache entries and assets,
     * which is useful when handling tens of thousand of resources,
     * but it may cause extra API calls in normal scenarios.
     * Use this option only if the default resource pool is causing you memory errors.
     *
     * @return ClientOptions
     */
    public function withLowMemoryResourcePool(): self
    {
        $this->usesLowMemoryResourcePool = true;

        return $this;
    }

    /**
     * @return bool
     */
    public function usesLowMemoryResourcePool(): bool
    {
        return $this->usesLowMemoryResourcePool;
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Cache;

use Contentful\Core\Resource\ResourceInterface;
use Contentful\Core\Resource\ResourcePoolInterface;
use Contentful\Delivery\Client\ClientInterface;
use Contentful\Delivery\Query;
use Contentful\Delivery\Resource\Locale;
use Psr\Cache\CacheItemPoolInterface;

abstract class BaseCacheHandler
{
    /**
     * @var ClientInterface
     */
    protected $client;

    /**
     * @var ResourcePoolInterface
     */
    protected $resourcePool;

    /**
     * @var CacheItemPoolInterface
     */
    protected $cacheItemPool;

    /**
     * CacheWarmer constructor.
     *
     * @param ClientInterface        $client
     * @param ResourcePoolInterface  $resourcePool
     * @param CacheItemPoolInterface $cacheItemPool
     */
    public function __construct(
        ClientInterface $client,
        ResourcePoolInterface $resourcePool,
        CacheItemPoolInterface $cacheItemPool
    ) {
        $this->client = $client;
        $this->resourcePool = $resourcePool;
        $this->cacheItemPool = $cacheItemPool;
    }

    /**
     * @param bool $cacheContent
     *
     * @return ResourceInterface[]
     */
    protected function fetchResources(bool $cacheContent = false)
    {
        $resources = [
            $this->client->getSpace(),
            $this->client->getEnvironment(),
        ];

        $query = (new Query())
            ->setLimit(100)
        ;
        foreach ($this->client->getContentTypes($query) as $contentType) {
            $resources[] = $contentType;
        }

        foreach ($resources as $resource) {
            yield $resource;
        }

        if ($cacheContent) {
            $locales = \array_map(function (Locale $locale) {
                return $locale->getCode();
            }, $this->client->getEnvironment()->getLocales());
            $locales[] = '*';

            foreach ($this->fetchCollection('Entry', $locales) as $entry) {
                yield $entry;
            }

            foreach ($this->fetchCollection('Asset', $locales) as $asset) {
                yield $asset;
            }
        }
    }

    /**
     * @param string   $type    Either 'Entry' or 'Asset'
     * @param string[] $locales
     *
     * @return \Generator
     */
    private function fetchCollection(string $type, array $locales): \Generator
    {
        foreach ($locales as $locale) {
            $skip = 0;
            do {
                $query = (new Query())
                    ->setLocale($locale)
                    ->setLimit(1000)
                    ->setSkip($skip)
                ;
                $resources = 'Entry' === $type
                    ? $this->client->getEntries($query)
                    : $this->client->getAssets($query);

                foreach ($resources as $resource) {
                    yield $resource;
                }

                $skip += 1000;
            } while ($resources->getTotal() > $resources->getSkip() + 1000);
        }
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Mapper;

use Contentful\Core\Resource\ResourceInterface;
use Contentful\Core\Resource\SystemPropertiesInterface;
use Contentful\Core\ResourceBuilder\MapperInterface;
use Contentful\Core\ResourceBuilder\ObjectHydrator;
use Contentful\Core\ResourceBuilder\ResourceBuilderInterface;
use Contentful\Delivery\Client\ClientInterface;
use Contentful\RichText\ParserInterface;

/**
 * BaseMapper class.
 */
abstract class BaseMapper implements MapperInterface
{
    /**
     * @var ResourceBuilderInterface
     */
    protected $builder;

    /**
     * @var ClientInterface
     */
    protected $client;

    /**
     * @var ParserInterface
     */
    protected $richTextParser;

    /**
     * @var ObjectHydrator
     */
    protected $hydrator;

    /**
     * BaseMapper constructor.
     *
     * @param ResourceBuilderInterface $builder
     * @param ClientInterface          $client
     * @param ParserInterface          $richTextParser
     */
    public function __construct(ResourceBuilderInterface $builder, ClientInterface $client, ParserInterface $richTextParser)
    {
        $this->builder = $builder;
        $this->client = $client;
        $this->richTextParser = $richTextParser;
        $this->hydrator = new ObjectHydrator();
    }

    /**
     * @param string $class
     * @param array  $data
     *
     * @return SystemPropertiesInterface
     */
    protected function createSystemProperties(string $class, array $data): SystemPropertiesInterface
    {
        $sys = $data['sys'];

        if (isset($sys['space']) && !$sys['space'] instanceof ResourceInterface) {
            $sys['space'] = $this->client->getSpace();
        }

        if (isset($sys['environment']) && !$sys['environment'] instanceof ResourceInterface) {
            $sys['environment'] = $this->client->getEnvironment();
        }

        if (isset($sys['contentType']) && !$sys['contentType'] instanceof ResourceInterface) {
            $sys['contentType'] = $this->client->getContentType($sys['contentType']['sys']['id']);
        }

        return new $class($sys);
    }

    /**
     * @param mixed       $fieldData
     * @param string|null $locale
     *
     * @return array
     */
    protected function normalizeFieldData($fieldData, string $locale = null)
    {
        if (!$locale) {
            return $fieldData;
        }

        return [$locale => $fieldData];
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Mapper;

use Contentful\Core\ResourceBuilder\ResourceBuilderInterface;
use Contentful\Delivery\Client\ClientInterface;
use Contentful\Delivery\Resource\ContentType as ContentTypeClass;
use Contentful\Delivery\Resource\DeletedEntry as ResourceClass;
use Contentful\Delivery\SystemProperties\ContentType as ContentTypeSystemProperties;
use Contentful\Delivery\SystemProperties\DeletedEntry as SystemProperties;
use Contentful\RichText\ParserInterface;

/**
 * DeletedEntry class.
 *
 * This class is responsible for converting raw API data into a PHP object
 * of class Contentful\Delivery\Resource\DeletedEntry.
 */
class DeletedEntry extends BaseMapper
{
    /**
     * @var ContentTypeClass
     */
    private $contentType;

    /**
     * {@inheritdoc}
     */
    public function __construct(
        ResourceBuilderInterface $builder,
        ClientInterface $client,
        ParserInterface $richTextParser
    ) {
        parent::__construct($builder, $client, $richTextParser);

        /** @var ContentTypeClass $contentType */
        $contentType = $this->hydrator->hydrate(ContentTypeClass::class, [
            'sys' => $this->createSystemProperties(ContentTypeSystemProperties::class, [
                'sys' => [
                    'id' => '__DeletedEntryContentType',
                    'type' => 'ContentType',
                    'revision' => 1,
                    'createdAt' => '2015-01-01T12:00:00Z',
                    'updatedAt' => '2015-01-01T12:00:00Z',
                    'environment' => $client->getEnvironment(),
                    'space' => $client->getSpace(),
                ],
            ]),
            'name' => 'Deleted Entry',
        ]);
        $this->contentType = $contentType;
    }

    /**
     * {@inheritdoc}
     */
    public function map($resource, array $data): ResourceClass
    {
        if (!isset($data['sys']['contentType'])) {
            $data['sys']['contentType'] = $this->contentType;
        }

        /** @var ResourceClass $deletedEntry */
        $deletedEntry = $this->hydrator->hydrate($resource ?: ResourceClass::class, [
            'sys' => $this->createSystemProperties(SystemProperties::class, $data),
        ]);

        return $deletedEntry;
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Synchronization;

use Contentful\Delivery\Resource\ContentType;

/**
 * A sync Query can be used to limit what type of resources and events should be synced.
 *
 * @see \Contentful\Delivery\Synchronization\Mananager Synchronization\Mananager
 */
class Query
{
    /**
     * Limit the sync to event to a specific type.
     *
     * @var string|null
     */
    private $type = 'all';

    /**
     * For entries, limit results to this content type.
     *
     * @var string|null
     */
    private $contentType;

    /**
     * Returns the parameters to execute this query.
     *
     * @return array
     */
    public function getQueryData(): array
    {
        $data = [
            'initial' => 'true',
            'type' => 'all' !== $this->type ? $this->type : null,
            'content_type' => $this->contentType,
        ];

        return $data;
    }

    /**
     * The urlencoded query string for this query.
     *
     * @return string
     */
    public function getQueryString(): string
    {
        return \http_build_query($this->getQueryData(), '', '&', \PHP_QUERY_RFC3986);
    }

    /**
     * Set the Type of event or resource to sync.
     *
     * Set to null to sync everything.
     *
     * Valid values for $type are:
     *  - all
     *  - Asset
     *  - Entry
     *  - Deletion
     *  - DeletedAsset
     *  - DeletedEntry
     *
     * @param string|null $type
     *
     * @throws \InvalidArgumentException when an invalid $type is set
     *
     * @return $this
     */
    public function setType(string $type = null)
    {
        $validTypes = ['all', 'Asset', 'Entry', 'Deletion', 'DeletedAsset', 'DeletedEntry'];
        if (!\in_array($type, $validTypes, true)) {
            throw new \InvalidArgumentException(\sprintf(
                'Unexpected type "%s".',
                $type
            ));
        }

        $this->type = $type;

        return $this;
    }

    /**
     * Set the content type to which results should be limited. Set to NULL to not filter for a content type.
     *
     * @param ContentType|string|null $contentType
     *
     * @return $this
     */
    public function setContentType($contentType)
    {
        if ($contentType instanceof ContentType) {
            $contentType = $contentType->getId();
        }

        $this->contentType = $contentType;

        $this->setType('Entry');

        return $this;
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Synchronization;

use Contentful\Core\Resource\ResourceInterface;
use Contentful\Core\ResourceBuilder\ResourceBuilderInterface;
use Contentful\Delivery\Client\SynchronizationClientInterface;

/**
 * The synchronization Manager can be used to sync a Space to this server.
 *
 * It provides notifications about added, updated and deleted assets and entries.
 *
 * @see https://www.contentful.com/developers/docs/concepts/sync/ Sync API
 */
class Manager
{
    /**
     * @var SynchronizationClientInterface
     */
    private $client;

    /**
     * @var ResourceBuilderInterface
     */
    private $builder;

    /**
     * @var bool
     */
    private $isDeliveryApi;

    /**
     * Manager constructor.
     *
     * Do not instantiate this class yourself,
     * use SynchronizationClientInterface::getSynchronizationManager() instead.
     *
     * @param SynchronizationClientInterface $client
     * @param ResourceBuilderInterface       $builder
     * @param bool                           $isDeliveryApi
     *
     * @see Client::getSynchronizationManager()
     */
    public function __construct(
        SynchronizationClientInterface $client,
        ResourceBuilderInterface $builder,
        bool $isDeliveryApi
    ) {
        $this->client = $client;
        $this->builder = $builder;
        $this->isDeliveryApi = $isDeliveryApi;
    }

    /**
     * @param string|null $token
     * @param Query|null  $query
     *
     * @return \Generator An instance of Result wrapped in a Generator object
     */
    public function sync(string $token = null, Query $query = null): \Generator
    {
        do {
            $result = $token ? $this->continueSync($token) : $this->startSync($query);

            yield $result;

            $token = $result->getToken();
        } while (!$result->isDone());
    }

    /**
     * Starts a new Synchronization.
     * The result will contain all the resources currently present in the space.
     * By calling Result::isDone(), it can be checked if there's another page of results,
     * if so call `continueSync` to get the next page.
     *
     * A Query object can be used to return only a subset of the space.
     *
     * @param Query|null $query
     *
     * @return Result
     */
    public function startSync(Query $query = null)
    {
        $query = null !== $query ? $query : new Query();
        $response = $this->client->syncRequest($query->getQueryData());

        return $this->buildResult($response);
    }

    /**
     * Continues the synchronization either at the next page,
     * or with the results since the initial synchronization.
     *
     * @param string|Result $token
     *
     * @throws \RuntimeException if this method is used for a subsequent sync when used with the Preview API
     *
     * @return Result
     */
    public function continueSync($token): Result
    {
        if ($token instanceof Result) {
            if (!$this->isDeliveryApi && $token->isDone()) {
                throw new \RuntimeException('Can not continue syncing when using the Content Preview API.');
            }

            $token = $token->getToken();
        }

        $response = $this->client->syncRequest(['sync_token' => $token]);

        return $this->buildResult($response);
    }

    /**
     * Build a Result from the API response.
     *
     * @param array $data
     *
     * @return Result
     */
    private function buildResult(array $data): Result
    {
        $token = $this->getTokenFromResponse($data);
        $done = isset($data['nextSyncUrl']);

        $items = \array_map(function (array $item): ResourceInterface {
            return $this->builder->build($item);
        }, $data['items']);

        return new Result($items, $token, $done);
    }

    /**
     * Parses the sync_token out of an URL supplied by the API.
     *
     * @param array $data The API response
     *
     * @return string
     */
    private function getTokenFromResponse(array $data): string
    {
        $url = $data['nextSyncUrl'] ?? $data['nextPageUrl'];

        $queryValues = [];
        \parse_str(\parse_url($url, \PHP_URL_QUERY), $queryValues);

        return $queryValues['sync_token'];
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery;

use Contentful\Core\Api\BaseQuery;

/**
 * A Query is used to filter and order collections when making API requests.
 */
class Query extends BaseQuery
{
    /**
     * The amount of levels of links that should be resolved.
     *
     * @var int|null
     */
    private $include;

    /**
     * The locale for which to query.
     *
     * @var string|null
     */
    private $locale;

    /**
     * Returns the parameters to execute this query.
     *
     * @return array
     */
    public function getQueryData(): array
    {
        $query = parent::getQueryData();
        if (null !== $this->include) {
            $query['include'] = $this->include;
        }
        if (null !== $this->locale) {
            $query['locale'] = $this->locale;
        }

        return $query;
    }

    /**
     * Set the amount of levels of links that should be resolved.
     *
     * @param int|null $include
     *
     * @return $this
     */
    public function setInclude(int $include = null)
    {
        $this->include = $include;

        return $this;
    }

    /**
     * Sets the locale for which content should be retrieved. Set it to `*` to retrieve all locales.
     *
     * @param string|null $locale
     *
     * @return $this
     */
    public function setLocale(string $locale = null)
    {
        $this->locale = $locale;

        return $this;
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\ResourcePool;

use Contentful\Core\Resource\ResourcePoolInterface;
use Contentful\Delivery\Client\JsonDecoderClientInterface;
use Contentful\Delivery\ClientOptions;

/**
 * Factory class.
 * This class handles the creation of resource pool objects.
 */
class Factory
{
    /**
     * Creates the appropriate ResourcePool object using the given client and options.
     *
     * @param JsonDecoderClientInterface $client
     * @param ClientOptions              $options
     *
     * @return ResourcePoolInterface
     */
    public static function create(JsonDecoderClientInterface $client, ClientOptions $options): ResourcePoolInterface
    {
        if ($options->usesLowMemoryResourcePool()) {
            return new Standard($client->getApi(), $client->getSpaceId(), $client->getEnvironmentId());
        }

        return new Extended(
            $client,
            $options->getCacheItemPool(),
            $options->hasCacheAutoWarmup(),
            $options->hasCacheContent()
        );
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Mapper;

use Contentful\Delivery\Resource\DeletedAsset as ResourceClass;
use Contentful\Delivery\SystemProperties\DeletedAsset as SystemProperties;

/**
 * DeletedAsset class.
 *
 * This class is responsible for converting raw API data into a PHP object
 * of class Contentful\Delivery\Resource\DeletedAsset.
 */
class DeletedAsset extends BaseMapper
{
    /**
     * {@inheritdoc}
     */
    public function map($resource, array $data): ResourceClass
    {
        /** @var ResourceClass $deletedAsset */
        $deletedAsset = $this->hydrator->hydrate($resource ?: ResourceClass::class, [
            'sys' => $this->createSystemProperties(SystemProperties::class, $data),
        ]);

        return $deletedAsset;
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Mapper;

use Contentful\Delivery\Resource\Environment as ResourceClass;
use Contentful\Delivery\Resource\Locale;
use Contentful\Delivery\SystemProperties\Environment as SystemProperties;

/**
 * Environment class.
 *
 * This class is responsible for converting raw API data into a PHP object
 * of class Contentful\Delivery\Resource\Environment.
 */
class Environment extends BaseMapper
{
    /**
     * {@inheritdoc}
     */
    public function map($resource, array $data): ResourceClass
    {
        /** @var ResourceClass $environment */
        $environment = $this->hydrator->hydrate($resource ?: ResourceClass::class, [
            'sys' => $this->createSystemProperties(SystemProperties::class, $data),
            'locales' => \array_map(function (array $localeData): Locale {
                /** @var Locale $locale */
                $locale = $this->builder->build($localeData);

                return $locale;
            }, $data['locales']),
        ]);

        return $environment;
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Cache;

use Contentful\Core\Resource\SystemPropertiesInterface;
use Contentful\Delivery\SystemProperties\LocalizedResource as LocalizedResourceSystemProperties;
use function GuzzleHttp\json_encode as guzzle_json_encode;

/**
 * CacheWarmer class.
 *
 * Use this class to save the needed cache information in a
 * PSR-6 compatible pool.
 */
class CacheWarmer extends BaseCacheHandler
{
    /**
     * @param bool $cacheContent
     *
     * @return bool
     */
    public function warmUp($cacheContent = false): bool
    {
        foreach ($this->fetchResources($cacheContent) as $resource) {
            /** @var SystemPropertiesInterface $sys */
            $sys = $resource->getSystemProperties();

            $options = $sys instanceof LocalizedResourceSystemProperties
                ? ['locale' => $sys->getLocale()]
                : [];
            $key = $this->resourcePool->generateKey($sys->getType(), $sys->getId(), $options);

            $item = $this->cacheItemPool->getItem($key);
            $item->set(guzzle_json_encode($resource));

            $this->cacheItemPool->saveDeferred($item);
        }

        return $this->cacheItemPool->commit();
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Resource;

use Contentful\Core\Api\Link;
use Contentful\Core\Resource\EntryInterface;
use Contentful\Core\Resource\ResourceArray;
use Contentful\Delivery\Client\ClientInterface;
use Contentful\Delivery\Query;
use Contentful\Delivery\Resource\ContentType\Field;
use Contentful\Delivery\SystemProperties\Entry as SystemProperties;

class Entry extends LocalizedResource implements EntryInterface, \ArrayAccess
{
    /**
     * @var array
     */
    protected $fields;

    /**
     * @var SystemProperties
     */
    protected $sys;

    /**
     * @var ClientInterface
     */
    protected $client;

    /**
     * {@inheritdoc}
     */
    public function getSystemProperties(): SystemProperties
    {
        return $this->sys;
    }

    /**
     * Returns the space this entry belongs to.
     *
     * @return Space
     */
    public function getSpace(): Space
    {
        return $this->sys->getSpace();
    }

    /**
     * Returns the environment this entry belongs to.
     *
     * @return Environment
     */
    public function getEnvironment(): Environment
    {
        return $this->sys->getEnvironment();
    }

    /**
     * @return ContentType
     */
    public function getContentType(): ContentType
    {
        return $this->sys->getContentType();
    }

    /**
     * @param string $name
     * @param array  $arguments
     *
     * @return mixed
     */
    public function __call(string $name, array $arguments)
    {
        if (0 === \mb_strpos($name, 'has')) {
            $field = $this->sys->getContentType()->getField($name, true);

            // Only works if the "has" is "magic", i.e.
            // the field is not actually called hasSomething.
            if (!$field) {
                return $this->has(
                    \mb_substr($name, 3),
                    $this->getLocaleFromInput($arguments[0] ?? null),
                    $arguments[1] ?? true
                );
            }
        }

        // Some templating languages might end up trying to access a field
        // using a method-like syntax "$entry->field()".
        // Even though it's not the suggested approach, we allow that syntax
        // for maximum compatibility purposes.
        if (0 === \mb_strpos($name, 'get')) {
            $name = \mb_substr($name, 3);
        }

        $locale = $this->getLocaleFromInput($arguments[0] ?? null);

        return $this->get(
            $name,
            $locale,
            (bool) ($arguments[1] ?? true)
        );
    }

    /**
     * Shortcut for accessing fields using $entry->fieldName.
     * It will use the locale currently defined.
     *
     * @param string $name
     *
     * @return mixed
     */
    public function __get(string $name)
    {
        return $this->get($name);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetGet($name)
    {
        return $this->get($name);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetExists($name)
    {
        return $this->has($name);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetSet($name, $value)
    {
        throw new \LogicException('Entry class does not support setting fields.');
    }

    /**
     * {@inheritdoc}
     */
    public function offsetUnset($name)
    {
        throw new \LogicException('Entry class does not support unsetting fields.');
    }

    /**
     * Checks whether the current entry has a field with a certain ID.
     *
     * @param string      $name
     * @param string|null $locale
     * @param bool        $checkLinksAreResolved
     *
     * @return bool
     */
    public function has(string $name, string $locale = null, bool $checkLinksAreResolved = true): bool
    {
        $field = $this->sys->getContentType()->getField($name, true);

        if (!$field) {
            return false;
        }

        if (!\array_key_exists($field->getId(), $this->fields)) {
            return false;
        }

        try {
            $result = $this->getUnresolvedField($field, $locale);
            if ($checkLinksAreResolved) {
                $this->resolveFieldLinks($result, $locale);
            }
        } catch (\Exception $exception) {
            return false;
        }

        return true;
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function __isset(string $name)
    {
        return $this->has($name);
    }

    /**
     * Returns all fields of the current entry, with some optimizations applied.
     * Links are resolved by default. If you want to get raw link objects rather than
     * complete resources, set the $resolveLinks parameter to false.
     *
     * @param string|null $locale
     * @param bool        $resolveLinks
     *
     * @return array
     */
    public function all(string $locale = null, bool $resolveLinks = true): array
    {
        $values = [];
        foreach ($this->getContentType()->getFields() as $field) {
            $result = $this->getUnresolvedField($field, $locale);

            $values[$field->getId()] = $resolveLinks
                ? $this->resolveFieldLinks($result, $locale)
                : $result;
        }

        return $values;
    }

    /**
     * Returns a the value of a field using the given locale.
     * It will also resolve links. If you want to access the ID of a link
     * or of an array of links, simply append "Id" to the end of the
     * $name parameter.
     *
     * ```
     * $author = $entry->get('author');
     * $id = $entry->get('authorId');
     * ```
     *
     * @param string      $name
     * @param string|null $locale
     * @param bool        $resolveLinks If set to false, links and array of links will not be resolved
     *
     * @return mixed
     */
    public function get(string $name, string $locale = null, bool $resolveLinks = true)
    {
        $field = $this->sys->getContentType()->getField($name, true);
        if ($field) {
            $result = $this->getUnresolvedField($field, $locale);

            return $resolveLinks
                ? $this->resolveFieldLinks($result, $locale)
                : $result;
        }

        // If no clean match was found using the provided field name,
        // let's attempt to see if we're fetching an ID of a link or array of links.
        $value = $this->getFieldWithId($name, $locale);
        if (null !== $value) {
            return $value;
        }

        throw new \InvalidArgumentException(\sprintf(
            'Trying to access non existent field "%s" on an entry with content type "%s" ("%s").',
            $name,
            $this->sys->getContentType()->getName(),
            $this->sys->getContentType()->getSystemProperties()->getId()
        ));
    }

    /**
     * Attempts to fetch a value given the current configuration.
     * It will return the raw field value,
     * without applying any transformation to it.
     *
     * @param Field       $field
     * @param string|null $locale
     *
     * @return mixed
     */
    private function getUnresolvedField(Field $field, string $locale = null)
    {
        // The field is not currently available on this resource,
        // but it exists in the content type, so we return an appropriate
        // default value.
        if (!isset($this->fields[$field->getId()])) {
            return 'Array' === $field->getType() ? [] : null;
        }

        $value = $this->fields[$field->getId()];

        // This also checks two things:
        // * the compatibility of the given locale with the one present in sys.locale
        // * the existence of the given locale among those available in the environment
        // If we're trying to access locale X and the entry was built with locale Y,
        // or the environment does not support such locale, an exception will be thrown.
        // To allow accessing multiple locales on a single entry, fetching it
        // with locale=* is required.
        $locale = $this->getLocaleFromInput($locale);

        if (\array_key_exists($locale, $value)) {
            return $value[$locale];
        }

        // If a field is not localized, it means that there are no values besides
        // $value[$defaultLocale], so because that check has already happened, we know
        // we're trying to access an invalid locale which is not correctly set.
        if (!$field->isLocalized()) {
            throw new \InvalidArgumentException(\sprintf(
                'Trying to access the non-localized field "%s" on content type "%s" using the non-default locale "%s".',
                $field->getName(),
                $this->sys->getContentType()->getName(),
                $locale
            ));
        }

        // If we reach this point, it means:
        // * the field is localized
        // * we're trying to get a non-default locale
        // * the entry was not built using a specific locale (i.e. all locales for a field are available)
        // Therefore, we can inspect the fallback chain for a suitable locale.
        $locale = $this->walkFallbackChain($value, $locale, $this->sys->getEnvironment());

        if ($locale) {
            return $value[$locale];
        }

        return 'Array' === $field->getType() ? [] : null;
    }

    /**
     * Given a field value, this method will resolve links
     * if it's a Link object or an array of links.
     *
     * @param mixed       $field
     * @param string|null $locale
     *
     * @return mixed
     */
    private function resolveFieldLinks($field, string $locale = null)
    {
        // If no locale is set, to resolve links we use either the special "*" locale,
        // or the default one, depending whether this entry was built using a locale or not
        if (null === $locale) {
            $locale = null === $this->sys->getLocale()
                ? '*'
                : $this->getLocale();
        }

        if ($field instanceof Link) {
            return $this->client->resolveLink($field, $locale);
        }

        if (\is_array($field) && isset($field[0]) && $field[0] instanceof Link) {
            return $this->client->resolveLinkCollection($field, $locale);
        }

        return $field;
    }

    /**
     * Checks whether the given $name parameter corresponds to an attempt
     * of fetching the ID of a link (or array of links).
     *
     * @param string      $name
     * @param string|null $locale
     *
     * @return string|string[]|null Returns null if $name is not a valid field ID string
     */
    private function getFieldWithId(string $name, string $locale = null)
    {
        if ('Id' !== \mb_substr($name, -2)) {
            return null;
        }

        $field = $this->sys->getContentType()->getField(\mb_substr($name, 0, -2), true);
        if (!$field) {
            return null;
        }

        if ('Link' !== $field->getType() && ('Array' !== $field->getType() || 'Link' !== $field->getItemsType())) {
            return null;
        }

        $value = $this->getUnresolvedField($field, $locale);
        if ($value instanceof Link) {
            return $value->getId();
        }

        return \array_map(function (Link $link) {
            return $link->getId();
        }, $value);
    }

    /**
     * Gets all entries that contain links to the current one.
     * You can provide a Query object in order to set parameters
     * such as locale, include, and sorting.
     *
     * @param Query|null $query
     *
     * @return ResourceArray
     */
    public function getReferences(Query $query = null): ResourceArray
    {
        $query = $query ?: new Query();
        $query->linksToEntry($this->getId());

        return $this->client->getEntries($query);
    }

    /**
     * {@inheritdoc}
     */
    public function jsonSerialize(): array
    {
        $locale = $this->sys->getLocale();

        $fields = new \stdClass();
        foreach ($this->fields as $name => $value) {
            $fields->$name = $locale ? $value[$locale] : $value;
        }

        return [
            'sys' => $this->sys,
            'fields' => $fields,
        ];
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Mapper;

use Contentful\Core\Api\DateTimeImmutable;
use Contentful\Core\Api\Link;
use Contentful\Core\Api\Location;
use Contentful\Delivery\Resource\ContentType as ResourceContentType;
use Contentful\Delivery\Resource\Entry as ResourceClass;
use Contentful\Delivery\SystemProperties\Entry as SystemProperties;
use function GuzzleHttp\json_decode as guzzle_json_decode;
use function GuzzleHttp\json_encode as guzzle_json_encode;

/**
 * Entry class.
 *
 * This class is responsible for converting raw API data into a PHP object
 * of class Contentful\Delivery\Resource\Entry.
 */
class Entry extends BaseMapper
{
    /**
     * {@inheritdoc}
     */
    public function map($resource, array $data): ResourceClass
    {
        /** @var SystemProperties $sys */
        $sys = $this->createSystemProperties(SystemProperties::class, $data);
        $locale = $sys->getLocale();

        // We normalize the field data to always contain locales.
        foreach ($data['fields'] ?? [] as $name => $value) {
            // If the value is an empty array, and no locale was used,
            // we remove the value as the entry itself will handle default values.
            if (!$locale && $value === []) {
                unset($data['fields'][$name]);
                continue;
            }

            $data['fields'][$name] = $locale ? [$locale => $value] : $value;
        }

        /** @var ResourceClass $entry */
        $entry = $this->hydrator->hydrate($resource ?: ResourceClass::class, [
            'sys' => $sys,
            'client' => $this->client,
            'fields' => isset($data['fields'])
                ? $this->buildFields($sys->getContentType(), $data['fields'], $resource)
                : [],
        ]);

        $entry->initLocales($entry->getSystemProperties()->getEnvironment()->getLocales());

        return $entry;
    }

    /**
     * @param ResourceContentType $contentType
     * @param array               $fields
     * @param ResourceClass|null  $previous
     *
     * @return array
     */
    private function buildFields(
        ResourceContentType $contentType,
        array $fields,
        ResourceClass $previous = null
    ): array {
        if ($previous) {
            $fields = $this->mergePreviousFields($fields, $previous);
        }

        foreach ($fields as $name => $data) {
            $field = $contentType->getField($name);

            // If field is empty, it means that the data currently available
            // for the content type is not correct.
            // Instead of failing and causing a type error, we fallback on a simple
            // field, and leave the handling of those edge cases to the user.
            if (!$field) {
                @\trigger_error(\sprintf(
                    'Entry of content type "%s" ("%s") being built contains field "%s" which is not present in the content type definition.'
                    .' Please check your cache for stale content type definitions.',
                    $contentType->getName(),
                    $contentType->getId(),
                    $name
                ), \E_USER_WARNING);
                $field = $contentType->addUnknownField($name);
            }

            foreach ($data as $locale => $value) {
                $fields[$name][$locale] = $this->formatValue(
                    $field->getType(),
                    $value,
                    $field->getItemsType()
                );
            }
        }

        return $fields;
    }

    /**
     * If a previous entry is given, this methods will combine all values from the $fields
     * array an those in the already built entry.
     * This is done so in the event of partial resource building (i.e. after having queried
     * the API using the "select" operator), the resulting object is increasingly updated,
     * therefore the new object will contain the biggest set of old plus new fields.
     *
     * @param array         $fields The field values that have been returned by the API
     * @param ResourceClass $entry  The previous entry object that was already built, if present
     *
     * @return array
     */
    private function mergePreviousFields(array $fields, ResourceClass $entry): array
    {
        // Entry fields have private access, so we use this trick to fetch them.
        // https://ocramius.github.io/blog/accessing-private-php-class-members-without-reflection/
        $extractor = \Closure::bind(function (ResourceClass $entry) {
            return $entry->fields;
        }, null, $entry);
        $currentFields = $extractor($entry);

        foreach ($fields as $name => $values) {
            if (!isset($currentFields[$name])) {
                $currentFields[$name] = [];
            }

            foreach ($values as $locale => $value) {
                $currentFields[$name][$locale] = $value;
            }
        }

        return $currentFields;
    }

    /**
     * Transforms values from the original JSON representation to an appropriate PHP representation.
     *
     * @param string      $type
     * @param mixed       $value
     * @param string|null $itemsType The type of the items in the array, if it's an array field
     *
     * @return mixed
     */
    private function formatValue(string $type, $value, string $itemsType = null)
    {
        // Certain fields are already built as objects (Location, Link, DateTimeImmutable)
        // if the entry has already been built partially.
        // We restore these objects to their JSON implementations to avoid conflicts.
        if (\is_object($value) && $value instanceof \JsonSerializable) {
            $value = guzzle_json_decode(guzzle_json_encode($value), true);
        }

        if (null === $value) {
            return null;
        }

        switch ($type) {
            case 'Array':
                return \array_map(function ($value) use ($itemsType) {
                    return $this->formatValue((string) $itemsType, $value);
                }, $value);
            case 'Date':
                return new DateTimeImmutable($value, new \DateTimeZone('UTC'));
            case 'Link':
                return new Link($value['sys']['id'], $value['sys']['linkType']);
            case 'Location':
                return new Location($value['lat'], $value['lon']);
            case 'RichText':
                return $this->richTextParser->parse($value);
            default:
                return $value;
        }
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery;

use Contentful\Core\Api\BaseClient;
use Contentful\Core\Api\Exception;
use Contentful\Core\Api\Link;
use Contentful\Core\Api\LinkResolverInterface;
use Contentful\Core\Resource\ResourceArray;
use Contentful\Core\Resource\ResourceInterface;
use Contentful\Core\Resource\ResourcePoolInterface;
use Contentful\Core\ResourceBuilder\ResourceBuilderInterface;
use Contentful\Delivery\Client\ClientInterface;
use Contentful\Delivery\Client\JsonDecoderClientInterface;
use Contentful\Delivery\Client\SynchronizationClientInterface;
use Contentful\Delivery\Resource\Asset;
use Contentful\Delivery\Resource\ContentType;
use Contentful\Delivery\Resource\Entry;
use Contentful\Delivery\Resource\Environment;
use Contentful\Delivery\Resource\Space;
use Contentful\Delivery\ResourcePool\Factory;
use Contentful\Delivery\Synchronization\Manager;
use Contentful\RichText\Parser;

/**
 * A Client is used to communicate the Contentful Delivery API.
 *
 * A Client is only responsible for one space and one environment.
 * When access to multiple spaces or environments is required, create multiple Clients.
 *
 * This class can be configured to use the Preview API instead of the Delivery API.
 * This grants access to not yet published content.
 */
class Client extends BaseClient implements ClientInterface, SynchronizationClientInterface, JsonDecoderClientInterface
{
    /**
     * @var string
     */
    const API_DELIVERY = 'DELIVERY';

    /**
     * @var string
     */
    const API_PREVIEW = 'PREVIEW';

    /**
     * The URI for the Delivery API.
     *
     * @var string
     */
    const URI_DELIVERY = 'https://cdn.contentful.com';

    /**
     * The URI for the Preview API.
     *
     * @var string
     */
    const URI_PREVIEW = 'https://preview.contentful.com';

    /**
     * @var ResourceBuilderInterface
     */
    private $builder;

    /**
     * @var ResourcePoolInterface
     */
    private $resourcePool;

    /**
     * @var bool
     */
    private $isDeliveryApi;

    /**
     * @var string|null
     */
    private $defaultLocale;

    /**
     * @var string
     */
    private $spaceId;

    /**
     * @var string
     */
    private $environmentId;

    /**
     * @var ScopedJsonDecoder
     */
    private $scopedJsonDecoder;

    /**
     * @var LinkResolverInterface
     */
    private $linkResolver;

    /**
     * @var Parser
     */
    private $richTextParser;

    /**
     * Client constructor.
     *
     * @param string             $token         Delivery API Access Token for the space used with this Client
     * @param string             $spaceId       ID of the space used with this Client
     * @param string             $environmentId ID of the environment used with this Client
     * @param ClientOptions|null $options
     */
    public function __construct(
        string $token,
        string $spaceId,
        string $environmentId = 'master',
        ClientOptions $options = null
    ) {
        $this->spaceId = $spaceId;
        $this->environmentId = $environmentId;

        $options = $options ?? new ClientOptions();

        // This works best as a negation:
        // We consider all as Delivery API except for those
        // explicitly set to Preview.
        $this->isDeliveryApi = self::URI_PREVIEW !== $options->getHost();
        $this->defaultLocale = $options->getDefaultLocale();

        $this->resourcePool = Factory::create($this, $options);
        $this->scopedJsonDecoder = new ScopedJsonDecoder($this->spaceId, $this->environmentId);
        $this->linkResolver = new LinkResolver($this, $this->resourcePool);
        $this->richTextParser = new Parser($this->linkResolver);
        $this->builder = new ResourceBuilder($this, $this->resourcePool, $this->richTextParser);

        parent::__construct($token, $options->getHost(), $options->getLogger(), $options->getHttpClient());
    }

    /**
     * {@inheritdoc}
     */
    public function getApi(): string
    {
        return $this->isDeliveryApi ? self::API_DELIVERY : self::API_PREVIEW;
    }

    /**
     * @return string
     */
    public function getSpaceId(): string
    {
        return $this->spaceId;
    }

    /**
     * @return string
     */
    public function getEnvironmentId(): string
    {
        return $this->environmentId;
    }

    /**
     * @return ResourceBuilderInterface
     */
    public function getResourceBuilder(): ResourceBuilderInterface
    {
        return $this->builder;
    }

    /**
     * @return Parser
     */
    public function getRichTextParser(): Parser
    {
        return $this->richTextParser;
    }

    /**
     * {@inheritdoc}
     */
    protected static function getSdkName(): string
    {
        return 'contentful.php';
    }

    /**
     * {@inheritdoc}
     */
    protected static function getPackageName(): string
    {
        return 'contentful/contentful';
    }

    /**
     * {@inheritdoc}
     */
    protected static function getApiContentType(): string
    {
        return 'application/vnd.contentful.delivery.v1+json';
    }

    /**
     * Returns the resource pool currently in use.
     *
     * @return ResourcePoolInterface
     */
    public function getResourcePool(): ResourcePoolInterface
    {
        return $this->resourcePool;
    }

    /**
     * Returns the locale to be used in a cache key.
     *
     * @param string|null $locale
     *
     * @return string
     */
    private function getLocaleForCacheKey(string $locale = null): string
    {
        if ($locale) {
            return $locale;
        }

        return $this->getEnvironment()->getDefaultLocale()->getCode();
    }

    /**
     * {@inheritdoc}
     */
    public function getAsset(string $assetId, string $locale = null): Asset
    {
        $locale = $locale ?: $this->defaultLocale;

        /** @var Asset $asset */
        $asset = $this->requestWithCache(
            '/spaces/'.$this->spaceId.'/environments/'.$this->environmentId.'/assets/'.$assetId,
            ['locale' => $locale],
            'Asset',
            $assetId,
            $this->getLocaleForCacheKey($locale)
        );

        return $asset;
    }

    /**
     * {@inheritdoc}
     */
    public function getAssets(Query $query = null): ResourceArray
    {
        $queryData = $query ? $query->getQueryData() : [];
        if (!isset($queryData['locale'])) {
            $queryData['locale'] = $this->defaultLocale;
        }

        /** @var ResourceArray $assets */
        $assets = $this->request(
            'GET',
            '/spaces/'.$this->spaceId.'/environments/'.$this->environmentId.'/assets',
            ['query' => $queryData]
        );

        return $assets;
    }

    /**
     * {@inheritdoc}
     */
    public function getContentType(string $contentTypeId): ContentType
    {
        /** @var ContentType $contentType */
        $contentType = $this->requestWithCache(
            '/spaces/'.$this->spaceId.'/environments/'.$this->environmentId.'/content_types/'.$contentTypeId,
            [],
            'ContentType',
            $contentTypeId
        );

        return $contentType;
    }

    /**
     * {@inheritdoc}
     */
    public function getContentTypes(Query $query = null): ResourceArray
    {
        /** @var ResourceArray $contentTypes */
        $contentTypes = $this->request(
            'GET',
            '/spaces/'.$this->spaceId.'/environments/'.$this->environmentId.'/content_types',
            ['query' => $query ? $query->getQueryData() : []]
        );

        return $contentTypes;
    }

    /**
     * {@inheritdoc}
     */
    public function getEnvironment(): Environment
    {
        if ($this->resourcePool->has('Environment', $this->environmentId)) {
            /** @var Environment $environment */
            $environment = $this->resourcePool->get('Environment', $this->environmentId);

            return $environment;
        }

        // Because in the CDA there is no native endpoint for handling environments,
        // we create a fake one in order to assign the collection of locales to it.
        // We could be using any sort of fake resource for this, like a "LocaleCollection" type,
        // but given that previously locales were part of the space, whereas now they conceptually
        // belong to an environment, we choose this kind of abstraction.
        $locales = $this->callApi(
            'GET',
            '/spaces/'.$this->spaceId.'/environments/'.$this->environmentId.'/locales'
        );
        $environmentData = [
            'sys' => [
                'id' => $this->environmentId,
                'type' => 'Environment',
            ],
            'locales' => $locales['items'],
        ];

        /** @var Environment $environment */
        $environment = $this->builder->build($environmentData);

        return $environment;
    }

    /**
     * {@inheritdoc}
     */
    public function getEntry(string $entryId, string $locale = null): Entry
    {
        $locale = $locale ?: $this->defaultLocale;

        /** @var Entry $entry */
        $entry = $this->requestWithCache(
            '/spaces/'.$this->spaceId.'/environments/'.$this->environmentId.'/entries/'.$entryId,
            ['locale' => $locale],
            'Entry',
            $entryId,
            $this->getLocaleForCacheKey($locale)
        );

        return $entry;
    }

    /**
     * {@inheritdoc}
     */
    public function getEntries(Query $query = null): ResourceArray
    {
        $queryData = $query ? $query->getQueryData() : [];
        if (!isset($queryData['locale'])) {
            $queryData['locale'] = $this->defaultLocale;
        }

        /** @var ResourceArray $entries */
        $entries = $this->request(
            'GET',
            '/spaces/'.$this->spaceId.'/environments/'.$this->environmentId.'/entries',
            ['query' => $queryData]
        );

        return $entries;
    }

    /**
     * {@inheritdoc}
     */
    public function getSpace(): Space
    {
        try {
            /** @var Space $space */
            $space = $this->requestWithCache(
                '/spaces/'.$this->spaceId,
                [],
                'Space',
                $this->spaceId
            );
        } catch (Exception $exception) {
            // An edge case with environments might result in space data not being available.
            // As it *is* technically needed, we provide a fake space object.
            /** @var Space $space */
            $space = $this->builder->build([
                'sys' => [
                    'id' => $this->spaceId,
                    'type' => 'Space',
                ],
                'name' => $this->spaceId,
            ]);
        }

        return $space;
    }

    /**
     * {@inheritdoc}
     */
    public function resolveLink(Link $link, string $locale = null): ResourceInterface
    {
        return $this->linkResolver->resolveLink($link, [
            'locale' => (string) $locale,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function resolveLinkCollection(array $links, string $locale = null): array
    {
        return $this->linkResolver->resolveLinkCollection($links, [
            'locale' => (string) $locale,
        ]);
    }

    /**
     * Parse a JSON string.
     *
     * @param string $json
     *
     * @throws \InvalidArgumentException When attempting to parse JSON belonging to a different space or environment
     *
     * @return ResourceInterface|ResourceArray
     */
    public function parseJson(string $json)
    {
        return $this->builder->build(
            $this->scopedJsonDecoder->decode($json)
        );
    }

    /**
     * Returns true when using the Delivery API.
     *
     * @return bool
     */
    public function isDeliveryApi(): bool
    {
        return $this->isDeliveryApi;
    }

    /**
     * Returns true when using the Preview API.
     *
     * @return bool
     */
    public function isPreviewApi(): bool
    {
        return !$this->isDeliveryApi;
    }

    /**
     * {@inheritdoc}
     */
    public function getSynchronizationManager(): Manager
    {
        return new Manager($this, $this->builder, $this->isDeliveryApi);
    }

    /**
     * {@inheritdoc}
     */
    public function syncRequest(array $queryData): array
    {
        return $this->callApi('GET', '/spaces/'.$this->spaceId.'/environments/'.$this->environmentId.'/sync', [
            'query' => $queryData,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function request(string $method, string $uri, array $options = []): ResourceInterface
    {
        $response = $this->callApi('GET', $uri, $options);

        return $this->builder->build($response);
    }

    /**
     * @param string      $uri
     * @param array       $query
     * @param string|null $type
     * @param string|null $resourceId
     * @param string|null $locale
     *
     * @return ResourceInterface|ResourceArray
     */
    private function requestWithCache(
        string $uri,
        array $query = [],
        string $type = null,
        string $resourceId = null,
        string $locale = null
    ) {
        if ($type && $resourceId && $this->resourcePool->has($type, $resourceId, ['locale' => $locale])) {
            return $this->resourcePool->get($type, $resourceId, ['locale' => $locale]);
        }

        return $this->request('GET', $uri, ['query' => $query]);
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Mapper;

use Contentful\Core\Resource\ResourceArray as ResourceClass;

/**
 * ResourceArray class.
 *
 * This class is responsible for converting raw API data into a PHP object
 * of class Contentful\Core\Resource\ResourceArray.
 */
class ResourceArray extends BaseMapper
{
    /**
     * {@inheritdoc}
     */
    public function map($resource, array $data): ResourceClass
    {
        return new ResourceClass(
            \array_map(function ($item) {
                return $this->builder->build($item);
            }, $data['items']),
            $data['total'],
            $data['limit'],
            $data['skip']
        );
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Resource;

use Contentful\Core\File\FileInterface;
use Contentful\Core\Resource\AssetInterface;
use Contentful\Delivery\SystemProperties\Asset as SystemProperties;

class Asset extends LocalizedResource implements AssetInterface
{
    /**
     * @var SystemProperties
     */
    protected $sys;

    /**
     * @var string[]
     */
    protected $title;

    /**
     * @var string[]
     */
    protected $description;

    /**
     * @var FileInterface[]
     */
    protected $file;

    /**
     * {@inheritdoc}
     */
    public function getSystemProperties(): SystemProperties
    {
        return $this->sys;
    }

    /**
     * Returns the space this asset belongs to.
     *
     * @return Space
     */
    public function getSpace(): Space
    {
        return $this->sys->getSpace();
    }

    /**
     * Returns the environment this asset belongs to.
     *
     * @return Environment
     */
    public function getEnvironment(): Environment
    {
        return $this->sys->getEnvironment();
    }

    /**
     * @param Locale|string|null $locale
     *
     * @return string|null
     */
    public function getTitle($locale = null)
    {
        /** @var string|null $title */
        $title = $this->getProperty('title', $locale);

        return $title;
    }

    /**
     * @param Locale|string|null $locale
     *
     * @return string|null
     */
    public function getDescription($locale = null)
    {
        /** @var string|null $description */
        $description = $this->getProperty('description', $locale);

        return $description;
    }

    /**
     * @param Locale|string|null $locale
     *
     * @return FileInterface|null
     */
    public function getFile($locale = null)
    {
        /** @var FileInterface|null $file */
        $file = $this->getProperty('file', $locale);

        return $file;
    }

    /**
     * @param string             $property
     * @param Locale|string|null $locale
     *
     * @throws \InvalidArgumentException when $locale is not one of the locales supported by the space
     *
     * @return string|FileInterface|null
     */
    private function getProperty(string $property, $locale = null)
    {
        $localeCode = $this->getLocaleFromInput($locale);

        // This checks happens after the call to getLocaleFromInput
        // to make sure the Exception for invalid locales is still thrown.
        if (null === $this->$property) {
            return null;
        }

        $localeCode = $this->walkFallbackChain($this->$property, $localeCode, $this->sys->getEnvironment());

        return null === $localeCode
            ? null
            : $this->{$property}[$localeCode];
    }

    /**
     * {@inheritdoc}
     */
    public function jsonSerialize(): array
    {
        $locale = $this->sys->getLocale();
        $asset = [
            'sys' => $this->sys,
            'fields' => [],
        ];

        if (null !== $this->title) {
            $asset['fields']['title'] = $locale
                ? $this->title[$locale]
                : $this->title;
        }

        if (null !== $this->description) {
            $asset['fields']['description'] = $locale
                ? $this->description[$locale]
                : $this->description;
        }

        if (null !== $this->file) {
            $asset['fields']['file'] = $locale
                ? $this->file[$locale]
                : $this->file;
        }

        $asset['fields'] = (object) $asset['fields'];

        return $asset;
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\ResourcePool;

use Contentful\Core\Resource\BaseResourcePool;
use Contentful\Core\Resource\ResourceInterface;
use Contentful\Delivery\SystemProperties\LocalizedResource as LocalizedResourceSystemProperties;

/**
 * Standard class.
 *
 * This class acts as a registry for current objects managed by the Client,
 * but only stores space, environment, and content types.
 * Everything else is skipped.
 */
class Standard extends BaseResourcePool
{
    /**
     * @var string
     */
    protected $api;

    /**
     * @var string
     */
    protected $spaceId;

    /**
     * @var string
     */
    protected $environmentId;

    /**
     * Simple constructor.
     *
     * @param string $api
     * @param string $spaceId
     * @param string $environmentId
     */
    public function __construct(string $api, string $spaceId, string $environmentId)
    {
        $this->api = $api;
        $this->spaceId = $spaceId;
        $this->environmentId = $environmentId;
    }

    /**
     * Determines whether the given resource type must be actually stored.
     *
     * @param string $type
     *
     * @return bool
     */
    protected function savesResource(string $type): bool
    {
        return \in_array($type, ['ContentType', 'Environment', 'Space'], true);
    }

    /**
     * @param ResourceInterface $resource
     *
     * @return string|null
     */
    protected function getResourceLocale(ResourceInterface $resource)
    {
        $sys = $resource->getSystemProperties();

        return $sys instanceof LocalizedResourceSystemProperties
            ? $sys->getLocale()
            : null;
    }

    /**
     * Skeleton method which a can be overridden.
     *
     * @param string $key
     * @param string $type
     */
    protected function warmUp(string $key, string $type)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function has(string $type, string $id, array $options = []): bool
    {
        if (!$this->savesResource($type)) {
            return false;
        }

        $key = $this->generateKey($type, $id, $options);
        $this->warmUp($key, $type);

        return isset($this->resources[$key]);
    }

    /**
     * {@inheritdoc}
     */
    public function save(ResourceInterface $resource): bool
    {
        if (!$this->savesResource($resource->getType())) {
            return false;
        }

        $key = $this->generateKey(
            $resource->getType(),
            $resource->getId(),
            ['locale' => $this->getResourceLocale($resource)]
        );

        $exists = isset($this->resources[$key]);
        $this->resources[$key] = $resource;

        return !$exists;
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $type, string $id, array $options = []): ResourceInterface
    {
        $locale = $options['locale'] ?? null;
        $key = $this->generateKey($type, $id, $options);
        $this->warmUp($key, $type);

        if (!$this->savesResource($type) || !isset($this->resources[$key])) {
            throw new \OutOfBoundsException(\sprintf(
                'Resource pool could not find a resource with type "%s", ID "%s"%s.',
                $type,
                $id,
                $locale ? ', and locale "'.$locale.'"' : ''
            ));
        }

        return $this->resources[$key];
    }

    /**
     * {@inheritdoc}
     */
    public function generateKey(string $type, string $id, array $options = []): string
    {
        $locale = \strtr($options['locale'] ?? '__ALL__', [
            '-' => '_',
            '*' => '__ALL__',
        ]);

        return 'contentful.'
            .$this->api
            .'.'
            .$this->spaceId
            .'.'
            .$this->environmentId
            .'.'
            .$type
            .'.'
            .$this->sanitize($id)
            .'.'
            .$locale;
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Mapper;

use Contentful\Delivery\Resource\Locale as ResourceClass;
use Contentful\Delivery\SystemProperties\Locale as SystemProperties;

/**
 * Locale class.
 *
 * This class is responsible for converting raw API data into a PHP object
 * of class Contentful\Delivery\Resource\Locale.
 */
class Locale extends BaseMapper
{
    /**
     * {@inheritdoc}
     */
    public function map($resource, array $data): ResourceClass
    {
        /** @var ResourceClass $locale */
        $locale = $this->hydrator->hydrate($resource ?: ResourceClass::class, [
            'sys' => $this->createSystemProperties(SystemProperties::class, $data),
            'code' => $data['code'],
            'name' => $data['name'],
            'default' => $data['default'],
            'fallbackCode' => $data['fallbackCode'],
        ]);

        return $locale;
    }
}


<?php

/**
 * This file is part of the contentful/contentful package.
 *
 * @copyright 2015-2019 Contentful GmbH
 * @license   MIT
 */

declare(strict_types=1);

namespace Contentful\Delivery\Resource;

use Contentful\Delivery\SystemProperties\Environment as SystemProperties;

class Environment extends BaseResource
{
    /**
     * @var SystemProperties
     */
    protected $sys;

    /**
     * @var Locale[]
     */
    protected $locales = [];

    /**
     * {@inheritdoc}
     */
    public function getSystemProperties(): SystemProperties
    {
        return $this->sys;
    }

    /**
     * @return Locale[]
     */
    public function getLocales(): array
    {
        return $this->locales;
    }

    /**
     * @param string $code Code of the locale to fetch the object for
     *
     * @throws \InvalidArgumentException When no locale with the given code exists
     *
     * @return Locale
     */
    public function getLocale(string $code): Locale
    {
        foreach ($this->locales as $locale) {
            if ($locale->getCode() === $code) {
                return $locale;
            }
        }

        throw new \InvalidArgumentException(\sprintf(
            'No locale with code "%s" exists in this environment.',
            $code
        ));
    }

    /**
     * Returns the default locale for this space.
     *
     * @throws \RuntimeException
     *
     * @return Locale
     */
    public function getDefaultLocale(): Locale
    {
        foreach ($this->locales as $locale) {
            if ($locale->isDefault()) {
                return $locale;
            }
        }

        throw new \RuntimeException('No locale marked as default exists in this environment.');
    }

    /**
     * {@inheritdoc}
     */
    public function jsonSerialize(): array
    {
        return [
            'sys' => $this->sys,
            'locales' => $this->locales,
        ];
    }
}


<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Flex;

use Composer\Config;
use Composer\Downloader\TransportException;
use Composer\IO\IOInterface;
use Composer\Util\RemoteFilesystem;

/**
 * Speedup Composer by downloading packages in parallel.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ParallelDownloader extends RemoteFilesystem
{
    private $io;
    private $downloader;
    private $quiet = true;
    private $progress = true;
    private $nextCallback;
    private $downloadCount;
    private $nextOptions = [];
    private $sharedState;
    private $fileName;
    private $lastHeaders;

    public static $cacheNext = false;
    protected static $cache = [];

    public function __construct(IOInterface $io, Config $config, array $options = [], $disableTls = false)
    {
        $this->io = $io;
        if (!method_exists(parent::class, 'getRemoteContents')) {
            $this->io->writeError('Composer >=1.7 not found, downloads will happen in sequence', true, IOInterface::DEBUG);
        } elseif (!\extension_loaded('curl')) {
            $this->io->writeError('ext-curl not found, downloads will happen in sequence', true, IOInterface::DEBUG);
        } else {
            $this->downloader = new CurlDownloader();
        }
        parent::__construct($io, $config, $options, $disableTls);
    }

    public function download(array &$nextArgs, callable $nextCallback, bool $quiet = true, bool $progress = true)
    {
        $previousState = [$this->quiet, $this->progress, $this->downloadCount, $this->nextCallback, $this->sharedState];
        $this->quiet = $quiet;
        $this->progress = $progress;
        $this->downloadCount = \count($nextArgs);
        $this->nextCallback = $nextCallback;
        $this->sharedState = (object) [
            'bytesMaxCount' => 0,
            'bytesMax' => 0,
            'bytesTransferred' => 0,
            'nextArgs' => &$nextArgs,
            'nestingLevel' => 0,
            'maxNestingReached' => false,
            'lastProgress' => 0,
            'lastUpdate' => microtime(true),
        ];

        if (!$this->quiet) {
            if (!$this->downloader && method_exists(parent::class, 'getRemoteContents')) {
                $this->io->writeError('<warning>Enable the "cURL" PHP extension for faster downloads</warning>');
            }
            $note = '\\' === \DIRECTORY_SEPARATOR ? '' : (false !== stripos(PHP_OS, 'darwin') ? '🎵' : '🎶');
            $note .= $this->downloader ? ('\\' !== \DIRECTORY_SEPARATOR ? ' 💨' : '') : '';
            $this->io->writeError('');
            $this->io->writeError(sprintf('<info>Prefetching %d packages</info> %s', $this->downloadCount, $note));
            $this->io->writeError('  - Downloading', false);
            if ($this->progress) {
                $this->io->writeError(' (<comment>0%</comment>)', false);
            }
        }
        try {
            $this->getNext();
            if ($this->quiet) {
                // no-op
            } elseif ($this->progress) {
                $this->io->overwriteError(' (<comment>100%</comment>)');
            } else {
                $this->io->writeError(' (<comment>100%</comment>)');
            }
        } finally {
            if (!$this->quiet) {
                $this->io->writeError('');
            }
            list($this->quiet, $this->progress, $this->downloadCount, $this->nextCallback, $this->sharedState) = $previousState;
        }
    }

    public function getOptions()
    {
        $options = array_replace_recursive(parent::getOptions(), $this->nextOptions);
        $this->nextOptions = [];

        return $options;
    }

    public function setNextOptions(array $options)
    {
        $this->nextOptions = parent::getOptions() !== $options ? $options : [];

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getLastHeaders()
    {
        return $this->lastHeaders ?? parent::getLastHeaders();
    }

    /**
     * {@inheritdoc}
     */
    public function copy($originUrl, $fileUrl, $fileName, $progress = true, $options = [])
    {
        $options = array_replace_recursive($this->nextOptions, $options);
        $this->nextOptions = [];
        $rfs = clone $this;
        $rfs->fileName = $fileName;
        $rfs->progress = $this->progress && $progress;

        try {
            return $rfs->get($originUrl, $fileUrl, $options, $fileName, $rfs->progress);
        } finally {
            $rfs->lastHeaders = null;
            $this->lastHeaders = $rfs->getLastHeaders();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getContents($originUrl, $fileUrl, $progress = true, $options = [])
    {
        return $this->copy($originUrl, $fileUrl, null, $progress, $options);
    }

    /**
     * @internal
     */
    public function callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax, $nativeDownload = true)
    {
        if (!$nativeDownload && STREAM_NOTIFY_SEVERITY_ERR === $severity) {
            throw new TransportException($message, $messageCode);
        }

        parent::callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax);

        if (!$state = $this->sharedState) {
            return;
        }

        if (STREAM_NOTIFY_FILE_SIZE_IS === $notificationCode) {
            ++$state->bytesMaxCount;
            $state->bytesMax += $bytesMax;
        }

        if (!$bytesMax || STREAM_NOTIFY_PROGRESS !== $notificationCode) {
            if ($state->nextArgs && !$nativeDownload) {
                $this->getNext();
            }

            return;
        }

        if (0 < $state->bytesMax) {
            $progress = $state->bytesMaxCount / $this->downloadCount;
            $progress *= 100 * ($state->bytesTransferred + $bytesTransferred) / $state->bytesMax;
        } else {
            $progress = 0;
        }

        if ($bytesTransferred === $bytesMax) {
            $state->bytesTransferred += $bytesMax;
        }

        if (null !== $state->nextArgs && !$this->quiet && $this->progress && 1 <= $progress - $state->lastProgress) {
            $progressTime = microtime(true);

            if (5 <= $progress - $state->lastProgress || 1 <= $progressTime - $state->lastUpdate) {
                $state->lastProgress = $progress;
                $this->io->overwriteError(sprintf(' (<comment>%d%%</comment>)', $progress), false);
                $state->lastUpdate = microtime(true);
            }
        }

        if (!$nativeDownload || !$state->nextArgs || $bytesTransferred === $bytesMax || $state->maxNestingReached) {
            return;
        }

        if (5 < $state->nestingLevel) {
            $state->maxNestingReached = true;
        } else {
            $this->getNext();
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function getRemoteContents($originUrl, $fileUrl, $context, array &$responseHeaders = null)
    {
        if (isset(self::$cache[$fileUrl])) {
            $result = self::$cache[$fileUrl];

            if (3 < \func_num_args()) {
                list($responseHeaders, $result) = $result;
            }

            return $result;
        }

        if (self::$cacheNext) {
            self::$cacheNext = false;

            if (3 < \func_num_args()) {
                $result = $this->getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders);
                self::$cache[$fileUrl] = [$responseHeaders, $result];
            } else {
                $result = $this->getRemoteContents($originUrl, $fileUrl, $context);
                self::$cache[$fileUrl] = $result;
            }

            return $result;
        }

        if (!$this->downloader) {
            return parent::getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders);
        }

        try {
            $result = $this->downloader->get($originUrl, $fileUrl, $context, $this->fileName);

            if (3 < \func_num_args()) {
                list($responseHeaders, $result) = $result;
            }

            return $result;
        } catch (TransportException $e) {
            $this->io->writeError('Retrying download: '.$e->getMessage(), true, IOInterface::DEBUG);

            return parent::getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders);
        } catch (\Throwable $e) {
            $responseHeaders = [];
            throw $e;
        }
    }

    private function getNext()
    {
        $state = $this->sharedState;
        ++$state->nestingLevel;

        try {
            while ($state->nextArgs && (!$state->maxNestingReached || 1 === $state->nestingLevel)) {
                try {
                    $state->maxNestingReached = false;
                    ($this->nextCallback)(...array_shift($state->nextArgs));
                } catch (TransportException $e) {
                    $this->io->writeError('Skipping download: '.$e->getMessage(), true, IOInterface::DEBUG);
                }
            }
        } finally {
            --$state->nestingLevel;
        }
    }
}