Passed
Push — master ( b42680...426658 )
by Marc
02:40
created

PerformanceContext   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 371
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 167
dl 0
loc 371
rs 9.0399
c 0
b 0
f 0
wmc 42

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