ControlledErrorManager::all()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
c 0
b 0
f 0
ccs 0
cts 6
cp 0
rs 10
cc 2
nc 2
nop 0
crap 6
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
    public function __construct(Kernel $kernel, $config = [])
44
    {
45
        $this->kernel = $kernel;
46
        $this->config = $config;
47
    }
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;
0 ignored issues
show
Deprecated Code introduced by
The function Symfony\Component\HttpKernel\Kernel::getRootDir() has been deprecated: since Symfony 4.2, use getProjectDir() instead ( Ignorable by Annotation )

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

154
                $path = /** @scrutinizer ignore-deprecated */ $this->kernel->getRootDir().'/'.$location;

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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(
0 ignored issues
show
Bug Best Practice introduced by
The expression yield new Ynlo\GraphQLBu...rror->getDescription()) returns the type Generator which is incompatible with the documented return type Ynlo\GraphQLBundle\Error...trolledError[]|iterable.
Loading history...
217
                            $error->getCategory(),
218
                            $error->getMessage(),
219
                            $error->getCode(),
220
                            $error->getDescription()
0 ignored issues
show
Bug introduced by
It seems like $error->getDescription() can also be of type null; however, parameter $description of Ynlo\GraphQLBundle\Error...ledError::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

220
                            /** @scrutinizer ignore-type */ $error->getDescription()
Loading history...
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