Skip to content

Commit 1d07a8f

Browse files
authored
Merge 293e765 into e7e3d0c
2 parents e7e3d0c + 293e765 commit 1d07a8f

File tree

15 files changed

+512
-26
lines changed

15 files changed

+512
-26
lines changed

Config/Processor.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
namespace Overblog\GraphQLBundle\Config;
4+
5+
use Symfony\Component\ExpressionLanguage\Expression;
6+
7+
class Processor
8+
{
9+
const DEFAULT_EXPRESSION_LANGUAGE_TRIGGER = '@=';
10+
11+
/** @var string */
12+
private $expressionLanguageTrigger;
13+
14+
/** @var callable[] */
15+
private $processors = [];
16+
17+
public function __construct($expressionLanguageTrigger = self::DEFAULT_EXPRESSION_LANGUAGE_TRIGGER, array $processors = [])
18+
{
19+
// add the default $processors to let users override it easily
20+
array_push($processors, [$this, 'convertStringToExpressionObject']);
21+
22+
$this->processors = $processors;
23+
$this->expressionLanguageTrigger = $expressionLanguageTrigger;
24+
}
25+
26+
public function process(array $configs)
27+
{
28+
foreach ($this->processors as $processor) {
29+
$configs = call_user_func($processor, $configs);
30+
}
31+
32+
return $configs;
33+
}
34+
35+
public static function staticProcess(
36+
array $configs,
37+
$expressionLanguageTrigger = self::DEFAULT_EXPRESSION_LANGUAGE_TRIGGER,
38+
array $processors = []
39+
) {
40+
return (new static($expressionLanguageTrigger, $processors))->process($configs);
41+
}
42+
43+
public function convertStringToExpressionObject(array $configs)
44+
{
45+
return array_map(function ($v) {
46+
if (is_array($v)) {
47+
return $this->convertStringToExpressionObject($v);
48+
} elseif (is_string($v) && 0 === strpos($v, $this->expressionLanguageTrigger)) {
49+
return new Expression(substr($v, 2));
50+
}
51+
52+
return $v;
53+
}, $configs);
54+
}
55+
56+
public static function processBeforeNormalization(array $configs)
57+
{
58+
$configs = static::processConfigsExtends($configs);
59+
$configs = static::removedVirtualTypes($configs);
60+
61+
return $configs;
62+
}
63+
64+
public static function removedVirtualTypes(array $configs)
65+
{
66+
return array_filter($configs, function ($config) {
67+
return !isset($config['virtual']) || true !== $config['virtual'];
68+
});
69+
}
70+
71+
public static function processConfigsExtends(array $configs)
72+
{
73+
foreach ($configs as $name => &$config) {
74+
if (!isset($config['type'])) {
75+
continue;
76+
}
77+
78+
$allowedTypes = [$config['type']];
79+
if ('object' === $config['type']) {
80+
$allowedTypes[] = 'interface';
81+
}
82+
$flattenExtends = self::flattenExtends($name, $configs, $allowedTypes);
83+
if (empty($flattenExtends)) {
84+
continue;
85+
}
86+
$config = self::extendsTypeConfig($name, $flattenExtends, $configs);
87+
}
88+
89+
return $configs;
90+
}
91+
92+
private static function extendsTypeConfig($child, array $parents, array $configs)
93+
{
94+
$parentTypes = array_intersect_key($configs, array_flip($parents));
95+
$parentTypes = array_reverse($parentTypes);
96+
$mergedParentsConfig = call_user_func_array('array_replace_recursive', array_column($parentTypes, 'config'));
97+
$childType = $configs[$child];
98+
// unset resolveType field resulting from the merge of a "interface" type
99+
if ('object' === $childType['type']) {
100+
unset($mergedParentsConfig['resolveType']);
101+
}
102+
103+
$configs = array_replace_recursive(['config' => $mergedParentsConfig], $childType);
104+
105+
return $configs;
106+
}
107+
108+
private static function flattenExtends($name, array $configs, array $allowedTypes, $child = null, array $typesTreated = [])
109+
{
110+
self::checkTypeExists($name, $configs, $child);
111+
self::checkCircularReferenceExtendsTypes($name, $typesTreated);
112+
self::checkAllowedExtendsTypes($name, $configs[$name], $allowedTypes, $child);
113+
114+
// flatten
115+
$config = $configs[$name];
116+
if (empty($config['extends']) || !is_array($config['extends'])) {
117+
return [];
118+
}
119+
$typesTreated[$name] = true;
120+
$flattenExtendsTypes = [];
121+
foreach ($config['extends'] as $typeToExtend) {
122+
$flattenExtendsTypes[] = $typeToExtend;
123+
$flattenExtendsTypes = array_merge(
124+
$flattenExtendsTypes,
125+
self::flattenExtends($typeToExtend, $configs, $allowedTypes, $name, $typesTreated)
126+
);
127+
}
128+
129+
return $flattenExtendsTypes;
130+
}
131+
132+
private static function checkTypeExists($name, array $configs, $child)
133+
{
134+
if (!isset($configs[$name])) {
135+
throw new \InvalidArgumentException(sprintf(
136+
'Type %s extends by %s not be found.',
137+
json_encode($name),
138+
json_encode($child)
139+
));
140+
}
141+
}
142+
143+
private static function checkCircularReferenceExtendsTypes($name, array $typesTreated)
144+
{
145+
if (isset($typesTreated[$name])) {
146+
throw new \InvalidArgumentException(sprintf(
147+
'Type circular inheritance detected (%s).',
148+
implode('->', array_merge(array_keys($typesTreated), [$name]))
149+
));
150+
}
151+
}
152+
153+
private static function checkAllowedExtendsTypes($name, array $config, array $allowedTypes, $child)
154+
{
155+
if (!in_array($config['type'], $allowedTypes)) {
156+
throw new \InvalidArgumentException(sprintf(
157+
'Type %s can\'t extends %s because %s is not allowed type (%s).',
158+
json_encode($name),
159+
json_encode($child),
160+
json_encode($config['type']),
161+
json_encode($allowedTypes)
162+
));
163+
}
164+
}
165+
}

