Similar::find()   F
last analyzed

Complexity

Conditions 21
Paths 454

Size

Total Lines 124
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 11
Bugs 0 Features 0
Metric Value
eloc 62
c 11
b 0
f 0
dl 0
loc 124
rs 0.7583
cc 21
nc 454
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Similar plugin for Craft CMS 3.x
4
 *
5
 * Similar for Craft lets you find elements, Entries, Categories, Commerce
6
 * Products, etc, that are similar, based on... other related elements.
7
 *
8
 * @link      https://nystudio107.com/
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
9
 * @copyright Copyright (c) 2018 nystudio107.com
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
10
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
11
12
namespace nystudio107\similar\services;
13
14
use Craft;
15
use craft\base\Component;
16
use craft\base\Element;
17
use craft\base\ElementInterface;
18
use craft\db\Table;
19
use craft\elements\db\ElementQuery;
20
use craft\elements\db\ElementQueryInterface;
21
use craft\elements\db\EntryQuery;
22
use craft\elements\Entry;
23
use craft\events\CancelableEvent;
24
25
use yii\base\Exception;
26
27
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
28
 * @author    nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
Tag value for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
29
 * @package   Similar
0 ignored issues
show
Coding Style introduced by
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
30
 * @since     1.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value for @since tag indented incorrectly; expected 3 spaces but found 5
Loading history...
31
 */
