Skip to content

Commit

Permalink
Fix type registry examples (webonyx#1485)
Browse files Browse the repository at this point in the history
  • Loading branch information
spawnia authored Nov 30, 2023
1 parent 687f3f2 commit bc19289
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 103 deletions.
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

0 comments on commit bc19289

Please sign in to comment.