From 8f2a3f00ff2fe2351c1959f52f424286ff73e066 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 09:28:48 +0200 Subject: [PATCH 01/10] Enhancement: Resolve Relations --- src/Domain/Value/Resolver/Relation.php | 30 +++++ .../Value/Resolver/RelationCollection.php | 87 ++++++++++++ src/Request/StoriesRequest.php | 2 + src/Resolver/ResolverInterface.php | 19 +++ src/Resolver/StoryResolver.php | 25 ++++ src/StoriesResolvedApi.php | 127 ++++++++++++++++++ 6 files changed, 290 insertions(+) create mode 100644 src/Domain/Value/Resolver/Relation.php create mode 100644 src/Domain/Value/Resolver/RelationCollection.php create mode 100644 src/Resolver/ResolverInterface.php create mode 100644 src/Resolver/StoryResolver.php create mode 100644 src/StoriesResolvedApi.php diff --git a/src/Domain/Value/Resolver/Relation.php b/src/Domain/Value/Resolver/Relation.php new file mode 100644 index 0000000..a80bbb6 --- /dev/null +++ b/src/Domain/Value/Resolver/Relation.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Storyblok\Api\Domain\Value\Resolver; + +use OskarStark\Value\TrimmedNonEmptyString; +use Webmozart\Assert\Assert; + +/** + * @author Silas Joisten + */ +final readonly class Relation +{ + public function __construct( + public string $value, + ) { + TrimmedNonEmptyString::fromString($value); + Assert::contains('.', $value); + } +} diff --git a/src/Domain/Value/Resolver/RelationCollection.php b/src/Domain/Value/Resolver/RelationCollection.php new file mode 100644 index 0000000..2c13878 --- /dev/null +++ b/src/Domain/Value/Resolver/RelationCollection.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Storyblok\Api\Domain\Value\Resolver; + +/** + * @author Silas Joisten + * + * @implements \IteratorAggregate + */ +final class RelationCollection implements \Countable, \IteratorAggregate +{ + /** + * @var list + */ + private array $items = []; + + /** + * @param list $items + */ + public function __construct( + array $items = [], + ) { + foreach ($items as $item) { + $this->add($item); + } + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->items); + } + + public function count(): int + { + return \count($this->items); + } + + public function add(Relation $relation): void + { + if ($this->has($relation)) { + return; + } + + $this->items[] = $relation; + } + + public function has(Relation $relation): bool + { + foreach ($this->items as $item) { + if ($item->value === $relation->value) { + return true; + } + } + + return false; + } + + public function remove(Relation $relation): void + { + foreach ($this->items as $key => $item) { + if ($item->value === $relation->value) { + unset($this->items[$key]); + + break; + } + } + } + + public function toString(): string + { + return implode(',', array_map(static fn (Relation $relation): string => $relation->value, $this->items)); + } +} diff --git a/src/Request/StoriesRequest.php b/src/Request/StoriesRequest.php index a415307..a22d985 100644 --- a/src/Request/StoriesRequest.php +++ b/src/Request/StoriesRequest.php @@ -19,6 +19,7 @@ use Storyblok\Api\Domain\Value\Field\FieldCollection; use Storyblok\Api\Domain\Value\Filter\FilterCollection; use Storyblok\Api\Domain\Value\IdCollection; +use Storyblok\Api\Domain\Value\Resolver\RelationCollection; use Storyblok\Api\Domain\Value\Tag\TagCollection; use Webmozart\Assert\Assert; @@ -38,6 +39,7 @@ public function __construct( public ?FieldCollection $excludeFields = null, public ?TagCollection $withTags = null, public ?IdCollection $excludeIds = null, + public ?RelationCollection $withRelations = null, public ?Version $version = null, ) { Assert::stringNotEmpty($language); diff --git a/src/Resolver/ResolverInterface.php b/src/Resolver/ResolverInterface.php new file mode 100644 index 0000000..6ad69fd --- /dev/null +++ b/src/Resolver/ResolverInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Storyblok\Api\Resolver; + +interface ResolverInterface +{ + public function resolve(array $target, array $relations): array; +} diff --git a/src/Resolver/StoryResolver.php b/src/Resolver/StoryResolver.php new file mode 100644 index 0000000..eb3090c --- /dev/null +++ b/src/Resolver/StoryResolver.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Storyblok\Api\Resolver; + +final readonly class StoryResolver implements ResolverInterface +{ + public function resolve(array $target, array $relations): array + { + // dd($target, $relations); + // TODO: Relation resolving here. + + return $target; + } +} diff --git a/src/StoriesResolvedApi.php b/src/StoriesResolvedApi.php new file mode 100644 index 0000000..0cebf54 --- /dev/null +++ b/src/StoriesResolvedApi.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Storyblok\Api; + +use Storyblok\Api\Domain\Value\Dto\Version; +use Storyblok\Api\Domain\Value\Id; +use Storyblok\Api\Domain\Value\Uuid; +use Storyblok\Api\Request\StoriesRequest; +use Storyblok\Api\Resolver\ResolverInterface; +use Storyblok\Api\Response\StoriesResponse; +use Storyblok\Api\Response\StoryResponse; +use Webmozart\Assert\Assert; + +/** + * @author Silas Joisten + */ +final readonly class StoriesResolvedApi implements StoriesApiInterface +{ + public function __construct( + private StoriesApiInterface $storiesApi, + private ResolverInterface $resolver, + ) { + } + + public function all(?StoriesRequest $request = null): StoriesResponse + { + Assert::notNull($request); + Assert::notNull($request->withRelations); + + $response = $this->storiesApi->all($request); + + $stories = []; + + foreach ($response->stories as $story) { + $stories[] = $this->resolver->resolve($story, $response->rels); + } + + return new StoriesResponse( + $response->total, + $response->pagination, + [ + 'cv' => $response->cv, + 'rels' => $response->rels, + 'links' => $response->links, + 'stories' => $stories, + ], + ); + } + + public function allByContentType(string $contentType, ?StoriesRequest $request = null): StoriesResponse + { + Assert::notNull($request); + Assert::notNull($request->withRelations); + + $response = $this->storiesApi->allByContentType($contentType, $request); + + $stories = []; + + foreach ($response->stories as $story) { + $stories[] = $this->resolver->resolve($story, $response->rels); + } + + return new StoriesResponse( + $response->total, + $response->pagination, + [ + 'cv' => $response->cv, + 'rels' => $response->rels, + 'links' => $response->links, + 'stories' => $stories, + ], + ); + } + + public function bySlug(string $slug, string $language = 'default', ?Version $version = null): StoryResponse + { + $response = $this->storiesApi->bySlug($slug, $language, $version); + + $story = $this->resolver->resolve($response->story, $response->rels); + + return new StoryResponse([ + 'cv' => $response->cv, + 'rels' => $response->rels, + 'links' => $response->links, + 'story' => $story, + ]); + } + + public function byUuid(Uuid $uuid, string $language = 'default', ?Version $version = null): StoryResponse + { + $response = $this->storiesApi->byUuid($uuid, $language, $version); + + $story = $this->resolver->resolve($response->story, $response->rels); + + return new StoryResponse([ + 'cv' => $response->cv, + 'rels' => $response->rels, + 'links' => $response->links, + 'story' => $story, + ]); + } + + public function byId(Id $id, string $language = 'default', ?Version $version = null): StoryResponse + { + $response = $this->storiesApi->byId($id, $language, $version); + + $story = $this->resolver->resolve($response->story, $response->rels); + + return new StoryResponse([ + 'cv' => $response->cv, + 'rels' => $response->rels, + 'links' => $response->links, + 'story' => $story, + ]); + } +} From f56b7f27b1d9488c539c6f9756496803238be2eb Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 09:53:03 +0200 Subject: [PATCH 02/10] Fix --- src/Domain/Value/Resolver/Relation.php | 2 +- src/Request/StoriesRequest.php | 4 ++++ src/StoriesResolvedApi.php | 20 ++++++++++++++------ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Domain/Value/Resolver/Relation.php b/src/Domain/Value/Resolver/Relation.php index a80bbb6..cdd713c 100644 --- a/src/Domain/Value/Resolver/Relation.php +++ b/src/Domain/Value/Resolver/Relation.php @@ -25,6 +25,6 @@ public function __construct( public string $value, ) { TrimmedNonEmptyString::fromString($value); - Assert::contains('.', $value); + Assert::contains($value, '.'); } } diff --git a/src/Request/StoriesRequest.php b/src/Request/StoriesRequest.php index a22d985..fa3c9d4 100644 --- a/src/Request/StoriesRequest.php +++ b/src/Request/StoriesRequest.php @@ -87,6 +87,10 @@ public function toArray(): array $array['excluding_ids'] = $this->excludeIds->toString(); } + if (null !== $this->withRelations && $this->withRelations->count() > 0) { + $array['resolve_relations'] = $this->withRelations->toString(); + } + if (null !== $this->version) { $array['version'] = $this->version->value; } diff --git a/src/StoriesResolvedApi.php b/src/StoriesResolvedApi.php index 0cebf54..7e1efb1 100644 --- a/src/StoriesResolvedApi.php +++ b/src/StoriesResolvedApi.php @@ -35,11 +35,15 @@ public function __construct( public function all(?StoriesRequest $request = null): StoriesResponse { - Assert::notNull($request); - Assert::notNull($request->withRelations); - $response = $this->storiesApi->all($request); + if (null === $request + || null === $request->withRelations + || 0 === $request->withRelations->count() + ) { + return $response; + } + $stories = []; foreach ($response->stories as $story) { @@ -60,11 +64,15 @@ public function all(?StoriesRequest $request = null): StoriesResponse public function allByContentType(string $contentType, ?StoriesRequest $request = null): StoriesResponse { - Assert::notNull($request); - Assert::notNull($request->withRelations); - $response = $this->storiesApi->allByContentType($contentType, $request); + if (null === $request + || null === $request->withRelations + || 0 === $request->withRelations->count() + ) { + return $response; + } + $stories = []; foreach ($response->stories as $story) { From 219c4f789b170e3bc073390fa2c255d1e1f2b2ac Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 10:47:48 +0200 Subject: [PATCH 03/10] Fix --- src/Resolver/ResolverInterface.php | 11 +++++++++++ src/Resolver/StoryResolver.php | 25 +++++++++++++++++++++++-- src/StoriesApi.php | 1 + src/StoriesResolvedApi.php | 13 +++++++------ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/Resolver/ResolverInterface.php b/src/Resolver/ResolverInterface.php index 6ad69fd..e29a83e 100644 --- a/src/Resolver/ResolverInterface.php +++ b/src/Resolver/ResolverInterface.php @@ -13,7 +13,18 @@ namespace Storyblok\Api\Resolver; +/** + * @author Silas Joisten + */ interface ResolverInterface { + /** + * Resolves relations in the target content using the given relations collection. + * + * @param array $target The target story content containing UUIDs to resolve. + * @param array> $relations The target story content containing UUIDs to resolve. + * + * @return array + */ public function resolve(array $target, array $relations): array; } diff --git a/src/Resolver/StoryResolver.php b/src/Resolver/StoryResolver.php index eb3090c..7be649c 100644 --- a/src/Resolver/StoryResolver.php +++ b/src/Resolver/StoryResolver.php @@ -13,12 +13,33 @@ namespace Storyblok\Api\Resolver; +use Webmozart\Assert\Assert; + +/** + * @author Silas Joisten + */ final readonly class StoryResolver implements ResolverInterface { public function resolve(array $target, array $relations): array { - // dd($target, $relations); - // TODO: Relation resolving here. + $relationMap = []; + + foreach ($relations as $relation) { + Assert::keyExists($relation, 'uuid'); + $relationMap[$relation['uuid']] = $relation; + } + + foreach ($target as &$value) { + if (is_string($value) && array_key_exists($value, $relationMap)) { + $value = $relationMap[$value]; + + continue; + } + + if (is_array($value)) { + $value = $this->resolve($value, $relations); + } + } return $target; } diff --git a/src/StoriesApi.php b/src/StoriesApi.php index 84db034..03b9eac 100644 --- a/src/StoriesApi.php +++ b/src/StoriesApi.php @@ -15,6 +15,7 @@ use Storyblok\Api\Domain\Value\Dto\Version; use Storyblok\Api\Domain\Value\Id; +use Storyblok\Api\Domain\Value\Resolver\RelationCollection; use Storyblok\Api\Domain\Value\Total; use Storyblok\Api\Domain\Value\Uuid; use Storyblok\Api\Request\StoriesRequest; diff --git a/src/StoriesResolvedApi.php b/src/StoriesResolvedApi.php index 7e1efb1..04f8285 100644 --- a/src/StoriesResolvedApi.php +++ b/src/StoriesResolvedApi.php @@ -15,6 +15,7 @@ use Storyblok\Api\Domain\Value\Dto\Version; use Storyblok\Api\Domain\Value\Id; +use Storyblok\Api\Domain\Value\Resolver\RelationCollection; use Storyblok\Api\Domain\Value\Uuid; use Storyblok\Api\Request\StoriesRequest; use Storyblok\Api\Resolver\ResolverInterface; @@ -91,9 +92,9 @@ public function allByContentType(string $contentType, ?StoriesRequest $request = ); } - public function bySlug(string $slug, string $language = 'default', ?Version $version = null): StoryResponse + public function bySlug(string $slug, string $language = 'default', ?Version $version = null, ?RelationCollection $withRelations = null): StoryResponse { - $response = $this->storiesApi->bySlug($slug, $language, $version); + $response = $this->storiesApi->bySlug($slug, $language, $version, $withRelations); $story = $this->resolver->resolve($response->story, $response->rels); @@ -105,9 +106,9 @@ public function bySlug(string $slug, string $language = 'default', ?Version $ver ]); } - public function byUuid(Uuid $uuid, string $language = 'default', ?Version $version = null): StoryResponse + public function byUuid(Uuid $uuid, string $language = 'default', ?Version $version = null, ?RelationCollection $withRelations = null): StoryResponse { - $response = $this->storiesApi->byUuid($uuid, $language, $version); + $response = $this->storiesApi->byUuid($uuid, $language, $version, $withRelations); $story = $this->resolver->resolve($response->story, $response->rels); @@ -119,9 +120,9 @@ public function byUuid(Uuid $uuid, string $language = 'default', ?Version $versi ]); } - public function byId(Id $id, string $language = 'default', ?Version $version = null): StoryResponse + public function byId(Id $id, string $language = 'default', ?Version $version = null, ?RelationCollection $withRelations = null): StoryResponse { - $response = $this->storiesApi->byId($id, $language, $version); + $response = $this->storiesApi->byId($id, $language, $version, $withRelations); $story = $this->resolver->resolve($response->story, $response->rels); From ae03ded89edd84d1c69e86c23479a277566349cd Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 11:31:46 +0200 Subject: [PATCH 04/10] Adds tests --- src/Bridge/Faker/Generator.php | 27 ++-- .../Faker/Provider/StoryblokProvider.php | 9 ++ src/Domain/Value/Resolver/Relation.php | 2 +- src/Request/StoryRequest.php | 7 + src/Resolver/ResolverInterface.php | 4 +- src/Resolver/StoryResolver.php | 4 +- src/StoriesApi.php | 1 - src/StoriesResolvedApi.php | 1 - .../Value/Resolver/RelationCollectionTest.php | 130 ++++++++++++++++++ .../Domain/Value/Resolver/RelationTest.php | 50 +++++++ tests/Unit/Request/StoryRequestTest.php | 20 +++ 11 files changed, 235 insertions(+), 20 deletions(-) create mode 100644 tests/Unit/Domain/Value/Resolver/RelationCollectionTest.php create mode 100644 tests/Unit/Domain/Value/Resolver/RelationTest.php diff --git a/src/Bridge/Faker/Generator.php b/src/Bridge/Faker/Generator.php index 02b3b51..9d0b471 100644 --- a/src/Bridge/Faker/Generator.php +++ b/src/Bridge/Faker/Generator.php @@ -20,19 +20,20 @@ /** * @author Silas Joisten * - * @method array assetResponse(array $overrides = []) - * @method array datasourceDimensionResponse(array $overrides = []) - * @method array datasourceEntriesResponse(array $overrides = []) - * @method array datasourceEntryResponse(array $overrides = []) - * @method array datasourceResponse(array $overrides = []) - * @method array datasourcesResponse(array $overrides = []) - * @method array linkAlternateResponse(array $overrides = []) - * @method array linkResponse(array $overrides = []) - * @method array linksResponse(array $overrides = []) - * @method array spaceResponse(array $overrides = []) - * @method array storiesResponse(array $overrides = []) - * @method array storyResponse(array $overrides = []) - * @method array tagsResponse(array $overrides = []) + * @method array assetResponse(array $overrides = []) + * @method array datasourceDimensionResponse(array $overrides = []) + * @method array datasourceEntriesResponse(array $overrides = []) + * @method array datasourceEntryResponse(array $overrides = []) + * @method array datasourceResponse(array $overrides = []) + * @method array datasourcesResponse(array $overrides = []) + * @method array linkAlternateResponse(array $overrides = []) + * @method array linkResponse(array $overrides = []) + * @method array linksResponse(array $overrides = []) + * @method string relation() + * @method array spaceResponse(array $overrides = []) + * @method array storiesResponse(array $overrides = []) + * @method array storyResponse(array $overrides = []) + * @method array tagsResponse(array $overrides = []) */ final class Generator extends BaseGenerator { diff --git a/src/Bridge/Faker/Provider/StoryblokProvider.php b/src/Bridge/Faker/Provider/StoryblokProvider.php index d3c07f2..ce4d80b 100644 --- a/src/Bridge/Faker/Provider/StoryblokProvider.php +++ b/src/Bridge/Faker/Provider/StoryblokProvider.php @@ -503,4 +503,13 @@ public function assetResponse(array $overrides = []): array $overrides, ); } + + public function relation(): string + { + return \sprintf( + '%s.%s', + $this->generator->word(), + $this->generator->word(), + ); + } } diff --git a/src/Domain/Value/Resolver/Relation.php b/src/Domain/Value/Resolver/Relation.php index cdd713c..d63bc7a 100644 --- a/src/Domain/Value/Resolver/Relation.php +++ b/src/Domain/Value/Resolver/Relation.php @@ -25,6 +25,6 @@ public function __construct( public string $value, ) { TrimmedNonEmptyString::fromString($value); - Assert::contains($value, '.'); + Assert::regex($value, '/^([a-zA-Z].+)\.([a-zA-Z].+)$/'); } } diff --git a/src/Request/StoryRequest.php b/src/Request/StoryRequest.php index 9b455ba..dc8176c 100644 --- a/src/Request/StoryRequest.php +++ b/src/Request/StoryRequest.php @@ -14,6 +14,7 @@ namespace Storyblok\Api\Request; use Storyblok\Api\Domain\Value\Dto\Version; +use Storyblok\Api\Domain\Value\Resolver\RelationCollection; use Webmozart\Assert\Assert; /** @@ -24,6 +25,7 @@ public function __construct( public string $language = 'default', public ?Version $version = null, + public ?RelationCollection $withRelations = null, ) { Assert::stringNotEmpty($language); } @@ -32,6 +34,7 @@ public function __construct( * @return array{ * language: string, * version?: string, + * resolve_relations?: string, * } */ public function toArray(): array @@ -44,6 +47,10 @@ public function toArray(): array $array['version'] = $this->version->value; } + if (null !== $this->withRelations && $this->withRelations->count() > 0) { + $array['resolve_relations'] = $this->withRelations->toString(); + } + return $array; } } diff --git a/src/Resolver/ResolverInterface.php b/src/Resolver/ResolverInterface.php index e29a83e..55078ce 100644 --- a/src/Resolver/ResolverInterface.php +++ b/src/Resolver/ResolverInterface.php @@ -21,8 +21,8 @@ interface ResolverInterface /** * Resolves relations in the target content using the given relations collection. * - * @param array $target The target story content containing UUIDs to resolve. - * @param array> $relations The target story content containing UUIDs to resolve. + * @param array $target the target story content containing UUIDs to resolve + * @param array> $relations the target story content containing UUIDs to resolve * * @return array */ diff --git a/src/Resolver/StoryResolver.php b/src/Resolver/StoryResolver.php index 7be649c..5073c46 100644 --- a/src/Resolver/StoryResolver.php +++ b/src/Resolver/StoryResolver.php @@ -30,13 +30,13 @@ public function resolve(array $target, array $relations): array } foreach ($target as &$value) { - if (is_string($value) && array_key_exists($value, $relationMap)) { + if (\is_string($value) && \array_key_exists($value, $relationMap)) { $value = $relationMap[$value]; continue; } - if (is_array($value)) { + if (\is_array($value)) { $value = $this->resolve($value, $relations); } } diff --git a/src/StoriesApi.php b/src/StoriesApi.php index 03b9eac..84db034 100644 --- a/src/StoriesApi.php +++ b/src/StoriesApi.php @@ -15,7 +15,6 @@ use Storyblok\Api\Domain\Value\Dto\Version; use Storyblok\Api\Domain\Value\Id; -use Storyblok\Api\Domain\Value\Resolver\RelationCollection; use Storyblok\Api\Domain\Value\Total; use Storyblok\Api\Domain\Value\Uuid; use Storyblok\Api\Request\StoriesRequest; diff --git a/src/StoriesResolvedApi.php b/src/StoriesResolvedApi.php index 04f8285..8a526cd 100644 --- a/src/StoriesResolvedApi.php +++ b/src/StoriesResolvedApi.php @@ -21,7 +21,6 @@ use Storyblok\Api\Resolver\ResolverInterface; use Storyblok\Api\Response\StoriesResponse; use Storyblok\Api\Response\StoryResponse; -use Webmozart\Assert\Assert; /** * @author Silas Joisten diff --git a/tests/Unit/Domain/Value/Resolver/RelationCollectionTest.php b/tests/Unit/Domain/Value/Resolver/RelationCollectionTest.php new file mode 100644 index 0000000..4b76738 --- /dev/null +++ b/tests/Unit/Domain/Value/Resolver/RelationCollectionTest.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Storyblok\Api\Tests\Unit\Domain\Value\Resolver; + +use PHPUnit\Framework\TestCase; +use Storyblok\Api\Domain\Value\Resolver\Relation; +use Storyblok\Api\Domain\Value\Resolver\RelationCollection; +use Storyblok\Api\Tests\Util\FakerTrait; + +/** + * @author Silas Joisten + */ +final class RelationCollectionTest extends TestCase +{ + use FakerTrait; + + /** + * @test + */ + public function add(): void + { + $faker = self::faker(); + + $collection = new RelationCollection(); + self::assertEmpty($collection); + + $collection->add(new Relation($faker->relation())); + self::assertCount(1, $collection); + } + + /** + * @test + */ + public function remove(): void + { + $faker = self::faker(); + + $relation = new Relation($faker->relation()); + + $collection = new RelationCollection([$relation]); + self::assertCount(1, $collection); + + $collection->remove($relation); + self::assertEmpty($collection); + } + + /** + * @test + */ + public function hasReturnsTrue(): void + { + $faker = self::faker(); + + $relation = new Relation($faker->relation()); + + $collection = new RelationCollection([$relation, new Relation($faker->relation())]); + + self::assertTrue($collection->has($relation)); + } + + /** + * @test + */ + public function hasReturnsFalse(): void + { + $faker = self::faker(); + + $collection = new RelationCollection([new Relation($faker->relation())]); + + self::assertFalse($collection->has(new Relation($faker->relation()))); + } + + /** + * @test + */ + public function isCountable(): void + { + $faker = self::faker(); + + $relation = new Relation($faker->relation()); + + $collection = new RelationCollection([$relation]); + + self::assertSame(1, $collection->count()); + } + + /** + * @test + */ + public function toStringMethod(): void + { + $faker = self::faker(); + + $relations = [ + new Relation($relation1 = $faker->relation()), + new Relation($relation2 = $faker->relation()), + new Relation($relation3 = $faker->relation()), + ]; + + $collection = new RelationCollection($relations); + + self::assertSame(implode(',', [$relation1, $relation2, $relation3]), $collection->toString()); + } + + /** + * @test + */ + public function getIterator(): void + { + $faker = self::faker(); + + $relations = [ + new Relation($faker->relation()), + new Relation($faker->relation()), + ]; + + self::assertInstanceOf(\ArrayIterator::class, (new RelationCollection($relations))->getIterator()); + } +} diff --git a/tests/Unit/Domain/Value/Resolver/RelationTest.php b/tests/Unit/Domain/Value/Resolver/RelationTest.php new file mode 100644 index 0000000..1fe02ac --- /dev/null +++ b/tests/Unit/Domain/Value/Resolver/RelationTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Storyblok\Api\Tests\Unit\Domain\Value\Resolver; + +use PHPUnit\Framework\TestCase; +use Storyblok\Api\Domain\Value\Resolver\Relation; +use Storyblok\Api\Tests\Util\FakerTrait; + +/** + * @author Silas Joisten + */ +final class RelationTest extends TestCase +{ + use FakerTrait; + + /** + * @test + */ + public function value(): void + { + $value = self::faker()->relation(); + + self::assertSame($value, (new Relation($value))->value); + } + + /** + * @test + * + * @dataProvider \Ergebnis\DataProvider\StringProvider::arbitrary() + * @dataProvider \Ergebnis\DataProvider\StringProvider::blank() + * @dataProvider \Ergebnis\DataProvider\StringProvider::empty() + */ + public function valueInvalid(string $value): void + { + self::expectException(\InvalidArgumentException::class); + + new Relation($value); + } +} diff --git a/tests/Unit/Request/StoryRequestTest.php b/tests/Unit/Request/StoryRequestTest.php index 5243360..8e38b8f 100644 --- a/tests/Unit/Request/StoryRequestTest.php +++ b/tests/Unit/Request/StoryRequestTest.php @@ -15,6 +15,8 @@ use PHPUnit\Framework\TestCase; use Storyblok\Api\Domain\Value\Dto\Version; +use Storyblok\Api\Domain\Value\Resolver\Relation; +use Storyblok\Api\Domain\Value\Resolver\RelationCollection; use Storyblok\Api\Request\StoryRequest; use Storyblok\Api\Tests\Util\FakerTrait; @@ -67,4 +69,22 @@ public function toArrayWithVersion(): void 'version' => $version->value, ], $request->toArray()); } + + /** + * @test + */ + public function toArrayWithRelations(): void + { + $request = new StoryRequest( + withRelations: new RelationCollection([ + new Relation('root.relation'), + new Relation('root.another_relation'), + ]), + ); + + self::assertSame([ + 'language' => 'default', + 'resolve_relations' => 'root.relation,root.another_relation', + ], $request->toArray()); + } } From 2bdbd889837da5ce7605e548e608c29a76ef243d Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 11:33:35 +0200 Subject: [PATCH 05/10] Fix --- src/StoriesResolvedApi.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/StoriesResolvedApi.php b/src/StoriesResolvedApi.php index 8a526cd..4a36173 100644 --- a/src/StoriesResolvedApi.php +++ b/src/StoriesResolvedApi.php @@ -13,11 +13,10 @@ namespace Storyblok\Api; -use Storyblok\Api\Domain\Value\Dto\Version; use Storyblok\Api\Domain\Value\Id; -use Storyblok\Api\Domain\Value\Resolver\RelationCollection; use Storyblok\Api\Domain\Value\Uuid; use Storyblok\Api\Request\StoriesRequest; +use Storyblok\Api\Request\StoryRequest; use Storyblok\Api\Resolver\ResolverInterface; use Storyblok\Api\Response\StoriesResponse; use Storyblok\Api\Response\StoryResponse; @@ -91,9 +90,9 @@ public function allByContentType(string $contentType, ?StoriesRequest $request = ); } - public function bySlug(string $slug, string $language = 'default', ?Version $version = null, ?RelationCollection $withRelations = null): StoryResponse + public function bySlug(string $slug, ?StoryRequest $request = null): StoryResponse { - $response = $this->storiesApi->bySlug($slug, $language, $version, $withRelations); + $response = $this->storiesApi->bySlug($slug, $request); $story = $this->resolver->resolve($response->story, $response->rels); @@ -105,9 +104,9 @@ public function bySlug(string $slug, string $language = 'default', ?Version $ver ]); } - public function byUuid(Uuid $uuid, string $language = 'default', ?Version $version = null, ?RelationCollection $withRelations = null): StoryResponse + public function byUuid(Uuid $uuid, ?StoryRequest $request = null): StoryResponse { - $response = $this->storiesApi->byUuid($uuid, $language, $version, $withRelations); + $response = $this->storiesApi->byUuid($uuid, $request); $story = $this->resolver->resolve($response->story, $response->rels); @@ -119,9 +118,9 @@ public function byUuid(Uuid $uuid, string $language = 'default', ?Version $versi ]); } - public function byId(Id $id, string $language = 'default', ?Version $version = null, ?RelationCollection $withRelations = null): StoryResponse + public function byId(Id $id, ?StoryRequest $request = null): StoryResponse { - $response = $this->storiesApi->byId($id, $language, $version, $withRelations); + $response = $this->storiesApi->byId($id, $request); $story = $this->resolver->resolve($response->story, $response->rels); From 39ab86197628361a07594a87871be711539d05da Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 11:36:08 +0200 Subject: [PATCH 06/10] Fix --- src/Response/StoriesResponse.php | 2 +- src/Response/StoryResponse.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Response/StoriesResponse.php b/src/Response/StoriesResponse.php index c65146c..402e5e4 100644 --- a/src/Response/StoriesResponse.php +++ b/src/Response/StoriesResponse.php @@ -29,7 +29,7 @@ public int $cv; /** - * @var list + * @var list> */ public array $rels; diff --git a/src/Response/StoryResponse.php b/src/Response/StoryResponse.php index 8e217e1..48e61f2 100644 --- a/src/Response/StoryResponse.php +++ b/src/Response/StoryResponse.php @@ -27,7 +27,7 @@ public int $cv; /** - * @var list> + * @var list> */ public array $rels; From 54ce4379cc0eb6ac03c95d8fb7496526363e715d Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 11:49:36 +0200 Subject: [PATCH 07/10] Fix --- tests/Unit/Resolver/StoryResolverTest.php | 127 ++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/Unit/Resolver/StoryResolverTest.php diff --git a/tests/Unit/Resolver/StoryResolverTest.php b/tests/Unit/Resolver/StoryResolverTest.php new file mode 100644 index 0000000..ddcc545 --- /dev/null +++ b/tests/Unit/Resolver/StoryResolverTest.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Storyblok\Api\Tests\Unit\Resolver; + +use PHPUnit\Framework\TestCase; +use Storyblok\Api\Resolver\StoryResolver; +use Storyblok\Api\Tests\Util\FakerTrait; + +/** + * @author Silas Joisten + */ +final class StoryResolverTest extends TestCase +{ + use FakerTrait; + + /** + * @test + */ + public function resolve(): void + { + $resolver = new StoryResolver(); + + $faker = self::faker(); + + $story = [ + 'name' => $faker->word(), + 'content' => [ + 'uuid' => $faker->uuid(), + 'reference' => $referenceUuid = $faker->uuid(), + 'some_field' => $faker->word(), + ], + ]; + + $references = [ + $reference = [ + 'uuid' => $referenceUuid, + 'name' => $faker->word(), + 'another_field' => $faker->sentence(), + ], + ]; + + $expected = $story; + $expected['content']['reference'] = $reference; + + self::assertSame($expected, $resolver->resolve($story, $references)); + } + + /** + * @test + */ + public function resolveWithComplexStructure(): void + { + $resolver = new StoryResolver(); + + $faker = self::faker(); + + $story = [ + 'name' => $faker->word(), + 'content' => [ + 'uuid' => $faker->uuid(), + 'references' => [ + $referenceUuid1 = $faker->uuid(), + $referenceUuid2 = $faker->uuid(), + $referenceUuid3 = $faker->uuid(), + $referenceUuid4 = $faker->uuid(), + ], + 'blocks' => [ + [ + 'uuid' => $faker->uuid(), + 'type' => 'card', + 'block' => [ + 'uuid' => $faker->uuid(), + 'type' => 'button', + 'some_field' => $referenceUuid5 = $faker->uuid(), + ], + ], + ], + 'some_field' => $faker->word(), + ], + ]; + + $references = [ + [ + 'uuid' => $referenceUuid1, + 'name' => $faker->word(), + 'another_field' => $faker->sentence(), + ], + [ + 'uuid' => $referenceUuid2, + 'name' => $faker->word(), + 'another_field' => $faker->sentence(), + ], + [ + 'uuid' => $referenceUuid3, + 'name' => $faker->word(), + 'another_field' => $faker->sentence(), + ], + [ + 'uuid' => $referenceUuid4, + 'name' => $faker->word(), + 'another_field' => $faker->sentence(), + ], + [ + 'uuid' => $referenceUuid5, + 'name' => $faker->word(), + 'price' => $faker->randomNumber(), + ], + ]; + + $expected = $story; + $expected['content']['references'] = [$references[0], $references[1], $references[2], $references[3]]; + $expected['content']['blocks'][0]['block']['some_field'] = $references[4]; + + self::assertSame($expected, $resolver->resolve($story, $references)); + } +} From b76e944525631d431fa5a01294db6b23f74b961f Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 12:05:02 +0200 Subject: [PATCH 08/10] Adds Tests --- tests/Unit/Request/StoriesRequestTest.php | 188 ++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 tests/Unit/Request/StoriesRequestTest.php diff --git a/tests/Unit/Request/StoriesRequestTest.php b/tests/Unit/Request/StoriesRequestTest.php new file mode 100644 index 0000000..67b9980 --- /dev/null +++ b/tests/Unit/Request/StoriesRequestTest.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Storyblok\Api\Tests\Unit\Request; + +use PHPUnit\Framework\TestCase; +use Storyblok\Api\Domain\Value\Dto\Direction; +use Storyblok\Api\Domain\Value\Dto\SortBy; +use Storyblok\Api\Domain\Value\Dto\Version; +use Storyblok\Api\Domain\Value\Field\Field; +use Storyblok\Api\Domain\Value\Field\FieldCollection; +use Storyblok\Api\Domain\Value\Filter\FilterCollection; +use Storyblok\Api\Domain\Value\Filter\Filters\InFilter; +use Storyblok\Api\Domain\Value\Id; +use Storyblok\Api\Domain\Value\IdCollection; +use Storyblok\Api\Domain\Value\Resolver\Relation; +use Storyblok\Api\Domain\Value\Resolver\RelationCollection; +use Storyblok\Api\Domain\Value\Tag\Tag; +use Storyblok\Api\Domain\Value\Tag\TagCollection; +use Storyblok\Api\Request\StoriesRequest; +use Storyblok\Api\Tests\Util\FakerTrait; + +/** + * @author Silas Joisten + */ +final class StoriesRequestTest extends TestCase +{ + use FakerTrait; + + /** + * @test + */ + public function toArrayWithDefaults(): void + { + $request = new StoriesRequest(); + + self::assertSame([ + 'language' => 'default', + 'page' => 1, + 'per_page' => 25, + ], $request->toArray()); + } + + /** + * @test + */ + public function toArrayWithSortBy(): void + { + $request = new StoriesRequest( + sortBy: new SortBy('name', Direction::Asc), + ); + + self::assertSame([ + 'language' => 'default', + 'page' => 1, + 'per_page' => 25, + 'sort_by' => 'name:asc', + ], $request->toArray()); + } + + /** + * @test + */ + public function toArrayWithFilters(): void + { + $request = new StoriesRequest( + filters: new FilterCollection([ + new InFilter('name', ['foo', 'bar']), + ]), + ); + + self::assertSame([ + 'language' => 'default', + 'page' => 1, + 'per_page' => 25, + 'filter_query' => [ + 'name' => [ + 'in' => 'foo,bar', + ], + ], + ], $request->toArray()); + } + + /** + * @test + */ + public function toArrayWithExcludeFields(): void + { + $request = new StoriesRequest( + excludeFields: new FieldCollection([ + new Field('body'), + new Field('content'), + ]), + ); + + self::assertSame([ + 'language' => 'default', + 'page' => 1, + 'per_page' => 25, + 'excluding_fields' => 'body,content', + ], $request->toArray()); + } + + /** + * @test + */ + public function toArrayWithTags(): void + { + $request = new StoriesRequest( + withTags: new TagCollection([ + new Tag('foo'), + new Tag('bar'), + ]), + ); + + self::assertSame([ + 'language' => 'default', + 'page' => 1, + 'per_page' => 25, + 'with_tag' => 'foo,bar', + ], $request->toArray()); + } + + /** + * @test + */ + public function toArrayWithExcludeIds(): void + { + $request = new StoriesRequest( + excludeIds: new IdCollection([ + new Id(1), + ]), + ); + + self::assertSame([ + 'language' => 'default', + 'page' => 1, + 'per_page' => 25, + 'excluding_ids' => '1', + ], $request->toArray()); + } + + /** + * @test + */ + public function toArrayWithRelations(): void + { + $request = new StoriesRequest( + withRelations: new RelationCollection([ + new Relation('blog_post.category'), + ]), + ); + + self::assertSame([ + 'language' => 'default', + 'page' => 1, + 'per_page' => 25, + 'resolve_relations' => 'blog_post.category', + ], $request->toArray()); + } + + /** + * @test + */ + public function toArrayWithVersion(): void + { + $request = new StoriesRequest( + version: $version = Version::Published, + ); + + self::assertSame([ + 'language' => 'default', + 'page' => 1, + 'per_page' => 25, + 'version' => $version->value, + ], $request->toArray()); + } +} From 7176039c24991b52c499654e2d0df8b94197f9c7 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 13:36:06 +0200 Subject: [PATCH 09/10] Fix --- src/Resolver/StoryResolver.php | 1 + tests/Unit/Resolver/StoryResolverTest.php | 40 +++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/Resolver/StoryResolver.php b/src/Resolver/StoryResolver.php index 5073c46..92519e9 100644 --- a/src/Resolver/StoryResolver.php +++ b/src/Resolver/StoryResolver.php @@ -26,6 +26,7 @@ public function resolve(array $target, array $relations): array foreach ($relations as $relation) { Assert::keyExists($relation, 'uuid'); + Assert::uuid($relation['uuid']); $relationMap[$relation['uuid']] = $relation; } diff --git a/tests/Unit/Resolver/StoryResolverTest.php b/tests/Unit/Resolver/StoryResolverTest.php index ddcc545..97c31fb 100644 --- a/tests/Unit/Resolver/StoryResolverTest.php +++ b/tests/Unit/Resolver/StoryResolverTest.php @@ -56,6 +56,46 @@ public function resolve(): void self::assertSame($expected, $resolver->resolve($story, $references)); } + /** + * @test + */ + public function resolveThrowsExceptionWhenUuidKeyDoesNotExist(): void + { + $resolver = new StoryResolver(); + + $faker = self::faker(); + + $reference = [ + 'id' => $faker->uuid(), + 'name' => $faker->word(), + 'another_field' => $faker->sentence(), + ]; + + self::expectException(\InvalidArgumentException::class); + + $resolver->resolve(['name' => $faker->word()], [$reference]); + } + + /** + * @test + */ + public function resolveThrowsExceptionWhenUuidKeyContainsNoValidUuid(): void + { + $resolver = new StoryResolver(); + + $faker = self::faker(); + + $reference = [ + 'uuid' => $faker->word(), + 'name' => $faker->word(), + 'another_field' => $faker->sentence(), + ]; + + self::expectException(\InvalidArgumentException::class); + + $resolver->resolve(['name' => $faker->word()], [$reference]); + } + /** * @test */ From f70c3a1ad38a149822fa01495e96ceeb36dd3f9f Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Fri, 17 Jan 2025 14:01:46 +0200 Subject: [PATCH 10/10] Enhancment: Adds Limit --- src/Resolver/StoryResolver.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Resolver/StoryResolver.php b/src/Resolver/StoryResolver.php index 92519e9..c36f405 100644 --- a/src/Resolver/StoryResolver.php +++ b/src/Resolver/StoryResolver.php @@ -24,10 +24,16 @@ public function resolve(array $target, array $relations): array { $relationMap = []; - foreach ($relations as $relation) { + foreach ($relations as $key => $relation) { Assert::keyExists($relation, 'uuid'); Assert::uuid($relation['uuid']); $relationMap[$relation['uuid']] = $relation; + + // There is a limit of possible resolvable relations. + // @see https://www.storyblok.com/docs/api/content-delivery/v2/stories/retrieve-a-single-story + if (50 === $key) { + break; + } } foreach ($target as &$value) {