Skip to content

Adding special type for Paginator and FixedLengthPaginator classes #13

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

Merged
merged 1 commit into from
Oct 2, 2019
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/composer.lock
/vendor
/build
/build
/.phpunit.result.cache
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

<php>
<env name="APP_KEY" value="9E6B382F19C53C5327840752500B0260"/>
<env name="APP_DEBUG" value="true"/>
</php>

<filter>
Expand Down
43 changes: 43 additions & 0 deletions src/Mappers/PaginatorMissingParameterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Laravel\Mappers;

use Exception;
use GraphQL\Error\ClientAware;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeTrait;

class PaginatorMissingParameterException extends Exception implements ClientAware, CannotMapTypeExceptionInterface
{
use CannotMapTypeTrait;

public static function missingLimit(): self
{
return new self('In the items field of a paginator, you cannot add a "offset" without also adding a "limit"');
}

public static function noSubType(): self
{
return new self('Result sets implementing Laravel Paginator need to have a subtype. Please define it using @return annotation. For instance: "@return User[]"');
}

/**
* Returns true when exception message is safe to be displayed to a client.
*/
public function isClientSafe(): bool
{
return true;
}

/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*/
public function getCategory(): string
{
return 'pagination';
}
}
318 changes: 318 additions & 0 deletions src/Mappers/PaginatorTypeMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Laravel\Mappers;

use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\NullableType;
use GraphQL\Type\Definition\OutputType;
use GraphQL\Type\Definition\Type;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\Pagination\Paginator;
use Porpaginas\Result;
use RuntimeException;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
use TheCodingMachine\GraphQLite\Mappers\PorpaginasMissingParameterException;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
use TheCodingMachine\GraphQLite\Types\MutableInterface;
use TheCodingMachine\GraphQLite\Types\MutableInterfaceType;
use TheCodingMachine\GraphQLite\Types\MutableObjectType;
use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface;
use function get_class;
use function is_a;
use function strpos;
use function substr;

