Skip to content
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
12 changes: 11 additions & 1 deletion src/Dialect/DataApiBuilderDialect.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final class DataApiBuilderDialect implements GraphqlQueryDialect
{
public function extractCollection(array $data): array
{
/** @var array<string|int, mixed> $items */
/** @var array<string|int, mixed>[] $items */
$items = $data['items'] ?? [];

return $items;
Expand All @@ -32,6 +32,16 @@ public function applyQueryOptions(array $arguments, QueryOptions $options): arra

$arguments['orderBy'] = $options->orderBy;

if ($options->paginate) {
if ($options->limit) {
$arguments['first'] = $options->limit;
}

if ($options->cursor) {
$arguments['after'] = $options->cursor;
}
}

return $arguments;
}

Expand Down
4 changes: 2 additions & 2 deletions src/Dialect/GraphqlQueryDialect.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
interface GraphqlQueryDialect
{
/**
* @param array<string, mixed> $data
* @param array<string|int, mixed>[] $data
*
* @return array<string|int, mixed>
* @return array<string|int, mixed>[]
*/
public function extractCollection(array $data): array;

Expand Down
8 changes: 5 additions & 3 deletions src/GraphqlManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use GraphqlOrm\Metadata\GraphqlEntityMetadataFactory;
use GraphqlOrm\Query\Ast\QueryNode;
use GraphqlOrm\Query\GraphqlQueryCompiler;
use GraphqlOrm\Query\Pagination\PaginatedResult;
use GraphqlOrm\Query\QueryOptions;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

/**
Expand All @@ -39,14 +41,14 @@ public function __construct(
/**
* @param array<string, mixed> $variables
*
* @return T[]
* @return T[]|PaginatedResult<T>
*/
public function execute(QueryNode|string $graphql, callable $hydration, array $variables = [], ): array
public function execute(QueryNode|string $graphql, callable $hydration, QueryOptions $options = new QueryOptions(), array $variables = []): array|PaginatedResult
{
$context = new GraphqlExecutionContext();

if ($graphql instanceof QueryNode) {
$compiled = $this->getQueryCompiler()->compile($graphql);
$compiled = $this->getQueryCompiler()->compile($graphql, $options);
/** @var array<string|int, mixed> $ast */
$ast = json_decode(json_encode($graphql, JSON_THROW_ON_ERROR), true);
$context->trace->ast = $ast;
Expand Down
2 changes: 1 addition & 1 deletion src/Hydrator/EntityHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function __construct(
}

/**
* @param array<string, mixed> $data
* @param array<string|int, mixed> $data
*/
public function hydrate(
GraphqlEntityMetadata $metadata,
Expand Down
74 changes: 50 additions & 24 deletions src/Query/GraphqlQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use GraphqlOrm\Exception\InvalidGraphqlResponseException;
use GraphqlOrm\GraphqlManager;
use GraphqlOrm\Query\Ast\QueryNode;
use GraphqlOrm\Query\Pagination\PaginatedResult;

/**
* @template T of object
Expand All @@ -21,36 +22,38 @@ public function __construct(
private QueryNode|string $graphql,
private string $entityClass,
private GraphqlManager $manager,
private QueryOptions $options,
) {
}

public function getGraphQL(): string
{
if ($this->graphql instanceof QueryNode) {
return $this->manager->getQueryCompiler()->compile($this->graphql);
return $this->manager->getQueryCompiler()->compile($this->graphql, $this->options);
}

return $this->graphql;
}

/**
* @return T[]
* @return T[]|PaginatedResult<T>
*/
public function getResult(): array
public function getResult(): array|PaginatedResult
{
$metadata = $this
->manager
$metadata = $this->manager
->metadataFactory
->getMetadata($this->entityClass);

return $this
->manager
->execute($this->graphql, hydration: function ($result, $context) use ($metadata) {
return $this->manager->execute(
$this->graphql,
function ($result, $context) use ($metadata) {
$dialect = $this->manager->getDialect();
$data = $result['data'] ?? [];

if (!\array_key_exists($metadata->name, $data)) {
return [];
return $this->options->paginate
? new PaginatedResult([], false, false, null, [], fn () => null)
: [];
}

$root = $data[$metadata->name];
Expand All @@ -64,37 +67,60 @@ public function getResult(): array
}

$collection = $dialect->extractCollection($root);
$rows = !empty($collection) ? $collection : null;

if ($rows === null) {
return [];
}

if (!\is_array($rows)) {
throw InvalidGraphqlResponseException::expectedArray($rows);
if (array_is_list($collection)) {
$rows = $collection;
} else {
$rows = [$collection];
}

if (array_is_list($rows) === false) {
$rows = [$rows];
}

return array_map(fn ($row) => $this->manager
$items = array_map(
fn ($row) => $this->manager
->hydrator
->hydrate(
$metadata,
$row,
$context
),
->hydrate($metadata, $row, $context),
$rows
);
});

if (!$this->options->paginate) {
return $items;
}

$fetchPage = function (?string $cursor, array $newStack) {
$qb = clone $this;
$qb->options->cursor = $cursor;
$qb->options->cursorStack = $newStack;

return $qb->getResult();
};

return new PaginatedResult(
$items,
$root['hasNextPage'] ?? false,
\count($this->options->cursorStack) > 0,
$root['endCursor'] ?? null,
$this->options->cursorStack,
$fetchPage
);
},
$this->options
);
}

/**
* @return T|null
*/
public function getOneOrNullResult(): ?object
{
return $this->getResult()[0] ?? null;
$result = $this->getResult();

if ($result instanceof PaginatedResult) {
return $result->items[0] ?? null;
}

return $result[0] ?? null;
}
}
21 changes: 19 additions & 2 deletions src/Query/GraphqlQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ public function orderBy(string $orderBy, Direction $direction): self
return $this;
}

/**
* @return GraphqlQueryBuilder<T>
*/
public function paginate(?string $after = null): self
{
$this->options->paginate = true;

if ($after !== null) {
$this->options->cursor = $after;
$this->options->cursorStack[] = $after;
}

return $this;
}

public function expr(): ExpressionBuilder
{
return new ExpressionBuilder();
Expand All @@ -109,7 +124,8 @@ public function getQuery(): GraphqlQuery
return new GraphqlQuery(
$this->graphql,
$this->entityClass,
$this->manager
$this->manager,
$this->options
);
}

Expand Down Expand Up @@ -137,7 +153,8 @@ public function getQuery(): GraphqlQuery
return new GraphqlQuery(
$ast,
$this->entityClass,
$this->manager
$this->manager,
$this->options
);
}
}
4 changes: 2 additions & 2 deletions src/Query/GraphqlQueryCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public function __construct(
) {
}

public function compile(QueryNode $node): string
public function compile(QueryNode $node, QueryOptions $options): string
{
return $this->walker->walk($node);
return $this->walker->walk($node, $options);
}
}
58 changes: 58 additions & 0 deletions src/Query/Pagination/PaginatedResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace GraphqlOrm\Query\Pagination;

/**
* @template T of object
*/
final readonly class PaginatedResult
{
/**
* @param T[] $items
* @param string[] $cursorStack
*/
public function __construct(
public array $items,
public bool $hasNextPage,
public bool $hasPreviousPage,
public ?string $endCursor,
private array $cursorStack,
private \Closure $fetchPage,
) {
}

/**
* @return self<T>|null
*/
public function next(): ?self
{
if (!$this->hasNextPage) {
return null;
}

$newStack = [
...$this->cursorStack,
$this->endCursor,
];

return ($this->fetchPage)($this->endCursor, $newStack);
}

/**
* @return self<T>|null
*/
public function previous(): ?self
{
if (!$this->hasPreviousPage) {
return null;
}

$newStack = $this->cursorStack;
array_pop($newStack);
$previousCursor = empty($newStack) ? null : end($newStack);

return ($this->fetchPage)($previousCursor, $newStack);
}
}
7 changes: 7 additions & 0 deletions src/Query/QueryOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ final class QueryOptions

/** @var array<string, Direction>|null */
public ?array $orderBy = null;

public bool $paginate = false;

public ?string $cursor = null;

/** @var string[] */
public array $cursorStack = [];
}
36 changes: 34 additions & 2 deletions src/Query/Walker/DABGraphqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
use GraphqlOrm\Query\Ast\QueryNode;
use GraphqlOrm\Query\Direction;
use GraphqlOrm\Query\Printer\GraphqlPrinter;
use GraphqlOrm\Query\QueryOptions;

