MetaValue   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 290
Duplicated Lines 0 %

Test Coverage

Coverage 12.07%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 45
eloc 133
dl 0
loc 290
ccs 14
cts 116
cp 0.1207
rs 8.8
c 1
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getSiteLanguage() 0 20 4
A parseString() 0 19 5
F parseArray() 0 49 14
A cache() 0 31 4
F parseMetaString() 0 78 18

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
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\base\Element;
16
use craft\elements\Asset;
17
use craft\errors\SiteNotFoundException;
18
use craft\helpers\StringHelper;
19
use craft\web\View;
20
use nystudio107\seomatic\Seomatic;
21
use ReflectionClass;
22
use Throwable;
23
use Twig\Markup;
24
use yii\base\Exception;
25
use function in_array;
26
use function is_object;
27
use function is_string;
28
29
/**
30
 * @author    nystudio107
31
 * @package   Seomatic
32
 * @since     3.0.0
33
 */
34
class MetaValue
35
{
36
    // Constants
37
    // =========================================================================
38
39
    const MAX_TEMPLATE_LENGTH = 4096;
40
    const MAX_PARSE_TRIES = 5;
41
    // Semicolon because that is the resolved config key when rendering tags,
42
    // kebab-case because that is the config keys as defined in the config files.
43
    const NO_ALIASES = [
44
        'twitter:site',
45
        'twitter:creator',
46
        'twitterSite',
47
        'twitterCreator',
48
    ];
49
    const NO_PARSING = [
50
        'siteLinksSearchTarget',
51
    ];
52
    const PARSE_ONCE = [
53
        'target',
54
        'urlTemplate',
55
    ];
56
57
    // Static Properties
58
    // =========================================================================
59
60
    /**
61
     * @var array
62
     */
63
    public static $templateObjectVars;
64
65
    /**
66
     * @var array
67
     */
68
    public static $templatePreviewVars = [];
69
70
    /**
71
     * @var View
72
     */
73
    public static $view;
74
75
    // Static Methods
76
    // =========================================================================
77
78
    /**
79
     * @param string $metaValue
80
     * @param bool $resolveAliases Whether @ aliases should be resolved in
81
     *                               this string
82
     * @param bool $parseAsTwig Whether items should be parsed as a Twig
83
     *                               template in this string
84
     * @param int $tries The number of times to parse the string
85
     *
86
     * @return string
87
     */
88 1
    public static function parseString(
89
        $metaValue,
90
        bool $resolveAliases = true,
91
        bool $parseAsTwig = true,
92
        $tries = self::MAX_PARSE_TRIES
93
    ) {
94
        // If it's a string, and there are no dynamic tags, just return the template
95 1
        if (is_string($metaValue) && !StringHelper::contains($metaValue, '{', false)) {
96 1
            return self::parseMetaString($metaValue, $resolveAliases, $parseAsTwig) ?? $metaValue;
97
        }
98
        // Parse it repeatedly until it doesn't change
99
        $value = '';
100
        while ($metaValue !== $value && $tries) {
101
            $tries--;
102
            $value = $metaValue;
103
            $metaValue = self::parseMetaString($value, $resolveAliases, $parseAsTwig) ?? $metaValue;
104
        }
105
106
        return $metaValue;
107
    }
108
109
    /**
110
     * @param array $metaArray
111
     * @param bool $resolveAliases Whether @ aliases should be resolved in
112
     *                              this array
113
     * @param bool $parseAsTwig Whether items should be parsed as a Twig
114
     *                              template in this array
115
     * @param bool $recursive Whether to recursively parse the array
116
     */
117
    public static function parseArray(array &$metaArray, bool $resolveAliases = true, bool $parseAsTwig = true, bool $recursive = false)
118
    {
119
        // Do this here as well so that parseString() won't potentially be constantly switching modes
120
        // while parsing through the array
121
        $oldTemplateMode = self::$view->getTemplateMode();
122
        // Render in site template mode so that we get globals injected
123
        if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
124
            try {
125
                self::$view->setTemplateMode(self::$view::TEMPLATE_MODE_SITE);
126
            } catch (Exception $e) {
127
                Craft::error($e->getMessage(), __METHOD__);
128
            }
129
        }
130
        foreach ($metaArray as $key => $value) {
131
            if ($recursive && is_array($value)) {
132
                self::parseArray($value, $resolveAliases, $parseAsTwig, $recursive);
133
            }
134
            $shouldParse = $parseAsTwig;
135
            $shouldAlias = $resolveAliases;
136
            $tries = self::MAX_PARSE_TRIES;
137
            if (in_array($key, self::NO_ALIASES, true)) {
138
                $shouldAlias = false;
139
            }
140
            if (in_array($key, self::NO_PARSING, true)) {
141
                $shouldParse = false;
142
            }
143
            if (in_array($key, self::PARSE_ONCE, true)) {
144
                $tries = 1;
145
                if (is_string($value) && $value[0] !== '{') {
146
                    $shouldParse = false;
147
                }
148
            }
149
            if ($value !== null) {
150
                $metaArray[$key] = self::parseString($value, $shouldAlias, $shouldParse, $tries);
151
            }
152
        }
153
        // Restore the template mode
154
        if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
155
            try {
156
                self::$view->setTemplateMode($oldTemplateMode);
157
            } catch (Exception $e) {
158
                Craft::error($e->getMessage(), __METHOD__);
159
            }
160
        }
