Skip to content

Commit d225e93

Browse files
committed
added you may also like block
1 parent 03f5f39 commit d225e93

File tree

5 files changed

+251
-8
lines changed

5 files changed

+251
-8
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
<?php
2+
3+
/**
4+
* SiteBase
5+
* PHP Version 8.3
6+
*
7+
* @category CMS / Framework
8+
* @package Degami\Sitebase
9+
* @author Mirko De Grandis <degami@github.com>
10+
* @license MIT https://opensource.org/licenses/mit-license.php
11+
* @link https://github.com/degami/sitebase
12+
*/
13+
14+
namespace App\Base\Blocks;
15+
16+
use App\Base\Abstracts\Blocks\BaseCodeBlock;
17+
use App\Base\Abstracts\Controllers\AdminPage;
18+
use App\Base\Abstracts\Controllers\BasePage;
19+
use Degami\Basics\Exceptions\BasicException;
20+
use DI\DependencyException;
21+
use DI\NotFoundException;
22+
use Phpfastcache\Exceptions\PhpfastcacheSimpleCacheException;
23+
use Degami\Basics\Html\TagElement;
24+
use App\Base\Models\Cart;
25+
use App\App;
26+
use App\Base\Abstracts\Controllers\BaseHtmlPage;
27+
use App\Base\Models\CartItem;
28+
use App\Base\Interfaces\Model\ProductInterface;
29+
use App\Base\Abstracts\Models\FrontendModel;
30+
use App\Base\Controllers\Frontend\Commerce\Cart as CommerceCart;
31+
use App\Base\Tools\Search\AIManager as AISearchManager;
32+
33+
/**
34+
* "You May Like" Block
35+
*/
36+
class YouMayLikeProducts extends BaseCodeBlock
37+
{
38+
protected ?Cart $cart = null;
39+
protected ?AISearchManager $aiSearchManager = null;
40+
41+
public function getCart(?BaseHtmlPage $current_page = null) : ?Cart
42+
{
43+
if ($this->cart instanceof Cart) {
44+
return $this->cart;
45+
}
46+
47+
$cart = Cart::getCollection()->where([
48+
'user_id' => $current_page?->getCurrentUser()->getId(),
49+
'website_id' => $current_page?->getCurrentWebsite()->getId(),
50+
'is_active' => true,
51+
])->getFirst();
52+
53+
if ($cart instanceof Cart) {
54+
$this->cart = $cart;
55+
return $this->cart;
56+
}
57+
58+
return null;
59+
}
60+
61+
/**
62+
* {@inheritdoc}
63+
*
64+
* @param BasePage|null $current_page
65+
* @param array $data
66+
* @return string
67+
* @throws BasicException
68+
* @throws PhpfastcacheSimpleCacheException
69+
* @throws DependencyException
70+
* @throws NotFoundException
71+
*/
72+
public function renderHTML(?BasePage $current_page = null, array $data = []): string
73+
{
74+
if (!App::getInstance()->getEnvironment()->getVariable('ENABLE_COMMERCE', false)) {
75+
return '';
76+
}
77+
78+
if (!($current_page instanceof CommerceCart)) {
79+
return '';
80+
}
81+
82+
$config = array_filter(json_decode($data['config'] ?? '{}', true));
83+
84+
$route_info = $current_page?->getRouteInfo();
85+
86+
// $current_page_handler = $route_info->getHandler();
87+
if ($current_page?->getRouteGroup() == AdminPage::getRouteGroup() || $route_info?->isAdminRoute() || !$current_page?->getCurrentUser()) {
88+
return '';
89+
}
90+
91+
92+
$youMayLike = [];
93+
foreach ($this->getCart($current_page)->getItems() as $item) {
94+
$youMayLike = array_unique_by(
95+
array_merge($youMayLike, $this->getProductSuggestions($item)),
96+
fn ($el) => $el['type'] . '::' . $el['id']
97+
);
98+
}
99+
100+
$cartIdentifiers = array_map(function(CartItem $item) {
101+
return static::getClassBasename($item->getProduct()).'::'.$item->getProduct()->getId();
102+
}, $this->getCart($current_page)->getItems());
103+
104+
$suggestions = [];
105+
foreach ($youMayLike as $elem) {
106+
if (is_subclass_of($elem['modelClass'], ProductInterface::class)) {
107+
$suggestion = $this->containerCall([$elem['modelClass'], 'load'], ['id' => $elem['id']]);
108+
109+
if (in_array(static::getClassBasename($suggestion).'::'.$suggestion->getId(), $cartIdentifiers)) {
110+
continue;
111+
}
112+
113+
$suggestions[] = $suggestion;
114+
}
115+
}
116+
117+
$list = [];
118+
foreach ($suggestions as $suggestion) {
119+
/** @var FrontendModel $suggestion */
120+
$link = $this->containerMake(TagElement::class, ['options' => [
121+
'tag' => 'a',
122+
'attributes' => [
123+
'href' => $suggestion->getFrontendUrl(),
124+
],
125+
'text' => $suggestion->getTitle(),
126+
]]);
127+
128+
$sku = $this->containerMake(TagElement::class, ['options' => [
129+
'tag' => 'span',
130+
'attributes' => [
131+
'class' => 'sku',
132+
],
133+
'text' => $this->getUtils()->translate('SKU', locale: $current_page?->getCurrentLocale()) . ': ' . $suggestion->getSku(),
134+
]]);
135+
136+
$price = $this->containerMake(TagElement::class, ['options' => [
137+
'tag' => 'span',
138+
'attributes' => [
139+
'class' => 'price',
140+
],
141+
'text' => $this->getUtils()->translate('Price', locale: $current_page?->getCurrentLocale()) . ': ' . $this->getUtils()->formatPrice($suggestion->getPrice(), $this->getCart($current_page)->getCurrencyCode()),
142+
]]);
143+
144+
$list[] = '<div class="row"><div class="col-12">'.$link . '</div><div class="col d-flex justify-content-between">' . $sku . $price . '</div></div>';
145+
}
146+
147+
if (empty($list)) {
148+
return '';
149+
}
150+
151+
return "".$this->containerMake(TagElement::class, ['options' => [
152+
'tag' => 'div',
153+
'attributes' => [
154+
'class' => 'pt-5',
155+
],
156+
'text' => '<h2>'.$this->getUtils()->translate('You may also like', locale: $current_page?->getCurrentLocale()).'</h2><ul class="list-group"><li class="list-group-item">'.implode('</li><li class="list-group-item">', $list).'</li></ul>',
157+
]]);
158+
}
159+
160+
public function isCachable() : bool
161+
{
162+
return false;
163+
}
164+
165+
protected function getProductSuggestions(CartItem $item, ?BasePage $current_page = null) : array
166+
{
167+
/** @var ProductInterface $product */
168+
$product = $item->getProduct();
169+
170+
$locale = $current_page?->getCurrentLocale() ?? App::getInstance()->getCurrentLocale();
171+
$website_id = App::getInstance()->getSiteData()->getCurrentWebsiteId();
172+
173+
$cacheKey = 'youmaylike.'.$product->getSku().'.'.$locale.'.'.$website_id;
174+
175+
if (App::getInstance()->getCache()->has($cacheKey)) {
176+
$result = (array) App::getInstance()->getCache()->get($cacheKey);
177+
return array_map(fn ($el) => $el['data'], $result['docs']);
178+
}
179+
180+
/** @var FrontendModel $product */
181+
$embeddable = $this->getSearchManager()->getEmbeddableDataForFrontendModel($product);
182+
$result = $this->getSearchManager()->searchNearby(implode(' ', array_filter($embeddable)));
183+
184+
App::getInstance()->getCache()->set($cacheKey, $result, 1800);
185+
186+
return array_map(fn ($el) => $el['data'], $result['docs']);
187+
}
188+
189+
protected function getSearchManager(string $llmCode = 'googlegemini') : AISearchManager
190+
{
191+
if (!is_null($this->aiSearchManager)) {
192+
return $this->aiSearchManager;
193+
}
194+
195+
/** @var AISearchManager $embeddingManager */
196+
return $this->aiSearchManager = $this->containerMake(AISearchManager::class, [
197+
'llm' => $this->getAI()->getAIModel($llmCode),
198+
'model' => match ($llmCode) {
199+
'googlegemini' => 'text-embedding-004',
200+
'chatgpt' => 'text-embedding-3-small',
201+
'claude' => 'claude-2.0-embedding',
202+
'groq' => 'groq-vector-1',
203+
'mistral' => 'mistral-embedding-001',
204+
'perplexity' => 'perplexity-embedding-001',
205+
default => null,
206+
}
207+
]);
208+
}
209+
}