final class DABGraphqlWalker extends AbstractGraphqlWalker
{
public function walk(QueryNode $query): string
private QueryOptions $options;

public function walk(QueryNode $query, QueryOptions $options): string
{
$this->options = $options;

$this->printer = new GraphqlPrinter();

$this->printer->line($query->operation . ' {');
Expand All @@ -33,7 +38,7 @@ public function walk(QueryNode $query): string

private function walkRootField(FieldNode $field): void
{
$args = $this->formatArguments($field->arguments);
$args = $this->formatArguments($this->applyPaginationArguments($field->arguments));

$this->printer->line($field->name . $args . ' {');

Expand All @@ -51,11 +56,38 @@ private function walkRootField(FieldNode $field): void

$this->printer->line('}');

if ($this->options->paginate) {
$this->printer->line('hasNextPage');
$this->printer->line('endCursor');
}

$this->printer->outdent();

$this->printer->line('}');
}

/**
* @param array<string, mixed> $arguments
*
* @return array<string, mixed>
*/
private function applyPaginationArguments(array $arguments): array
{
if (!$this->options->paginate) {
return $arguments;
}

if ($this->options->limit !== null) {
$arguments['first'] = $this->options->limit;
}

if ($this->options->cursor !== null) {
$arguments['after'] = $this->options->cursor;
}

return $arguments;
}

protected function formatValue(mixed $value): string
{
if (\is_string($value)) {
Expand Down
Loading