Passed
Push — v4 ( 53a2b4...f1d7a8 )
by Andrew
44:05 queued 19:41
created

UrlHelper   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Test Coverage

Coverage 10%

Importance

Changes 7
Bugs 4 Features 1
Metric Value
wmc 49
eloc 108
c 7
b 4
f 1
dl 0
loc 250
ccs 11
cts 110
cp 0.1
rs 8.48

8 Methods

Rating   Name   Duplication   Size   Complexity  
A decomposeUrl() 0 18 4
A siteUrl() 0 26 6
B encodeUrlQueryParams() 0 33 9
A mergeUrlWithPath() 0 17 4
A getSiteUrlOverrideSetting() 0 19 5
C absoluteUrlWithProtocol() 0 40 14
A pageTriggerValue() 0 18 6
A urlHasSubDir() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like UrlHelper 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 UrlHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\helpers;
13
14
use Craft;
15
use craft\errors\SiteNotFoundException;
16
use craft\helpers\UrlHelper as CraftUrlHelper;
17
use nystudio107\seomatic\Seomatic;
18
use Throwable;
19
use yii\base\Exception;
20
use function is_string;
21
22
/**
23
 * @author    nystudio107
24
 * @package   Seomatic
25
 * @since     3.0.0
26
 */
27
class UrlHelper extends CraftUrlHelper
28
{
29
    // Public Static Properties
30
    // =========================================================================
31
32
    // Public Static Methods
33
    // =========================================================================
34
35
    /**
36
     * @inheritDoc
37
     */
38
    public static function siteUrl(string $path = '', $params = null, string $scheme = null, int $siteId = null): string
39
    {
40
        try {
41
            $siteUrl = self::getSiteUrlOverrideSetting($siteId);
42
        } catch (Throwable $e) {
43
            // That's okay
44
        }
45
        if (!empty($siteUrl)) {
46
            $siteUrl = MetaValue::parseString($siteUrl);
47
            // Extract out just the path part
48
            $parts = self::decomposeUrl($path);
49
            $path = $parts['path'] . $parts['suffix'];
50
            $url = self::mergeUrlWithPath($siteUrl, $path);
51
            // Handle trailing slashes properly for generated URLs
52
            $generalConfig = Craft::$app->getConfig()->getGeneral();
53
            if ($generalConfig->addTrailingSlashesToUrls && !preg_match('/(.+\?.*)|(\.[^\/]+$)/', $url)) {
54
                $url = rtrim($url, '/') . '/';
55
            }
56
            if (!$generalConfig->addTrailingSlashesToUrls) {
57
                $url = rtrim($url, '/');
58
            }
59
60
            return DynamicMeta::sanitizeUrl(parent::urlWithParams($url, $params ?? []), false, false);
61
        }
62
63
        return DynamicMeta::sanitizeUrl(parent::siteUrl($path, $params, $scheme, $siteId), false, false);
64
    }
65
66
    /**
67
     * Merge the $url and $path together, combining any overlapping path segments
68
     *
69
     * @param string $url
70
     * @param string $path
71
     * @return string
72
     */
73
    public static function mergeUrlWithPath(string $url, string $path): string
74
    {
75
        $overlap = 0;
76
        $url = rtrim($url, '/') . '/';
77
        $path = '/' . ltrim($path, '/');
78
        $urlOffset = strlen($url);
79
        $pathLength = strlen($path);
80
        $pathOffset = 0;
81
        while ($urlOffset > 0 && $pathOffset < $pathLength) {
82
            $urlOffset--;
83
            $pathOffset++;
84
            if (str_starts_with($path, substr($url, $urlOffset, $pathOffset))) {
85
                $overlap = $pathOffset;
86
            }
87
        }
88
89
        return rtrim($url, '/') . '/' . ltrim(substr($path, $overlap), '/');
90
    }
91
92
    /**
93
     * Return the page trigger and the value of the page trigger (null if it doesn't exist)
94
     *
95
     * @return array
96
     */
97
    public static function pageTriggerValue(): array
98
    {
99
        $pageTrigger = Craft::$app->getConfig()->getGeneral()->pageTrigger;
100
        if (!is_string($pageTrigger) || $pageTrigger === '') {
101
            $pageTrigger = 'p';
102
        }
103
        // Is this query string-based pagination?
104
        if ($pageTrigger[0] === '?') {
105
            $pageTrigger = trim($pageTrigger, '?=');
106
        }
107
        // Avoid conflict with the path param
108
        $pathParam = Craft::$app->getConfig()->getGeneral()->pathParam;
109
        if ($pageTrigger === $pathParam) {
110
            $pageTrigger = $pathParam === 'p' ? 'pg' : 'p';
111
        }
112
        $pageTriggerValue = Craft::$app->getRequest()->getParam($pageTrigger);
113
114
        return [$pageTrigger, $pageTriggerValue];
115
    }
116
117
    /**
118
     * Return an absolute URL with protocol that curl will be happy with
119
     *
120
     * @param string $url
121
     *
122
     * @return string
123
     */
124
    public static function absoluteUrlWithProtocol($url): string
125
    {
126
        // Make this a full URL
127
        if (!self::isAbsoluteUrl($url)) {
128
            $protocol = 'http';
129
            if (isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'], 'on') === 0 || $_SERVER['HTTPS'] == 1)
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && strcasecmp...PROTO'], 'https') === 0, Probably Intended Meaning: IssetNode && (strcasecmp...ROTO'], 'https') === 0)
Loading history...
130
                || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0
131
            ) {
132
                $protocol = 'https';
133
            }
