This package complements github.com/overblog/dataloader-php (and github.com/overblog/dataloader-bundle) by making it straightforward to write data loaders - typically for use with a GraphQL api - that load entities via Doctrine ORM.
You can find an example GraphQL server implemented in example/
As explained here, in overblog/dataloader-php, the resolver function $batchLoadFn
is required to return elements in the same order as the $keys
provided. Any values not found in the source must be substituded for (with NULL
).
The example given in the documentation of dataloader-bundle is thus incorrect, because a SQL WHERE x.id IN (?, ?, ...)
does not make any guarantee that the input order will be respected. Moreover, if 1 or more id's are not available in the database, they obviously will not be returned.
This package takes care of both these issues for you.
Currently this package only supports a loader implementation for PostgreSQL via Doctrine ORM where it relies on the IDX
function. This function needs to be registered with Doctrine:
doctrine:
orm:
entity_managers:
default:
dql:
string_functions:
idx: Pander\DataLoaderSupport\Doctrine\IdxFunction
Then you can register the service as follows:
services:
Pander\DataLoaderSupport\LoaderInterface:
class: Pander\DataLoaderSupport\PostgreSqlLoader
bind:
$entityManager: '@Doctrine\ORM\EntityManagerInterface'
$promiseAdapter: '@overblog_graphql.promise_adapter'
$hydrationMode: "array"
A typical example would have a data loader that loads User
entities from the database.
The configuration with dataloader-bundle would look like:
overblog_dataloader:
loaders:
users:
batch_load_fn: '@App\Loader\UserLoader:load'
with a schema like:
query {
users {
id
username
}
}
and the implementation of the UserLoader
would look like:
<?php
use App\Entity\User;
use Pander\DataLoaderSupport\LoaderInterface;
class UserLoader {
public function __construct(LoaderInterface $loader) {
// ..
}
public function load(array $userIds): Promise {
return $this->loader->load(User::class, $userIds);
}
}
where the LoaderInterface
implementation takes care of loading all $userIds
in the correct order and making sure that any missing values are replaced with NULL
s.
In addition to loading entities directly by entity class and ids (using the load
method), this package supports 2 additional methods of loading entities.
If your GraphQL schema contains parent-child relations, you can deduplicate & batch these relations using a data loader. Imagine the following schema:
query {
adult {
id
name
children {
id
name
}
}
}
backed by the following Doctrine entity:
<?php
use Doctrine\Common\Collections;
use Doctrine\ORM\Mapping as ORM;
class Adult {
#[ORM\Id()]
#[ORM\Column()]
public ?int $id = null;
#[ORM\Column()]
public string $name;
#[ORM\OneToMany(targetEntity: Child::class)]
public Collection $children;
}
class Child {
#[ORM\Id()]
#[ORM\Column()]
public ?int $id = null;
#[ORM\Column()]
public string $name;
#[ORM\ManyToOne()]
public ?Adult $adult = null;
}
which can easily be loaded with the following loader:
<?php
use App\Entity\Child;
use Pander\DataLoaderSupport\LoaderInterface;
class AdultChildrenLoader {
public function __construct(LoaderInterface $loader) {
// ..
}
public function load(array $adultIds): Promise {
return $this->loader->loadByParent(Child::class, 'adult', $adultIds);
}
}
It could also be that the Doctrine relation is defined via a join table. Consider the previous entities but now with a join table from Adult to Child:
<?php
use Doctrine\Common\Collections;
use Doctrine\ORM\Mapping as ORM;
class Adult {
#[ORM\Id()]
#[ORM\Column()]
public ?int $id = null;
#[ORM\Column()]
public string $name;
#[ORM\ManyToMany(targetEntity: Child::class)]
#[ORM\JoinTable()]
public Collection $children;
}
class Child {
#[ORM\Id()]
#[ORM\Column()]
public ?int $id = null;
#[ORM\Column()]
public string $name;
}
the loader will look like:
<?php
use App\Entity\Adult;
use Pander\DataLoaderSupport\LoaderInterface;
class AdultChildrenLoader {
public function __construct(LoaderInterface $loader) {
// ..
}
public function load(array $adultIds): Promise {
return $this->loader->loadByJoinTable(Adult::class, 'children', $adultIds);
}
}