diff --git a/src/Contracts/Schema/Container.php b/src/Contracts/Schema/Container.php index 5663e17..c134612 100644 --- a/src/Contracts/Schema/Container.php +++ b/src/Contracts/Schema/Container.php @@ -15,7 +15,6 @@ interface Container { - /** * Does a schema exist for the supplied resource type? * @@ -43,18 +42,18 @@ public function schemaClassFor(ResourceType|string $type): string; /** * Get a schema for the provided model class. * - * @param string|object $model + * @param class-string|object $model * @return Schema */ - public function schemaForModel($model): Schema; + public function schemaForModel(string|object $model): Schema; /** * Does a schema exist for the provided model class? * - * @param string|object $model + * @param class-string|object $model * @return bool */ - public function existsForModel($model): bool; + public function existsForModel(string|object $model): bool; /** * Get the fully qualified model class for the provided resource type. diff --git a/src/Contracts/Schema/Schema.php b/src/Contracts/Schema/Schema.php index a5d095d..a99ac6e 100644 --- a/src/Contracts/Schema/Schema.php +++ b/src/Contracts/Schema/Schema.php @@ -17,34 +17,12 @@ interface Schema extends Traversable { - /** * Get the JSON:API resource type. * - * @return string - */ - public static function type(): string; - - /** - * Get the fully-qualified class name of the model. - * - * @return string - */ - public static function model(): string; - - /** - * Get the fully-qualified class name of the resource. - * - * @return string - */ - public static function resource(): string; - - /** - * Get the resource type as it appears in URIs. - * - * @return string + * @return non-empty-string */ - public static function uriType(): string; + public function type(): string; /** * Get a repository for the resource. diff --git a/src/Contracts/Schema/StaticSchema/ServerConventions.php b/src/Contracts/Schema/StaticSchema/ServerConventions.php new file mode 100644 index 0000000..e89acd3 --- /dev/null +++ b/src/Contracts/Schema/StaticSchema/ServerConventions.php @@ -0,0 +1,39 @@ + $schema + * @return non-empty-string + */ + public function getTypeFor(string $schema): string; + + /** + * Resolve the JSON:API resource type as it appears in URIs, for the provided resource type. + * + * @param non-empty-string $type + * @return non-empty-string|null + */ + public function getUriTypeFor(string $type): ?string; + + /** + * @param class-string $schema + * @return class-string + */ + public function getResourceClassFor(string $schema): string; +} \ No newline at end of file diff --git a/src/Contracts/Schema/StaticSchema/StaticContainer.php b/src/Contracts/Schema/StaticSchema/StaticContainer.php new file mode 100644 index 0000000..1a9a8e5 --- /dev/null +++ b/src/Contracts/Schema/StaticSchema/StaticContainer.php @@ -0,0 +1,69 @@ + + */ +interface StaticContainer extends IteratorAggregate +{ + /** + * Get a static schema for the specified schema class. + * + * @param class-string|Schema $schema + * @return StaticSchema + */ + public function schemaFor(string|Schema $schema): StaticSchema; + + /** + * Does a schema exist for the supplied JSON:API resource type? + * + * @param ResourceType|non-empty-string $type + * @return bool + */ + public function exists(ResourceType|string $type): bool; + + /** + * Get the (non-static) schema class for a JSON:API resource type. + * + * @param ResourceType|non-empty-string $type + * @return class-string + */ + public function schemaClassFor(ResourceType|string $type): string; + + /** + * Get the fully qualified model class for the provided JSON:API resource type. + * + * @param ResourceType|non-empty-string $type + * @return string + */ + public function modelClassFor(ResourceType|string $type): string; + + /** + * Get the JSON:API resource type for the provided type as it appears in URLs. + * + * @param non-empty-string $uriType + * @return ResourceType|null + */ + public function typeForUri(string $uriType): ?ResourceType; + + /** + * Get a list of all the supported JSON:API resource types. + * + * @return array + */ + public function types(): array; +} \ No newline at end of file diff --git a/src/Contracts/Schema/StaticSchema/StaticSchema.php b/src/Contracts/Schema/StaticSchema/StaticSchema.php new file mode 100644 index 0000000..e8fba99 --- /dev/null +++ b/src/Contracts/Schema/StaticSchema/StaticSchema.php @@ -0,0 +1,52 @@ + + */ + public function getSchemaClass(): string; + + /** + * Get the JSON:API resource type. + * + * @return non-empty-string + */ + public function getType(): string; + + /** + * Get the JSON:API resource type as it appears in URIs. + * + * @return non-empty-string + */ + public function getUriType(): string; + + /** + * Get the fully-qualified class name of the model. + * + * @return class-string + */ + public function getModel(): string; + + /** + * Get the fully-qualified class name of the resource. + * + * @return class-string + */ + public function getResourceClass(): string; +} \ No newline at end of file diff --git a/src/Contracts/Schema/StaticSchema/StaticSchemaFactory.php b/src/Contracts/Schema/StaticSchema/StaticSchemaFactory.php new file mode 100644 index 0000000..be85db8 --- /dev/null +++ b/src/Contracts/Schema/StaticSchema/StaticSchemaFactory.php @@ -0,0 +1,26 @@ +> $schemas + * @return Generator + */ + public function make(iterable $schemas): Generator; +} \ No newline at end of file diff --git a/src/Core/Resources/Container.php b/src/Core/Resources/Container.php index 8131dda..1c462ce 100644 --- a/src/Core/Resources/Container.php +++ b/src/Core/Resources/Container.php @@ -22,22 +22,15 @@ use function is_object; use function sprintf; -class Container implements ContainerContract +final readonly class Container implements ContainerContract { - - /** - * @var Factory - */ - private Factory $factory; - /** * Container constructor. * * @param Factory $factory */ - public function __construct(Factory $factory) + public function __construct(private Factory $factory) { - $this->factory = $factory; } /** diff --git a/src/Core/Resources/Factory.php b/src/Core/Resources/Factory.php index c3aedd0..1aedc57 100644 --- a/src/Core/Resources/Factory.php +++ b/src/Core/Resources/Factory.php @@ -14,27 +14,21 @@ use LaravelJsonApi\Contracts\Resources\Factory as FactoryContract; use LaravelJsonApi\Contracts\Schema\Container as SchemaContainer; use LaravelJsonApi\Contracts\Schema\Schema; +use LaravelJsonApi\Core\Schema\StaticSchema\StaticContainer; use LogicException; use Throwable; use function sprintf; -class Factory implements FactoryContract +final readonly class Factory implements FactoryContract { - - /** - * @var SchemaContainer - * - */ - protected SchemaContainer $schemas; - /** * Factory constructor. * + * @param StaticContainer $staticSchemas * @param SchemaContainer $schemas */ - public function __construct(SchemaContainer $schemas) + public function __construct(private StaticContainer $staticSchemas, private SchemaContainer $schemas) { - $this->schemas = $schemas; } /** @@ -72,9 +66,10 @@ public function createResource(object $model): JsonApiResource */ protected function build(Schema $schema, object $model): JsonApiResource { - $fqn = $schema->resource(); + $fqn = $this->staticSchemas + ->schemaFor($schema) + ->getResourceClass(); return new $fqn($schema, $model); } - } diff --git a/src/Core/Schema/Attributes/Model.php b/src/Core/Schema/Attributes/Model.php new file mode 100644 index 0000000..c297d02 --- /dev/null +++ b/src/Core/Schema/Attributes/Model.php @@ -0,0 +1,27 @@ +>|null */ - private array $aliases; + private ?array $models = null; /** * Container constructor. * * @param ContainerResolver $container * @param Server $server - * @param iterable $schemas + * @param StaticContainer $staticSchemas */ - public function __construct(ContainerResolver $container, Server $server, iterable $schemas) - { - $this->container = $container; - $this->server = $server; - $this->types = []; - $this->uriTypes = []; - $this->models = []; - $this->schemas = []; - $this->aliases = []; - - foreach ($schemas as $schemaClass) { - $type = $schemaClass::type(); - $this->types[$type] = $schemaClass; - $this->uriTypes[$schemaClass::uriType()] = $type; - $this->models[$schemaClass::model()] = $schemaClass; - } - - ksort($this->types); + public function __construct( + private readonly ContainerResolver $container, + private readonly Server $server, + private readonly StaticContainer $staticSchemas, + ) { } /** @@ -91,9 +59,7 @@ public function __construct(ContainerResolver $container, Server $server, iterab */ public function exists(string|ResourceType $resourceType): bool { - $resourceType = (string) $resourceType; - - return isset($this->types[$resourceType]); + return $this->staticSchemas->exists($resourceType); } /** @@ -101,9 +67,9 @@ public function exists(string|ResourceType $resourceType): bool */ public function schemaFor(string|ResourceType $resourceType): Schema { - return $this->resolve( - $this->schemaClassFor($resourceType), - ); + $class = $this->staticSchemas->schemaClassFor($resourceType); + + return $this->resolve($class); } /** @@ -111,13 +77,7 @@ public function schemaFor(string|ResourceType $resourceType): Schema */ public function schemaClassFor(string|ResourceType $type): string { - $type = (string) $type; - - if (isset($this->types[$type])) { - return $this->types[$type]; - } - - throw new LogicException("No schema for JSON:API resource type {$resourceType}."); + return $this->staticSchemas->schemaClassFor($type); } /** @@ -125,15 +85,13 @@ public function schemaClassFor(string|ResourceType $type): string */ public function modelClassFor(string|ResourceType $resourceType): string { - return $this - ->schemaFor($resourceType) - ->model(); + return $this->staticSchemas->modelClassFor($resourceType); } /** * @inheritDoc */ - public function existsForModel($model): bool + public function existsForModel(string|object $model): bool { return !empty($this->resolveModelClassFor($model)); } @@ -141,7 +99,7 @@ public function existsForModel($model): bool /** * @inheritDoc */ - public function schemaForModel($model): Schema + public function schemaForModel(string|object $model): Schema { if ($class = $this->resolveModelClassFor($model)) { return $this->resolve( @@ -160,9 +118,7 @@ public function schemaForModel($model): Schema */ public function schemaTypeForUri(string $uriType): ?ResourceType { - $value = $this->uriTypes[$uriType] ?? null; - - return $value ? new ResourceType($value) : null; + return $this->staticSchemas->typeForUri($uriType); } /** @@ -170,7 +126,7 @@ public function schemaTypeForUri(string $uriType): ?ResourceType */ public function types(): array { - return array_keys($this->types); + return $this->staticSchemas->types(); } /** @@ -181,6 +137,13 @@ public function types(): array */ private function resolveModelClassFor(string|object $model): ?string { + if ($this->models === null) { + $this->models = []; + foreach ($this->staticSchemas as $staticSchema) { + $this->models[$staticSchema->getModel()] = $staticSchema->getSchemaClass(); + } + } + $model = is_object($model) ? get_class($model) : $model; $model = $this->aliases[$model] ?? $model; @@ -226,6 +189,7 @@ private function make(string $schemaClass): Schema $schema = $this->container->instance()->make($schemaClass, [ 'schemas' => $this, 'server' => $this->server, + 'static' => $this->staticSchemas->schemaFor($schemaClass), ]); } catch (Throwable $ex) { throw new RuntimeException("Unable to create schema {$schemaClass}.", 0, $ex); diff --git a/src/Core/Schema/Schema.php b/src/Core/Schema/Schema.php index ff0b723..4f68496 100644 --- a/src/Core/Schema/Schema.php +++ b/src/Core/Schema/Schema.php @@ -24,11 +24,10 @@ use LaravelJsonApi\Contracts\Schema\Schema as SchemaContract; use LaravelJsonApi\Contracts\Schema\SchemaAware as SchemaAwareContract; use LaravelJsonApi\Contracts\Schema\Sortable; +use LaravelJsonApi\Contracts\Schema\StaticSchema\StaticSchema; use LaravelJsonApi\Contracts\Server\Server; use LaravelJsonApi\Contracts\Store\Repository; -use LaravelJsonApi\Core\Resources\ResourceResolver; use LaravelJsonApi\Core\Support\Arr; -use LaravelJsonApi\Core\Support\Str; use LogicException; use Traversable; use function array_keys; @@ -37,18 +36,6 @@ abstract class Schema implements SchemaContract, IteratorAggregate { - /** - * @var Server - */ - protected Server $server; - - /** - * The resource type as it appears in URIs. - * - * @var string|null - */ - protected static ?string $uriType = null; - /** * The key name for the resource "id". * @@ -92,16 +79,6 @@ abstract class Schema implements SchemaContract, IteratorAggregate */ private ?array $relations = null; - /** - * @var callable|null - */ - private static $resourceTypeResolver; - - /** - * @var callable|null - */ - private static $resourceResolver; - /** * Get the resource fields. * @@ -110,79 +87,23 @@ abstract class Schema implements SchemaContract, IteratorAggregate abstract public function fields(): iterable; /** - * Specify the callback to use to guess the resource type from the schema class. - * - * @param callable $resolver - * @return void - */ - public static function guessTypeUsing(callable $resolver): void - { - static::$resourceTypeResolver = $resolver; - } - - /** - * @inheritDoc - */ - public static function type(): string - { - $resolver = static::$resourceTypeResolver ?: new TypeResolver(); - - return $resolver(static::class); - } - - /** - * @inheritDoc - */ - public static function model(): string - { - if (isset(static::$model)) { - return static::$model; - } - - throw new LogicException('The model class name must be set.'); - } - - /** - * Specify the callback to use to guess the resource class from the schema class. + * Schema constructor. * - * @param callable $resolver - * @return void - */ - public static function guessResourceUsing(callable $resolver): void - { - static::$resourceResolver = $resolver; - } - - /** - * @inheritDoc + * @param Server $server + * @param StaticSchema $static */ - public static function resource(): string - { - $resolver = static::$resourceResolver ?: new ResourceResolver(); - - return $resolver(static::class); + public function __construct( + protected readonly Server $server, + protected readonly StaticSchema $static, + ) { } /** * @inheritDoc */ - public static function uriType(): string - { - if (static::$uriType) { - return static::$uriType; - } - - return static::$uriType = Str::dasherize(static::type()); - } - - /** - * Schema constructor. - * - * @param Server $server - */ - public function __construct(Server $server) + public function type(): string { - $this->server = $server; + return $this->static->getType(); } /** @@ -208,7 +129,7 @@ public function url($extra = [], bool $secure = null): string { $extra = Arr::wrap($extra); - array_unshift($extra, $this->uriType()); + array_unshift($extra, $this->static->getUriType()); return $this->server->url($extra, $secure); } diff --git a/src/Core/Schema/StaticSchema/DefaultConventions.php b/src/Core/Schema/StaticSchema/DefaultConventions.php new file mode 100644 index 0000000..0c82660 --- /dev/null +++ b/src/Core/Schema/StaticSchema/DefaultConventions.php @@ -0,0 +1,47 @@ + $schema + * @param ServerConventions $conventions + */ + public function __construct( + private string $schema, + private ServerConventions $conventions, + ) { + $this->reflection = new ReflectionClass($this->schema); + } + + /** + * @inheritDoc + */ + public function getSchemaClass(): string + { + return $this->schema; + } + + /** + * @inheritDoc + */ + public function getType(): string + { + $type = null; + + if ($attribute = $this->attribute(Type::class)) { + $type = $attribute->type; + } + + return $type ?? $this->conventions->getTypeFor($this->schema); + } + + /** + * @inheritDoc + */ + public function getUriType(): string + { + $uri = null; + + if ($attribute = $this->attribute(Type::class)) { + $uri = $attribute->uri; + } + + return $uri ?? $this->conventions->getUriTypeFor( + $this->getType(), + ); + } + + /** + * @inheritDoc + */ + public function getModel(): string + { + if ($attribute = $this->attribute(Model::class)) { + return $attribute->value; + } + + throw new RuntimeException('Model attribute not found on schema: ' . $this->schema); + } + + /** + * @inheritDoc + */ + public function getResourceClass(): string + { + if ($attribute = $this->attribute(ResourceClass::class)) { + return $attribute->value; + } + + return $this->conventions->getResourceClassFor($this->schema); + } + + /** + * @template TAttribute + * @param class-string $class + * @return TAttribute|null + */ + private function attribute(string $class): ?object + { + $attribute = $this->reflection->getAttributes($class)[0] ?? null; + + return $attribute?->newInstance(); + } +} \ No newline at end of file diff --git a/src/Core/Schema/StaticSchema/StaticContainer.php b/src/Core/Schema/StaticSchema/StaticContainer.php new file mode 100644 index 0000000..6911a7f --- /dev/null +++ b/src/Core/Schema/StaticSchema/StaticContainer.php @@ -0,0 +1,131 @@ +, StaticSchema> + */ + private array $schemas = []; + + /** + * @var array> + */ + private array $types = []; + + /** + * @var array|null + */ + private ?array $uriTypes = null; + + /** + * StaticContainer constructor. + * + * @param iterable $schemas + */ + public function __construct(iterable $schemas) + { + foreach ($schemas as $schema) { + assert($schema instanceof StaticSchema); + $class = $schema->getSchemaClass(); + $this->schemas[$class] = $schema; + $this->types[$schema->getType()] = $class; + } + + ksort($this->types); + } + + /** + * @inheritDoc + */ + public function schemaFor(string|Schema $schema): StaticSchema + { + $schema = is_object($schema) ? $schema::class : $schema; + + return $this->schemas[$schema] ?? throw new RuntimeException('Schema does not exist: ' . $schema); + } + + /** + * @inheritDoc + */ + public function exists(ResourceType|string $type): bool + { + return isset($this->types[(string) $type]); + } + + /** + * @inheritDoc + */ + public function schemaClassFor(ResourceType|string $type): string + { + return $this->types[(string) $type] ?? throw new RuntimeException('Unrecognised resource type: ' . $type); + } + + /** + * @inheritDoc + */ + public function modelClassFor(ResourceType|string $type): string + { + $schema = $this->schemaFor( + $this->schemaClassFor($type), + ); + + return $schema->getModel(); + } + + /** + * @inheritDoc + */ + public function typeForUri(string $uriType): ?ResourceType + { + if ($this->uriTypes === null) { + $this->uriTypes = []; + foreach ($this->schemas as $schema) { + $this->uriTypes[$schema->getUriType()] = $schema->getType(); + } + } + + $type = $this->uriTypes[$uriType] ?? null; + + if ($type !== null) { + return new ResourceType($type); + } + + throw new RuntimeException('Unrecognised URI type: ' . $uriType); + } + + /** + * @inheritDoc + */ + public function types(): array + { + return array_keys($this->types); + } + + /** + * @return Generator + */ + public function getIterator(): Generator + { + foreach ($this->schemas as $schema) { + yield $schema; + } + } +} \ No newline at end of file diff --git a/src/Core/Schema/StaticSchema/StaticSchemaFactory.php b/src/Core/Schema/StaticSchema/StaticSchemaFactory.php new file mode 100644 index 0000000..26805ea --- /dev/null +++ b/src/Core/Schema/StaticSchema/StaticSchemaFactory.php @@ -0,0 +1,40 @@ +conventions), + ); + } + } +} \ No newline at end of file diff --git a/src/Core/Schema/StaticSchema/ThreadCachedStaticSchema.php b/src/Core/Schema/StaticSchema/ThreadCachedStaticSchema.php new file mode 100644 index 0000000..f8b93e4 --- /dev/null +++ b/src/Core/Schema/StaticSchema/ThreadCachedStaticSchema.php @@ -0,0 +1,112 @@ +|null + */ + private ?string $schemaClass = null; + + /** + * @var non-empty-string|null + */ + private ?string $type = null; + + /** + * @var non-empty-string|null + */ + private ?string $uriType = null; + + /** + * @var class-string|null + */ + private ?string $model = null; + + /** + * @var class-string|null + */ + private ?string $resourceClass = null; + + /** + * ThreadCachedStaticSchema constructor. + * + * @param StaticSchema $base + */ + public function __construct(private readonly StaticSchema $base) + { + } + + /** + * @inheritDoc + */ + public function getSchemaClass(): string + { + if ($this->schemaClass !== null) { + return $this->schemaClass; + } + + return $this->schemaClass = $this->base->getSchemaClass(); + } + + /** + * @inheritDoc + */ + public function getType(): string + { + if ($this->type !== null) { + return $this->type; + } + + return $this->type = $this->base->getType(); + } + + /** + * @inheritDoc + */ + public function getUriType(): string + { + if ($this->uriType !== null) { + return $this->uriType; + } + + return $this->uriType = $this->base->getUriType(); + } + + /** + * @inheritDoc + */ + public function getModel(): string + { + if ($this->model !== null) { + return $this->model; + } + + return $this->model = $this->base->getModel(); + } + + /** + * @inheritDoc + */ + public function getResourceClass(): string + { + if ($this->resourceClass !== null) { + return $this->resourceClass; + } + + return $this->resourceClass = $this->base->getResourceClass(); + } +} \ No newline at end of file diff --git a/src/Core/Server/Server.php b/src/Core/Server/Server.php index 713501d..be86e6a 100644 --- a/src/Core/Server/Server.php +++ b/src/Core/Server/Server.php @@ -22,6 +22,8 @@ use LaravelJsonApi\Contracts\Encoder\Factory as EncoderFactory; use LaravelJsonApi\Contracts\Resources\Container as ResourceContainerContract; use LaravelJsonApi\Contracts\Schema\Container as SchemaContainerContract; +use LaravelJsonApi\Contracts\Schema\Schema; +use LaravelJsonApi\Contracts\Schema\StaticSchema\StaticContainer as StaticContainerContract; use LaravelJsonApi\Contracts\Server\Server as ServerContract; use LaravelJsonApi\Contracts\Store\Store as StoreContract; use LaravelJsonApi\Core\Auth\Container as AuthContainer; @@ -29,6 +31,8 @@ use LaravelJsonApi\Core\Resources\Container as ResourceContainer; use LaravelJsonApi\Core\Resources\Factory as ResourceFactory; use LaravelJsonApi\Core\Schema\Container as SchemaContainer; +use LaravelJsonApi\Core\Schema\StaticSchema\StaticContainer; +use LaravelJsonApi\Core\Schema\StaticSchema\StaticSchemaFactory; use LaravelJsonApi\Core\Store\Store; use LaravelJsonApi\Core\Support\AppResolver; use LogicException; @@ -52,6 +56,11 @@ abstract class Server implements ServerContract */ private string $name; + /** + * @var StaticContainerContract|null + */ + private ?StaticContainerContract $staticContainer = null; + /** * @var SchemaContainerContract|null */ @@ -70,7 +79,7 @@ abstract class Server implements ServerContract /** * Get the server's list of schemas. * - * @return array + * @return array> */ abstract protected function allSchemas(): array; @@ -118,7 +127,7 @@ public function schemas(): SchemaContainerContract return $this->schemas = new SchemaContainer( $this->app->container(), $this, - $this->allSchemas(), + $this->staticSchemas(), ); } @@ -132,7 +141,10 @@ public function resources(): ResourceContainerContract } return $this->resources = new ResourceContainer( - new ResourceFactory($this->schemas()), + new ResourceFactory( + $this->staticSchemas(), + $this->schemas(), + ), ); } @@ -220,4 +232,20 @@ protected function app(): Application { return $this->app->instance(); } + + /** + * @return StaticContainerContract + */ + private function staticSchemas(): StaticContainerContract + { + if ($this->staticContainer) { + return $this->staticContainer; + } + + $staticSchemaFactory = new StaticSchemaFactory(); + + return $this->staticContainer = new StaticContainer( + $staticSchemaFactory->make($this->allSchemas()) + ); + } } diff --git a/tests/Unit/Schema/StaticSchema/DefaultConventionsTest.php b/tests/Unit/Schema/StaticSchema/DefaultConventionsTest.php new file mode 100644 index 0000000..ac11053 --- /dev/null +++ b/tests/Unit/Schema/StaticSchema/DefaultConventionsTest.php @@ -0,0 +1,116 @@ +conventions = new DefaultConventions(); + } + + /** + * @return array> + */ + public static function typeProvider(): array + { + return [ + [ + 'App\JsonApi\V1\Posts\PostSchema', + 'posts', + ], + [ + 'App\JsonApi\V1\Posts\BlogPostSchema', + 'blog-posts', + ], + ]; + } + + /** + * @param string $schema + * @param string $expected + * @return void + * @dataProvider typeProvider + */ + public function testType(string $schema, string $expected): void + { + $this->assertSame($expected, $this->conventions->getTypeFor($schema)); + } + + /** + * @return array> + */ + public static function uriTypeProvider(): array + { + return [ + ['posts', 'posts'], + ['blogPosts', 'blog-posts'], + ['blog_posts', 'blog-posts'], + ]; + } + + /** + * @param string $type + * @param string $expected + * @return void + * @dataProvider uriTypeProvider + */ + public function testUriType(string $type, string $expected): void + { + $this->assertSame($expected, $this->conventions->getUriTypeFor($type)); + } + + /** + * @return array> + */ + public static function resourceClassProvider(): array + { + return [ + [ + 'App\JsonApi\V1\Posts\PostSchema', + JsonApiResource::class, + ], + [ + 'LaravelJsonApi\Core\Tests\Unit\Schema\StaticSchema\TestSchema', + TestResource::class, + ], + ]; + } + + /** + * @param string $schema + * @param string $expected + * @return void + * @dataProvider resourceClassProvider + */ + public function testResourceClass(string $schema, string $expected): void + { + $this->assertSame($expected, $this->conventions->getResourceClassFor($schema)); + } +} \ No newline at end of file diff --git a/tests/Unit/Schema/StaticSchema/ReflectionStaticSchemaTest.php b/tests/Unit/Schema/StaticSchema/ReflectionStaticSchemaTest.php new file mode 100644 index 0000000..39e2761 --- /dev/null +++ b/tests/Unit/Schema/StaticSchema/ReflectionStaticSchemaTest.php @@ -0,0 +1,115 @@ +conventions = $this->createMock(ServerConventions::class); + } + + /** + * @return void + */ + public function testDefaults(): void + { + $this->conventions + ->method('getTypeFor') + ->with(PostSchema::class) + ->willReturn('blog-posts'); + + $this->conventions + ->method('getUriTypeFor') + ->with('blog-posts') + ->willReturn('blog_posts'); + + $this->conventions + ->method('getResourceClassFor') + ->with(PostSchema::class) + ->willReturn('App\JsonApi\MyResource'); + + $schema = new ReflectionStaticSchema(PostSchema::class, $this->conventions); + + $this->assertSame(PostSchema::class, $schema->getSchemaClass()); + $this->assertSame('App\Models\Post', $schema->getModel()); + $this->assertSame('blog-posts', $schema->getType()); + $this->assertSame('blog_posts', $schema->getUriType()); + $this->assertSame('App\JsonApi\MyResource', $schema->getResourceClass()); + } + + /** + * @return void + */ + public function testCustomised(): void + { + $this->conventions + ->expects($this->never()) + ->method($this->anything()); + + $schema = new ReflectionStaticSchema(TagSchema::class, $this->conventions); + + $this->assertSame(TagSchema::class, $schema->getSchemaClass()); + $this->assertSame('App\Models\Tag', $schema->getModel()); + $this->assertSame('tags', $schema->getType()); + $this->assertSame('blog-tags', $schema->getUriType()); + $this->assertSame('App\JsonApi\Tags\TagResource', $schema->getResourceClass()); + } +} \ No newline at end of file diff --git a/tests/Unit/Schema/StaticSchema/StaticContainerTest.php b/tests/Unit/Schema/StaticSchema/StaticContainerTest.php new file mode 100644 index 0000000..35d8739 --- /dev/null +++ b/tests/Unit/Schema/StaticSchema/StaticContainerTest.php @@ -0,0 +1,161 @@ +createSchema('App\JsonApi\V1\Post\PostSchema', 'posts'); + $b = $this->createSchema('App\JsonApi\V1\Comments\CommentSchema', 'comments'); + $c = $this->createSchema('App\JsonApi\V1\Tags\TagSchema', 'tags'); + + $container = new StaticContainer([$a, $b, $c]); + + $this->assertSame([$a, $b, $c], iterator_to_array($container)); + $this->assertSame($a, $container->schemaFor('App\JsonApi\V1\Post\PostSchema')); + $this->assertSame($b, $container->schemaFor('App\JsonApi\V1\Comments\CommentSchema')); + $this->assertSame($c, $container->schemaFor('App\JsonApi\V1\Tags\TagSchema')); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Schema does not exist: App\JsonApi\V1\Foo\FooSchema'); + + $container->schemaFor('App\JsonApi\V1\Foo\FooSchema'); + } + + /** + * @return void + */ + public function testSchemaClassFor(): void + { + $container = new StaticContainer([ + $this->createSchema($a = 'App\JsonApi\V1\Post\PostSchema', 'posts'), + $this->createSchema($b = 'App\JsonApi\V1\Comments\CommentSchema', 'comments'), + $this->createSchema($c = 'App\JsonApi\V1\Tags\TagSchema', 'tags'), + ]); + + $this->assertSame($a, $container->schemaClassFor('posts')); + $this->assertSame($b, $container->schemaClassFor('comments')); + $this->assertSame($c, $container->schemaClassFor('tags')); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unrecognised resource type: blog-posts'); + + $container->schemaClassFor('blog-posts'); + } + + /** + * @return void + */ + public function testExists(): void + { + $a = $this->createSchema('App\JsonApi\V1\Post\PostSchema', 'posts'); + $b = $this->createSchema('App\JsonApi\V1\Comments\CommentSchema', 'comments'); + $c = $this->createSchema('App\JsonApi\V1\Tags\TagSchema', 'tags'); + + $container = new StaticContainer([$a, $b, $c]); + + foreach (['posts', 'comments', 'tags'] as $type) { + $this->assertTrue($container->exists($type)); + $this->assertTrue($container->exists(new ResourceType($type))); + } + + $this->assertFalse($container->exists('blog-posts')); + } + + /** + * @return void + */ + public function testModelClassFor(): void + { + $container = new StaticContainer([ + $a = $this->createSchema('App\JsonApi\V1\Post\PostSchema', 'posts'), + $b = $this->createSchema('App\JsonApi\V1\Comments\CommentSchema', 'comments'), + $c = $this->createSchema('App\JsonApi\V1\Tags\TagSchema', 'tags'), + ]); + + $a->method('getModel')->willReturn('App\Models\Post'); + $b->method('getModel')->willReturn('App\Models\Comments'); + $c->method('getModel')->willReturn('App\Models\Tags'); + + $this->assertSame('App\Models\Post', $container->modelClassFor('posts')); + $this->assertSame('App\Models\Comments', $container->modelClassFor('comments')); + $this->assertSame('App\Models\Tags', $container->modelClassFor('tags')); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unrecognised resource type: blog-posts'); + + $container->modelClassFor('blog-posts'); + } + + /** + * @return void + */ + public function testTypeForUri(): void + { + $a = $this->createSchema('App\JsonApi\V1\Post\PostSchema', 'posts'); + $b = $this->createSchema('App\JsonApi\V1\Comments\CommentSchema', 'comments'); + $c = $this->createSchema('App\JsonApi\V1\Tags\TagSchema', 'tags'); + + $a->expects($this->once())->method('getUriType')->willReturn('blog-posts'); + $b->expects($this->once())->method('getUriType')->willReturn('blog-comments'); + $c->expects($this->once())->method('getUriType')->willReturn('blog-tags'); + + $container = new StaticContainer([$a, $b, $c]); + + $this->assertObjectEquals(new ResourceType('comments'), $container->typeForUri('blog-comments')); + $this->assertObjectEquals(new ResourceType('tags'), $container->typeForUri('blog-tags')); + $this->assertObjectEquals(new ResourceType('posts'), $container->typeForUri('blog-posts')); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unrecognised URI type: foobar'); + + $container->typeForUri('foobar'); + } + + /** + * @return void + */ + public function testTypes(): void + { + $container = new StaticContainer([ + $this->createSchema('App\JsonApi\V1\Post\PostSchema', 'posts'), + $this->createSchema('App\JsonApi\V1\Comments\CommentSchema', 'comments'), + $this->createSchema('App\JsonApi\V1\Tags\TagSchema', 'tags'), + ]); + + $this->assertSame(['comments', 'posts', 'tags'], $container->types()); + } + + /** + * @param string $schemaClass + * @param string $type + * @return MockObject&StaticSchema + */ + private function createSchema(string $schemaClass, string $type): StaticSchema&MockObject + { + $mock = $this->createMock(StaticSchema::class); + $mock->method('getSchemaClass')->willReturn($schemaClass); + $mock->method('getType')->willReturn($type); + + return $mock; + } +} \ No newline at end of file diff --git a/tests/Unit/Schema/StaticSchema/ThreadCachedStaticSchemaTest.php b/tests/Unit/Schema/StaticSchema/ThreadCachedStaticSchemaTest.php new file mode 100644 index 0000000..d83e70f --- /dev/null +++ b/tests/Unit/Schema/StaticSchema/ThreadCachedStaticSchemaTest.php @@ -0,0 +1,98 @@ +schema = new ThreadCachedStaticSchema( + $this->base = $this->createMock(StaticSchema::class), + ); + } + + /** + * @return void + */ + public function testType(): void + { + $this->base + ->expects($this->once()) + ->method('getType') + ->willReturn('tags'); + + $this->assertSame('tags', $this->schema->getType()); + $this->assertSame('tags', $this->schema->getType()); + } + + /** + * @return void + */ + public function testUriType(): void + { + $this->base + ->expects($this->once()) + ->method('getUriType') + ->willReturn('blog-tags'); + + $this->assertSame('blog-tags', $this->schema->getUriType()); + $this->assertSame('blog-tags', $this->schema->getUriType()); + } + + /** + * @return void + */ + public function testModel(): void + { + $this->base + ->expects($this->once()) + ->method('getModel') + ->willReturn($model = 'App\Models\Post'); + + $this->assertSame($model, $this->schema->getModel()); + $this->assertSame($model, $this->schema->getModel()); + } + + /** + * @return void + */ + public function testResourceClass(): void + { + $this->base + ->expects($this->once()) + ->method('getResourceClass') + ->willReturn($class = 'App\JsonApi\V1\Tags\TagResource'); + + $this->assertSame($class, $this->schema->getResourceClass()); + $this->assertSame($class, $this->schema->getResourceClass()); + } +} \ No newline at end of file