Passed
Push — master ( 9da6fd...0354d3 )
by Rafael
05:30
created

ControlledErrorManager   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 238
Duplicated Lines 0 %

Test Coverage

Coverage 3.77%

Importance

Changes 0
Metric Value
wmc 39
dl 0
loc 238
ccs 4
cts 106
cp 0.0377
rs 8.2857
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A saveCache() 0 3 1
A cacheFileName() 0 3 1
A clear() 0 5 2
A __construct() 0 4 1
B loadAllErrors() 0 31 6
A get() 0 3 1
A loadFromCache() 0 7 3
A add() 0 13 2
A all() 0 7 2
C controlledExceptions() 0 72 19
A has() 0 3 1
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\Error;
12
13
use Symfony\Component\Finder\Finder;
14
use Symfony\Component\Finder\SplFileInfo;
15
use Symfony\Component\HttpKernel\Kernel;
16
use Ynlo\GraphQLBundle\Exception\ControlledErrorInterface;
17
18
class ControlledErrorManager
19
{
20
    protected $kernel;
21
22
    /**
23
     * @var array
24
     */
25
    protected $config = [];
26
27
    /**
28
     * @var bool
29
     */
30
    protected $loaded = false;
31
32
    /**
33
     * @var array|MappedControlledError[]
34
     */
35
    protected $errors = [];
36
37
    /**
38
     * ControlledErrorManager constructor.
39
     *
40
     * @param Kernel $kernel
41
     * @param array  $config
42
     */
43 22
    public function __construct(Kernel $kernel, $config = [])
44
    {
45 22
        $this->kernel = $kernel;
46 22
        $this->config = $config;
47 22
    }
48
49
    /**
50
     * @return array|MappedControlledError[]
51
     */
52
    public function all(): array
53
    {
54
        if (!$this->loaded) {
55
            $this->loadAllErrors();
56
        }
57
58
        return $this->errors;
59
    }
60
61
    /**
62
     * @param MappedControlledError $error
63
     */
64
    public function add(MappedControlledError $error)
65
    {
66
        if ($this->has($error->getCode())) {
67
            $message = sprintf(
68
                'Duplicate error definition, the error code "%s" can\'t be used for "%s" because this code is already used with: "%s"',
69
                $error->getCode(),
70
                $error->getDescription(),
71
                $this->all()[$error->getCode()]->getDescription()
72
            );
73
            throw new \LogicException($message);
74
        }
75
76
        $this->errors[$error->getCode()] = $error;
77
    }
78
79
    /**
80
     * @param string $code
81
     *
82
     * @return bool
83
     */
84
    public function has($code)
85
    {
86
        return isset($this->all()[$code]);
87
    }
88
89
    /**
90
     * @param string $code
91
     *
92
     * @return MappedControlledError
93
     */
94
    public function get($code)
95
    {
96
        return $this->all()[$code];
97
    }
98
99
    /**
100
     * Clear errors and cache
101
     */
102
    public function clear(): void
103
    {
104
        $this->errors = [];
105
        if (file_exists($this->cacheFileName())) {
106
            @unlink($this->cacheFileName());
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

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

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...
107
        }
108
    }
109
110
    /**
111
     * @return void
112
     */
113
    private function loadAllErrors(): void
114
    {
115
        $this->loadFromCache();
116
        if ($this->loaded) {
117
            return;
118
        }
119
120
        $loadedErrors = [];
121
        foreach ($this->config['map'] as $code => $error) {
122
            $loadedErrors[] = new MappedControlledError(
123
                $error['category'],
124
                $error['message'],
125
                $code,
126
                $error['description']
127
            );
128
        }
129
130
        if ($this->config['autoload']['enabled'] ?? false) {
131
            foreach ($this->controlledExceptions() as $error) {
132
                $loadedErrors[] = $error;
133
            }
134
        }
135
136
        $this->loaded = true;
137
        foreach ($loadedErrors as $error) {
138
            $this->add($error);
139
        }
140
141
        ksort($this->errors, SORT_NATURAL);
142
143
        $this->saveCache();
144
    }
145
146
    /**
147
     * @return MappedControlledError[]|iterable
148
     */
149
    private function controlledExceptions(): iterable
150
    {
151
        $paths = [];
152
        if (Kernel::VERSION_ID >= 40000) {
153
            foreach ($this->config['autoload']['locations'] ?? [] as $location) {
154
                $path = $this->kernel->getRootDir().'/'.$location;
155
                if (file_exists($path)) {
156
                    $paths[$path] = 'App\\'.$location;
157
                }
158
            }
159
        }
160
161
        foreach ($this->kernel->getBundles() as $bundle) {
162
            foreach ($this->config['autoload']['locations'] ?? [] as $location) {
163
                $path = $bundle->getPath().'/'.$location;
164
                if (file_exists($path)) {
165
                    $paths[$path] = $bundle->getNamespace().'\\'.$location;
166
                }
167
            }
168
        }
169
170
        foreach ($paths as $path => $namespace) {
171
            $finder = new Finder();
172
            $finder
173
                ->in($path)
174
                ->name('*.php');
175
176
            /** @var SplFileInfo[] $files */
177
            $files = $finder->files();
178
            foreach ($files as $file) {
179
                $className = sprintf(
180
                    '%s\%s',
181
                    $namespace,
182
                    preg_replace(
183
                        '/.php$/',
184
                        null,
185
                        str_replace('/', '\\', $file->getRelativePathname())
186
                    )
187
                );
188
                if (class_exists($className)) {
189
                    $allowed = false;
190
                    if ($whitelist = $this->config['autoload']['whitelist'] ?? []) {
191
                        foreach ($whitelist as $exp) {
192
                            if (preg_match($exp, $className)) {
193
                                $allowed = true;
194
                                continue;
195
                            }
196
                        }
197
                    }
198
199
                    if ($blackList = $this->config['autoload']['blacklist'] ?? []) {
200
                        foreach ($blackList as $exp) {
201
                            if (preg_match($exp, $className)) {
202
                                $allowed = false;
203
                                continue;
204
                            }
205
                        }
206
                    }
207
208
                    if (!$allowed) {
209
                        continue;
210
                    }
211
212
                    $ref = new \ReflectionClass($className);
213
                    if ($ref->implementsInterface(ControlledErrorInterface::class) && $ref->isInstantiable()) {
214
                        /** @var ControlledErrorInterface $error */
215
                        $error = $ref->newInstanceWithoutConstructor();
216
                        yield  new MappedControlledError(
217
                            $error->getCategory(),
218
                            $error->getMessage(),
219
                            $error->getCode(),
220
                            $error->getDescription()
221
                        );
222
                    }
223
                }
224
            }
225
        }
226
    }
227
228
    /**
229
     * @return string
230
     */
231
    private function cacheFileName(): string
232
    {
233
        return $this->kernel->getCacheDir().DIRECTORY_SEPARATOR.'graphql.controlled_errors.meta';
234
    }
235
236
    /**
237
     * Load cache
238
     */
239
    private function loadFromCache(): void
240
    {
241
        if (file_exists($this->cacheFileName())) {
242
            $content = @file_get_contents($this->cacheFileName());
243
            if ($content) {
244
                $this->loaded = true;
245
                $this->errors = unserialize($content, ['allowed_classes' => [MappedControlledError::class]]);
246
            }
247
        }
248
    }
249
250
    /**
251
     * Save cache
252
     */
253
    private function saveCache(): void
254
    {
255
        file_put_contents($this->cacheFileName(), serialize($this->errors));
256
    }
257
}
258