DependencyInjection/OverblogGraphQLTypesExtension.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Overblog\GraphQLBundle\DependencyInjection;
44

55
use Overblog\GraphQLBundle\OverblogGraphQLBundle;
6+
use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException;
67
use Symfony\Component\Config\Resource\FileResource;
78
use Symfony\Component\DependencyInjection\ContainerBuilder;
89
use Symfony\Component\Finder\Finder;
@@ -33,8 +34,11 @@ class OverblogGraphQLTypesExtension extends Extension
3334

3435
public function load(array $configs, ContainerBuilder $container)
3536
{
36-
$configuration = $this->getConfiguration($configs, $container);
37-
$config = $this->processConfiguration($configuration, $configs);
37+
$this->checkTypesDuplication($configs);
38+
// flatten config is a requirement to support inheritance
39+
$flattenConfig = [call_user_func_array('array_merge', $configs)];
40+
$configuration = $this->getConfiguration($flattenConfig, $container);
41+
$config = $this->processConfiguration($configuration, $flattenConfig);
3842

3943
$container->setParameter($this->getAlias().'.config', $config);
4044
}
@@ -71,6 +75,20 @@ private function prependExtensionConfigFromFiles($type, $files, ContainerBuilder
7175
}
7276
}
7377

78+
private function checkTypesDuplication(array $typeConfigs)
79+
{
80+
$types = call_user_func_array('array_merge', array_map('array_keys', $typeConfigs));
81+
$duplications = array_keys(array_filter(array_count_values($types), function ($count) {
82+
return $count > 1;
83+
}));
84+
if (!empty($duplications)) {
85+
throw new ForbiddenOverwriteException(sprintf(
86+
'Types (%s) cannot be overwritten. See inheritance doc section for more details.',
87+
implode(', ', array_map('json_encode', $duplications))
88+
));
89+
}
90+
}
91+
7492
private function mappingConfig(array $config, ContainerBuilder $container)
7593
{
7694
// use default value if needed

DependencyInjection/TypesConfiguration.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ function ($type) {
8080
->cannotBeOverwritten()
8181
->children()
8282
->enumNode('type')->values(self::$types)->isRequired()->end()
83+
->arrayNode('extends')
84+
->prototype('scalar')->info('Types to extends.')->end()
85+
->end()
86+
->booleanNode('virtual')->info('Virtual types will not be generated.')->defaultFalse()->end()
8387
->append(Config\ObjectTypeDefinition::create()->getDefinition())
8488
->append(Config\EnumTypeDefinition::create()->getDefinition())
8589
->append(Config\InterfaceTypeDefinition::create()->getDefinition())
@@ -114,6 +118,13 @@ private function addBeforeNormalization(ArrayNodeDefinition $node)
114118
};
115119

116120
$node
121+
// process beforeNormalization
122+
->beforeNormalization()
123+
->ifTrue($typeKeyExists)
124+
->then(function ($types) {
125+
return call_user_func([Config\Processor::class, 'processBeforeNormalization'], $types);
126+
})
127+
->end()
117128
// set type config.name
118129
->beforeNormalization()
119130
->ifTrue($typeKeyExists)
@@ -143,11 +154,10 @@ private function addBeforeNormalization(ArrayNodeDefinition $node)
143154
->end()
144155
// normalized relay-mutation-payload
145156
->beforeNormalization()
146-
->ifTrue(function ($types) {
147-
return !empty($types) && is_array($types);
148-
})
157+
->ifTrue($typeKeyExists)
149158
->then($this->relayNormalizer('relay-mutation-payload', PayloadDefinition::class))
150-
->end();
159+
->end()
160+
;
151161
}
152162

