Passed
Push — master ( 319fcb...7a107a )
by Rafael
04:41
created

DefinitionRegistry::loadCache()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 10
c 0
b 0
f 0
ccs 6
cts 6
cp 1
rs 9.4285
cc 3
eloc 5
nc 3
nop 1
crap 3
1
<?php
2
/*******************************************************************************
3
 *  This file is part of the GraphQL Bundle package.
4
 *
5
 *  (c) YnloUltratech <[email protected]>
6
 *
7
 *  For the full copyright and license information, please view the LICENSE
8
 *  file that was distributed with this source code.
9
 ******************************************************************************/
10
11
namespace Ynlo\GraphQLBundle\Definition\Registry;
12
13
use Doctrine\Common\Util\Inflector;
14
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
15
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
16
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
17
use Symfony\Component\Config\Definition\Processor;
18
use Ynlo\GraphQLBundle\Definition\DefinitionInterface;
19
use Ynlo\GraphQLBundle\Definition\FieldsAwareDefinitionInterface;
20
use Ynlo\GraphQLBundle\Definition\Loader\DefinitionLoaderInterface;
21
use Ynlo\GraphQLBundle\Definition\MetaAwareInterface;
22
use Ynlo\GraphQLBundle\Definition\Plugin\DefinitionPluginInterface;
23
use Ynlo\GraphQLBundle\Extension\EndpointNotValidException;
24
25
/**
26
 * DefinitionRegistry
27
 */
