Passed
Push — v1 ( 1a72e5...92dfb3 )
by Andrew
08:37 queued 03:57
created

src/services/Similar.php (4 issues)

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/
9
 * @copyright Copyright (c) 2018 nystudio107.com
10
 */
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\elements\db\ElementQuery;
19
use craft\elements\db\ElementQueryInterface;
20
use craft\elements\db\EntryQuery;
21
use craft\elements\Entry;
22
use craft\events\CancelableEvent;
23
24
use yii\base\Exception;
25
26
/**
27
 * @author    nystudio107.com
28
 * @package   Similar
29
 * @since     1.0.0
30
 */
31
class Similar extends Component
32
{
33
    // Public Properties
34
    // =========================================================================
35
36
    /**
37
     * @var string The previous order in the query
38
     */
39
    public $preOrder;
40
41
    public $limit;
42
43
    public $targetElements;
44
    // Public Methods
45
    // =========================================================================
46
47
    /**
48
     * @param $data
49
     *
50
     * @return mixed
51
     * @throws Exception
52
     */
53
    public function find($data)
54
    {
55
        if (!isset($data['element'])) {
56
            throw new Exception('Required parameter `element` was not supplied to `craft.similar.find`.');
57
        }
58
59
        if (!isset($data['context'])) {
60
            throw new Exception('Required parameter `context` was not supplied to `craft.similar.find`.');
61
        }
62
63
        /** @var Element $element */
64
        $element = $data['element'];
65
        $context = $data['context'];
66
        $criteria = $data['criteria'] ?? [];
67
        if (\is_object($criteria)) {
68
            /** @var ElementQueryInterface $criteria */
69
            $criteria = $criteria->toArray();
70
        }
71
72
        // Get an ElementQuery for this Element
73
        $elementClass = \is_object($element) ? \get_class($element) : $element;
74
        /** @var EntryQuery $query */
75
        $query = $this->getElementQuery($elementClass, $criteria);
76
77
        // If the $query is null, just return an empty Entry
78
        if (!$query) { // no results
0 ignored issues
show
$query is of type craft\elements\db\EntryQuery, thus it always evaluated to true. If $query can have other possible types, add them to src/services/Similar.php:74
Loading history...
79
            return new Entry();
80
        }
81
82
        // Stash any orderBy directives from the $query for our anonymous function
83
        $this->preOrder = $query->orderBy;
84
        $this->limit = $query->limit;
85
        // Extract the $tagIds from the $context
86
        if (\is_array($context)) {
87
            $tagIds = $context;
88
        } else {
89
            /** @var ElementQueryInterface $context */
90
            $tagIds = $context->ids();
91
        }
92
        $this->targetElements = $tagIds;
93
94
        // We need to modify the actual craft\db\Query after the ElementQuery has been prepared
95
        $query->on(ElementQuery::EVENT_AFTER_PREPARE, [$this, 'eventAfterPrepareHandler']);
96
        // Return the data as an array, and only fetch the `id` and `siteId`
97
        $query->asArray(true);
98
        $query->select(['elements.id', 'elements_sites.siteId']);
99
        $query->andWhere('elements.id != :id', ['id' => $element->id]);
100
        $query->andWhere(['in', '{{%relations}}.targetId', $tagIds]);
101
        $query->leftJoin('{{%relations}}', 'elements.id={{%relations}}.sourceId');
102
        $results = $query->all();
103
104
        // Fetch the elements based on the returned `id` and `siteId`
105
        $elements = Craft::$app->getElements();
106
        $models = [];
107
        foreach ($results as $config) {
108
            $model = $elements->getElementById($config['id'], $elementClass, $config['siteId']);
109
            if ($model) {
110
                // The `count` property is added dynamically by our CountBehavior behavior
111
                /** @noinspection PhpUndefinedFieldInspection */
112
                $model->count = $config['count'];
0 ignored issues
show
Accessing count on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
113
                $models[] = $model;
114
            }
115
        }
116
117
        return $models;
118
    }
119
120
    // Protected Methods
121
    // =========================================================================
122
123
    /**
124
     * @param CancelableEvent $event
125
     */
126
    protected function eventAfterPrepareHandler(CancelableEvent $event)
127
    {
128
        /** @var ElementQuery $query */
129
        $query = $event->sender;
130
        // Add in the `count` param so we know how many were fetched
131
        $query->query->addSelect(['COUNT(*) as count']);
0 ignored issues
show
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

131
        $query->query->/** @scrutinizer ignore-call */ 
132
                       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...
132
        $query->query->orderBy('count DESC, '.str_replace('`', '', $this->preOrder));
133
        $query->query->groupBy('{{%relations}}.sourceId');
134
135
        $query->query->andWhere(['in', '{{%relations}}.targetId', $this->targetElements]);
136
        $query->subQuery->limit(null); // inner limit to null -> fetch all possible entries, sort them afterwards
0 ignored issues
show
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

136
        $query->subQuery->/** @scrutinizer ignore-call */ 
137
                          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...
137
        $query->query->limit($this->limit); // or whatever limit is set
138
139
        $query->subQuery->groupBy('elements.id');
140
        $event->isValid = true;
141
    }
142
143
    /**
144
     * Returns the element query based on $elementType and $criteria
145
     *
146
     * @var string|ElementInterface $elementType
147
     * @var array                   $criteria
148
     *
149
     * @return ElementQueryInterface
150
     */
151
    protected function getElementQuery($elementType, array $criteria): ElementQueryInterface
152
    {
153
        /** @var string|ElementInterface $elementType */
154
        $query = $elementType::find();
155
        Craft::configure($query, $criteria);
156
157
        return $query;
158
    }
159
}
160