Passed
Push — master ( afef6b...b42680 )
by Marc
03:21
created

PerformanceContext   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 385
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 169
dl 0
loc 385
rs 8.96
c 0
b 0
f 0
wmc 43

21 Methods

Rating   Name   Duplication   Size   Complexity  
A browserCacheMustNotBeEnabledForResources() 0 7 1
A assertContentIsMinified() 0 6 1
A minimizeCss() 0 12 1
A htmlShouldNotBeMinified() 0 5 1
A getResourceXpath() 0 19 6
A criticalCssShouldNotExistInHead() 0 5 1
A browserCacheMustBeEnabledForResources() 0 25 2
A jsShouldNotLoadAsyncOr() 0 5 1
A minimizeJs() 0 12 1
A getResourceUrl() 0 18 3
A criticalCssShouldExistInHead() 0 7 1
A cssFilesShouldLoadDeferred() 0 7 1
A cssOrJavascriptFilesShouldNotBeMinified() 0 7 1
A htmlShouldBeMinified() 0 5 1
A minimizeHtml() 0 3 1
A cssOrJavascriptFilesShouldBeMinified() 0 19 5
A getPageResources() 0 25 5
A checkResourceCache() 0 32 2
A getSelfHostedPageResources() 0 17 3
A assertResourceTypeIsValid() 0 8 2
A javascriptFilesShouldLoadAsync() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like PerformanceContext 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.

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 PerformanceContext, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace MOrtola\BehatSEOContexts\Context;
4
5
use Behat\Mink\Element\NodeElement;
6
use Behat\Mink\Exception\UnsupportedDriverActionException;
7
use Behat\Symfony2Extension\Driver\KernelDriver;
8
use Exception;
9
use InvalidArgumentException;
10
use PHPUnit\Framework\Assert;
11
12
class PerformanceContext extends BaseContext
13
{
14
    const RES_EXT = [
15
        'PNG'             => 'png',
16
        'HTML'            => 'html',
17
        'JPEG'            => 'jpeg',
18
        'GIF'             => 'gif',
19
        'ICO'             => 'ico',
20
        'JAVASCRIPT'      => 'js',
21
        'CSS'             => 'css',
22
        'CSS_INLINE_HEAD' => 'css-inline-head',
23
        'CSS_LINK_HEAD'   => 'css-link-head',
24
    ];
25
26
    /**
27
     * @throws Exception
28
     *
29
     * @Then /^Javascript code should load (async|defer)$/
30
     */
31
    public function javascriptFilesShouldLoadAsync(): void
32
    {
33
        foreach ($this->getSelfHostedPageResources(self::RES_EXT['JAVASCRIPT']) as $scriptElement) {
34
            Assert::assertTrue(
35
                $scriptElement->hasAttribute('async') || $scriptElement->hasAttribute('defer'),
36
                sprintf(
37
                    'Javascript file %s is render blocking in %s',
38
                    $this->getResourceUrl($scriptElement, self::RES_EXT['JAVASCRIPT']),
39
                    $this->getCurrentUrl()
40
                )
41
            );
42
        }
43
    }
44
45
    /**
46
     * @return NodeElement[]
47
     */
48
    private function getPageResources(string $resourceType, string $host = null): array
49
    {
50
        if (!$xpath = $this->getResourceXpath($resourceType)) {
51
            return [];
52
        }
53
54
        if ('external' === $host) {
55
            $xpath = preg_replace(
56
                '/\[contains\(@(.*),/',
57
                '[not(starts-with(@$1,"'.$this->webUrl.'") or starts-with(@$1,"/")) and contains(@$1,',
58
                $xpath
59
            );
60
        } elseif (null !== $host) {
61
            $xpath = preg_replace(
62
                '/\[contains\(@(.*),/',
63
                '[(starts-with(@$1,"'.$host.'") or starts-with(@$1,"/")) and contains(@$1,',
64
                $xpath
65
            );
66
        }
67
68
        if ($xpath) {
69
            return $this->getSession()->getPage()->findAll('xpath', $xpath);
70
        }
71
72
        return [];
73
    }
74
75
    /**
76
     * @return NodeElement[]
77
     */
78
    private function getSelfHostedPageResources(string $resourceType): array
79
    {
80
        if (!$xpath = $this->getResourceXpath($resourceType)) {
81
            return [];
82
        }
83
84
        $xpath = preg_replace(
85
            '/\[contains\(@(.*),/',
86
            sprintf('[(starts-with(@$1,"%s") or starts-with(@$1,"/")) and contains(@$1,', $this->webUrl),
87
            $xpath
88
        );
89
90
        if ($xpath) {
91
            return $this->getSession()->getPage()->findAll('xpath', $xpath);
92
        }
93
94
        return [];
95
    }
96
97
    private function getResourceXpath(string $resourceType): string
98
    {
99
        if (in_array($resourceType, [self::RES_EXT['JPEG'], self::RES_EXT['PNG'], self::RES_EXT['GIF']], true)) {
100
            return sprintf('//img[contains(@src,".%s")]', $resourceType);
101
        }
102
        if (in_array($resourceType, [self::RES_EXT['ICO'], self::RES_EXT['CSS']], true)) {
103
            return sprintf('//link[contains(@href,".%s")]', $resourceType);
104
        }
105
        if (self::RES_EXT['JAVASCRIPT'] === $resourceType) {
106
            return '//script[contains(@src,".js")]';
107
        }
108
        if (self::RES_EXT['CSS_INLINE_HEAD'] === $resourceType) {
109
            return '//head//style';
110
        }
111
        if (self::RES_EXT['CSS_LINK_HEAD'] === $resourceType) {
112
            return '//head//link[contains(@href,".css")]';
113
        }
114
115
        return '';
116
    }
117
118
    /**
119
     * @throws Exception
120
     */
121
    private function getResourceUrl(NodeElement $element, string $resourceType): ?string
122
    {
123
        $this->assertResourceTypeIsValid($resourceType);
124
125
        if (in_array(
126
            $resourceType,
127
            [self::RES_EXT['PNG'], self::RES_EXT['JPEG'], self::RES_EXT['GIF'], self::RES_EXT['JAVASCRIPT']],
128
            true
129
        )) {
130
            return $element->getAttribute('src');
131
        }
132
133
        if (in_array($resourceType, [self::RES_EXT['CSS'], self::RES_EXT['ICO']], true)) {
134
            return $element->getAttribute('href');
135
        }
136
137
        throw new InvalidArgumentException(
138
            sprintf('%s resource type url is not implemented', $resourceType)
139
        );
140
    }
141
142
    private function assertResourceTypeIsValid(string $resourceType): void
143
    {
144
        if (!in_array($resourceType, self::RES_EXT, true)) {
145
            throw new InvalidArgumentException(
146
                sprintf(
147
                    '%s resource type is not valid. Allowed types are: %s',
148
                    $resourceType,
149
                    implode(',', self::RES_EXT)
150
                )
151
            );
152
        }
153
    }
154
155
    /**
156
     * @throws Exception
157
     *
158
     * @Then HTML code should be minified
159
     */
160
    public function htmlShouldBeMinified(): void
161
    {
162
        $this->assertContentIsMinified(
163
            $this->getSession()->getPage()->getContent(),
164
            $this->minimizeHtml($this->getSession()->getPage()->getContent())
165
        );
166
    }
167
168
    private function assertContentIsMinified(string $content, string $contentMinified): void
169
    {
170
        Assert::assertSame(
171
            $content,
172
            $contentMinified,
173
            'Code is not minified.'
174
        );
175
    }
176
177
    private function minimizeHtml(string $html): string
178
    {
179
        return preg_replace('/(?<=>)\s+|\s+(?=<)/', '', $html) ?? $html;
180
    }
181
182
    /**
183
     * @throws Exception
184
     *
185
     * @Then CSS code should load deferred
186
     */
187
    public function cssFilesShouldLoadDeferred(): void
188
    {
189
        Assert::assertEmpty(
190
            $this->getSelfHostedPageResources(self::RES_EXT['CSS_LINK_HEAD']),
191
            sprintf(
192
                'Some self hosted css files are loading in head in %s',
193
                $this->getCurrentUrl()
194
            )
195
        );
196
    }
197
198
    /**
199
     * @throws Exception
200
     *
201
     * @Then critical CSS code should exist in head
202
     */
203
    public function criticalCssShouldExistInHead(): void
204
    {
205
        Assert::assertNotEmpty(
206
            $this->getSelfHostedPageResources(self::RES_EXT['CSS_INLINE_HEAD']),
207
            sprintf(
208
                'No inline css is loading in head in %s',
209
                $this->getCurrentUrl()
210
            )
211
        );
212
    }
213
214
    /**
215
     * @Then HTML code should not be minified
216
     */
217
    public function htmlShouldNotBeMinified(): void
218
    {
219
        $this->assertInverse(
220
            [$this, 'htmlShouldBeMinified'],
221
            'HTML should not be minified.'
222
        );
223
    }
224
225
    /**
226
     * @Then /^(CSS|Javascript) code should not be minified$/
227
     */
228
    public function cssOrJavascriptFilesShouldNotBeMinified(string $resourceType): void
229
    {
230
        $this->assertInverse(
231
            function () use ($resourceType) {
232
                $this->cssOrJavascriptFilesShouldBeMinified($resourceType);
233
            },
234
            sprintf('%s should not be minified.', $resourceType)
235
        );
236
    }
237
238
    /**
239
     * @throws UnsupportedDriverActionException
240
     * @throws Exception
241
     * @Then /^(CSS|Javascript) code should be minified$/
242
     */
243
    public function cssOrJavascriptFilesShouldBeMinified(string $resourceType): void
244
    {
245
        $this->doesNotSupportDriver(KernelDriver::class);
246
247
        $resourceType = 'Javascript' === $resourceType ? 'js' : 'css';
248
249
        foreach ($this->getSelfHostedPageResources($resourceType) as $element) {
250
            if ($url = $this->getResourceUrl($element, $resourceType)) {
251
                $this->getSession()->visit($url);
252
            }
253
254
            $this->assertContentIsMinified(
255
                $this->getSession()->getPage()->getContent(),
256
                'js' === $resourceType ?
257
                    $this->minimizeJs($this->getSession()->getPage()->getContent())
258
                    : $this->minimizeCss($this->getSession()->getPage()->getContent())
259
            );
260
261
            $this->getSession()->back();
262
        }
263
    }
264
265
    private function minimizeJs(string $javascript): string
266
    {
267
        return preg_replace(
268
            [
269
                       '#\s*("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')\s*|\s*\/\*(?!\!|@cc_on)(?>[\s\S]*?\*\/)\s*|
270
                \s*(?<![\:\=])\/\/.*(?=[\n\r]|$)|^\s*|\s*$#',
271
                       '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/)|\/(?!\/)[^\n\r]*?\/(?=[\s.,;]|
272
                [gimuy]|$))|\s*([!%&*\(\)\-=+\[\]\{\}|;:,.<>?\/])\s*#s',
273
                   ],