161
162
        // Remove any empty values
163
        $metaArray = array_filter(
164
            $metaArray,
165
            [ArrayHelper::class, 'preserveNumerics']
166
        );
167
    }
168
169
    /**
170
     * Get the language from a siteId
171
     *
172
     * @param null|int $siteId
173
     *
174
     * @return string
175
     */
176
    public static function getSiteLanguage(int $siteId = null): string
177
    {
178
        if ($siteId === null) {
179
            try {
180
                $siteId = Craft::$app->getSites()->getCurrentSite()->id;
181
            } catch (SiteNotFoundException $e) {
182
                $siteId = 1;
183
                Craft::error($e->getMessage(), __METHOD__);
184
            }
185
        }
186
        $site = Craft::$app->getSites()->getSiteById($siteId);
187
        if ($site) {
188
            $language = $site->language;
189
        } else {
190
            $language = Craft::$app->language;
191
        }
192
        $language = strtolower($language);
193
        $language = str_replace('_', '-', $language);
194
195
        return $language;
196
    }
197
198
    /**
199
     * Cache frequently accessed properties locally
200
     */
201
    public static function cache()
202
    {
203
        self::$templateObjectVars = [
204
            'seomatic' => Seomatic::$seomaticVariable,
205
        ];
206
207
        $element = Seomatic::$matchedElement;
208
        /** @var Element $element */
209
        if ($element !== null) {
210
            $refHandle = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $refHandle is dead and can be removed.
Loading history...
211
            // Get a fallback from the element's root class name
212
            $reflector = new ReflectionClass($element);
213
            $refHandle = strtolower($reflector->getShortName());
214
            $elementRefHandle = $element::refHandle();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $elementRefHandle is correct as $element::refHandle() targeting craft\base\Element::refHandle() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
215
            // Use the SeoElement interface to get the refHandle
216
            $metaBundleSourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
217
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundleSourceType);
218
            if ($seoElement) {
219
                $elementRefHandle = $seoElement::getElementRefHandle();
220
            }
221
            // Prefer $element::refHandle()
222
            $matchedElementType = $elementRefHandle ?? $refHandle;
223
            if ($matchedElementType) {
224
                self::$templateObjectVars[$matchedElementType] = $element;
225
                self::$templatePreviewVars[$matchedElementType] = $element;
226
            }
227
        }
228
        self::$templatePreviewVars['object'] = self::$templateObjectVars;
229
        self::$templatePreviewVars['seomatic'] = Seomatic::$seomaticVariable;
230
231
        self::$view = Seomatic::$sandboxView;
232
    }
233
234
    // Protected Methods
235
    // =========================================================================