134
            if (self::isProtocolRelativeUrl($url)) {
135
                try {
136
                    $url = self::urlWithScheme($url, $protocol);
137
                } catch (SiteNotFoundException $e) {
138
                    Craft::error($e->getMessage(), __METHOD__);
139
                }
140
            } else {
141
                try {
142
                    $url = self::siteUrl($url, null, $protocol);
143
                    if (self::isProtocolRelativeUrl($url)) {
144
                        $url = self::urlWithScheme($url, $protocol);
145
                    }
146
                } catch (Exception $e) {
147
                    Craft::error($e->getMessage(), __METHOD__);
148
                }
149
            }
150
        }
151
        // Ensure that any spaces in the URL are encoded
152
        $url = str_replace(' ', '%20', $url);
153
154
        // Handle trailing slashes properly for generated URLs
155
        $generalConfig = Craft::$app->getConfig()->getGeneral();
156
        if ($generalConfig->addTrailingSlashesToUrls && !preg_match('/(.+\?.*)|(\.[^\/]+$)/', $url)) {
157
            $url = rtrim($url, '/') . '/';
158
        }
159
        if (!$generalConfig->addTrailingSlashesToUrls) {
160
            $url = rtrim($url, '/');
161
        }
162
163
        return DynamicMeta::sanitizeUrl($url, false, false);
164
    }
165
166
    /**
167
     * urlencode() just the query parameters in the URL
168
     *
169
     * @param string $url
170
     * @return string
171
     */
172 2
    public static function encodeUrlQueryParams(string $url): string
173
    {
174 2
        $urlParts = parse_url($url);
175 2
        $encodedUrl = "";
176 2
        if (isset($urlParts['scheme'])) {
177
            $encodedUrl .= $urlParts['scheme'] . '://';
178
        }
179 2
        if (isset($urlParts['host'])) {
180
            $encodedUrl .= $urlParts['host'];
181
        }
182 2
        if (isset($urlParts['port'])) {
183
            $encodedUrl .= ':' . $urlParts['port'];
184
        }
185 2
        if (isset($urlParts['path'])) {
186 2
            $encodedUrl .= $urlParts['path'];
187
        }
188 2
        if (isset($urlParts['query'])) {
189
            $query = explode('&', $urlParts['query']);
190
            foreach ($query as $j => $value) {
191
                $value = explode('=', $value, 2);
192
                if (count($value) === 2) {
193
                    $query[$j] = urlencode($value[0]) . '=' . urlencode($value[1]);
194
                } else {
195
                    $query[$j] = urlencode($value[0]);
196
                }
197
            }
198
            $encodedUrl .= '?' . implode('&', $query);
199
        }
200 2
        if (isset($urlParts['fragment'])) {
201
            $encodedUrl .= '#' . $urlParts['fragment'];
202
        }
203
204 2
        return $encodedUrl;
205
    }
206
207
    /**
208
     * Return whether this URL has a sub-directory as part of it
209
     *
210
     * @param string $url
211
     * @return bool
212
     */
213
    public static function urlHasSubDir(string $url): bool
214
    {
215
        return !empty(parse_url(trim($url, '/'), PHP_URL_PATH));
216
    }
217
218
    /**
219
     * Return the siteUrlOverride setting, which can be a string or an array of site URLs
220
     * indexed by the site handle
221
     *
222
     * @param int|null $siteId
223
     * @return string
224
     * @throws Exception
225
     * @throws SiteNotFoundException
226
     */
227
    public static function getSiteUrlOverrideSetting(?int $siteId = null): string
228
    {
229
        // If the override is a string, just return it
230
        $siteUrlOverride = Seomatic::$settings->siteUrlOverride;
231
        if (is_string($siteUrlOverride)) {
232
            return $siteUrlOverride;
233
        }
234
        // If the override is an array, pluck the appropriate one by handle
235
        if (is_array($siteUrlOverride)) {
0 ignored issues
show
introduced by
The condition is_array($siteUrlOverride) is always true.
Loading history...
236
            $sites = Craft::$app->getSites();
237
            $site = $sites->getCurrentSite();
238
            if ($siteId !== null) {
239
                $site = $sites->getSiteById($siteId, true);
240
                if (!$site) {
241
                    throw new Exception('Invalid site ID: ' . $siteId);
242
                }
243
            }
244
245
            return $siteUrlOverride[$site->handle] ?? '';
246
        }
0 ignored issues
show
Bug Best Practice introduced by
The function implicitly returns null when the if condition on line 235 is false. This is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
247
    }
248
249
    // Protected Methods
250
    // =========================================================================
251
252
    /**
253
     * Decompose a url into a prefix, path, and suffix
254
     *
255
     * @param $pathOrUrl
256
     *
257
     * @return array
258
     */
259
    protected static function decomposeUrl($pathOrUrl): array
260
    {
261
        $result = array();
262
263
        if (filter_var($pathOrUrl, FILTER_VALIDATE_URL)) {
264
            $url_parts = parse_url($pathOrUrl);
265
            $result['prefix'] = $url_parts['scheme'] . '://' . $url_parts['host'];
266
            $result['path'] = $url_parts['path'] ?? '';
267
            $result['suffix'] = '';
268
            $result['suffix'] .= empty($url_parts['query']) ? '' : '?' . $url_parts['query'];
269
            $result['suffix'] .= empty($url_parts['fragment']) ? '' : '#' . $url_parts['fragment'];
270
        } else {
271
            $result['prefix'] = '';
272
            $result['path'] = $pathOrUrl;
273
            $result['suffix'] = '';
274
        }
275
276
        return $result;
277
    }
278
}
279