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
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:

jobs:
tests:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
Expand All @@ -21,7 +21,6 @@ jobs:
- '8.2'
- '8.3'
symfony-version:
- '5.4.*'
- '6.3.*'
- '7.0.*'
- '7.1.*'
Expand Down Expand Up @@ -69,7 +68,7 @@ jobs:
run: composer test

coding-standard:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
name: Coding Standard
steps:
- name: "Checkout"
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ Here the list of existing promise adapters:
* **[GuzzleHttp/Promises](https://github.com/guzzle/promises)**: overblog_dataloader.guzzle_http_promise_adapter
* **[Webonyx/GraphQL-PHP](https://github.com/webonyx/graphql-php) Sync Promise**: overblog_dataloader.webonyx_graphql_sync_promise_adapter

## Configuration using attributes

This bundle supports autoconfiguration via attributes. Add `Overblog\DataLoaderBundle\Attribute\AsDataLoader` on the service you want to expose as data loader:

````php
<?php

#[Overblog\DataLoaderBundle\Attribute\AsDataLoader(name: "users", alias: "users_dataloader")]
class UserLoader {
public function __invoke(array $ids): array
{
return ["John", "Steve", "Nash"];
}
}

?>

````

## Combine with GraphQLBundle

This bundle can be use with [GraphQLBundle](https://github.com/overblog/GraphQLBundle).
Expand Down
24 changes: 24 additions & 0 deletions src/Attribute/AsDataLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/*
* This file is part of the OverblogDataLoaderBundle package.
*
* (c) Overblog <http://github.com/overblog/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Overblog\DataLoaderBundle\Attribute;

#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
class AsDataLoader
{
public function __construct(
public readonly string $name,
public readonly ?string $method = null,
public readonly ?string $alias = null,
public readonly ?array $options = [],
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

/*
* This file is part of the OverblogDataLoaderBundle package.
*
* (c) Overblog <http://github.com/overblog/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Overblog\DataLoaderBundle\DependencyInjection\CompilerPass;

use Overblog\DataLoader\DataLoader;
use Overblog\DataLoader\Option;
use Overblog\DataLoaderBundle\DependencyInjection\Support;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class RegisterDataLoadersFromTagsPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
foreach ($container->findTaggedServiceIds('overblog.dataloader') as $serviceId => $tags) {
foreach ($tags as $attributes) {
$batchLoadFn = isset($attributes['method'])
? [new Reference($serviceId), $attributes['method']]
: Support::buildCallableFromScalar($attributes['batch_load_fn']);

$this->registerDataLoader($container, $attributes, $batchLoadFn);
}
}
}

private function registerDataLoader(ContainerBuilder $container, array $loaderConfig, mixed $batchLoadFn): void
{
$dataLoaderServiceID = Support::generateDataLoaderServiceIDFromName($loaderConfig['name'], $container);
$optionServiceID = Support::generateDataLoaderOptionServiceIDFromName($loaderConfig['name'], $container);

$container->register($optionServiceID, Option::class)
->setPublic(false)
->setArguments([Support::buildOptionsParams($loaderConfig['options'])]);

$definition = $container->register($dataLoaderServiceID, DataLoader::class)
->setPublic(true)
->addTag('kernel.reset', ['method' => 'clearAll'])
->setArguments([
$batchLoadFn,
new Reference($loaderConfig['promise_adapter']),
new Reference($optionServiceID),
]);

if (isset($loaderConfig['factory'])) {
$definition->setFactory(Support::buildCallableFromScalar($loaderConfig['factory']));
}

if (isset($loaderConfig['alias'])) {
$container
->setAlias($loaderConfig['alias'], $dataLoaderServiceID)
->setPublic(true);
}
}
}
101 changes: 23 additions & 78 deletions src/DependencyInjection/OverblogDataLoaderExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@

namespace Overblog\DataLoaderBundle\DependencyInjection;

use Overblog\DataLoader\DataLoader;
use Overblog\DataLoaderBundle\Attribute\AsDataLoader;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;

final class OverblogDataLoaderExtension extends Extension
{
Expand All @@ -27,89 +29,32 @@ public function load(array $configs, ContainerBuilder $container): void
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);

foreach ($config['loaders'] as $name => $loaderConfig) {
$loaderConfig = array_replace($config['defaults'], $loaderConfig);
$dataLoaderServiceID = $this->generateDataLoaderServiceIDFromName($name, $container);
$OptionServiceID = $this->generateDataLoaderOptionServiceIDFromName($name, $container);
$batchLoadFn = $this->buildCallableFromScalar($loaderConfig['batch_load_fn']);

$container->register($OptionServiceID, 'Overblog\\DataLoader\\Option')
->setPublic(false)
->setArguments([$this->buildOptionsParams($loaderConfig['options'])]);

$definition = $container->register($dataLoaderServiceID, 'Overblog\\DataLoader\\DataLoader')
->setPublic(true)
->addTag('kernel.reset', ['method' => 'clearAll'])
->setArguments([
$batchLoadFn,
new Reference($loaderConfig['promise_adapter']),
new Reference($OptionServiceID),
])
;

if (isset($loaderConfig['factory'])) {
$definition->setFactory($this->buildCallableFromScalar($loaderConfig['factory']));
$container->registerAttributeForAutoconfiguration(AsDataLoader::class, function (ChildDefinition $definition, AsDataLoader $attribute, \ReflectionClass|\ReflectionMethod $reflector) use ($config) {
if ($reflector instanceof \ReflectionMethod && null !== $attribute->method) {
throw new \LogicException(sprintf('Parameter "method" for attribute "%s" must be NULL when applied on a method.', AsDataLoader::class));
}

if (isset($loaderConfig['alias'])) {
$container->setAlias($loaderConfig['alias'], $dataLoaderServiceID);
$container->getAlias($loaderConfig['alias'])->setPublic(true);
}
$definition->addTag('overblog.dataloader', array_merge($config['defaults'], [
'name' => $attribute->name,
'alias' => $attribute->alias,
'method' => $reflector instanceof \ReflectionMethod ? $reflector->getName() : ($attribute->method ?? '__invoke'),
'options' => array_merge($config['defaults']['options'], $attribute->options ?? []),
]));
});

foreach ($config['loaders'] as $name => $loaderConfig) {
$container->register(Support::generateDataLoaderServiceIDFromName($name, $container), DataLoader::class)
->addTag('overblog.dataloader', array_merge($config['defaults'], [
'name' => $name,
'alias' => $loaderConfig['alias'] ?? null,
'batch_load_fn' => $loaderConfig['batch_load_fn'],
'options' => array_merge($config['defaults']['options'], $loaderConfig['options'] ?? []),
]));
}
}

public function getAlias(): string
{
return 'overblog_dataloader';
}

private function generateDataLoaderServiceIDFromName($name, ContainerBuilder $container): string
{
return sprintf('%s.%s_loader', $this->getAlias(), $container->underscore($name));
}

private function generateDataLoaderOptionServiceIDFromName($name, ContainerBuilder $container): string
{
return sprintf('%s_option', $this->generateDataLoaderServiceIDFromName($name, $container));
}

private function buildOptionsParams(array $options): array
{
$optionsParams = [];

$optionsParams['batch'] = $options['batch'];
$optionsParams['cache'] = $options['cache'];
$optionsParams['maxBatchSize'] = $options['max_batch_size'];
$optionsParams['cacheMap'] = new Reference($options['cache_map']);
$optionsParams['cacheKeyFn'] = $this->buildCallableFromScalar($options['cache_key_fn']);

return $optionsParams;
}

private function buildCallableFromScalar($scalar): mixed
{
$matches = null;

if (null === $scalar) {
return null;
}

if (preg_match(Configuration::SERVICE_CALLABLE_NOTATION_REGEX, $scalar, $matches)) {
$function = new Reference($matches['service_id']);
if (empty($matches['method'])) {
return $function;
} else {
return [$function, $matches['method']];
}
} elseif (preg_match(Configuration::PHP_CALLABLE_NOTATION_REGEX, $scalar, $matches)) {
$function = $matches['function'];
if (empty($matches['method'])) {
return $function;
} else {
return [$function, $matches['method']];
}
}

return null;
return Support::getAlias();
}
}
76 changes: 76 additions & 0 deletions src/DependencyInjection/Support.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

/*
* This file is part of the OverblogDataLoaderBundle package.
*
* (c) Overblog <http://github.com/overblog/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Overblog\DataLoaderBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/** @internal */
class Support
{
private static string $alias = 'overblog_dataloader';

public static function getAlias(): string
{
return self::$alias;
}

public static function generateDataLoaderServiceIDFromName(string $name, ContainerBuilder $container): string
{
return sprintf('%s.%s_loader', static::$alias, $container::underscore($name));
}

public static function generateDataLoaderOptionServiceIDFromName(string $name, ContainerBuilder $container): string
{
return sprintf('%s_option', static::generateDataLoaderServiceIDFromName($name, $container));
}

public static function buildCallableFromScalar($scalar): mixed
{
$matches = null;

if (null === $scalar) {
return null;
}

if (preg_match(Configuration::SERVICE_CALLABLE_NOTATION_REGEX, $scalar, $matches)) {
$function = new Reference($matches['service_id']);
if (empty($matches['method'])) {
return $function;
} else {
return [$function, $matches['method']];
}
} elseif (preg_match(Configuration::PHP_CALLABLE_NOTATION_REGEX, $scalar, $matches)) {
$function = $matches['function'];
if (empty($matches['method'])) {
return $function;
} else {
return [$function, $matches['method']];
}
}

return null;
}

public static function buildOptionsParams(array $options): array
{
$optionsParams = [];

$optionsParams['batch'] = $options['batch'];
$optionsParams['cache'] = $options['cache'];
$optionsParams['maxBatchSize'] = $options['max_batch_size'];
$optionsParams['cacheMap'] = new Reference($options['cache_map']);
$optionsParams['cacheKeyFn'] = self::buildCallableFromScalar($options['cache_key_fn']);

return $optionsParams;
}
}
9 changes: 9 additions & 0 deletions src/OverblogDataLoaderBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

namespace Overblog\DataLoaderBundle;

use Overblog\DataLoaderBundle\DependencyInjection\CompilerPass\RegisterDataLoadersFromTagsPass;
use Overblog\DataLoaderBundle\DependencyInjection\OverblogDataLoaderExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;

Expand All @@ -21,4 +23,11 @@ public function getContainerExtension(): ?ExtensionInterface
{
return new OverblogDataLoaderExtension();
}

public function build(ContainerBuilder $container)
{
parent::build($container);

$container->addCompilerPass(new RegisterDataLoadersFromTagsPass());
}
}
Loading