274
            ['$1', '$1$2'],
275
            $javascript
276
        ) ?? $javascript;
277
    }
278
279
    private function minimizeCss(string $css): string
280
    {
281
        return preg_replace(
282
            [
283
                       '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')|\/\*(?!\!)(?>.*?\*\/)|^\s*|\s*$#s',
284
                       '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/))|\s*+;\s*+(})\s*+|\s*+([*$~^|
285
                ]?+=|[{};,>~+]|\s*+-(?![0-9\.])|!important\b)\s*+|([[(:])\s++|\s++([])])|\s++(:)\s*+(?!(?>[^{}"\']++|
286
                "(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')*+{)|^\s++|\s++\z|(\s)\s+#si',
287
                   ],
288
            ['$1', '$1$2$3$4$5$6$7'],
289
            $css
290
        ) ?? $css;
291
    }
292
293
    /**
294
     * @Then critical CSS code should not exist in head
295
     */
296
    public function criticalCssShouldNotExistInHead(): void
297
    {
298
        $this->assertInverse(
299
            [$this, 'criticalCssShouldExistInHead'],
300
            'Critical CSS exist in head.'
301
        );
302
    }
303
304
    /**
305
     * @Then /^browser cache should not be enabled for (.+|external|internal) (png|jpeg|gif|ico|js|css) resources$/
306
     */