153163
/**

Generator/TypeGenerator.php

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,43 @@
44

55
use Composer\Autoload\ClassLoader;
66
use GraphQL\Type\Definition\ResolveInfo;
7+
use Overblog\GraphQLBundle\Config\Processor;
78
use Overblog\GraphQLBundle\Definition\Argument;
89
use Overblog\GraphQLBundle\Error\UserWarning;
910
use Overblog\GraphQLGenerator\Generator\TypeGenerator as BaseTypeGenerator;
10-
use Symfony\Component\ExpressionLanguage\Expression;
1111
use Symfony\Component\Filesystem\Filesystem;
1212

1313
class TypeGenerator extends BaseTypeGenerator
1414
{
1515
const USE_FOR_CLOSURES = '$container, $request, $user, $token';
1616

17+
const DEFAULT_CONFIG_PROCESSOR = [Processor::class, 'staticProcess'];
18+
1719
private $cacheDir;
1820

1921
private $defaultResolver;
2022

23+
private $configProcessor;
24+
2125
private $configs;
2226

2327
private $useClassMap = true;
2428

2529
private static $classMapLoaded = false;
2630

27-
public function __construct($classNamespace, array $skeletonDirs, $cacheDir, callable $defaultResolver, array $configs, $useClassMap = true)
31+
public function __construct(
32+
$classNamespace,
33+
array $skeletonDirs,
34+
$cacheDir,
35+
callable $defaultResolver,
36+
array $configs,
37+
$useClassMap = true,
38+
callable $configProcessor = null)
2839
{
2940
$this->setCacheDir($cacheDir);
3041
$this->defaultResolver = $defaultResolver;
31-
$this->configs = $this->processConfigs($configs);
42+
$this->configProcessor = null === $configProcessor ? static::DEFAULT_CONFIG_PROCESSOR : $configProcessor;
43+
$this->configs = $configs;
3244
$this->useClassMap = $useClassMap;
3345
parent::__construct($classNamespace, $skeletonDirs);
3446
}
@@ -194,7 +206,8 @@ public function compile($mode)
194206
$fs = new Filesystem();
195207
$fs->remove($cacheDir);
196208
}
197-
$classes = $this->generateClasses($this->configs, $cacheDir, $mode);
209+
$configs = call_user_func($this->configProcessor, $this->configs);
210+
$classes = $this->generateClasses($configs, $cacheDir, $mode);
198211

199212
if ($writeMode && $this->useClassMap) {
200213
$content = "<?php\nreturn ".var_export($classes, true).';';
@@ -233,20 +246,4 @@ private function getClassesMap()
233246
{
234247
return $this->getCacheDir().'/__classes.map';
235248
}
236-
237-
private function processConfigs(array $configs)
238-
{
239-
return array_map(
240-
function ($v) {
241-
if (is_array($v)) {
242-
return call_user_func([$this, 'processConfigs'], $v);
243-
} elseif (is_string($v) && 0 === strpos($v, '@=')) {
244-
return new Expression(substr($v, 2));
245-
}
246-
247-
return $v;
248-
},
249-
$configs
250-
);
251-
}
252249
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Documentation
2323
- [Input Object](Resources/doc/definitions/type-system/input-object.md)
2424
- [Lists](Resources/doc/definitions/type-system/lists.md)
2525
- [Non-Null](Resources/doc/definitions/type-system/non-null.md)
26+
- [Type Inheritance](Resources/doc/definitions/type-inheritance.md)
2627
- [Schema](Resources/doc/definitions/schema.md)
2728
- [Resolver](Resources/doc/definitions/resolver.md)
2829
- [Mutation](Resources/doc/definitions/mutation.md)

Resources/doc/definitions/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Definitions
22
===========
33

44
* [Type System](type-system/index.md)
5+
* [Type Inheritance](type-inheritance.md)
56
* [Schema](schema.md)
67

78
Go further

0 commit comments

Comments
 (0)