app/base/models/GiftCard.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use App\Base\Traits\ProductTrait;
1919
use App\Base\GraphQl\GraphQLExport;
2020
use App\Base\Models\MediaElement;
21+
use Exception;
2122

2223
/**
2324
* Gift Card Model
@@ -83,7 +84,11 @@ public function getMedia(): ?MediaElement
8384
return null;
8485
}
8586

86-
return $this->setMedia(MediaElement::load($this->getMediaId()))->media;
87+
try {
88+
return $this->setMedia(MediaElement::load($this->getMediaId()))->media;
89+
} catch (Exception $e) {}
90+
91+
return null;
8792
}
8893

8994
public function setMedia(?MediaElement $media): self

app/base/tools/Search/AIManager.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,6 @@ public function getIndexDataForFrontendModel(FrontendModel $object) : array
114114

115115
$type = strtolower(static::getClassBasename($modelClass));
116116

117-
$fields_to_index = ['title', 'content'];
118-
if (method_exists($modelClass, 'exposeToIndexer')) {
119-
$fields_to_index = $this->containerCall([$modelClass, 'exposeToIndexer']);
120-
}
121117

122118
$body = [];
123119

@@ -128,6 +124,22 @@ public function getIndexDataForFrontendModel(FrontendModel $object) : array
128124

129125
$body['type'] = $type;
130126

127+
$embeddable = $this->getEmbeddableDataForFrontendModel($object);
128+
129+
$body['embedding'] = $this->llm->embed(implode(' ', array_filter($embeddable)), $this->model);
130+
131+
return ['_id' => $type . '_' . $object->getId(), '_data' => $body];
132+
}
133+
134+
public function getEmbeddableDataForFrontendModel(FrontendModel $object) : array
135+
{
136+
$modelClass = get_class($object);
137+
138+
$fields_to_index = ['title', 'content'];
139+
if (method_exists($modelClass, 'exposeToIndexer')) {
140+
$fields_to_index = $this->containerCall([$modelClass, 'exposeToIndexer']);
141+
}
142+
131143
$embeddable = [];
132144
foreach ($fields_to_index as $field_name) {
133145
$embeddable[$field_name] = $object->getData($field_name);
@@ -143,9 +155,8 @@ public function getIndexDataForFrontendModel(FrontendModel $object) : array
143155

144156
return null;
145157
}, $embeddable);
146-
$body['embedding'] = $this->llm->embed(implode(' ', array_filter($embeddable)), $this->model);
147158

148-
return ['_id' => $type . '_' . $object->getId(), '_data' => $body];
159+
return $embeddable;
149160
}
150161

151162
/**

app/site/models/DownloadableProduct.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ public function getMedia(bool $reset = false): ?MediaElement
159159
}
160160

161161
if (empty($this->media) || $reset == true) {
162-
$this->media = MediaElement::load($this->getMediaId()) ?: null;
162+
try {
163+
$this->media = MediaElement::load($this->getMediaId()) ?: null;
164+
} catch (Exception $e) {}
163165
}
164166

165167
return $this->media;

globals/functions.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,19 @@ function isJson($content)
6161

6262
return is_array($decoded) || is_object($decoded);
6363
}
64+
65+
function array_unique_by(array $items, callable $callback): array {
66+
$seen = [];
67+
$result = [];
68+
69+
foreach ($items as $item) {
70+
$key = $callback($item);
71+
72+
if (!isset($seen[$key])) {
73+
$seen[$key] = true;
74+
$result[] = $item;
75+
}
76+
}
77+
78+
return $result;
79+
}

0 commit comments

Comments
 (0)