Completed
Push — master ( 0b0b24...54ff37 )
by Colin
02:37
created

Environment   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 394
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 52
lcom 1
cbo 10
dl 0
loc 394
ccs 145
cts 145
cp 1
rs 7.44
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A mergeConfig() 0 6 1
A setConfig() 0 6 1
A getConfig() 0 4 1
A addBlockParser() 0 9 1
A addInlineParser() 0 17 3
A addDelimiterProcessor() 0 8 1
A addBlockRenderer() 0 13 2
A addInlineRenderer() 0 13 2
A getBlockParsers() 0 8 2
A getInlineParsersForCharacter() 0 12 3
A getDelimiterProcessors() 0 8 2
A getBlockRenderersForClass() 0 8 2
A getInlineRenderersForClass() 0 8 2
A getExtensions() 0 4 1
A addExtension() 0 9 1
A initializeExtensions() 0 16 3
A injectEnvironmentAndConfigurationIfNeeded() 0 10 3
A createCommonMarkEnvironment() 0 17 1
A createGFMEnvironment() 0 7 1
A getInlineParserCharacterRegex() 0 4 1
A addEventListener() 0 18 5
A dispatch() 0 16 4
A buildInlineParserCharacterRegex() 0 15 2
A assertUninitialized() 0 6 2
A getRenderersByClass() 0 18 4

How to fix   Complexity   

Complex Class