class PaginatorTypeMapper implements TypeMapperInterface
{
/** @var array<string, MutableInterface&(MutableObjectType|MutableInterfaceType)> */
private $cache = [];
/** @var RecursiveTypeMapperInterface */
private $recursiveTypeMapper;

public function __construct(RecursiveTypeMapperInterface $recursiveTypeMapper)
{
$this->recursiveTypeMapper = $recursiveTypeMapper;
}

/**
* Returns true if this type mapper can map the $className FQCN to a GraphQL type.
*
* @param string $className The exact class name to look for (this function does not look into parent classes).
*/
public function canMapClassToType(string $className): bool
{
return is_a($className, Paginator::class, true);
}

/**
* Maps a PHP fully qualified class name to a GraphQL type.
*
* @param string $className The exact class name to look for (this function does not look into parent classes).
* @param (OutputType&Type)|null $subType An optional sub-type if the main class is an iterator that needs to be typed.
*
* @return MutableObjectType|MutableInterfaceType
*
* @throws CannotMapTypeExceptionInterface
*/
public function mapClassToType(string $className, ?OutputType $subType): MutableInterface
{
if (! $this->canMapClassToType($className)) {
throw CannotMapTypeException::createForType($className);
}
if ($subType === null) {
throw PaginatorMissingParameterException::noSubType();
}

return $this->getObjectType(is_a($className, LengthAwarePaginator::class, true), $subType);
}

/**
* @param OutputType&Type $subType
*
* @return MutableObjectType|MutableInterfaceType
*/
private function getObjectType(bool $countable, OutputType $subType): MutableInterface
{
if (! isset($subType->name)) {
throw new RuntimeException('Cannot get name property from sub type ' . get_class($subType));
}

$name = $subType->name;

$typeName = 'PaginatorResult_' . $name;

if ($subType instanceof NullableType) {
$subType = Type::nonNull($subType);
}

if (! isset($this->cache[$typeName])) {
$this->cache[$typeName] = new MutableObjectType([
'name' => $typeName,
'fields' => static function () use ($subType, $countable) {
$fields = [
'items' => [
'type' => Type::nonNull(Type::listOf($subType)),
'resolve' => static function (Paginator $root) {
return $root->items();
},
],
'firstItem' => [
'type' => Type::int(),
'description' => 'Get the "index" of the first item being paginated.',
'resolve' => static function (Paginator $root): int {
return $root->firstItem();
},
],
'lastItem' => [
'type' => Type::int(),
'description' => 'Get the "index" of the last item being paginated.',
'resolve' => static function (Paginator $root): int {
return $root->lastItem();
},
],
'hasMorePages' => [
'type' => Type::boolean(),
'description' => 'Determine if there are more items in the data source.',
'resolve' => static function (Paginator $root): bool {
return $root->hasMorePages();
},
],
'perPage' => [
'type' => Type::int(),
'description' => 'Get the number of items shown per page.',
'resolve' => static function (Paginator $root): int {
return $root->perPage();
},
],
'hasPages' => [
'type' => Type::boolean(),
'description' => 'Determine if there are enough items to split into multiple pages.',
'resolve' => static function (Paginator $root): bool {
return $root->hasPages();
},
],
'currentPage' => [
'type' => Type::int(),
'description' => 'Determine the current page being paginated.',
'resolve' => static function (Paginator $root): int {
return $root->currentPage();
},
],
'isEmpty' => [
'type' => Type::boolean(),
'description' => 'Determine if the list of items is empty or not.',
'resolve' => static function (Paginator $root): bool {
return $root->isEmpty();
},
],
'isNotEmpty' => [
'type' => Type::boolean(),
'description' => 'Determine if the list of items is not empty.',
'resolve' => static function (Paginator $root): bool {
return $root->isNotEmpty();
},
],
];

if ($countable) {
$fields['totalCount'] = [
'type' => Type::int(),
'description' => 'The total count of items.',
'resolve' => static function (LengthAwarePaginator $root): int {
return $root->total();
}];
$fields['lastPage'] = [
'type' => Type::int(),
'description' => 'Get the page number of the last available page.',
'resolve' => static function (LengthAwarePaginator $root): int {
return $root->lastPage();
}];
}

return $fields;
},
]);
}

return $this->cache[$typeName];
}

/**
* Returns true if this type mapper can map the $typeName GraphQL name to a GraphQL type.
*
* @param string $typeName The name of the GraphQL type
*/
public function canMapNameToType(string $typeName): bool
{
return strpos($typeName, 'PaginatorResult_') === 0 || strpos($typeName, 'LengthAwarePaginatorResult_') === 0;
}

/**
* Returns a GraphQL type by name (can be either an input or output type)
*
* @param string $typeName The name of the GraphQL type
*
* @return Type&((ResolvableMutableInputInterface&InputObjectType)|MutableObjectType|MutableInterfaceType)
*
* @throws CannotMapTypeExceptionInterface
*/
public function mapNameToType(string $typeName): Type
{
if (strpos($typeName, 'LengthAwarePaginatorResult_') === 0) {
$subTypeName = substr($typeName, 27);
$lengthAware = true;
} elseif (strpos($typeName, 'PaginatorResult_') === 0) {
$subTypeName = substr($typeName, 16);
$lengthAware = false;
} else {
throw CannotMapTypeException::createForName($typeName);
}

$subType = $this->recursiveTypeMapper->mapNameToType($subTypeName);

if (! $subType instanceof OutputType) {
throw CannotMapTypeException::mustBeOutputType($subTypeName);
}

return $this->getObjectType($lengthAware, $subType);
}

/**
* Returns the list of classes that have matching input GraphQL types.
*
* @return string[]
*/
public function getSupportedClasses(): array
{
// We cannot get the list of all possible porpaginas results but this is not an issue.
// getSupportedClasses is only useful to get classes that can be hidden behind interfaces
// and Porpaginas results are not part of those.
return [];
}

/**
* Returns true if this type mapper can map the $className FQCN to a GraphQL input type.
*/
public function canMapClassToInputType(string $className): bool
{
return false;
}

/**
* Maps a PHP fully qualified class name to a GraphQL input type.
*
* @return ResolvableMutableInputInterface&InputObjectType
*/
public function mapClassToInputType(string $className): ResolvableMutableInputInterface
{
throw CannotMapTypeException::createForInputType($className);
}

/**
* Returns true if this type mapper can extend an existing type for the $className FQCN
*
* @param MutableInterface&(MutableObjectType|MutableInterfaceType) $type
*/
public function canExtendTypeForClass(string $className, MutableInterface $type): bool
{
return false;
}

/**
* Extends the existing GraphQL type that is mapped to $className.
*
* @param MutableInterface&(MutableObjectType|MutableInterfaceType) $type
*
* @throws CannotMapTypeExceptionInterface
*/
public function extendTypeForClass(string $className, MutableInterface $type): void
{
throw CannotMapTypeException::createForExtendType($className, $type);
}

/**
* Returns true if this type mapper can extend an existing type for the $typeName GraphQL type
*
* @param MutableInterface&(MutableObjectType|MutableInterfaceType) $type
*/
public function canExtendTypeForName(string $typeName, MutableInterface $type): bool
{
return false;
}

/**
* Extends the existing GraphQL type that is mapped to the $typeName GraphQL type.
*
* @param MutableInterface&(MutableObjectType|MutableInterfaceType) $type
*
* @throws CannotMapTypeExceptionInterface
*/
public function extendTypeForName(string $typeName, MutableInterface $type): void
{
throw CannotMapTypeException::createForExtendName($typeName, $type);
}

/**
* Returns true if this type mapper can decorate an existing input type for the $typeName GraphQL input type
*/
public function canDecorateInputTypeForName(string $typeName, ResolvableMutableInputInterface $type): bool
{
return false;
}

/**
* Decorates the existing GraphQL input type that is mapped to the $typeName GraphQL input type.
*
* @param ResolvableMutableInputInterface&InputObjectType $type
*
* @throws CannotMapTypeExceptionInterface
*/
public function decorateInputTypeForName(string $typeName, ResolvableMutableInputInterface $type): void
{
throw CannotMapTypeException::createForDecorateName($typeName, $type);
}
}
18 changes: 18 additions & 0 deletions src/Mappers/PaginatorTypeMapperFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php


namespace TheCodingMachine\GraphQLite\Laravel\Mappers;


use TheCodingMachine\GraphQLite\FactoryContext;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperFactoryInterface;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;

class PaginatorTypeMapperFactory implements TypeMapperFactoryInterface
{

public function create(FactoryContext $context): TypeMapperInterface
{
return new PaginatorTypeMapper($context->getRecursiveTypeMapper());
}
}
Loading