From 54d705b3d89171f2d59951ca29fb009d8ed6ace2 Mon Sep 17 00:00:00 2001 From: Douglas Greenshields Date: Mon, 17 Apr 2017 20:41:22 +0100 Subject: [PATCH] turn whole library async first --- composer.json | 1 + src/Contentful.php | 115 +++++++-- src/Entry.php | 6 +- src/GuzzleAbstractionTrait.php | 14 +- src/ResourceBuilder.php | 428 +++++++++++++++++---------------- src/ResourceEnvelope.php | 35 +++ tests/ContentfulTest.php | 4 +- tests/EntryTest.php | 43 +++- tests/ResourceBuilderTest.php | 54 +++-- tests/ResourceEnvelopeTest.php | 21 ++ 10 files changed, 445 insertions(+), 276 deletions(-) diff --git a/composer.json b/composer.json index 163b932..d85fd80 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "require": { "php": ">=5.6", "guzzlehttp/guzzle": "^6.0", + "guzzlehttp/promises": "~1.1", "psr/cache": "~1.0.0" }, "require-dev": { diff --git a/src/Contentful.php b/src/Contentful.php index e3b5e50..b1a14f9 100644 --- a/src/Contentful.php +++ b/src/Contentful.php @@ -4,8 +4,8 @@ use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\RequestException; +use function GuzzleHttp\Promise\all; use function GuzzleHttp\Promise\coroutine; -use GuzzleHttp\Promise\FulfilledPromise; use function GuzzleHttp\Promise\promise_for; use GuzzleHttp\Promise\PromiseInterface; use Markup\Contentful\Cache\NullCacheItemPool; @@ -103,7 +103,9 @@ public function __construct(array $spaces, array $options = []) public function getSpace($space = null, array $options = []) { if ($space instanceof SpaceInterface) { - return $space; + return ($this->isAsyncCall($options)) + ? promise_for($space) + : $space; } else { $spaceName = $space; } @@ -133,7 +135,9 @@ public function getSpace($space = null, array $options = []) public function getEntry($id, $space = null, array $options = []) { if ($this->envelope->hasEntry($id)) { - return $this->envelope->findEntry($id); + return ($this->isAsyncCall($options)) + ? promise_for($this->envelope->findEntry($id)) + : $this->envelope->findEntry($id); } $spaceName = ($space instanceof SpaceInterface) ? $space->getName() : $space; $spaceData = $this->getSpaceDataForName(($space instanceof SpaceInterface) ? $space->getName() : $space); @@ -187,7 +191,9 @@ public function getEntries(array $parameters = [], $space = null, array $options public function getAsset($id, $space = null, array $options = []) { if ($this->envelope->hasAsset($id)) { - return $this->envelope->findAsset($id); + return ($this->isAsyncCall($options)) + ? promise_for($this->envelope->findAsset($id)) + : $this->envelope->findAsset($id); } $spaceName = ($space instanceof SpaceInterface) ? $space->getName() : $space; $spaceData = $this->getSpaceDataForName($spaceName); @@ -215,7 +221,9 @@ public function getAsset($id, $space = null, array $options = []) public function getContentType($id, $space = null, array $options = []) { if ($this->envelope->hasContentType($id)) { - return $this->envelope->findContentType($id); + return ($this->isAsyncCall($options)) + ? promise_for($this->envelope->findContentType($id)) + : $this->envelope->findContentType($id); } //fetch them all and pick one out, as it is likely we'll want to access others @@ -242,6 +250,14 @@ function ($contentTypes) use ($id) { public function getContentTypes(array $parameters = [], $space = null, array $options = []) { $spaceName = ($space instanceof SpaceInterface) ? $space->getName() : $space; + if (!$parameters) { + $stashedContentTypes = $this->envelope->getAllContentTypesForSpace($spaceName); + if (null !== $stashedContentTypes) { + return ($this->isAsyncCall($options)) + ? promise_for($stashedContentTypes) + : $stashedContentTypes; + } + } $spaceData = $this->getSpaceDataForName(($space instanceof SpaceInterface) ? $space->getName() : $space); $api = ($spaceData['preview_mode']) ? self::PREVIEW_API : self::CONTENT_DELIVERY_API; @@ -292,26 +308,40 @@ function () use ($name, $space, $options) { } /** - * @param Link $link + * @param Link $link * @param array $options - * @return ResourceInterface|PromiseInterface + * @return PromiseInterface */ public function resolveLink($link, array $options = []) { //check whether the "link" is already actually a resolved resource if ($link instanceof ResourceInterface) { - return $link; + return promise_for($link); } try { switch ($link->getLinkType()) { case 'Entry': - return $this->getEntry($link->getId(), $link->getSpaceName(), $options); + return $this->getEntry( + $link->getId(), + $link->getSpaceName(), + array_merge($options, ['async' => true]) + ); case 'Asset': - return $this->getAsset($link->getId(), $link->getSpaceName(), $options); + return $this->getAsset( + $link->getId(), + $link->getSpaceName(), + array_merge($options, ['async' => true]) + ); case 'ContentType': - return $this->getContentType($link->getId(), $link->getSpaceName(), $options); + return $this->getContentType( + $link->getId(), + $link->getSpaceName(), + array_merge($options, ['async' => true]) + ); default: - throw new \InvalidArgumentException(sprintf('Tried to resolve unknown link type "%s".', $link->getLinkType())); + throw new \InvalidArgumentException( + sprintf('Tried to resolve unknown link type "%s".', $link->getLinkType()) + ); } } catch (ResourceUnavailableException $e) { throw new LinkUnresolvableException($link, null, 0, $e); @@ -387,21 +417,24 @@ function () use ($spaceData, $spaceName, $endpointUrl, $exceptionMessage, $api, * Returns a built response if it passes test, or null if it doesn't. * * @param string $json - * @return ResourceInterface|ResourceArray|null + * @return PromiseInterface */ $buildResponseFromJson = function ($json) use ($spaceData, $assetDecorator, $shouldBuildTypedResources, $test) { $json = (is_array($json)) ? $json : json_decode($json, true); if (null === $json) { return null; } - $builtResponse = $this->buildResponseFromRaw( + + return $this->buildResponseFromRaw( $json, $spaceData['name'], $assetDecorator, $shouldBuildTypedResources + )->then( + function ($builtResponse) use ($test) { + return (call_user_func($test, $builtResponse)) ? $builtResponse : null; + } ); - - return (call_user_func($test, $builtResponse)) ? $builtResponse : null; }; $log = function ($description, $isCacheHit, $type) use ($timer, $queryType, $api) { $this->logger->log( @@ -425,7 +458,7 @@ function () use ($spaceData, $spaceName, $endpointUrl, $exceptionMessage, $api, LogInterface::TYPE_RESPONSE ); - $builtResponse = $buildResponseFromJson($cacheItemJson); + $builtResponse = (yield $buildResponseFromJson($cacheItemJson)); if ($builtResponse) { yield promise_for($builtResponse); return; @@ -444,7 +477,7 @@ function () use ($spaceData, $spaceName, $endpointUrl, $exceptionMessage, $api, true, LogInterface::TYPE_RESOURCE ); - $builtResponse = $buildResponseFromJson($fallbackJson); + $builtResponse = (yield $buildResponseFromJson($fallbackJson)); if ($builtResponse) { yield promise_for($builtResponse); return; @@ -478,7 +511,12 @@ function () use ($spaceData, $spaceName, $endpointUrl, $exceptionMessage, $api, /** * @var ResponseInterface $response */ - $response = (yield $this->sendRequestAsync($request, $queryParams)); + $response = ($shouldBuildTypedResources) + ? array_values((yield all([ + $this->sendRequestWithQueryParams($request, $queryParams), + $this->ensureContentTypesLoaded($spaceName) + ])))[0] + : (yield $this->sendRequestWithQueryParams($request, $queryParams)); } catch (RequestException $e) { /** * @var CacheItemInterface $fallbackCacheItem @@ -496,12 +534,12 @@ function () use ($spaceData, $spaceName, $endpointUrl, $exceptionMessage, $api, $writeCacheItem->set($fallbackJson); $writeCache->save($writeCacheItem); - yield promise_for($this->buildResponseFromRaw( + yield $this->buildResponseFromRaw( json_decode($fallbackJson, true), $spaceData['name'], $assetDecorator, $shouldBuildTypedResources - )); + ); return; } } @@ -542,7 +580,7 @@ function () use ($spaceData, $spaceName, $endpointUrl, $exceptionMessage, $api, $responseJson = json_encode( (!$unavailableException) ? $this->responseAsArrayFromJson($response) : null ); - $builtResponse = ($responseJson) ? $buildResponseFromJson($responseJson) : null; + $builtResponse = ($responseJson) ? (yield $buildResponseFromJson($responseJson)) : null; $isValidResponse = (bool) $builtResponse; //save into cache @@ -587,7 +625,7 @@ function () use ($spaceData, $spaceName, $endpointUrl, $exceptionMessage, $api, true, LogInterface::TYPE_RESOURCE ); - $builtResponse = $buildResponseFromJson($fallbackJson); + $builtResponse = (yield $buildResponseFromJson($fallbackJson)); if ($builtResponse) { yield promise_for($builtResponse); return; @@ -722,10 +760,10 @@ private function setApiVersionHeaderOnRequest($request, $api) /** * @param array $data - * @param string $spaceName - * @param AssetDecoratorInterface $assetDecorator + * @param null $spaceName + * @param AssetDecoratorInterface|null $assetDecorator * @param bool $useTypedResources - * @return ResourceInterface + * @return PromiseInterface */ private function buildResponseFromRaw( array $data, @@ -891,4 +929,29 @@ private function resolveContentTypeNameFilter(IncompleteParameterInterface $filt return $contentTypeFilterProvider->createForContentTypeName($filter->getValue(), $spaceName); } + + /** + * @param string $spaceName + * @return PromiseInterface + */ + private function ensureContentTypesLoaded($spaceName) + { + return $this->getContentTypes([], $spaceName, ['async' => true, 'untyped' => true]) + ->then( + function ($types) use ($spaceName) { + $this->envelope->insertAllContentTypesForSpace($types, $spaceName); + + return $types; + } + ); + } + + /** + * @param array $options + * @return bool + */ + private function isAsyncCall(array $options) + { + return isset($options['async']) && true === $options['async']; + } } diff --git a/src/Entry.php b/src/Entry.php index e9066c3..ee9b5b3 100644 --- a/src/Entry.php +++ b/src/Entry.php @@ -57,7 +57,7 @@ public function getField($key) if ($this->fields[$key] instanceof Link) { if (!isset($this->resolvedLinks[$key])) { try { - $resolvedLink = call_user_func($this->resolveLinkFunction, $this->fields[$key]); + $resolvedLink = call_user_func($this->resolveLinkFunction, $this->fields[$key])->wait(); } catch (LinkUnresolvableException $e) { $resolvedLink = null; } @@ -70,7 +70,7 @@ public function getField($key) if (!isset($this->resolvedLinks[$key])) { $this->resolvedLinks[$key] = array_filter(array_map(function ($link) { try { - $resolvedLink = call_user_func($this->resolveLinkFunction, $link); + $resolvedLink = call_user_func($this->resolveLinkFunction, $link)->wait(); } catch (LinkUnresolvableException $e) { //if the link is unresolvable we should consider it not published and return null so this is filtered out return null; @@ -122,6 +122,8 @@ public function offsetUnset($offset) } /** + * Sets a function that can return a promise for a resolved link. + * * @param callable $function * @return self */ diff --git a/src/GuzzleAbstractionTrait.php b/src/GuzzleAbstractionTrait.php index fcfbe5b..b6f02b5 100644 --- a/src/GuzzleAbstractionTrait.php +++ b/src/GuzzleAbstractionTrait.php @@ -18,22 +18,12 @@ trait GuzzleAbstractionTrait */ private $guzzle; - /** - * @param Request $request - * @param array $queryParams - * @return ResponseInterface - */ - private function sendRequestWithQueryParams(Request $request, array $queryParams = []) - { - return $this->guzzle->send($request, [RequestOptions::QUERY => $queryParams]); - } - /** * @param Request $request * @param array $queryParams * @return PromiseInterface */ - private function sendRequestAsync(Request $request, array $queryParams = []) + private function sendRequestWithQueryParams(Request $request, array $queryParams = []) { return $this->guzzle->sendAsync($request, [RequestOptions::QUERY => $queryParams]); } @@ -74,7 +64,7 @@ private function setHeaderOnRequest(Request $request, $header, $value) } /** - * @param \GuzzleHttp\Psr7\Response $response + * @param ResponseInterface $response * @return array */ private function responseAsArrayFromJson($response) diff --git a/src/ResourceBuilder.php b/src/ResourceBuilder.php index b0ecc21..7c75024 100644 --- a/src/ResourceBuilder.php +++ b/src/ResourceBuilder.php @@ -2,6 +2,10 @@ namespace Markup\Contentful; +use function GuzzleHttp\Promise\all; +use function GuzzleHttp\Promise\coroutine; +use function GuzzleHttp\Promise\promise_for; +use GuzzleHttp\Promise\PromiseInterface; use Markup\Contentful\Decorator\AssetDecoratorInterface; use Markup\Contentful\Decorator\NullAssetDecorator; @@ -34,161 +38,188 @@ public function __construct(ResourceEnvelope $envelope = null) * @param array $data The raw data returned from the Contentful APIs. * @param string $spaceName The name being used for the space this data is from. * @param AssetDecoratorInterface $assetDecorator - * @return mixed A Contentful resource. + * @return PromiseInterface */ public function buildFromData(array $data, $spaceName = null, AssetDecoratorInterface $assetDecorator = null) { - $assetDecorator = $assetDecorator ?: new NullAssetDecorator(); - $buildFromData = function ($data) use ($spaceName, $assetDecorator) { - return $this->buildFromData($data, $spaceName, $assetDecorator); - }; - if ($this->isArrayResourceData($data)) { - return array_map($buildFromData, $data); - } - $metadata = $this->buildMetadataFromSysData($data['sys'], $buildFromData); - if (!$metadata->getType()) { - throw new \InvalidArgumentException('Resource data must always have a type in its system properties.'); - } - switch ($metadata->getType()) { - case 'Space': - $locales = []; - $defaultLocale = null; - foreach ($data['locales'] as $locale) { - $localeObj = new Locale($locale['code'], $locale['name']); - if (isset($locale['default']) && $locale['default']) { - $defaultLocale = $localeObj; - } - $locales[] = $localeObj; - } - - return new Space($data['name'], $metadata, $locales, $defaultLocale); - case 'Entry': - $fields = []; - if (isset($data['fields'])) { - foreach ($data['fields'] as $name => $fieldData) { - if ($this->isResourceData($fieldData)) { - $fields[$name] = $buildFromData($fieldData); - } elseif ($this->isArrayResourceData($fieldData)) { - $fields[$name] = array_map(function ($itemData) use ($buildFromData) { - return $buildFromData($itemData); - }, $fieldData); - } else { - $fields[$name] = $fieldData; - } - } - } - $entry = new Entry($fields, $metadata); - if (null !== $this->resolveLinkFunction) { - $entry->setResolveLinkFunction($this->resolveLinkFunction); + return coroutine( + function () use ($data, $spaceName, $assetDecorator) { + $assetDecorator = $assetDecorator ?: new NullAssetDecorator(); + $buildFromData = function ($data) use ($spaceName, $assetDecorator) { + return $this->buildFromData($data, $spaceName, $assetDecorator); + }; + if ($this->isArrayResourceData($data)) { + yield all( + array_map($buildFromData, $data) + ); + return; } - if ($this->useDynamicEntries) { - $contentType = $metadata->getContentType(); - if ($contentType instanceof Link) { - $contentType = call_user_func($this->resolveLinkFunction, $contentType); - } - $entry = new DynamicEntry($entry, $contentType); + /** @var Metadata $metadata */ + $metadata = (yield $this->buildMetadataFromSysData($data['sys'], $buildFromData)); + if (!$metadata->getType()) { + throw new \InvalidArgumentException('Resource data must always have a type in its system properties.'); } - $this->envelope->insertEntry($entry); - - return $entry; - case 'Asset': - $asset = new Asset( - (isset($data['fields']['title'])) ? $data['fields']['title'] : '', - (isset($data['fields']['description'])) ? $data['fields']['description'] : '', - new AssetFile( - (isset($data['fields']['file']) && $data['fields']['file']['fileName']) ? $data['fields']['file']['fileName'] : '', - (isset($data['fields']['file']) && $data['fields']['file']['contentType']) ? $data['fields']['file']['contentType'] : '', - (isset($data['fields']['file']) && isset($data['fields']['file']['details'])) ? $data['fields']['file']['details'] : [], - (isset($data['fields']['file'])) ? ((isset($data['fields']['file']['url'])) ? $data['fields']['file']['url'] : $data['fields']['file']['upload']) : '' - ), - $metadata - ); - $asset = $assetDecorator->decorate($asset); - $this->envelope->insertAsset($asset); - - return $asset; - case 'ContentType': - $buildContentTypeField = function ($fieldData) { - $options = []; - if (isset($fieldData['localized'])) { - $options['localized'] = $fieldData['localized']; - } - if (isset($fieldData['required'])) { - $options['required'] = $fieldData['required']; - } - return new ContentTypeField( - $fieldData['id'], - $fieldData['name'], - $fieldData['type'], - (isset($fieldData['items'])) ? $fieldData['items'] : [], - $options - ); - }; - - $contentType = new ContentType( - $data['name'], - (isset($data['description'])) ? $data['description'] : '', - array_map(function ($fieldData) use ($buildContentTypeField) { - return $buildContentTypeField($fieldData); - }, (isset($data['fields'])) ? $data['fields'] : []), - $metadata, - (isset($data['displayField'])) ? $data['displayField'] : null - ); - $this->envelope->insertContentType($contentType); + switch ($metadata->getType()) { + case 'Space': + $locales = []; + $defaultLocale = null; + foreach ($data['locales'] as $locale) { + $localeObj = new Locale($locale['code'], $locale['name']); + if (isset($locale['default']) && $locale['default']) { + $defaultLocale = $localeObj; + } + $locales[] = $localeObj; + } - return $contentType; - case 'Link': - switch ($metadata->getLinkType()) { + yield promise_for(new Space($data['name'], $metadata, $locales, $defaultLocale)); + return; case 'Entry': - $entry = $this->envelope->findEntry($metadata->getId()); - if ($entry) { - return $entry; + $fields = []; + if (isset($data['fields'])) { + foreach ($data['fields'] as $name => $fieldData) { + if ($this->isResourceData($fieldData)) { + $fields[$name] = (yield $buildFromData($fieldData)); + } elseif ($this->isArrayResourceData($fieldData)) { + $fields[$name] = (yield all( + array_map(function ($itemData) use ($buildFromData) { + return $buildFromData($itemData); + }, $fieldData) + )); + } else { + $fields[$name] = $fieldData; + } + } } - break; + $entry = new Entry($fields, $metadata); + if (null !== $this->resolveLinkFunction) { + $entry->setResolveLinkFunction($this->resolveLinkFunction); + } + if ($this->useDynamicEntries) { + $contentType = $metadata->getContentType(); + if ($contentType instanceof Link) { + $contentType = (yield call_user_func($this->resolveLinkFunction, $contentType)); + } + $entry = new DynamicEntry($entry, $contentType); + } + $this->envelope->insert($entry); + + yield promise_for($entry); + return; case 'Asset': - $asset = $this->envelope->findAsset($metadata->getId()); - if ($asset) { - return $asset; + $asset = new Asset( + (isset($data['fields']['title'])) ? $data['fields']['title'] : '', + (isset($data['fields']['description'])) ? $data['fields']['description'] : '', + new AssetFile( + (isset($data['fields']['file']) && $data['fields']['file']['fileName']) ? $data['fields']['file']['fileName'] : '', + (isset($data['fields']['file']) && $data['fields']['file']['contentType']) ? $data['fields']['file']['contentType'] : '', + (isset($data['fields']['file']) && isset($data['fields']['file']['details'])) ? $data['fields']['file']['details'] : [], + (isset($data['fields']['file'])) ? ((isset($data['fields']['file']['url'])) ? $data['fields']['file']['url'] : $data['fields']['file']['upload']) : '' + ), + $metadata + ); + $asset = $assetDecorator->decorate($asset); + $this->envelope->insert($asset); + + yield promise_for($asset); + return; + case 'ContentType': + $buildContentTypeField = function ($fieldData) { + $options = []; + if (isset($fieldData['localized'])) { + $options['localized'] = $fieldData['localized']; + } + if (isset($fieldData['required'])) { + $options['required'] = $fieldData['required']; + } + + return new ContentTypeField( + $fieldData['id'], + $fieldData['name'], + $fieldData['type'], + (isset($fieldData['items'])) ? $fieldData['items'] : [], + $options + ); + }; + + $contentType = new ContentType( + $data['name'], + (isset($data['description'])) ? $data['description'] : '', + array_map(function ($fieldData) use ($buildContentTypeField) { + return $buildContentTypeField($fieldData); + }, (isset($data['fields'])) ? $data['fields'] : []), + $metadata, + (isset($data['displayField'])) ? $data['displayField'] : null + ); + $this->envelope->insert($contentType); + + yield promise_for($contentType); + return; + case 'Link': + switch ($metadata->getLinkType()) { + case 'Entry': + $entry = $this->envelope->findEntry($metadata->getId()); + if ($entry) { + yield promise_for($entry); + return; + } + break; + case 'Asset': + $asset = $this->envelope->findAsset($metadata->getId()); + if ($asset) { + yield promise_for($asset); + return; + } + break; + default: + break; } - break; + + yield promise_for(new Link($metadata, $spaceName)); + return; + case 'Array': + yield $this->addToEnvelope( + (isset($data['includes'])) ? $data['includes'] : [], + $buildFromData + ); + + $resolveResourceData = all( + array_map( + function ($itemData) use ($buildFromData) { + return coroutine( + function () use ($itemData, $buildFromData) { + $envelopeResource = $this->resolveResourceDataToEnvelopeResource($itemData); + if ($envelopeResource) { + yield $envelopeResource; + return; + } + $resource = (yield $buildFromData($itemData)); + $this->envelope->insert($resource); + + yield $resource; + } + ); + }, + $data['items'] + ) + ); + + yield promise_for(new ResourceArray( + (yield $resolveResourceData), + intval($data['total']), + intval($data['limit']), + intval($data['skip']), + $this->envelope + )); + return; default: break; } - - return new Link($metadata, $spaceName); - case 'Array': - $this->addToEnvelope((isset($data['includes'])) ? $data['includes'] : [], $buildFromData); - $resources = new ResourceArray( - array_map(function ($itemData) use ($buildFromData) { - $envelopeResource = $this->resolveResourceDataToEnvelopeResource($itemData); - if ($envelopeResource) { - return $envelopeResource; - } - $resource = $buildFromData($itemData); - $this->insertResourceIntoEnvelope($resource); - - return $resource; - }, $data['items']), - intval($data['total']), - intval($data['limit']), - intval($data['skip']), - $this->envelope - ); - $this->envelope->insert($resources); - - return $resources; - default: - break; - } - - return null; + return; + } + ); } - /** - * @param callable $function - * @return self - */ public function setResolveLinkFunction(callable $function) { $this->resolveLinkFunction = $function; @@ -208,39 +239,43 @@ public function setUseDynamicEntries($useDynamicEntries) } /** - * @param array $sys + * @param array $sys * @param callable $buildFromData - * @return Metadata + * @return PromiseInterface */ private function buildMetadataFromSysData(array $sys, callable $buildFromData) { - $metadata = new Metadata(); - if (isset($sys['id'])) { - $metadata->setId($sys['id']); - } - if (isset($sys['type'])) { - $metadata->setType($sys['type']); - } - if (isset($sys['space'])) { - $metadata->setSpace($buildFromData($sys['space'])); - } - if (isset($sys['contentType'])) { - $metadata->setContentType($buildFromData($sys['contentType'])); - } - if (isset($sys['linkType'])) { - $metadata->setLinkType($sys['linkType']); - } - if (isset($sys['revision'])) { - $metadata->setRevision(intval($sys['revision'])); - } - if (isset($sys['createdAt'])) { - $metadata->setCreatedAt(new \DateTime($sys['createdAt'])); - } - if (isset($sys['updatedAt'])) { - $metadata->setUpdatedAt(new \DateTime($sys['updatedAt'])); - } + return coroutine( + function () use ($sys, $buildFromData) { + $metadata = new Metadata(); + if (isset($sys['id'])) { + $metadata->setId($sys['id']); + } + if (isset($sys['type'])) { + $metadata->setType($sys['type']); + } + if (isset($sys['space'])) { + $metadata->setSpace((yield $buildFromData($sys['space']))); + } + if (isset($sys['contentType'])) { + $metadata->setContentType((yield $buildFromData($sys['contentType']))); + } + if (isset($sys['linkType'])) { + $metadata->setLinkType($sys['linkType']); + } + if (isset($sys['revision'])) { + $metadata->setRevision(intval($sys['revision'])); + } + if (isset($sys['createdAt'])) { + $metadata->setCreatedAt(new \DateTime($sys['createdAt'])); + } + if (isset($sys['updatedAt'])) { + $metadata->setUpdatedAt(new \DateTime($sys['updatedAt'])); + } - return $metadata; + yield $metadata; + } + ); } /** @@ -267,26 +302,6 @@ private function resolveResourceDataToEnvelopeResource(array $data) } } - /** - * @param ResourceInterface $resource - */ - private function insertResourceIntoEnvelope(ResourceInterface $resource) - { - if ($resource instanceof EntryInterface) { - $this->envelope->insertEntry($resource); - - return; - } - if ($resource instanceof ContentTypeInterface) { - $this->envelope->insertContentType($resource); - - return; - } - if ($resource instanceof AssetInterface) { - $this->envelope->insertAsset($resource); - } - } - /** * Tests whether the provided data represents a resource, or a link to a resource. * @@ -319,28 +334,33 @@ private function isArrayResourceData($data) } /** - * @param array $includesData Raw data from a search response in an 'includes' node - * @return ResourceEnvelope + * @param array $includesData + * @param callable $buildFromData + * @return PromiseInterface */ private function addToEnvelope(array $includesData, callable $buildFromData) { - if (isset($includesData['Entry'])) { - foreach ($includesData['Entry'] as $entryData) { - if (!isset($entryData['sys']['id']) || $this->envelope->hasEntry($entryData['sys']['id'])) { - continue; + return coroutine( + function () use ($includesData, $buildFromData) { + if (isset($includesData['Entry'])) { + foreach ($includesData['Entry'] as $entryData) { + if (!isset($entryData['sys']['id']) || $this->envelope->hasEntry($entryData['sys']['id'])) { + continue; + } + $this->envelope->insertEntry((yield $buildFromData($entryData))); + } } - $this->envelope->insertEntry($buildFromData($entryData)); - } - } - if (isset($includesData['Asset'])) { - foreach ($includesData['Asset'] as $assetData) { - if (!isset($assetData['sys']['id']) || $this->envelope->hasAsset($assetData['sys']['id'])) { - continue; + if (isset($includesData['Asset'])) { + foreach ($includesData['Asset'] as $assetData) { + if (!isset($assetData['sys']['id']) || $this->envelope->hasAsset($assetData['sys']['id'])) { + continue; + } + $this->envelope->insertAsset((yield $buildFromData($assetData))); + } } - $this->envelope->insertAsset($buildFromData($assetData)); - } - } - return $this->envelope; + yield promise_for($this->envelope); + } + ); } } diff --git a/src/ResourceEnvelope.php b/src/ResourceEnvelope.php index d2bd822..4094c75 100644 --- a/src/ResourceEnvelope.php +++ b/src/ResourceEnvelope.php @@ -28,11 +28,17 @@ class ResourceEnvelope */ private $contentTypes; + /** + * A set of all known content types for specific groups. + */ + private $contentTypeGroups; + public function __construct() { $this->entries = []; $this->assets = []; $this->contentTypes = []; + $this->contentTypeGroups = []; } /** @@ -173,6 +179,35 @@ public function insertContentType(ContentTypeInterface $contentType) return $this; } + /** + * @param ContentTypeInterface[] $contentTypes + * @param string $space + */ + public function insertAllContentTypesForSpace($contentTypes, $space) + { + $this->contentTypeGroups[$space] = $contentTypes; + foreach ($contentTypes as $contentType) { + $this->insertContentType($contentType); + } + + return $this; + } + + /** + * Gets all content types for a given space if they are saved into the envelope, null otherwise. + * + * @param string $space + * @return ContentTypeInterface[]|null + */ + public function getAllContentTypesForSpace($space) + { + if (!isset($this->contentTypeGroups[$space])) { + return null; + } + + return $this->contentTypeGroups[$space]; + } + /** * @return int */ diff --git a/tests/ContentfulTest.php b/tests/ContentfulTest.php index ecbb09f..c46d5ba 100644 --- a/tests/ContentfulTest.php +++ b/tests/ContentfulTest.php @@ -445,7 +445,7 @@ public function testResolveContentTypeLink() $metadata->setId($data['id']); $link = new Link($metadata); $contentful = $this->getContentful(null, array_merge($this->options, $handlerOption)); - $contentType = $contentful->resolveLink($link); + $contentType = $contentful->resolveLink($link)->wait(); $this->assertInstanceOf(ContentTypeInterface::class, $contentType); $this->assertEquals('cat', $contentType->getId());//of course, in a real situation this would be the same as the ID in the link - but this is the ID in the mock data $this->assertEquals('Name', $contentType->getDisplayField()->getName()); @@ -458,7 +458,7 @@ public function testQueryIsLoggedIfLoggerTrue() $contentful->getEntries([new EqualFilter(new SystemProperty('id'), 'nyancat')]); $logs = $contentful->getLogs(); $this->assertCount(1, $logs); - $this->assertContainsOnlyInstancesOf('Markup\Contentful\Log\LogInterface', $logs); + $this->assertContainsOnlyInstancesOf(LogInterface::class, $logs); $log = reset($logs); $this->assertEquals(LogInterface::RESOURCE_ENTRY, $log->getResourceType()); } diff --git a/tests/EntryTest.php b/tests/EntryTest.php index e35ba29..df57a0e 100644 --- a/tests/EntryTest.php +++ b/tests/EntryTest.php @@ -2,18 +2,39 @@ namespace Markup\Contentful\Tests; +use GuzzleHttp\Promise\Promise; +use function GuzzleHttp\Promise\promise_for; +use Markup\Contentful\AssetInterface; use Markup\Contentful\Entry; +use Markup\Contentful\EntryInterface; use Markup\Contentful\Exception\LinkUnresolvableException; +use Markup\Contentful\Link; +use Markup\Contentful\MetadataInterface; use Mockery as m; class EntryTest extends \PHPUnit_Framework_TestCase { + /** + * @var array + */ + private $fields; + + /** + * @var MetadataInterface|m\MockInterface + */ + private $metadata; + + /** + * @var Entry + */ + private $entry; + protected function setUp() { $this->fields = [ 'foo' => 'bar', ]; - $this->metadata = m::mock('Markup\Contentful\MetadataInterface'); + $this->metadata = m::mock(MetadataInterface::class); $this->entry = new Entry($this->fields, $this->metadata); } @@ -24,7 +45,7 @@ protected function tearDown() public function testIsEntry() { - $this->assertInstanceOf('Markup\Contentful\EntryInterface', $this->entry); + $this->assertInstanceOf(EntryInterface::class, $this->entry); } public function testGetFieldsUsingArrayAccess() @@ -43,10 +64,10 @@ public function testGetField() public function testResolveLink() { - $link = m::mock('Markup\Contentful\Link'); - $asset = m::mock('Markup\Contentful\AssetInterface'); + $link = m::mock(Link::class); + $asset = m::mock(AssetInterface::class); $callback = function ($link) use ($asset) { - return $asset; + return promise_for($asset); }; $fields = [ 'asset' => $link, @@ -64,9 +85,11 @@ public function testUnknownMethodFallsBackToFieldLookup() public function testUnresolvedLinkFiltersOutFromList() { - $link = m::mock('Markup\Contentful\Link')->shouldIgnoreMissing(); + $link = m::mock(Link::class)->shouldIgnoreMissing(); $callback = function ($link) { - throw new LinkUnresolvableException($link); + return new Promise(function () use ($link) { + throw new LinkUnresolvableException($link); + }); }; $fields = [ 'assets' => [$link], @@ -79,9 +102,11 @@ public function testUnresolvedLinkFiltersOutFromList() public function testUnresolvedSingleLinkEmitsNull() { - $link = m::mock('Markup\Contentful\Link')->shouldIgnoreMissing(); + $link = m::mock(Link::class)->shouldIgnoreMissing(); $callback = function ($link) { - throw new LinkUnresolvableException($link); + return new Promise(function () use ($link) { + throw new LinkUnresolvableException($link); + }); }; $fields = [ 'asset' => $link, diff --git a/tests/ResourceBuilderTest.php b/tests/ResourceBuilderTest.php index b3e5bab..288ec70 100644 --- a/tests/ResourceBuilderTest.php +++ b/tests/ResourceBuilderTest.php @@ -2,10 +2,23 @@ namespace Markup\Contentful\Tests; +use Markup\Contentful\AssetInterface; +use Markup\Contentful\ContentTypeField; +use Markup\Contentful\ContentTypeInterface; +use Markup\Contentful\EntryInterface; +use Markup\Contentful\Link; +use Markup\Contentful\Locale; +use Markup\Contentful\ResourceArray; use Markup\Contentful\ResourceBuilder; +use Markup\Contentful\SpaceInterface; class ResourceBuilderTest extends \PHPUnit_Framework_TestCase { + /** + * @var ResourceBuilder + */ + private $builder; + protected function setUp() { $this->builder = new ResourceBuilder(); @@ -32,11 +45,11 @@ public function testBuildSpace() ], ], ]; - $space = $this->builder->buildFromData($data, $spaceName); - $this->assertInstanceOf('Markup\Contentful\SpaceInterface', $space); + $space = $this->builder->buildFromData($data, $spaceName)->wait(); + $this->assertInstanceOf(SpaceInterface::class, $space); $this->assertEquals('cfexampleapi', $space->getId()); $locale = $space->getLocales()[1]; - $this->assertInstanceOf('Markup\Contentful\Locale', $locale); + $this->assertInstanceOf(Locale::class, $locale); $this->assertEquals('Klingon', $locale->getName()); } @@ -75,17 +88,17 @@ public function testBuildEntry() ], ] ]; - $entry = $this->builder->buildFromData($data, $spaceName); - $this->assertInstanceOf('Markup\Contentful\EntryInterface', $entry); + $entry = $this->builder->buildFromData($data, $spaceName)->wait(); + $this->assertInstanceOf(EntryInterface::class, $entry); $this->assertEquals('cat', $entry->getId()); $spaceLink = $entry->getSpace(); - $this->assertInstanceOf('Markup\Contentful\Link', $spaceLink); + $this->assertInstanceOf(Link::class, $spaceLink); $this->assertEquals('Space', $spaceLink->getLinkType()); $fields = $entry->getFields(); $this->assertCount(7, $fields); $this->assertEquals('Rainbow', $fields['color']); $bestFriend = $fields['bestFriend']; - $this->assertInstanceOf('Markup\Contentful\Link', $bestFriend); + $this->assertInstanceOf(Link::class, $bestFriend); $this->assertEquals('happycat', $bestFriend->getId()); } @@ -125,8 +138,8 @@ public function testBuildAsset() ], ], ]; - $asset = $this->builder->buildFromData($data, $spaceName); - $this->assertInstanceOf('Markup\Contentful\AssetInterface', $asset); + $asset = $this->builder->buildFromData($data, $spaceName)->wait(); + $this->assertInstanceOf(AssetInterface::class, $asset); $this->assertEquals('nyancat.png', $asset->getFilename()); $this->assertEquals('//images.contentful.com/cfexampleapi/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png', $asset->getUrl()); $this->assertEquals(12273, $asset->getFileSizeInBytes()); @@ -162,8 +175,8 @@ public function testBuildAssetFromUploadDataForm() ], ], ]; - $asset = $this->builder->buildFromData($data, $spaceName); - $this->assertInstanceOf('Markup\Contentful\AssetInterface', $asset); + $asset = $this->builder->buildFromData($data, $spaceName)->wait(); + $this->assertInstanceOf(AssetInterface::class, $asset); $this->assertEquals('//images.contentful.com/cfexampleapi/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png', $asset->getUrl()); $this->assertNull($asset->getFileSizeInBytes()); } @@ -171,7 +184,6 @@ public function testBuildAssetFromUploadDataForm() public function testBuildContentType() { $spaceName = 'example_name'; - $spaceId = 'i_am_the_space'; $data = [ 'sys' => [ 'type' => 'ContentType', @@ -206,11 +218,11 @@ public function testBuildContentType() ], ], ]; - $contentType = $this->builder->buildFromData($data, $spaceName); - $this->assertInstanceOf('Markup\Contentful\ContentTypeInterface', $contentType); + $contentType = $this->builder->buildFromData($data, $spaceName)->wait(); + $this->assertInstanceOf(ContentTypeInterface::class, $contentType); $this->assertEquals('Cat', $contentType->getName()); $fields = $contentType->getFields(); - $this->assertContainsOnlyInstancesOf('Markup\Contentful\ContentTypeField', $fields); + $this->assertContainsOnlyInstancesOf(ContentTypeField::class, $fields); $this->assertTrue($fields['name']->isLocalized()); $this->assertFalse($fields['likes']->isLocalized()); $this->assertEquals([], $fields['lifes']->getItems()); @@ -285,9 +297,9 @@ public function testBuildEntryArray() ] ], ]; - $entries = $this->builder->buildFromData($data, $spaceName); + $entries = $this->builder->buildFromData($data, $spaceName)->wait(); $this->assertCount(2, $entries); - $this->assertContainsOnlyInstancesOf('Markup\Contentful\EntryInterface', $entries); + $this->assertContainsOnlyInstancesOf(EntryInterface::class, $entries); $this->assertEquals('cat2', $entries[1]->getId()); } @@ -328,11 +340,11 @@ public function testBuildEntryWithArrayOfLinks() ], ] ]; - $entry = $this->builder->buildFromData($data, $spaceName); + $entry = $this->builder->buildFromData($data, $spaceName)->wait(); $links = $entry->getField('bestFriend'); $this->assertInternalType('array', $links); $this->assertCount(1, $links); - $this->assertContainsOnlyInstancesOf('Markup\Contentful\Link', $links); + $this->assertContainsOnlyInstancesOf(Link::class, $links); } public function testBuildArrayOfEntries() @@ -514,8 +526,8 @@ public function testBuildArrayOfEntries() ], ], ]; - $array = $this->builder->buildFromData($data, $spaceName); - $this->assertInstanceOf('Markup\Contentful\ResourceArray', $array); + $array = $this->builder->buildFromData($data, $spaceName)->wait(); + $this->assertInstanceOf(ResourceArray::class, $array); $this->assertCount(1, $array); } } diff --git a/tests/ResourceEnvelopeTest.php b/tests/ResourceEnvelopeTest.php index 9b423ae..76757d1 100644 --- a/tests/ResourceEnvelopeTest.php +++ b/tests/ResourceEnvelopeTest.php @@ -5,6 +5,7 @@ use Markup\Contentful\AssetInterface; use Markup\Contentful\ContentTypeInterface; use Markup\Contentful\EntryInterface; +use Markup\Contentful\ResourceArray; use Markup\Contentful\ResourceEnvelope; use Mockery as m; @@ -128,4 +129,24 @@ public function testSetAndAccessUsingGenericInsertMethod() $this->assertSame($contentType1, $this->envelope->findContentTypeByName($contentTypeName1)); $this->assertNull($this->envelope->findContentTypeByName('unknown')); } + + public function testSetAllContentTypesForSpace() + { + $space = 'ejrhgejkgh'; + $id = 'id'; + $contentType = m::mock(ContentTypeInterface::class); + $contentType + ->shouldReceive('getId') + ->andReturn($id); + $resourceArray = new ResourceArray( + [$contentType], + 1, + 0, + 1, + $this->envelope + ); + $this->envelope->insertAllContentTypesForSpace($resourceArray, $space); + $this->assertSame($contentType, $this->envelope->getAllContentTypesForSpace($space)[0]); + $this->assertSame($contentType, $this->envelope->findContentType($id)); + } }