Complex classes like Environment often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Environment, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
9
 *  - (c) John MacFarlane
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace League\CommonMark\Environment;
16
17
use League\CommonMark\Configuration\Configuration;
18
use League\CommonMark\Configuration\ConfigurationAwareInterface;
19
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
20
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
21
use League\CommonMark\Event\AbstractEvent;
22
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
23
use League\CommonMark\Extension\ExtensionInterface;
24
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
25
use League\CommonMark\Parser\Block\BlockParserInterface;
26
use League\CommonMark\Parser\Inline\InlineParserInterface;
27
use League\CommonMark\Renderer\Block\BlockRendererInterface;
28
use League\CommonMark\Renderer\Inline\InlineRendererInterface;
29
use League\CommonMark\Util\PrioritizedList;
30
31
final class Environment implements ConfigurableEnvironmentInterface
32
{
33
    /**
34
     * @var ExtensionInterface[]
35
     */
36
    private $extensions = [];
37
38
    /**
39
     * @var ExtensionInterface[]
40
     */
41
    private $uninitializedExtensions = [];
42
43
    /**
44
     * @var bool
45
     */
46
    private $extensionsInitialized = false;
47
48
    /**
49
     * @var PrioritizedList<BlockParserInterface>
50
     */
51
    private $blockParsers;
52
53
    /**
54
     * @var PrioritizedList<InlineParserInterface>
55
     */
56
    private $inlineParsers;
57
58
    /**
59
     * @var array<string, PrioritizedList<InlineParserInterface>>
60
     */
61
    private $inlineParsersByCharacter = [];
62
63
    /**
64
     * @var DelimiterProcessorCollection
65
     */
66
    private $delimiterProcessors;
67
68
    /**
69
     * @var array<string, PrioritizedList<BlockRendererInterface>>
70
     */
71
    private $blockRenderersByClass = [];
72
73
    /**
74
     * @var array<string, PrioritizedList<InlineRendererInterface>>
75
     */
76
    private $inlineRenderersByClass = [];
77
78
    /**
79
     * @var array<string, PrioritizedList<callable>>
80
     */
81
    private $listeners = [];
82
83
    /**
84
     * @var Configuration
85
     */
86
    private $config;
87
88
    /**
89
     * @var string
90
     */
91
    private $inlineParserCharacterRegex;
92
93
    /**
94
     * @param array<string, mixed> $config
95
     */
96 2652
    public function __construct(array $config = [])
97
    {
98 2652
        $this->config = new Configuration($config);
99
100 2652
        $this->blockParsers = new PrioritizedList();
101 2652
        $this->inlineParsers = new PrioritizedList();
102 2652
        $this->delimiterProcessors = new DelimiterProcessorCollection();
103 2652
    }
104
105 2511
    public function mergeConfig(array $config = []): void
106
    {
107 2511
        $this->assertUninitialized('Failed to modify configuration.');
108
109 2508
        $this->config->merge($config);
110 2508
    }
111
112 6
    public function setConfig(array $config = []): void
113
    {
114 6
        $this->assertUninitialized('Failed to modify configuration.');
115
116 3
        $this->config->replace($config);
117 3
    }
118
119 2514
    public function getConfig($key = null, $default = null)
120
    {
121 2514
        return $this->config->get($key, $default);
122
    }
123
124 2502
    public function addBlockParser(BlockParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
125
    {
126 2502
        $this->assertUninitialized('Failed to add block parser.');
127
128 2499
        $this->blockParsers->add($parser, $priority);
129 2499
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
130
131 2499
        return $this;
132
    }
133
134 2505
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
135
    {
136 2505
        $this->assertUninitialized('Failed to add inline parser.');
137
138 2502
        $this->inlineParsers->add($parser, $priority);
139 2502
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
140
141 2502
        foreach ($parser->getCharacters() as $character) {
142 2499
            if (!isset($this->inlineParsersByCharacter[$character])) {
143 2499
                $this->inlineParsersByCharacter[$character] = new PrioritizedList();
144
            }
145
146 2499
            $this->inlineParsersByCharacter[$character]->add($parser, $priority);
147
        }
148
149 2502
        return $this;
150
    }
151
152 2502
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
153
    {
154 2502
        $this->assertUninitialized('Failed to add delimiter processor.');
155 2499
        $this->delimiterProcessors->add($processor);
156 2499
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
157
158 2499
        return $this;
159
    }
160
161 2508
    public function addBlockRenderer($blockClass, BlockRendererInterface $blockRenderer, int $priority = 0): ConfigurableEnvironmentInterface
162
    {
163 2508
        $this->assertUninitialized('Failed to add block renderer.');
164
165 2505
        if (!isset($this->blockRenderersByClass[$blockClass])) {
166 2505
            $this->blockRenderersByClass[$blockClass] = new PrioritizedList();
167
        }
168
169 2505
        $this->blockRenderersByClass[$blockClass]->add($blockRenderer, $priority);
170 2505
        $this->injectEnvironmentAndConfigurationIfNeeded($blockRenderer);
171
172 2505
        return $this;
173
    }
174
175 2508
    public function addInlineRenderer(string $inlineClass, InlineRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
176
    {
177 2508
        $this->assertUninitialized('Failed to add inline renderer.');
178
179 2505
        if (!isset($this->inlineRenderersByClass[$inlineClass])) {
180 2505
            $this->inlineRenderersByClass[$inlineClass] = new PrioritizedList();
181
        }
182
183 2505
        $this->inlineRenderersByClass[$inlineClass]->add($renderer, $priority);
184 2505
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
185
186 2505
        return $this;
187
    }
188
189 2526
    public function getBlockParsers(): iterable
190
    {
191 2526
        if (!$this->extensionsInitialized) {
192 36
            $this->initializeExtensions();
193
        }
194
195 2526
        return $this->blockParsers->getIterator();
196
    }
197
198 2451
    public function getInlineParsersForCharacter(string $character): iterable
199
    {
200 2451
        if (!$this->extensionsInitialized) {
201 18
            $this->initializeExtensions();
202
        }
203
204 2451
        if (!isset($this->inlineParsersByCharacter[$character])) {
205 2277
            return [];
206
        }
207
208 1374
        return $this->inlineParsersByCharacter[$character]->getIterator();
209
    }
210
211 2454
    public function getDelimiterProcessors(): DelimiterProcessorCollection
212
    {
213 2454
        if (!$this->extensionsInitialized) {
214 6
            $this->initializeExtensions();
215
        }
216
217 2454
        return $this->delimiterProcessors;
218
    }
219
220 2496
    public function getBlockRenderersForClass(string $blockClass): iterable
221
    {
222 2496
        if (!$this->extensionsInitialized) {
223 15
            $this->initializeExtensions();
224
        }
225
226 2496
        return $this->getRenderersByClass($this->blockRenderersByClass, $blockClass, BlockRendererInterface::class);
227
    }
228
229 2208
    public function getInlineRenderersForClass(string $inlineClass): iterable
230
    {
231 2208
        if (!$this->extensionsInitialized) {
232 18
            $this->initializeExtensions();
233
        }
234
235 2208
        return $this->getRenderersByClass($this->inlineRenderersByClass, $inlineClass, InlineRendererInterface::class);
236
    }
237
238
    /**
239
     * Get all registered extensions
240
     *
241
     * @return ExtensionInterface[]
242
     */
243 12
    public function getExtensions(): iterable
244
    {
245 12
        return $this->extensions;
246
    }
247
248
    /**
249
     * Add a single extension
250
     *
251
     * @param ExtensionInterface $extension
252
     *
253
     * @return $this
254
     */
255 2514
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
256
    {
257 2514
        $this->assertUninitialized('Failed to add extension.');
258
259 2511
        $this->extensions[] = $extension;
260 2511
        $this->uninitializedExtensions[] = $extension;
261
262 2511
        return $this;
263
    }
264
265 2589
    private function initializeExtensions(): void
266
    {
267
        // Ask all extensions to register their components
268 2589
        while (!empty($this->uninitializedExtensions)) {
269 2493
            foreach ($this->uninitializedExtensions as $i => $extension) {
270 2493
                $extension->register($this);
271 2493
                unset($this->uninitializedExtensions[$i]);
272
            }
273
        }
274
275 2589
        $this->extensionsInitialized = true;
276
277
        // Lastly, let's build a regex which matches non-inline characters
278
        // This will enable a huge performance boost with inline parsing
279 2589
        $this->buildInlineParserCharacterRegex();
280 2589
    }
281
282
    /**
283
     * @param object $object
284
     */
285 2556
    private function injectEnvironmentAndConfigurationIfNeeded($object): void
286
    {
287 2556
        if ($object instanceof EnvironmentAwareInterface) {
288 2508
            $object->setEnvironment($this);
289
        }
290
291 2556
        if ($object instanceof ConfigurationAwareInterface) {
292 2508
            $object->setConfiguration($this->config);
293
        }
294 2556
    }
295
296 2502
    public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
297
    {
298 2502
        $environment = new static();
299 2502
        $environment->addExtension(new CommonMarkCoreExtension());
300 2502
        $environment->mergeConfig([
301 834
            'renderer' => [
302 1668
                'block_separator' => "\n",
303
                'inner_separator' => "\n",
304
                'soft_break'      => "\n",
305
            ],
306 2502
            'html_input'         => self::HTML_INPUT_ALLOW,
307
            'allow_unsafe_links' => true,
308
            'max_nesting_level'  => \INF,
309
        ]);
310
311 2502
        return $environment;
312
    }
313
314 72
    public static function createGFMEnvironment(): ConfigurableEnvironmentInterface
315
    {
316 72
        $environment = self::createCommonMarkEnvironment();
317 72
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
318
319 72
        return $environment;
320
    }
321
322 2262
    public function getInlineParserCharacterRegex(): string
323
    {
324 2262
        return $this->inlineParserCharacterRegex;
325
    }
326
327 258
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
328
    {
329 258
        $this->assertUninitialized('Failed to add event listener.');
330
331 255
        if (!isset($this->listeners[$eventClass])) {
332 255
            $this->listeners[$eventClass] = new PrioritizedList();
333
        }
334
335 255
        $this->listeners[$eventClass]->add($listener, $priority);
336
337 255
        if (\is_object($listener)) {
338 255
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
339 36
        } elseif (\is_array($listener) && \is_object($listener[0])) {
340 36
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
341
        }
342
343 255
        return $this;
344
    }
345
346 2496
    public function dispatch(AbstractEvent $event): void
347
    {
348 2496
        if (!$this->extensionsInitialized) {
349 2496
            $this->initializeExtensions();
350
        }
351
352 2496
        $type = \get_class($event);
353
354 2496
        foreach ($this->listeners[$type] ?? [] as $listener) {
355 252
            if ($event->isPropagationStopped()) {
356 3
                return;
357
            }
358
359 252
            $listener($event);
360
        }
361 2493
    }
362
363 2589
    private function buildInlineParserCharacterRegex(): void
364
    {
365 2589
        $chars = \array_unique(\array_merge(
366 2589
            \array_keys($this->inlineParsersByCharacter),
367 2589
            $this->delimiterProcessors->getDelimiterCharacters()
368
        ));
369
370 2589
        if (empty($chars)) {
371
            // If no special inline characters exist then parse the whole line
372 84
            $this->inlineParserCharacterRegex = '/^.+$/u';
373
        } else {
374
            // Match any character which inline parsers are not interested in
375 2505
            $this->inlineParserCharacterRegex = '/^[^' . \preg_quote(\implode('', $chars), '/') . ']+/u';
376
        }
377 2589
    }
378
379
    /**
380
     * @param string $message
381
     *
382
     * @throws \RuntimeException
383
     */
384 2607
    private function assertUninitialized(string $message): void
385
    {
386 2607
        if ($this->extensionsInitialized) {
387 27
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
388
        }
389 2580
    }
390
391
    /**
392
     * @param array<string, PrioritizedList> $list
393
     * @param string                         $class
394
     * @param string                         $type
395
     *
396
     * @return iterable
397
     *
398
     * @phpstan-template T
399
     *
400
     * @phpstan-param array<string, PrioritizedList<T>> $list
401
     * @phpstan-param string                            $class
402
     * @phpstan-param class-string<T>                   $type
403
     *
404
     * @phpstan-return iterable<T>
405
     */
406 2514
    private function getRenderersByClass(array &$list, string $class, string $type): iterable
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
407
    {
408
        // If renderers are defined for this specific class, return them immediately
409 2514
        if (isset($list[$class])) {
410 2499
            return $list[$class];
411
        }
412
413 51
        while ($parent = \get_parent_class($parent ?? $class)) {
414 42
            if (!isset($list[$parent])) {
415 12
                continue;
416
            }
417
418
            // "Cache" this result to avoid future loops
419 36
            return $list[$class] = $list[$parent];
420
        }
421
422 15
        return [];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type documented by League\CommonMark\Enviro...nt::getRenderersByClass of type League\CommonMark\Environment\iterable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
423
    }
424
}
425