Completed
Push — 1.5 ( 5cf186...5e2eb3 )
by Colin
09:22 queued 08:01
created

Environment   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 394
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 100%

Importance

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

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getConfig() 0 4 1
A mergeConfig() 0 6 1
A __construct() 0 8 1
A setConfig() 0 6 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 5

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;
16
17
use League\CommonMark\Block\Parser\BlockParserInterface;
18
use League\CommonMark\Block\Renderer\BlockRendererInterface;
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\CommonMarkCoreExtension;
23
use League\CommonMark\Extension\ExtensionInterface;
24
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
25
use League\CommonMark\Inline\Parser\InlineParserInterface;
26
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
27
use League\CommonMark\Util\Configuration;
28
use League\CommonMark\Util\ConfigurationAwareInterface;
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 2952
    public function __construct(array $config = [])
97
    {
98 2952
        $this->config = new Configuration($config);
99
100 2952
        $this->blockParsers = new PrioritizedList();
101 2952
        $this->inlineParsers = new PrioritizedList();
102 2952
        $this->delimiterProcessors = new DelimiterProcessorCollection();
103 2952
    }
104
105 2811
    public function mergeConfig(array $config = [])
106
    {
107 2811
        $this->assertUninitialized('Failed to modify configuration.');
108
109 2808
        $this->config->merge($config);
110 2808
    }
111
112 18
    public function setConfig(array $config = [])
113
    {
114 18
        $this->assertUninitialized('Failed to modify configuration.');
115
116 15
        $this->config->replace($config);
117 15
    }
118
119 2814
    public function getConfig($key = null, $default = null)
120
    {
121 2814
        return $this->config->get($key, $default);
122
    }