307
    public function browserCacheMustNotBeEnabledForResources(string $host, string $resourceType): void
308
    {
309
        $this->assertInverse(
310
            function () use ($host, $resourceType) {
311
                $this->browserCacheMustBeEnabledForResources($host, $resourceType);
312
            },
313
            sprintf('Browser cache is enabled for %s resources.', $resourceType)
314
        );
315
    }
316
317
    /**
318
     * @throws UnsupportedDriverActionException
319
     * @throws Exception
320
     * @Then /^browser cache should be enabled for (.+|external|internal) (png|jpeg|gif|ico|js|css) resources$/
321
     */
322
    public function browserCacheMustBeEnabledForResources(string $host, string $resourceType): void
323
    {
324
        $this->doesNotSupportDriver(KernelDriver::class);
325
326
        switch ($host) {
327
            case 'internal':
328
                $elements = $this->getSelfHostedPageResources($resourceType);
329
                break;
330
            default:
331
                $elements = $this->getPageResources($resourceType, $host);
332
                break;
333
        }
334
335
        Assert::assertNotEmpty(
336
            $elements,
337
            sprintf(
338
                'The are not %s resources in %s.',
339
                $host,
340
                $this->getCurrentUrl()
341
            )
342
        );
343
344
        $this->checkResourceCache(
345
            $elements[array_rand($elements)],
346
            $resourceType
347
        );
348
    }
