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
introduced
by
![]() |
|||||||
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
|
|||||||
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
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. ![]() |
|||||||
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
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. ![]() |
|||||||
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 |