236
237
    /**
238
     * @param string|object $metaValue
239
     * @param bool $resolveAliases Whether @ aliases should be resolved
240
     *                                     in this string
241
     * @param bool $parseAsTwig Whether items should be parsed as a
242
     *                                     Twig template in this string
243
     *
244
     * @return null|string
245
     */
246 1
    protected static function parseMetaString($metaValue, bool $resolveAliases = true, bool $parseAsTwig = true)
247
    {
248
        // Handle being passed in a string
249 1
        if (is_string($metaValue)) {
250 1
            if ($resolveAliases) {
251
                // Resolve it as an alias
252 1
                if (Seomatic::$craft31) {
253
                    try {
254 1
                        $alias = Craft::parseEnv($metaValue);
0 ignored issues
show
Deprecated Code introduced by
The function Craft::parseEnv() has been deprecated: in 3.7.29. [[App::parseEnv()]] should be used instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

254
                        $alias = /** @scrutinizer ignore-deprecated */ Craft::parseEnv($metaValue);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
255
                    } catch (\Exception $e) {
256 1
                        $alias = false;
257
                    }
258
                } else {
259
                    try {
260
                        $alias = Craft::getAlias($metaValue, false);
261
                    } catch (\Exception $e) {
262
                        $alias = false;
263
                    }
264
                }
265 1
                if (is_string($alias)) {
266 1
                    $metaValue = $alias;
267
                }
268
            }
269
            // Ensure we aren't passed in an absurdly large object template to parse
270 1
            if (strlen($metaValue) > self::MAX_TEMPLATE_LENGTH) {
271
                $metaValue = mb_substr($metaValue, 0, self::MAX_TEMPLATE_LENGTH);
272
            }
273
            // If there are no dynamic tags, just return the template
274 1
            if (!$parseAsTwig || !StringHelper::contains($metaValue, '{', false)) {
275 1
                return trim(html_entity_decode($metaValue, ENT_NOQUOTES, 'UTF-8'));
276
            }
277
            $oldTemplateMode = self::$view->getTemplateMode();
278
            try {
279
                // Render in site template mode so that we get globals injected
280
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
281
                    self::$view->setTemplateMode(self::$view::TEMPLATE_MODE_SITE);
282
                }
283
                // Render the template out
284
                $metaValue = trim(html_entity_decode(
285
                    self::$view->renderObjectTemplate($metaValue, self::$templateObjectVars, self::$templatePreviewVars),
286
                    ENT_NOQUOTES,
287
                    'UTF-8'
288
                ));
289
                // Restore the template mode
290
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
291
                    self::$view->setTemplateMode($oldTemplateMode);
292
                }
293
            } catch (Throwable $e) {
294
                $metaValue = Craft::t(
295
                    'seomatic',
296
                    'Error rendering `{template}` -> {error}',
297
                    ['template' => $metaValue, 'error' => $e->getMessage() . ' - ' . print_r($metaValue, true)]
0 ignored issues
show
Bug introduced by
Are you sure print_r($metaValue, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

297
                    ['template' => $metaValue, 'error' => $e->getMessage() . ' - ' . /** @scrutinizer ignore-type */ print_r($metaValue, true)]
Loading history...
298
                );
299
                Craft::error($metaValue, __METHOD__);
300
                Craft::$app->getErrorHandler()->logException($e);
301
                // Restore the template mode
302
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
303
                    try {
304
                        self::$view->setTemplateMode($oldTemplateMode);
305
                    } catch (Exception $e) {
306
                        Craft::error($e->getMessage(), __METHOD__);
307
                    }
308
                }
309
310
                return null;
311
            }
312
        }
313
        // Handle being passed in an object
314
        if (is_object($metaValue)) {
315
            if ($metaValue instanceof Markup) {
316
                return trim(html_entity_decode((string)$metaValue, ENT_NOQUOTES, 'UTF-8'));
317
            }
318
            if ($metaValue instanceof Asset) {
319
                return $metaValue->uri;
320
            }
321
        }
322
323
        return $metaValue;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $metaValue also could return the type object which is incompatible with the documented return type null|string.
Loading history...
324
    }
325
}
326