123
124 2802
    public function addBlockParser(BlockParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
125
    {
126 2802
        $this->assertUninitialized('Failed to add block parser.');
127
128 2799
        $this->blockParsers->add($parser, $priority);
129 2799
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
130
131 2799
        return $this;
132
    }
133
134 2805
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
135
    {
136 2805
        $this->assertUninitialized('Failed to add inline parser.');
137
138 2802
        $this->inlineParsers->add($parser, $priority);
139 2802
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
140
141 2802
        foreach ($parser->getCharacters() as $character) {
142 2799
            if (!isset($this->inlineParsersByCharacter[$character])) {
143 2799
                $this->inlineParsersByCharacter[$character] = new PrioritizedList();
144
            }
145
146 2799
            $this->inlineParsersByCharacter[$character]->add($parser, $priority);
147
        }
148
149 2802
        return $this;
150
    }
151
152 2802
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
153
    {
154 2802
        $this->assertUninitialized('Failed to add delimiter processor.');
155 2799
        $this->delimiterProcessors->add($processor);
156 2799
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
157
158 2799
        return $this;
159
    }
160
161 2808
    public function addBlockRenderer($blockClass, BlockRendererInterface $blockRenderer, int $priority = 0): ConfigurableEnvironmentInterface
162
    {
163 2808
        $this->assertUninitialized('Failed to add block renderer.');
164
165 2805
        if (!isset($this->blockRenderersByClass[$blockClass])) {
166 2805
            $this->blockRenderersByClass[$blockClass] = new PrioritizedList();
167
        }
168
169 2805
        $this->blockRenderersByClass[$blockClass]->add($blockRenderer, $priority);
170 2805
        $this->injectEnvironmentAndConfigurationIfNeeded($blockRenderer);
171
172 2805
        return $this;
173
    }
174
175 2808
    public function addInlineRenderer(string $inlineClass, InlineRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
176
    {
177 2808
        $this->assertUninitialized('Failed to add inline renderer.');
178
179 2805
        if (!isset($this->inlineRenderersByClass[$inlineClass])) {
180 2805
            $this->inlineRenderersByClass[$inlineClass] = new PrioritizedList();
181
        }
182
183 2805
        $this->inlineRenderersByClass[$inlineClass]->add($renderer, $priority);
184 2805
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
185
186 2805
        return $this;
187
    }
188
189 2823
    public function getBlockParsers(): iterable
190
    {
191 2823
        if (!$this->extensionsInitialized) {
192 36
            $this->initializeExtensions();
193
        }
194
195 2823
        return $this->blockParsers->getIterator();
196
    }
197
198 2748
    public function getInlineParsersForCharacter(string $character): iterable
199
    {
200 2748
        if (!$this->extensionsInitialized) {
201 18
            $this->initializeExtensions();
202
        }
203
204 2748
        if (!isset($this->inlineParsersByCharacter[$character])) {
205 2574
            return [];
206
        }
207
208 1668
        return $this->inlineParsersByCharacter[$character]->getIterator();
209
    }
210
211 2751
    public function getDelimiterProcessors(): DelimiterProcessorCollection
212
    {
213 2751
        if (!$this->extensionsInitialized) {
214 6
            $this->initializeExtensions();
215
        }
216
217 2751
        return $this->delimiterProcessors;
218
    }
219
220 2793
    public function getBlockRenderersForClass(string $blockClass): iterable
221
    {
222 2793
        if (!$this->extensionsInitialized) {
223 15
            $this->initializeExtensions();
224
        }
225
226 2793
        return $this->getRenderersByClass($this->blockRenderersByClass, $blockClass, BlockRendererInterface::class);
227
    }
228
229 2502
    public function getInlineRenderersForClass(string $inlineClass): iterable
230
    {
231 2502
        if (!$this->extensionsInitialized) {
232 18
            $this->initializeExtensions();
233
        }
234
235 2502
        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 2814
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
256
    {
257 2814
        $this->assertUninitialized('Failed to add extension.');
258
259 2811
        $this->extensions[] = $extension;
260 2811
        $this->uninitializedExtensions[] = $extension;
261
262 2811
        return $this;
263
    }
264
265 2889
    private function initializeExtensions(): void
266
    {
267
        // Ask all extensions to register their components
268 2889
        while (!empty($this->uninitializedExtensions)) {
269 2793
            foreach ($this->uninitializedExtensions as $i => $extension) {
270 2793
                $extension->register($this);
271 2793
                unset($this->uninitializedExtensions[$i]);
272
            }
273
        }
274
275 2886
        $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 2886
        $this->buildInlineParserCharacterRegex();
280 2886
    }
281
282
    /**
283
     * @param object $object
284
     */
285 2856
    private function injectEnvironmentAndConfigurationIfNeeded($object): void
286
    {
287 2856
        if ($object instanceof EnvironmentAwareInterface) {
288 2808
            $object->setEnvironment($this);
289
        }
290
291 2856
        if ($object instanceof ConfigurationAwareInterface) {
292 2808
            $object->setConfiguration($this->config);
293
        }
294 2856
    }
295
296 2802
    public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
297
    {
298 2802
        $environment = new static();
299 2802
        $environment->addExtension(new CommonMarkCoreExtension());
300 2802
        $environment->mergeConfig([
301 1868
            'renderer' => [
302 934
                'block_separator' => "\n",
303
                'inner_separator' => "\n",
304
                'soft_break'      => "\n",
305
            ],
306 2802
            'html_input'         => self::HTML_INPUT_ALLOW,
307
            'allow_unsafe_links' => true,
308
            'max_nesting_level'  => \INF,
309
        ]);
310
311 2802
        return $environment;
312
    }
313
314 132
    public static function createGFMEnvironment(): ConfigurableEnvironmentInterface
315
    {
316 132
        $environment = self::createCommonMarkEnvironment();
317 132
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
318
319 132
        return $environment;
320
    }
321
322 2559
    public function getInlineParserCharacterRegex(): string
323
    {
324 2559
        return $this->inlineParserCharacterRegex;
325
    }
326
327 522
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
328
    {
329 522
        $this->assertUninitialized('Failed to add event listener.');
330
331 519
        if (!isset($this->listeners[$eventClass])) {
332 519
            $this->listeners[$eventClass] = new PrioritizedList();
333
        }
334
335 519
        $this->listeners[$eventClass]->add($listener, $priority);
336
337 519
        if (\is_object($listener)) {
338 498
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
339 99
        } elseif (\is_array($listener) && \is_object($listener[0])) {
340 99
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
341
        }
342
343 519
        return $this;
344
    }
345
346 2796
    public function dispatch(AbstractEvent $event): void
347
    {
348 2796
        if (!$this->extensionsInitialized) {
349 2796
            $this->initializeExtensions();
350
        }
351
352 2793
        $type = \get_class($event);
353
354 2793
        foreach ($this->listeners[$type] ?? [] as $listener) {
355 516
            if ($event->isPropagationStopped()) {
356 3
                return;
357
            }
358
359 516
            $listener($event);
360
        }
361 2790
    }
362
363 2886
    private function buildInlineParserCharacterRegex(): void
364
    {
365 2886
        $chars = \array_unique(\array_merge(
366 2886
            \array_keys($this->inlineParsersByCharacter),
367 2886
            $this->delimiterProcessors->getDelimiterCharacters()
368
        ));
369
370 2886
        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 2802
            $this->inlineParserCharacterRegex = '/^[^' . \preg_quote(\implode('', $chars), '/') . ']+/u';
376
        }
377 2886
    }
378
379
    /**
380
     * @param string $message
381
     *
382
     * @throws \RuntimeException
383
     */
384 2907
    private function assertUninitialized(string $message): void
385
    {
386 2907
        if ($this->extensionsInitialized) {
387 27
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
388
        }
389 2880
    }
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 2811
    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 2811
        if (isset($list[$class])) {
410 2796
            return $list[$class];
411
        }
412
413 69
        while (\class_exists($parent = $parent ?? $class) && $parent = \get_parent_class($parent)) {
414 60
            if (!isset($list[$parent])) {
415 12
                continue;
416
            }
417
418
            // "Cache" this result to avoid future loops
419 54
            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\Environment::getRenderersByClass of type League\CommonMark\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