Skip to content

Commit fdfd770

Browse files
authored
Merge pull request #38 from rpander93/feat/asdataloader-attribute
feat: add AsDataLoader attribute
2 parents 6ac3ab3 + d7a8f9e commit fdfd770

File tree

16 files changed

+367
-231
lines changed

16 files changed

+367
-231
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212

1313
jobs:
1414
tests:
15-
runs-on: ubuntu-20.04
15+
runs-on: ubuntu-24.04
1616
strategy:
1717
fail-fast: false
1818
matrix:
@@ -21,7 +21,6 @@ jobs:
2121
- '8.2'
2222
- '8.3'
2323
symfony-version:
24-
- '5.4.*'
2524
- '6.3.*'
2625
- '7.0.*'
2726
- '7.1.*'
@@ -69,7 +68,7 @@ jobs:
6968
run: composer test
7069

7170
coding-standard:
72-
runs-on: ubuntu-20.04
71+
runs-on: ubuntu-24.04
7372
name: Coding Standard
7473
steps:
7574
- name: "Checkout"

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,25 @@ Here the list of existing promise adapters:
8585
* **[GuzzleHttp/Promises](https://github.com/guzzle/promises)**: overblog_dataloader.guzzle_http_promise_adapter
8686
* **[Webonyx/GraphQL-PHP](https://github.com/webonyx/graphql-php) Sync Promise**: overblog_dataloader.webonyx_graphql_sync_promise_adapter
8787
88+
## Configuration using attributes
89+
90+
This bundle supports autoconfiguration via attributes. Add `Overblog\DataLoaderBundle\Attribute\AsDataLoader` on the service you want to expose as data loader:
91+
92+
````php
93+
<?php
94+
95+
#[Overblog\DataLoaderBundle\Attribute\AsDataLoader(name: "users", alias: "users_dataloader")]
96+
class UserLoader {
97+
public function __invoke(array $ids): array
98+
{
99+
return ["John", "Steve", "Nash"];
100+
}
101+
}
102+
103+
?>
104+
105+
````
106+
88107
## Combine with GraphQLBundle
89108

90109
This bundle can be use with [GraphQLBundle](https://github.com/overblog/GraphQLBundle).

src/Attribute/AsDataLoader.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the OverblogDataLoaderBundle package.
5+
*
6+
* (c) Overblog <http://github.com/overblog/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Overblog\DataLoaderBundle\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
15+
class AsDataLoader
16+
{
17+
public function __construct(
18+
public readonly string $name,
19+
public readonly ?string $method = null,
20+
public readonly ?string $alias = null,
21+
public readonly ?array $options = [],
22+
) {
23+
}
24+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the OverblogDataLoaderBundle package.
5+
*
6+
* (c) Overblog <http://github.com/overblog/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Overblog\DataLoaderBundle\DependencyInjection\CompilerPass;
13+
14+
use Overblog\DataLoader\DataLoader;
15+
use Overblog\DataLoader\Option;
16+
use Overblog\DataLoaderBundle\DependencyInjection\Support;
17+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
21+
class RegisterDataLoadersFromTagsPass implements CompilerPassInterface
22+
{
23+
public function process(ContainerBuilder $container)
24+
{
25+
foreach ($container->findTaggedServiceIds('overblog.dataloader') as $serviceId => $tags) {
26+
foreach ($tags as $attributes) {
27+
$batchLoadFn = isset($attributes['method'])
28+
? [new Reference($serviceId), $attributes['method']]
29+
: Support::buildCallableFromScalar($attributes['batch_load_fn']);
30+
31+
$this->registerDataLoader($container, $attributes, $batchLoadFn);
32+
}
33+
}
34+
}
35+
36+
private function registerDataLoader(ContainerBuilder $container, array $loaderConfig, mixed $batchLoadFn): void
37+
{
38+
$dataLoaderServiceID = Support::generateDataLoaderServiceIDFromName($loaderConfig['name'], $container);
39+
$optionServiceID = Support::generateDataLoaderOptionServiceIDFromName($loaderConfig['name'], $container);
40+
41+
$container->register($optionServiceID, Option::class)
42+
->setPublic(false)
43+
->setArguments([Support::buildOptionsParams($loaderConfig['options'])]);
44+
45+
$definition = $container->register($dataLoaderServiceID, DataLoader::class)
46+
->setPublic(true)
47+
->addTag('kernel.reset', ['method' => 'clearAll'])
48+
->setArguments([
49+
$batchLoadFn,
50+
new Reference($loaderConfig['promise_adapter']),
51+
new Reference($optionServiceID),
52+
]);
53+
54+
if (isset($loaderConfig['factory'])) {
55+
$definition->setFactory(Support::buildCallableFromScalar($loaderConfig['factory']));
56+
}
57+
58+
if (isset($loaderConfig['alias'])) {
59+
$container
60+
->setAlias($loaderConfig['alias'], $dataLoaderServiceID)
61+
->setPublic(true);
62+
}
63+
}
64+
}

src/DependencyInjection/OverblogDataLoaderExtension.php

Lines changed: 23 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
namespace Overblog\DataLoaderBundle\DependencyInjection;
1313

14+
use Overblog\DataLoader\DataLoader;
15+
use Overblog\DataLoaderBundle\Attribute\AsDataLoader;
1416
use Symfony\Component\Config\FileLocator;
17+
use Symfony\Component\DependencyInjection\ChildDefinition;
1518
use Symfony\Component\DependencyInjection\ContainerBuilder;
1619
use Symfony\Component\DependencyInjection\Extension\Extension;
1720
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
18-
use Symfony\Component\DependencyInjection\Reference;
1921

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

30-
foreach ($config['loaders'] as $name => $loaderConfig) {
31-
$loaderConfig = array_replace($config['defaults'], $loaderConfig);
32-
$dataLoaderServiceID = $this->generateDataLoaderServiceIDFromName($name, $container);
33-
$OptionServiceID = $this->generateDataLoaderOptionServiceIDFromName($name, $container);
34-
$batchLoadFn = $this->buildCallableFromScalar($loaderConfig['batch_load_fn']);
35-
36-
$container->register($OptionServiceID, 'Overblog\\DataLoader\\Option')
37-
->setPublic(false)
38-
->setArguments([$this->buildOptionsParams($loaderConfig['options'])]);
39-
40-
$definition = $container->register($dataLoaderServiceID, 'Overblog\\DataLoader\\DataLoader')
41-
->setPublic(true)
42-
->addTag('kernel.reset', ['method' => 'clearAll'])
43-
->setArguments([
44-
$batchLoadFn,
45-
new Reference($loaderConfig['promise_adapter']),
46-
new Reference($OptionServiceID),
47-
])
48-
;
49-
50-
if (isset($loaderConfig['factory'])) {
51-
$definition->setFactory($this->buildCallableFromScalar($loaderConfig['factory']));
32+
$container->registerAttributeForAutoconfiguration(AsDataLoader::class, function (ChildDefinition $definition, AsDataLoader $attribute, \ReflectionClass|\ReflectionMethod $reflector) use ($config) {
33+
if ($reflector instanceof \ReflectionMethod && null !== $attribute->method) {
34+
throw new \LogicException(sprintf('Parameter "method" for attribute "%s" must be NULL when applied on a method.', AsDataLoader::class));
5235
}
5336

54-
if (isset($loaderConfig['alias'])) {
55-
$container->setAlias($loaderConfig['alias'], $dataLoaderServiceID);
56-
$container->getAlias($loaderConfig['alias'])->setPublic(true);
57-
}
37+
$definition->addTag('overblog.dataloader', array_merge($config['defaults'], [
38+
'name' => $attribute->name,
39+
'alias' => $attribute->alias,
40+
'method' => $reflector instanceof \ReflectionMethod ? $reflector->getName() : ($attribute->method ?? '__invoke'),
41+
'options' => array_merge($config['defaults']['options'], $attribute->options ?? []),
42+
]));
43+
});
44+
45+
foreach ($config['loaders'] as $name => $loaderConfig) {
46+
$container->register(Support::generateDataLoaderServiceIDFromName($name, $container), DataLoader::class)
47+
->addTag('overblog.dataloader', array_merge($config['defaults'], [
48+
'name' => $name,
49+
'alias' => $loaderConfig['alias'] ?? null,
50+
'batch_load_fn' => $loaderConfig['batch_load_fn'],
51+
'options' => array_merge($config['defaults']['options'], $loaderConfig['options'] ?? []),
52+
]));
5853
}
5954
}
6055

6156
public function getAlias(): string
6257
{
63-
return 'overblog_dataloader';
64-
}
65-
66-
private function generateDataLoaderServiceIDFromName($name, ContainerBuilder $container): string
67-
{
68-
return sprintf('%s.%s_loader', $this->getAlias(), $container->underscore($name));
69-
}
70-
71-
private function generateDataLoaderOptionServiceIDFromName($name, ContainerBuilder $container): string
72-
{
73-
return sprintf('%s_option', $this->generateDataLoaderServiceIDFromName($name, $container));
74-
}
75-
76-
private function buildOptionsParams(array $options): array
77-
{
78-
$optionsParams = [];
79-
80-
$optionsParams['batch'] = $options['batch'];
81-
$optionsParams['cache'] = $options['cache'];
82-
$optionsParams['maxBatchSize'] = $options['max_batch_size'];
83-
$optionsParams['cacheMap'] = new Reference($options['cache_map']);
84-
$optionsParams['cacheKeyFn'] = $this->buildCallableFromScalar($options['cache_key_fn']);
85-
86-
return $optionsParams;
87-
}
88-
89-
private function buildCallableFromScalar($scalar): mixed
90-
{
91-
$matches = null;
92-
93-
if (null === $scalar) {
94-
return null;
95-
}
96-
97-
if (preg_match(Configuration::SERVICE_CALLABLE_NOTATION_REGEX, $scalar, $matches)) {
98-
$function = new Reference($matches['service_id']);
99-
if (empty($matches['method'])) {
100-
return $function;
101-
} else {
102-
return [$function, $matches['method']];
103-
}
104-
} elseif (preg_match(Configuration::PHP_CALLABLE_NOTATION_REGEX, $scalar, $matches)) {
105-
$function = $matches['function'];
106-
if (empty($matches['method'])) {
107-
return $function;
108-
} else {
109-
return [$function, $matches['method']];
110-
}
111-
}
112-
113-
return null;
58+
return Support::getAlias();
11459
}
11560
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the OverblogDataLoaderBundle package.
5+
*
6+
* (c) Overblog <http://github.com/overblog/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Overblog\DataLoaderBundle\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
17+
/** @internal */
18+
class Support
19+
{
20+
private static string $alias = 'overblog_dataloader';
21+
22+
public static function getAlias(): string
23+
{
24+
return self::$alias;
25+
}
26+
27+
public static function generateDataLoaderServiceIDFromName(string $name, ContainerBuilder $container): string
28+
{
29+
return sprintf('%s.%s_loader', static::$alias, $container::underscore($name));
30+
}
31+
32+
public static function generateDataLoaderOptionServiceIDFromName(string $name, ContainerBuilder $container): string
33+
{
34+
return sprintf('%s_option', static::generateDataLoaderServiceIDFromName($name, $container));
35+
}
36+
37+
public static function buildCallableFromScalar($scalar): mixed
38+
{
39+
$matches = null;
40+
41+
if (null === $scalar) {
42+
return null;
43+
}
44+
45+
if (preg_match(Configuration::SERVICE_CALLABLE_NOTATION_REGEX, $scalar, $matches)) {
46+
$function = new Reference($matches['service_id']);
47+
if (empty($matches['method'])) {
48+
return $function;
49+
} else {
50+
return [$function, $matches['method']];
51+
}
52+
} elseif (preg_match(Configuration::PHP_CALLABLE_NOTATION_REGEX, $scalar, $matches)) {
53+
$function = $matches['function'];
54+
if (empty($matches['method'])) {
55+
return $function;
56+
} else {
57+
return [$function, $matches['method']];
58+
}
59+
}
60+
61+
return null;
62+
}
63+
64+
public static function buildOptionsParams(array $options): array
65+
{
66+
$optionsParams = [];
67+
68+
$optionsParams['batch'] = $options['batch'];
69+
$optionsParams['cache'] = $options['cache'];
70+
$optionsParams['maxBatchSize'] = $options['max_batch_size'];
71+
$optionsParams['cacheMap'] = new Reference($options['cache_map']);
72+
$optionsParams['cacheKeyFn'] = self::buildCallableFromScalar($options['cache_key_fn']);
73+
74+
return $optionsParams;
75+
}
76+
}

src/OverblogDataLoaderBundle.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Overblog\DataLoaderBundle;
1313

14+
use Overblog\DataLoaderBundle\DependencyInjection\CompilerPass\RegisterDataLoadersFromTagsPass;
1415
use Overblog\DataLoaderBundle\DependencyInjection\OverblogDataLoaderExtension;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
1517
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
1618
use Symfony\Component\HttpKernel\Bundle\Bundle;
1719

@@ -21,4 +23,11 @@ public function getContainerExtension(): ?ExtensionInterface
2123
{
2224
return new OverblogDataLoaderExtension();
2325
}
26+
27+
public function build(ContainerBuilder $container)
28+
{
29+
parent::build($container);
30+
31+
$container->addCompilerPass(new RegisterDataLoadersFromTagsPass());
32+
}
2433
}

0 commit comments

Comments
 (0)