first commit
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\DynamicRelatedProductsBundle\Admin\Actions;
|
||||
|
||||
use KupShop\AdminBundle\Admin\Actions\AbstractAction;
|
||||
use KupShop\AdminBundle\Admin\Actions\ActionResult;
|
||||
use KupShop\AdminBundle\Admin\Actions\IAction;
|
||||
use KupShop\DynamicRelatedProductsBundle\Utils\DynamicRelatedProductsService;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
class RefreshDataAction extends AbstractAction implements IAction
|
||||
{
|
||||
#[Required]
|
||||
public DynamicRelatedProductsService $relatedProductsService;
|
||||
|
||||
public function getTypes(): array
|
||||
{
|
||||
return ['DynamicRelatedProducts'];
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'glyphicon glyphicon-refresh';
|
||||
}
|
||||
|
||||
public function showInMassEdit(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Aktualizovat data';
|
||||
}
|
||||
|
||||
public function execute(&$data, array $config, string $type): ActionResult
|
||||
{
|
||||
$this->relatedProductsService->updateDynamicRelatedProductsById((int) $this->getId());
|
||||
|
||||
return new ActionResult(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\DynamicRelatedProductsBundle\Admin;
|
||||
|
||||
use KupShop\AdminBundle\Admin\ProductsFilter;
|
||||
|
||||
class DynamicRelatedProducts extends \Window
|
||||
{
|
||||
protected $template = 'window/DynamicRelatedProducts.tpl';
|
||||
protected $tableName = 'products_related_dynamic';
|
||||
|
||||
public function get_vars(): array
|
||||
{
|
||||
$data = parent::get_vars();
|
||||
$this->unserializeCustomData($data['body']['data']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function processFormData(): array
|
||||
{
|
||||
$data = parent::processFormData();
|
||||
$this->unserializeCustomData($data);
|
||||
|
||||
$data['data']['filter_selection'] = ProductsFilter::cleanFilter($data['data']['filter_selection']);
|
||||
$data['data']['filter_match'] = ProductsFilter::cleanFilter($data['data']['filter_match']);
|
||||
|
||||
$this->serializeCustomData($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
return DynamicRelatedProducts::class;
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
$txt_str['DynamicRelatedProducts'] = [
|
||||
'name' => 'Název pravidla',
|
||||
'related_type' => 'Typ souvisejícího zboží',
|
||||
'manual_reload' => 'Manuálně vygenerovat produkty',
|
||||
'flapDynamicRelatedProduct' => 'Správa pravidla dynamického souvisejícího zboží',
|
||||
|
||||
'filter_selection_title' => 'Produkty, ke kterým chcete přiřadit související zboží',
|
||||
'filter_match_title' => 'Produkty, které se přiřadí jako související zboží',
|
||||
|
||||
'toolbar_list' => 'Seznam pravidel',
|
||||
'toolbar_add' => 'Přidat pravidlo',
|
||||
|
||||
'titleEdit' => 'Editovat pravidlo',
|
||||
'titleAdd' => 'Přidat pravidlo',
|
||||
|
||||
'created' => 'Vytvořeno',
|
||||
'updated_at' => 'Vygenerováno',
|
||||
'related_count' => 'Počet vygenerovaných záznamů',
|
||||
|
||||
'activityEdited' => 'Upraveny dynamické produkty: %s',
|
||||
'activityAdded' => 'Přidány dynamické produkty: %s',
|
||||
'activityDeleted' => 'Smazány dynamické produkty: %s',
|
||||
|
||||
'search' => 'Hledat',
|
||||
'selectType' => 'Vyberte typ',
|
||||
'searchName' => 'Hledat podle názvu',
|
||||
'searchOnlyThis' => 'Vyhledat pouze tento údaj',
|
||||
];
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use KupShop\AdminBundle\AdminList\BaseList;
|
||||
|
||||
class DynamicRelatedProductsList extends BaseList
|
||||
{
|
||||
protected $tableName = 'products_related_dynamic';
|
||||
protected ?string $tableAlias = 't';
|
||||
|
||||
protected $tableDef = [
|
||||
'id' => 't.id',
|
||||
'fields' => [
|
||||
'name' => ['field' => 't.name', 'translate' => true],
|
||||
'related_type' => [],
|
||||
'created' => ['field' => 't.created_at', 'render' => 'renderDateTime', 'translate' => true],
|
||||
'updated_at' => ['field' => 't.updated_at', 'render' => 'renderDateTime', 'translate' => true],
|
||||
'related_count' => [],
|
||||
],
|
||||
];
|
||||
|
||||
public function customizeTableDef($tableDef): array
|
||||
{
|
||||
$tableDef = parent::customizeTableDef($tableDef);
|
||||
$tableDef['fields']['related_type'] = [
|
||||
'field' => 'type_name',
|
||||
'translate' => true,
|
||||
'spec' => function (Query\QueryBuilder $qb) {
|
||||
$qb->addSelect('b.name as type_name')
|
||||
->leftJoin('t', 'products_related_types', 'b', 't.id_products_related_types = b.id');
|
||||
}];
|
||||
|
||||
$tableDef['fields']['related_count'] = [
|
||||
'field' => 'count',
|
||||
'translate' => true,
|
||||
'spec' => function (Query\QueryBuilder $qb) {
|
||||
$count = sqlQueryBuilder()
|
||||
->select('COUNT(*)')
|
||||
->from('products_related', 'pr')
|
||||
->andWhere('pr.id_products_related_dynamic = t.id');
|
||||
|
||||
$qb->addSubselect($count, 'count');
|
||||
}];
|
||||
|
||||
return $tableDef;
|
||||
}
|
||||
|
||||
public function getFilterQuery(): \Query\QueryBuilder
|
||||
{
|
||||
$qb = parent::getFilterQuery();
|
||||
|
||||
if ($name = getVal('name')) {
|
||||
$qb->andWhere(\KupShop\CatalogBundle\Query\Search::searchFields($name, [
|
||||
['field' => 'name', 'match' => 'both'],
|
||||
]));
|
||||
}
|
||||
|
||||
if (!empty($relatedTypes = getVal('product_related_types'))) {
|
||||
$qb->andWhere(\KupShop\AdminBundle\Query\Invert::checkInvert(
|
||||
\Query\Operator::inIntArray($relatedTypes, 'id_products_related_types'),
|
||||
(bool) getVal('product_related_types_invert')));
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{extends "[AdminBundle]actions/baseAction.tpl"}
|
||||
|
||||
{block actionContent}
|
||||
<div class="infobox">
|
||||
<p>
|
||||
Kliknutím na Provést ihned aktualizujete související zboží u všech produktů, kterých se toto nastavení týká. Standardně k aktualizaci dochází jednou denně v noci.
|
||||
</p>
|
||||
</div>
|
||||
{/block}
|
||||
@@ -0,0 +1,37 @@
|
||||
{extends file="[shared]/menu.tpl"}
|
||||
|
||||
{block name="menu-items" append}
|
||||
<li class="nav-header smaller"><i class="glyphicon glyphicon-search"></i><span>{'search'|translate}</span></li>
|
||||
<ul class="nav-sub nav-pills">
|
||||
<form target="mainFrame" method="get" data-search="form" action="launch.php?s=list.php&type=DynamicRelatedProducts"
|
||||
class="form-inline">
|
||||
<input type="hidden" name="s" value="list.php" data-search="always">
|
||||
<input type="hidden" name="type" value="DynamicRelatedProducts" data-search="always"/>
|
||||
<li class="small-hidden">
|
||||
<div class="form-group" data-search="item">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" name="name" value="" placeholder="{'searchName'|translate}"/>
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary btn-sm" title="{'searchOnlyThis'|translate}"><i
|
||||
class="glyphicon glyphicon-search"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group invert">
|
||||
<select data-autocomplete="productsRelatedTypes" data-preload="productsRelatedTypes" id="product_related_types"
|
||||
name="product_related_types[]" multiple="multiple"
|
||||
class="selecter selecter-ajax"
|
||||
data-placeholder="{'selectType'|translate}"></select>
|
||||
{inversion field="product_related_types"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="reset" id="resetBtn" value="{'delete'|translate:"orders"}" class="btn btn-danger btn-sm"/>
|
||||
<input type="submit" value="{'searchBtn'|translate:"orders"}" class="btn btn-primary btn-sm"/>
|
||||
<input type="hidden" name="s" value="list.php"><input type="hidden" name="type" value="{$type}"/>
|
||||
</div>
|
||||
</li>
|
||||
</form>
|
||||
</ul>
|
||||
{/block}
|
||||
@@ -0,0 +1,48 @@
|
||||
{extends file="[shared]/window.tpl"}
|
||||
|
||||
{block tabs}
|
||||
{windowTab id='flapDynamicRelatedProduct'}
|
||||
{/block}
|
||||
|
||||
{block tabsContent}
|
||||
<div id="flapDynamicRelatedProduct" class="tab-pane fade active in boxFlex">
|
||||
<div class="form-group">
|
||||
<div class="col-md-6">
|
||||
<div class="">
|
||||
<label>{'name'|translate}</label>
|
||||
</div>
|
||||
<div class="">
|
||||
<input type="text" class="form-control input-sm" name="data[name]" maxlength="100" value="{$body.data.name}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="">
|
||||
<label>{'related_type'|translate}</label>
|
||||
</div>
|
||||
<div class="">
|
||||
<select
|
||||
data-autocomplete="productsRelatedTypes"
|
||||
data-preload="productsRelatedTypes"
|
||||
name="data[id_products_related_types]"
|
||||
class="required selecter small"
|
||||
required="required"
|
||||
>
|
||||
<option value="{$body.data.id_products_related_types}" selected></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>{'filter_selection_title'|translate}</h5>
|
||||
{include "block.productsFilter.tpl" filter_size=2 filter=$body.data.data.filter_selection filterName="selection" filterInputName="data[data][filter_selection]"}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>{'filter_match_title'|translate}</h5>
|
||||
{include "block.productsFilter.tpl" filter_size=2 filter=$body.data.data.filter_match filterName="match" filterInputName="data[data][filter_match]"}
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\DynamicRelatedProductsBundle\AdminRegister;
|
||||
|
||||
use KupShop\AdminBundle\AdminRegister\AdminRegister;
|
||||
use KupShop\AdminBundle\AdminRegister\IAdminRegisterDynamic;
|
||||
use KupShop\AdminBundle\AdminRegister\IAdminRegisterStatic;
|
||||
|
||||
class DynamicRelatedProductsAdminRegister extends AdminRegister implements IAdminRegisterDynamic, IAdminRegisterStatic
|
||||
{
|
||||
public function getDynamicMenu(): array
|
||||
{
|
||||
return [
|
||||
static::createMenuItem('productsMenu',
|
||||
[
|
||||
'name' => 'DynamicRelatedProducts',
|
||||
'title' => translate('DynamicRelatedProducts', 'navigation'),
|
||||
'left' => 's=menu.php&type=DynamicRelatedProducts',
|
||||
'right' => 's=list.php&type=DynamicRelatedProducts',
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPermissions(): array
|
||||
{
|
||||
return [
|
||||
static::createPermissions('DynamicRelatedProducts', [], ['DYNAMIC_RELATED_PRODUCTS']),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\DynamicRelatedProductsBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class DynamicRelatedProductsBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\DynamicRelatedProductsBundle\EventListener;
|
||||
|
||||
use KupShop\DynamicRelatedProductsBundle\Utils\DynamicRelatedProductsService;
|
||||
use KupShop\KupShopBundle\Event\CronEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class CronListener implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly DynamicRelatedProductsService $productsRelatedDynamicService,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CronEvent::RUN_EXPENSIVE => [
|
||||
['handleReload', 200],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function handleReload(): void
|
||||
{
|
||||
$collectionOfDynamicRelations = sqlQueryBuilder()
|
||||
->select('prodreldyn.id')
|
||||
->from('products_related_dynamic', 'prodreldyn')
|
||||
->execute()
|
||||
->fetchFirstColumn();
|
||||
|
||||
foreach ($collectionOfDynamicRelations as $id) {
|
||||
$this->productsRelatedDynamicService->updateDynamicRelatedProductsById($id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\DynamicRelatedProductsBundle\Inspections\Compile;
|
||||
|
||||
use KupShop\SystemInspectionBundle\Inspections\Compile\CompileInspectionInterface;
|
||||
use KupShop\SystemInspectionBundle\Inspections\Inspection;
|
||||
use KupShop\SystemInspectionBundle\InspectionWriters\MessageTypes\SimpleMessage;
|
||||
|
||||
class ModuleInspection extends Inspection implements CompileInspectionInterface
|
||||
{
|
||||
public function runInspection(): ?array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
if (
|
||||
!findModule(\Modules::PRODUCTS_RELATED, \Modules::SUB_TYPES)
|
||||
&& !findModule(\Modules::DYNAMIC_RELATED_PRODUCTS)
|
||||
) {
|
||||
$errors[] = new SimpleMessage(
|
||||
sprintf('Module "%s" must be enabled with submodule "%s".', \Modules::PRODUCTS_RELATED, \Modules::SUB_TYPES)
|
||||
);
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
_defaults:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
KupShop\DynamicRelatedProductsBundle\:
|
||||
resource: ../../{Admin/Actions,Utils,AdminRegister,EventListener,Services,Inspections}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\DynamicRelatedProductsBundle\Resources\upgrade;
|
||||
|
||||
class DynamicRelatedProductsUpgrade extends \UpgradeNew
|
||||
{
|
||||
public function check_ProductsRelatedDynamicTable(): bool
|
||||
{
|
||||
return $this->checkTableExists('products_related_dynamic');
|
||||
}
|
||||
|
||||
/** [2023-09-26] Create dynamic products related table */
|
||||
public function upgrade_ProductsRelatedDynamicTable(): void
|
||||
{
|
||||
sqlQuery('
|
||||
CREATE TABLE products_related_dynamic (
|
||||
id INT AUTO_INCREMENT,
|
||||
id_products_related_types INT NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
data LONGTEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP() NULL,
|
||||
updated_at DATETIME NULL,
|
||||
CONSTRAINT products_related_dynamic_pk PRIMARY KEY (id)
|
||||
);
|
||||
');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_AddColumnToProductsRelated(): bool
|
||||
{
|
||||
return $this->checkColumnExists('products_related', 'id_products_related_dynamic');
|
||||
}
|
||||
|
||||
/** [2023-09-26] Added id_products_related_dynamic column to products_related table */
|
||||
public function upgrade_AddColumnToProductsRelated(): void
|
||||
{
|
||||
sqlQuery('
|
||||
ALTER TABLE products_related
|
||||
ADD id_products_related_dynamic INT NULL,
|
||||
ADD CONSTRAINT products_related_products_related_dynamic_id_fk
|
||||
FOREIGN KEY (id_products_related_dynamic) REFERENCES products_related_dynamic (id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_AddForeignKey(): bool
|
||||
{
|
||||
return findModule(\Modules::PRODUCTS_RELATED, \Modules::SUB_TYPES) && $this->checkForeignKeyExists('products_related_dynamic', 'id_products_related_types');
|
||||
}
|
||||
|
||||
/** [2023-10-17] Added foreign key for products_related_types */
|
||||
public function upgrade_AddForeignKey(): void
|
||||
{
|
||||
sqlQuery('
|
||||
ALTER TABLE products_related_dynamic
|
||||
ADD CONSTRAINT products_related_dynamic_products_related_types_id_fk
|
||||
FOREIGN KEY (id_products_related_types) REFERENCES products_related_types (id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"products_related_types": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Foo"
|
||||
}
|
||||
],
|
||||
"products_related": [],
|
||||
"products_related_dynamic": [
|
||||
{
|
||||
"id": 1,
|
||||
"id_products_related_types": 1,
|
||||
"name": "Bar",
|
||||
"data": "{\"filter_selection\":{\"products\":[\"1\"]},\"filter_match\":{\"products\":[\"5\"]}}",
|
||||
"created_at": "2023-10-02 07:56:49",
|
||||
"updated_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\DynamicRelatedProductsBundle\Tests;
|
||||
|
||||
use KupShop\DynamicRelatedProductsBundle\Utils\DynamicRelatedProductsService;
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use Query\Operator;
|
||||
|
||||
class DynamicRelatedProductsServiceTest extends \DatabaseTestCase
|
||||
{
|
||||
private const defaultId = 1;
|
||||
|
||||
/**
|
||||
* @dataProvider provideFilterData
|
||||
*/
|
||||
public function testQuantityAfterInsert(
|
||||
int $automaticQuantity,
|
||||
int $manualQuantity,
|
||||
string $filter,
|
||||
?array $insertRelated = null,
|
||||
?callable $after = null,
|
||||
): void {
|
||||
sqlQueryBuilder()
|
||||
->update('products_related_dynamic')
|
||||
->directValues(['data' => $filter])
|
||||
->where(Operator::equals(['id' => self::defaultId]))
|
||||
->execute();
|
||||
|
||||
if ($insertRelated) {
|
||||
foreach ($insertRelated as [$a,$b,$c]) {
|
||||
sqlQueryBuilder()
|
||||
->insert('products_related')
|
||||
->directValues([
|
||||
'id_top_product' => $a,
|
||||
'id_rel_product' => $b,
|
||||
'type' => self::defaultId,
|
||||
'id_products_related_dynamic' => $c,
|
||||
])
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/** @var DynamicRelatedProductsService $service */
|
||||
$service = ServiceContainer::getService(DynamicRelatedProductsService::class);
|
||||
$result = $service->updateDynamicRelatedProductsById(self::defaultId);
|
||||
$this->assertTrue($result);
|
||||
|
||||
$prepareMatch = function (bool $automatic = false) {
|
||||
$conds = $automatic ? 'isNotNull' : 'isNull';
|
||||
|
||||
return sqlQueryBuilder()
|
||||
->select('*')
|
||||
->from('products_related')
|
||||
->andWhere(Operator::equals(['id_top_product' => 1]))
|
||||
->andWhere(Operator::$conds('id_products_related_dynamic'))
|
||||
->execute()
|
||||
->rowCount();
|
||||
};
|
||||
|
||||
$this->assertEquals($manualQuantity, $prepareMatch(false));
|
||||
$this->assertEquals($automaticQuantity, $prepareMatch(true));
|
||||
if ($after) {
|
||||
$after();
|
||||
}
|
||||
}
|
||||
|
||||
public function provideFilterData(): \Generator
|
||||
{
|
||||
yield 'add products to empty list' => [
|
||||
'automaticQuantity' => 1,
|
||||
'manualQuantity' => 2,
|
||||
'filter' => '{"filter_selection":{"products":["1"]},"filter_match":{"products":["5"]}}',
|
||||
'insertRelated' => [
|
||||
[1, 2, null],
|
||||
[1, 3, null],
|
||||
],
|
||||
];
|
||||
yield 'insert with existing manual relation' => [
|
||||
'automaticQuantity' => 0,
|
||||
'manualQuantity' => 1,
|
||||
'filter' => '{"filter_selection":{"products":["1"]},"filter_match":{"products":["2"]}}',
|
||||
'insertRelated' => [
|
||||
[1, 2, null],
|
||||
],
|
||||
];
|
||||
yield 'replace existing relations' => [
|
||||
'automaticQuantity' => 2,
|
||||
'manualQuantity' => 2,
|
||||
'filter' => '{"filter_selection":{"products":["1"]},"filter_match":{"products":["6", "7"]}}',
|
||||
'insertRelated' => [
|
||||
[1, 2, null],
|
||||
[1, 3, null],
|
||||
[1, 4, self::defaultId],
|
||||
[1, 5, self::defaultId],
|
||||
],
|
||||
];
|
||||
yield 'added to existing relations' => [
|
||||
'automaticQuantity' => 4,
|
||||
'manualQuantity' => 2,
|
||||
'filter' => '{"filter_selection":{"products":["1"]},"filter_match":{"products":["4", "5", "6", "7"]}}',
|
||||
'insertRelated' => [
|
||||
[1, 2, null],
|
||||
[1, 3, null],
|
||||
[1, 4, self::defaultId],
|
||||
[1, 5, self::defaultId],
|
||||
],
|
||||
];
|
||||
yield 'delete when filter does not match relations' => [
|
||||
'automaticQuantity' => 0,
|
||||
'manualQuantity' => 2,
|
||||
'filter' => '{"filter_selection":{"products":["1"]},"filter_match":{"products":[]}}',
|
||||
'insertRelated' => [
|
||||
[1, 2, null],
|
||||
[1, 3, null],
|
||||
[1, 4, self::defaultId],
|
||||
[1, 5, self::defaultId],
|
||||
],
|
||||
];
|
||||
|
||||
yield 'update when other relations exists' => [
|
||||
'automaticQuantity' => 2,
|
||||
'manualQuantity' => 2,
|
||||
'filter' => '{"filter_selection":{"products":["1"]},"filter_match":{"products":["4", "5"]}}',
|
||||
'insertRelated' => [
|
||||
[1, 2, null],
|
||||
[1, 3, null],
|
||||
[2, 4, self::defaultId],
|
||||
[2, 5, self::defaultId],
|
||||
[1, 4, self::defaultId],
|
||||
[1, 5, self::defaultId],
|
||||
],
|
||||
];
|
||||
yield 'replace exists relations with wrong id_top_product' => [
|
||||
'automaticQuantity' => 0,
|
||||
'manualQuantity' => 2,
|
||||
'filter' => '{"filter_selection":{"products":["2"]},"filter_match":{"products":["4", "5"]}}',
|
||||
'insertRelated' => [
|
||||
[1, 2, null],
|
||||
[1, 3, null],
|
||||
[1, 4, self::defaultId],
|
||||
[1, 5, self::defaultId],
|
||||
],
|
||||
'after' => function () {
|
||||
$this->assertEquals(
|
||||
2,
|
||||
sqlQueryBuilder()
|
||||
->select('*')
|
||||
->from('products_related')
|
||||
->andWhere(Operator::equals(['id_top_product' => 2]))
|
||||
->andWhere(Operator::isNotNull('id_products_related_dynamic'))
|
||||
->execute()
|
||||
->rowCount()
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
yield 'before added 2 products' => [
|
||||
'automaticQuantity' => 2,
|
||||
'manualQuantity' => 2,
|
||||
'filter' => '{"filter_selection":{"products":["1"]},"filter_match":{"products":["4", "5"]}}',
|
||||
'insertRelated' => [
|
||||
[1, 2, null],
|
||||
[1, 3, null],
|
||||
],
|
||||
];
|
||||
|
||||
yield 'delete one product id and check prev state' => [
|
||||
'automaticQuantity' => 1,
|
||||
'manualQuantity' => 2,
|
||||
'filter' => '{"filter_selection":{"products":["1"]},"filter_match":{"products":["4"]}}',
|
||||
'insertRelated' => [
|
||||
[1, 2, null],
|
||||
[1, 3, null],
|
||||
[1, 4, self::defaultId],
|
||||
[1, 5, self::defaultId],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getDataSet(): \IteratorAggregate
|
||||
{
|
||||
return $this->getJsonDataSetFromFile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\DynamicRelatedProductsBundle\Utils;
|
||||
|
||||
use KupShop\CatalogBundle\Util\ProductsFilterSpecs;
|
||||
use Query\Operator;
|
||||
|
||||
class DynamicRelatedProductsService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductsFilterSpecs $productsFilterSpecs,
|
||||
) {
|
||||
}
|
||||
|
||||
private function applyFilter($data): array
|
||||
{
|
||||
$specs = $this->productsFilterSpecs->getSpecs($data);
|
||||
|
||||
if (!$specs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$array = sqlQueryBuilder()
|
||||
->select('p.id')
|
||||
->from('products', 'p')
|
||||
->andWhere($specs)
|
||||
->orderBy('p.id', 'DESC')
|
||||
->execute()
|
||||
->fetchFirstColumn();
|
||||
|
||||
return array_unique($array);
|
||||
}
|
||||
|
||||
public function prepareDataForDifferenceInsert(int $id, array $values): void
|
||||
{
|
||||
$oldValues = sqlQueryBuilder()
|
||||
->select('id_top_product', 'id_rel_product', 'position', 'type', 'id_products_related_dynamic')
|
||||
->from('products_related')
|
||||
->where(Operator::equals([
|
||||
'id_products_related_dynamic' => $id,
|
||||
]))
|
||||
->execute()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$currentDatabaseValues = [];
|
||||
foreach ($oldValues as $v) {
|
||||
$key = sprintf('%s-%s-%s', $v['id_top_product'], $v['id_rel_product'], $v['id_products_related_dynamic']);
|
||||
$currentDatabaseValues[$key] = $v;
|
||||
}
|
||||
|
||||
$savedValues = array_keys($currentDatabaseValues);
|
||||
$valuesKeys = array_keys($values);
|
||||
$delete = array_diff($savedValues, $valuesKeys);
|
||||
$insert = array_diff($valuesKeys, $savedValues);
|
||||
|
||||
foreach ($delete as $v) {
|
||||
sqlQueryBuilder()
|
||||
->delete('products_related')
|
||||
->where(Operator::equals($currentDatabaseValues[$v]))
|
||||
->execute();
|
||||
}
|
||||
|
||||
foreach ($insert as $v) {
|
||||
sqlQueryBuilder()
|
||||
->insert('products_related')
|
||||
->directValues($values[$v])
|
||||
->onDuplicateKeyUpdate([
|
||||
'id_top_product' => 'id_top_product',
|
||||
])
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
public function updateDynamicRelatedProductsById(int $id): bool
|
||||
{
|
||||
$data = sqlQueryBuilder()->select('*')
|
||||
->from('products_related_dynamic')
|
||||
->andWhere(Operator::equals(['id' => $id]))
|
||||
->execute()
|
||||
->fetch();
|
||||
|
||||
$filter = json_decode($data['data'], true);
|
||||
$fixedPosition = 1000;
|
||||
$fixedLengthOfMatch = 10;
|
||||
|
||||
$multiCollection = [];
|
||||
$primary = $this->applyFilter($filter['filter_selection'] ?? []);
|
||||
$secondary = array_slice($this->applyFilter($filter['filter_match'] ?? []), 0, $fixedLengthOfMatch);
|
||||
|
||||
$adds = [
|
||||
'type' => $data['id_products_related_types'],
|
||||
'id_products_related_dynamic' => $data['id'],
|
||||
];
|
||||
|
||||
foreach ($primary as $item) {
|
||||
foreach ($secondary as $subItem) {
|
||||
if ($item === $subItem) {
|
||||
continue;
|
||||
}
|
||||
$key = sprintf('%s-%s-%s', $item, $subItem, $data['id_products_related_types']);
|
||||
|
||||
$multiCollection[$key] = [
|
||||
'id_top_product' => $item,
|
||||
'id_rel_product' => $subItem,
|
||||
'position' => $fixedPosition,
|
||||
] + $adds;
|
||||
}
|
||||
}
|
||||
|
||||
$this->prepareDataForDifferenceInsert($id, $multiCollection);
|
||||
$this->updateLastGeneratedDate($id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function updateLastGeneratedDate(int $id): void
|
||||
{
|
||||
sqlQueryBuilder()
|
||||
->update('products_related_dynamic')
|
||||
->directValues([
|
||||
'updated_at' => (new \DateTimeImmutable(timezone: new \DateTimeZone('Europe/Prague')))->format('Y-m-d H:i:s'),
|
||||
])
|
||||
->where(Operator::equals(['id' => $id]))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user