0 ignored issues
show
Coding Style introduced by
Missing @link tag in class comment
Loading history...
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
32
class Similar extends Component
33
{
34
    // Public Properties
35
    // =========================================================================
36
37
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
38
     * @var string The previous order in the query
39
     */
40
    public $preOrder;
41
42
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
43
     * @var int
44
     */
45
    public $limit;
46
47
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
48
     * @var Element[]
49
     */
50
    public $targetElements;
51
52
    // Public Methods
53
    // =========================================================================
54
55
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
56
     * @param $data
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
57
     *
58
     * @return mixed
59
     * @throws Exception
60
     */
61
    public function find($data)
62
    {
63
        if (!isset($data['element'])) {
64
            throw new Exception('Required parameter `element` was not supplied to `craft.similar.find`.');
65
        }
66
67
        if (!isset($data['context'])) {
68
            throw new Exception('Required parameter `context` was not supplied to `craft.similar.find`.');
69
        }
70
71
        /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
72
        $element = $data['element'];
73
        $context = $data['context'];
74
        $criteria = $data['criteria'] ?? [];
75
76
        if (\is_object($criteria)) {
77
            /** @var ElementQueryInterface $criteria */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
78
            $criteria = $criteria->toArray();
79
        }
80
81
        // Get an ElementQuery for this Element
82
        $elementClass = \is_object($element) ? \get_class($element) : $element;
83
        /** @var EntryQuery $query */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
84
        $query = $this->getElementQuery($elementClass, $criteria);
85
86
        // If the $query is null, just return an empty Entry
87
        if (!$query) { // no results
0 ignored issues
show
introduced by
$query is of type craft\elements\db\EntryQuery, thus it always evaluated to true.
Loading history...
88
            return new Entry();
89
        }
90
91
        // Stash any orderBy directives from the $query for our anonymous function
92
        $this->preOrder = $query->orderBy;
93
        $this->limit = $query->limit;
0 ignored issues
show
Documentation Bug introduced by
It seems like $query->limit can also be of type yii\db\ExpressionInterface. However, the property $limit is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
94
        // Extract the $tagIds from the $context
95
        if (\is_array($context)) {
96
            $tagIds = $context;
97
        } else {
98
            /** @var ElementQueryInterface $context */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
99
            $tagIds = $context->ids();
100
        }
101
        $this->targetElements = $tagIds;
102
103
        // We need to modify the actual craft\db\Query after the ElementQuery has been prepared
104
        $query->on(ElementQuery::EVENT_AFTER_PREPARE, [$this, 'eventAfterPrepareHandler']);
105
        // Return the data as an array, and only fetch the `id` and `siteId`
106
        $query->asArray(true);
107
        $query->select(['elements.id', 'elements_sites.siteId']);
108
        $query->andWhere(['not', ['elements.id' => $element->id]]);
109
110
        // Unless site criteria is provided, force the element's site.
111
        if (empty($criteria['siteId']) && empty($criteria['site'])) {
112
            $query->andWhere(['elements_sites.siteId' => $element->siteId]);
113
        }
114
115
        $query->andWhere(['in', 'relations.targetId', $tagIds]);
116
        $query->leftJoin(['relations' => Table::RELATIONS], '[[elements.id]] = [[relations.sourceId]]');
117
        $results = $query->all();
118
119
        // Fetch the elements based on the returned `id` and `siteId`
120
        $elements = Craft::$app->getElements();
0 ignored issues
show
Unused Code introduced by
The assignment to $elements is dead and can be removed.
Loading history...
121
        $models = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $models is dead and can be removed.
Loading history...
122
123
        $queryConditions = [];
124
        $similarCounts = [];
125
126
        // Build the query conditions for a new element query.
127
        // The reason we have to do it in two steps is because the `count` property is added by a behavior after element creation
128
        // So if we just try to tack that on in the original query, it will throw an error on element creation
129
        foreach ($results as $config) {
130
            $siteId = $config['siteId'];
131
            $elementId = $config['id'];
132
133
            if ($elementId && $siteId) {
134
                if (empty($queryConditions[$siteId])) {
135
                    $queryConditions[$siteId] = [];
136
                }
137
138
                // Write down elements per site and similar counts
139
                $queryConditions[$siteId][] = $elementId;
140
                $key = $siteId . '-' . $elementId;
141
                $similarCounts[$key] = $config['count'];
142
            }
143
        }
144
145
        if (empty($results)) {
146
            return [];
147
        }
148
149
        // Fetch all the elements in one fell swoop, including any preset eager-loaded conditions
150
        $query = $this->getElementQuery($elementClass, $criteria);
151
152
        // Make sure we fetch the elements that are similar only
153
        $query->on(ElementQuery::EVENT_AFTER_PREPARE, function (CancelableEvent $event) use ($queryConditions) {
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
Bug introduced by
The method on() does not exist on craft\elements\db\ElementQueryInterface. Did you maybe mean one()? ( Ignorable by Annotation )

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

153
        $query->/** @scrutinizer ignore-call */ 
154
                on(ElementQuery::EVENT_AFTER_PREPARE, function (CancelableEvent $event) use ($queryConditions) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
154
            /** @var ElementQuery $query */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
155
            $query = $event->sender;
156
            $first = true;
157
158
            foreach ($queryConditions as $siteId => $elementIds) {
159
                $method = $first ? 'where' : 'orWhere';
160
                $query->subQuery->$method(['and', [
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
161
                    'elements_sites.siteId' => $siteId,
162
                    'elements.id' => $elementIds]
163
                ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
164
            }
165
        });
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
166
167
        $elements = $query->all();
168
169
        foreach ($elements as $element) {
170
            // The `count` property is added dynamically by our CountBehavior behavior
171
            $key = $element->siteId . '-' . $element->id;
0 ignored issues
show
Bug introduced by
Accessing siteId on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
Accessing id on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
172
            if (!empty($similarCounts[$key])) {
173
                /** @noinspection PhpUndefinedFieldInspection */
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
174
                $element->count = $similarCounts[$key];
0 ignored issues
show
Bug introduced by
Accessing count on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
175
            }
176
        }
177
178
        if (empty($data['criteria']['orderBy'])) {
179
            usort($elements, function ($a, $b) {
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
180
                return $a->count < $b->count ? 1 : ($a->count == $b->count ? 0 : -1);
181
            });
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
182
        }
183
184
        return $elements;
185
    }
186
187
    // Protected Methods
188
    // =========================================================================
189
190
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
191
     * @param CancelableEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
192
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
193
    protected function eventAfterPrepareHandler(CancelableEvent $event)
194
    {
195
        /** @var ElementQuery $query */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
196
        $query = $event->sender;
197
        // Add in the `count` param so we know how many were fetched
198
        $query->query->addSelect(['COUNT(*) as count']);
0 ignored issues
show
Bug introduced by
The method addSelect() does not exist on null. ( Ignorable by Annotation )

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

198
        $query->query->/** @scrutinizer ignore-call */ 
199
                       addSelect(['COUNT(*) as count']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
199
        if (is_array($this->preOrder)) {
0 ignored issues
show
introduced by
The condition is_array($this->preOrder) is always false.
Loading history...
200
            $query->query->orderBy(array_merge([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
201
                'count' => 'DESC',
202
            ], $this->preOrder));
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
203
        } elseif (is_string($this->preOrder)) {
0 ignored issues
show
introduced by
The condition is_string($this->preOrder) is always true.
Loading history...
204
            $query->query->orderBy('count DESC, '.str_replace('`', '', $this->preOrder));
205
        }
206
        $query->query->groupBy(['relations.sourceId', 'elements.id', 'elements_sites.siteId']);
207
208
        $query->query->andWhere(['in', 'relations.targetId', $this->targetElements]);
209
        $query->subQuery->limit(null); // inner limit to null -> fetch all possible entries, sort them afterwards
0 ignored issues
show
Bug introduced by
The method limit() does not exist on null. ( Ignorable by Annotation )

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

209
        $query->subQuery->/** @scrutinizer ignore-call */ 
210
                          limit(null); // inner limit to null -> fetch all possible entries, sort them afterwards

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
210
        $query->query->limit($this->limit); // or whatever limit is set
211
212
        $query->subQuery->groupBy(['elements.id', 'content.id', 'elements_sites.id']);
213
214
        if ($query instanceof EntryQuery) {
215
            $query->subQuery->addGroupBy(['entries.postDate']);
216
        }
217
218
        if ($query->withStructure || ($query->withStructure !== false && $query->structureId)) {
219
            $query->subQuery->addGroupBy(['structureelements.structureId', 'structureelements.lft']);
220
        }
221
        $event->isValid = true;
222
    }
223
224
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $criteria should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $elementType should have a doc-comment as per coding-style.
Loading history...
225
     * Returns the element query based on $elementType and $criteria
226
     *
227
     * @var string|ElementInterface $elementType
228
     * @var array                   $criteria
229
     *
230
     * @return ElementQueryInterface
231
     */
232
    protected function getElementQuery($elementType, array $criteria): ElementQueryInterface
233
    {
234
        /** @var string|ElementInterface $elementType */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
235
        $query = $elementType::find();
236
        Craft::configure($query, $criteria);
237
238
        return $query;
239
    }
240
}
241