28
class DefinitionRegistry
29
{
30
    /**
31
     * This endpoint is used as default endpoint
32
     */
33
    public const DEFAULT_ENDPOINT = 'default';
34
35
    /**
36
     * @var iterable|DefinitionLoaderInterface[]
37
     */
38
    private $loaders;
39
40
    /**
41
     * @var iterable|DefinitionPluginInterface[]
42
     */
43
    private $plugins;
44
45
    /**
46
     * @var array|Endpoint[]
47
     */
48
    private static $endpoints = [];
49
50
    /**
51
     * @var array
52
     */
53
    protected $endpointsConfig = [];
54
55
    /**
56
     * @var string
57
     */
58
    private $cacheDir;
59
60
    /**
61
     * DefinitionRegistry constructor.
62
     *
63
     * @param iterable|DefinitionLoaderInterface[] $loaders
64
     * @param iterable|DefinitionPluginInterface[] $plugins
65
     * @param null|string                          $cacheDir
66
     * @param array                                $endpointsConfig
67
     */
68 22
    public function __construct(iterable $loaders, iterable $plugins, ?string $cacheDir = null, array $endpointsConfig = [])
69
    {
70 22
        $this->loaders = $loaders;
71 22
        $this->plugins = $plugins;
72 22
        $this->cacheDir = $cacheDir;
73 22
        $this->endpointsConfig = array_merge($endpointsConfig['endpoints'] ?? [], [self::DEFAULT_ENDPOINT => []]);
74 22
    }
75
76
    /**
77
     * Get endpoint schema
78
     *
79
     * For internal use can get the default endpoint (contains all definitions)
80
     * For consumers must get the endpoint for current consumer.
81
     *
82
     * @param string $name
83
     *
84
     * @return Endpoint
85
     *
86
     * @throws EndpointNotValidException
87
     */
88 22
    public function getEndpoint($name = self::DEFAULT_ENDPOINT): Endpoint
89
    {
90 22
        $endpoints = $this->endpointsConfig;
91 22
        unset($endpoints[self::DEFAULT_ENDPOINT]);
92 22
        $endpointsNames = array_keys($endpoints);
93 22
        if (self::DEFAULT_ENDPOINT !== $name && !\in_array($name, $endpointsNames)) {
94
            throw new EndpointNotValidException(
95
                sprintf(
96
                    '"%s" is not a valid configured endpoint, use one of the following endpoints: [%s]',
97
                    $name,
98
                    implode($endpointsNames, ',')
0 ignored issues
show
Unused Code introduced by
The call to implode() has too many arguments starting with ','. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

98
                    /** @scrutinizer ignore-call */ 
99
                    implode($endpointsNames, ',')

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
99
                )
100
            );
101
        }
102
103
        //use first static cache
104 22
        if (isset(self::$endpoints[$name])) {
105 22
            return self::$endpoints[$name];
106
        }
107
108
        //use file cache
109 1
        self::$endpoints[$name] = $this->loadCache($name);
110
111
        //retry after load from file
112 1
        if (isset(self::$endpoints[$name]) && self::$endpoints[$name] instanceof Endpoint) {
113
            return self::$endpoints[$name];
114
        }
115
116 1
        $this->initialize($name);
117
118 1
        return self::$endpoints[$name];
119
    }
120
121
    /**
122
     * remove the specification cache
123
     */
124
    public function clearCache()
125
    {
126
        foreach ($this->endpointsConfig as $name => $config) {
127
            @unlink($this->cacheFileName($name));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

127
            /** @scrutinizer ignore-unhandled */ @unlink($this->cacheFileName($name));

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
128
            $this->initialize($name);
129
        }
130
    }
131
132
    /**
133
     * Initialize endpoint
134
     *
135
     * @param string $name
136
     */
137 1
    protected function initialize(string $name)
138
    {
139 1
        $rawDefault = $this->loadCache('default.raw');
140 1
        if (!$rawDefault) {
141 1
            $rawDefault = new Endpoint(self::DEFAULT_ENDPOINT);
142
143 1
            foreach ($this->loaders as $loader) {
144 1
                $loader->loadDefinitions($rawDefault);
145
            }
146
147 1
            $this->saveCache('default.raw', $rawDefault);
148
        }
149
150 1
        if (!isset(self::$endpoints[$name])) {
151
            //copy from raw to specific endpoint
152 1
            self::$endpoints[$name] = new Endpoint($name);
153 1
            self::$endpoints[$name]->setTypes($rawDefault->allTypes());
154 1
            self::$endpoints[$name]->setMutations($rawDefault->allMutations());
155 1
            self::$endpoints[$name]->setQueries($rawDefault->allQueries());
156
157 1
            $this->compile(self::$endpoints[$name]);
158
        }
159
160 1
        $this->saveCache($name, self::$endpoints[$name]);
161 1
    }
162
163 1
    protected function cacheFileName($name): string
164
    {
165 1
        return sprintf('%s%sgraphql.registry_definitions_%s.meta', $this->cacheDir, DIRECTORY_SEPARATOR, Inflector::tableize($name));
166
    }
167
168 1
    protected function loadCache($name): ?Endpoint
169
    {
170 1
        if (file_exists($this->cacheFileName($name))) {
171 1
            $content = @file_get_contents($this->cacheFileName($name));
172 1
            if ($content) {
173 1
                return unserialize($content, ['allowed_classes' => true]);
174
            }
175
        }
176
177 1
        return null;
178
    }
179
180 1
    protected function saveCache($name, Endpoint $endpoint): void
181
    {
182 1
        file_put_contents($this->cacheFileName($name), serialize($endpoint));
183 1
    }
184
185
    /**
186
     * Verify endpoint definitions and do some tasks to prepare the endpoint
187
     *
188
     * @param Endpoint $endpoint
189
     */
190 1
    protected function compile(Endpoint $endpoint): void
191
    {
192
        //run all extensions for each definition
193 1
        foreach ($this->plugins as $plugin) {
194
            //run extensions recursively in all types and fields
195 1
            foreach ($endpoint->allTypes() as $type) {
196 1
                $this->configureDefinition($plugin, $type, $endpoint);
197 1
                if ($type instanceof FieldsAwareDefinitionInterface) {
198 1
                    foreach ($type->getFields() as $field) {
199 1
                        $this->configureDefinition($plugin, $field, $endpoint);
200 1
                        foreach ($field->getArguments() as $argument) {
201 1
                            $this->configureDefinition($plugin, $argument, $endpoint);
202
                        }
203
                    }
204
                }
205
            }
206
207
            //run extension in all queries
208 1
            foreach ($endpoint->allQueries() as $query) {
209 1
                $this->configureDefinition($plugin, $query, $endpoint);
210 1
                foreach ($query->getArguments() as $argument) {
211 1
                    $this->configureDefinition($plugin, $argument, $endpoint);
212
                }
213
            }
214
215
            //run extensions in all mutations
216 1
            foreach ($endpoint->allMutations() as $mutation) {
217 1
                $this->configureDefinition($plugin, $mutation, $endpoint);
218 1
                foreach ($mutation->getArguments() as $argument) {
219 1
                    $this->configureDefinition($plugin, $argument, $endpoint);
220
                }
221
            }
222
223 1
            $plugin->configureEndpoint($endpoint);
224
        }
225 1
    }
226
227
    /**
228
     * @param DefinitionPluginInterface $plugin
229
     * @param DefinitionInterface       $definition
230
     * @param Endpoint                  $endpoint
231
     */
232 1
    protected function configureDefinition(DefinitionPluginInterface $plugin, DefinitionInterface $definition, Endpoint $endpoint)
233
    {
234 1
        $config = [];
235 1
        if ($definition instanceof MetaAwareInterface) {
0 ignored issues
show
introduced by
$definition is always a sub-type of Ynlo\GraphQLBundle\Definition\MetaAwareInterface.
Loading history...
236 1
            $treeBuilder = new TreeBuilder();
237
            /** @var NodeBuilder $root */
238 1
            $root = $treeBuilder->root($plugin->getName());
239 1
            $plugin->buildConfig($root);
0 ignored issues
show
Bug introduced by
$root of type Symfony\Component\Config...ion\Builder\NodeBuilder is incompatible with the type Symfony\Component\Config...der\ArrayNodeDefinition expected by parameter $root of Ynlo\GraphQLBundle\Defin...nterface::buildConfig(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

239
            $plugin->buildConfig(/** @scrutinizer ignore-type */ $root);
Loading history...
240
241 1
            if ($definition->hasMeta($plugin->getName())) {
242 1
                $options = $definition->getMeta($plugin->getName());
243 1
                $processor = new Processor();
244
245
                try {
246 1
                    $options = $plugin->normalizeConfig($definition, $options);
247 1
                    $config = $processor->process($treeBuilder->buildTree(), [$options]);
248
                } catch (InvalidConfigurationException $exception) {
249
                    $error = sprintf('Error compiling schema definition "%s", %s', $definition->getName(), $exception->getMessage());
250
                    throw new \RuntimeException($error, 0, $exception);
251
                }
252
            }
253
        }
254 1
        $plugin->configure($definition, $endpoint, $config);
255 1
    }
256
}
257