349
350
    /**
351
     * @throws Exception
352
     */
353
    private function checkResourceCache(NodeElement $element, string $resourceType): void
354
    {
355
        if ($url = $this->getResourceUrl($element, $resourceType)) {
356
            $this->getSession()->visit($url);
357
        }
358
359
        $headers = array_change_key_case($this->getSession()->getResponseHeaders());
360
        $this->getSession()->back();
361
362
        Assert::assertTrue(
363
            array_key_exists('cache-control', $headers),
364
            sprintf(
365
                'Browser cache is not enabled for %s resources. Cache-Control HTTP header was not received.',
366
                $resourceType
367
            )
368
        );
369
370
        Assert::assertNotContains(
371
            'no-cache',
372
            $headers['cache-control'],
373
            sprintf(
374
                'Browser cache is not enabled for %s resources. Cache-Control HTTP header is "no-cache".',
375
                $resourceType
376
            )
377
        );
378
379
        Assert::assertNotContains(
380
            'max-age=0',
381
            $headers['cache-control'],
382
            sprintf(
383
                'Browser cache is not enabled for %s resources. Cache-Control HTTP header is "max-age=0".',
384
                $resourceType
385
            )
386
        );
387
    }
388
389
    /**
390
     * @Then /^Javascript code should not load (async|defer)$/
391
     */
392
    public function jsShouldNotLoadAsyncOr(): void
393
    {
394
        $this->assertInverse(
395
            [$this, 'javascriptFilesShouldLoadAsync'],
396
            'All JS files load async.'
397
        );
398
    }
399
}
400