Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix type registry examples #1485

Merged
merged 5 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 42 additions & 19 deletions docs/schema-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,43 +157,66 @@ final class AuthorType extends ObjectType

// Types.php
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\NamedType;

final class Types
{
/** @var array<string, Type&NamedType> */
private static array $types = [];

/** @return \Closure(): Type&NamedType */
public static function get(string $classname): \Closure
/** @return Type&NamedType */
public static function load(string $typeName): Type
{
return static fn () => self::byClassName($classname);
}
if (isset(self::$types[$typeName])) {
return self::$types[$typeName];
}

public static function byTypeName(string $typeName): Type&NamedType
{
return match ($typeName) {
'Boolean' => self::boolean(),
'Float' => self::float(),
'ID' => self::id(),
'Int' => self::int(),
default => self::$types[$typeName] ?? throw new \Exception("Unknown GraphQL type: {$typeName}."),
// For every type, this class must define a method with the same name
// but the first letter is in lower case.
$methodName = match ($typeName) {
'ID' => 'id',
default => lcfirst($typeName),
};
if (! method_exists(self::class, $methodName)) {
throw new \Exception("Unknown GraphQL type: {$typeName}.");
}

$type = self::{$methodName}(); // @phpstan-ignore-line variable static method call
if (is_callable($type)) {
$type = $type();
}

return self::$types[$typeName] = $type;
}

private static function byClassName(string $classname): Type
/** @return Type&NamedType */
private static function byClassName(string $className): Type
{
$parts = \explode('\\', $classname);
$typeName = \preg_replace('~Type$~', '', $parts[\count($parts) - 1]);
$classNameParts = explode('\\', $className);
$baseClassName = end($classNameParts);
// All type classes must use the suffix Type.
// This prevents name collisions between types and PHP keywords.
$typeName = preg_replace('~Type$~', '', $baseClassName);

// Type loading is very similar to PHP class loading, but keep in mind
// that the **typeLoader** must always return the same instance of a type.
// We can enforce that in our type registry by caching known types.
return self::$types[$typeName] ??= new $classname;
return self::$types[$typeName] ??= new $className;
}

public static function author(): callable { return self::get(AuthorType::class); }
public static function story(): callable { return self::get(StoryType::class); }
/** @return \Closure(): (Type&NamedType) */
private static function lazyByClassName(string $className): \Closure
{
return static fn () => self::byClassName($className);
}

public static function boolean(): ScalarType { return Type::boolean(); }
public static function float(): ScalarType { return Type::float(); }
public static function id(): ScalarType { return Type::id(); }
public static function int(): ScalarType { return Type::int(); }
public static function string(): ScalarType { return Type::string(); }
public static function author(): callable { return self::lazyByClassName(AuthorType::class); }
public static function story(): callable { return self::lazyByClassName(StoryType::class); }
...
}

Expand All @@ -214,7 +237,7 @@ $schema = new Schema([
],
],
]),
'typeLoader' => Types::byTypeName(...),
'typeLoader' => Types::load(...),
]);
```

Expand Down
173 changes: 90 additions & 83 deletions examples/01-blog/Blog/Types.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,143 +29,150 @@ final class Types
/** @var array<string, Type&NamedType> */
private static array $types = [];

public static function user(): callable
/**
* @throws \Exception
*
* @return Type&NamedType
*/
public static function load(string $typeName): Type
{
return self::get(UserType::class);
if (isset(self::$types[$typeName])) {
return self::$types[$typeName];
}

// For every type, this class must define a method with the same name
// but the first letter is in lower case.
switch ($typeName) {
case 'ID':
$methodName = 'id';
break;
default:
$methodName = \lcfirst($typeName);
}
if (! method_exists(self::class, $methodName)) {
throw new \Exception("Unknown GraphQL type: {$typeName}.");
}

$type = self::{$methodName}(); // @phpstan-ignore-line variable static method call
if (is_callable($type)) {
$type = $type();
}

return self::$types[$typeName] = $type;
}

public static function story(): callable
/**
* @param class-string<Type&NamedType> $className
*
* @return Type&NamedType
*/
private static function byClassName(string $className): Type
{
return self::get(StoryType::class);
$classNameParts = \explode('\\', $className);
$baseClassName = end($classNameParts);
// All type classes must use the suffix Type.
// This prevents name collisions between types and PHP keywords.
$typeName = \preg_replace('~Type$~', '', $baseClassName);
assert(is_string($typeName), 'regex is statically known to be correct');

// Type loading is very similar to PHP class loading, but keep in mind
// that the **typeLoader** must always return the same instance of a type.
// We can enforce that in our type registry by caching known types.
return self::$types[$typeName] ??= new $className();
}

public static function comment(): callable
/**
* @param class-string<Type&NamedType> $classname
*
* @return \Closure(): (Type&NamedType)
*/
private static function lazyByClassName(string $classname): \Closure
{
return self::get(CommentType::class);
return static fn () => self::byClassName($classname);
}

public static function image(): callable
/** @throws InvariantViolation */
public static function boolean(): ScalarType
{
return self::get(ImageType::class);
return Type::boolean();
}

public static function node(): callable
/** @throws InvariantViolation */
public static function float(): ScalarType
{
return self::get(NodeType::class);
return Type::float();
}

public static function mention(): callable
/** @throws InvariantViolation */
public static function id(): ScalarType
{
return self::get(SearchResultType::class);
return Type::id();
}

public static function imageSize(): callable
/** @throws InvariantViolation */
public static function int(): ScalarType
{
return self::get(ImageSizeType::class);
return Type::int();
}

public static function contentFormat(): callable
/** @throws InvariantViolation */
public static function string(): ScalarType
{
return self::get(ContentFormatType::class);
return Type::string();
}

public static function storyAffordances(): callable
public static function user(): callable
{
return self::get(StoryAffordancesType::class);
return self::lazyByClassName(UserType::class);
}

public static function email(): callable
public static function story(): callable
{
return self::get(EmailType::class);
return self::lazyByClassName(StoryType::class);
}

public static function url(): callable
public static function comment(): callable
{
return self::get(UrlType::class);
return self::lazyByClassName(CommentType::class);
}

/**
* @param class-string<Type&NamedType> $classname
*
* @return \Closure(): Type
*/
private static function get(string $classname): \Closure
public static function image(): callable
{
return static fn () => self::byClassName($classname);
return self::lazyByClassName(ImageType::class);
}

/** @param class-string<Type&NamedType> $classname */
private static function byClassName(string $classname): Type
public static function node(): callable
{
$parts = \explode('\\', $classname);

$withoutTypePrefix = \preg_replace('~Type$~', '', $parts[\count($parts) - 1]);
assert(is_string($withoutTypePrefix), 'regex is statically known to be correct');

$cacheName = \strtolower($withoutTypePrefix);

if (! isset(self::$types[$cacheName])) {
return self::$types[$cacheName] = new $classname();
}

return self::$types[$cacheName];
return self::lazyByClassName(NodeType::class);
}

/**
* @throws \Exception
*
* @return Type&NamedType
*/
public static function byTypeName(string $shortName): Type
public static function mention(): callable
{
$cacheName = \strtolower($shortName);

if (isset(self::$types[$cacheName])) {
return self::$types[$cacheName];
}

$method = \lcfirst($shortName);
switch ($method) {
case 'boolean':
return self::boolean();
case 'float':
return self::float();
case 'id':
return self::id();
case 'int':
return self::int();
}

throw new \Exception("Unknown graphql type: {$shortName}");
return self::lazyByClassName(SearchResultType::class);
}

/** @throws InvariantViolation */
public static function boolean(): ScalarType
public static function imageSize(): callable
{
return Type::boolean();
return self::lazyByClassName(ImageSizeType::class);
}

/** @throws InvariantViolation */
public static function float(): ScalarType
public static function contentFormat(): callable
{
return Type::float();
return self::lazyByClassName(ContentFormatType::class);
}

/** @throws InvariantViolation */
public static function id(): ScalarType
public static function storyAffordances(): callable
{
return Type::id();
return self::lazyByClassName(StoryAffordancesType::class);
}

/** @throws InvariantViolation */
public static function int(): ScalarType
public static function email(): callable
{
return Type::int();
return self::lazyByClassName(EmailType::class);
}

/** @throws InvariantViolation */
public static function string(): ScalarType
public static function url(): callable
{
return Type::string();
return self::lazyByClassName(UrlType::class);
}
}
2 changes: 1 addition & 1 deletion examples/01-blog/graphql.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
$schema = new Schema(
(new SchemaConfig())
->setQuery(new QueryType())
->setTypeLoader([Types::class, 'byTypename'])
->setTypeLoader([Types::class, 'load'])
);

// Prepare context that will be available in all field resolvers (as 3rd argument):
Expand Down
Loading