module/Application/src/Service/SpecificationsService.php
<?php
namespace Application\Service;
use Application\ItemNameFormatter;
use Application\Model\Item;
use Application\Model\ItemParent;
use Application\Model\Picture;
use Application\Model\VehicleType;
use Application\Spec\Table\Car as CarSpecTable;
use Application\Validator\Attrs\IsFloatOrNull;
use Application\Validator\Attrs\IsIntOrNull;
use ArrayAccess;
use ArrayObject;
use Autowp\User\Model\User;
use Exception;
use Laminas\Db\Adapter\Adapter;
use Laminas\Db\Sql;
use Laminas\Db\TableGateway\TableGateway;
use Laminas\I18n\Translator\TranslatorInterface;
use Laminas\InputFilter\ArrayInput;
use Laminas\InputFilter\Input;
use Laminas\Paginator;
use NumberFormatter;
use function array_diff;
use function array_intersect;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_replace;
use function array_reverse;
use function array_unique;
use function Autowp\Commons\currentFromResultSetInterface;
use function count;
use function implode;
use function in_array;
use function is_array;
use function reset;
use function serialize;
use function strlen;
class SpecificationsService
{
private const
DEFAULT_ZONE_ID = 1,
ENGINE_ZONE_ID = 5,
BUS_ZONE_ID = 3;
private const BUS_VEHICLE_TYPES = [19, 39, 28, 32];
private const
TOP_PERSPECTIVES = [10, 1, 7, 8, 11, 12, 2, 4, 13, 5],
BOTTOM_PERSPECTIVES = [13, 2, 9, 6, 5];
public const NULL_VALUE_STR = '-';
private const
WEIGHT_NONE = 0,
WEIGHT_FIRST_ACTUAL = 1,
WEIGHT_SECOND_ACTUAL = 0.1,
WEIGHT_WRONG = -1;
private TableGateway $attributeTable;
private TableGateway $listOptionsTable;
private array $listOptions = [];
private array $listOptionsChilds = [];
private TableGateway $unitTable;
private array $units;
private TableGateway $userValueTable;
private array $attributes;
private array $childs;
private array $zoneAttrs = [];
private array $carChildsCache = [];
private array $engineAttributes;
private TableGateway $typeTable;
private array $types;
private User $userModel;
private array $valueWeights = [];
private TranslatorInterface $translator;
private ItemNameFormatter $itemNameFormatter;
private Item $itemModel;
private ItemParent $itemParent;
private Picture $picture;
private VehicleType $vehicleType;
private TableGateway $zoneAttributeTable;
private TableGateway $userValueFloatTable;
private TableGateway $userValueIntTable;
private TableGateway $userValueListTable;
private TableGateway $userValueStringTable;
private TableGateway $valueTable;
private TableGateway $valueFloatTable;
private TableGateway $valueIntTable;
private TableGateway $valueListTable;
private TableGateway $valueStringTable;
public function __construct(
TranslatorInterface $translator,
ItemNameFormatter $itemNameFormatter,
Item $itemModel,
ItemParent $itemParent,
Picture $picture,
VehicleType $vehicleType,
User $userModel,
TableGateway $unitTable,
TableGateway $listOptionsTable,
TableGateway $typeTable,
TableGateway $attributeTable,
TableGateway $zoneAttributeTable,
TableGateway $userValueTable,
TableGateway $userValueFloatTable,
TableGateway $userValueIntTable,
TableGateway $userValueListTable,
TableGateway $userValueStringTable,
TableGateway $valueTable,
TableGateway $valueFloatTable,
TableGateway $valueIntTable,
TableGateway $valueListTable,
TableGateway $valueStringTable
) {
$this->translator = $translator;
$this->itemNameFormatter = $itemNameFormatter;
$this->itemModel = $itemModel;
$this->itemParent = $itemParent;
$this->picture = $picture;
$this->vehicleType = $vehicleType;
$this->userModel = $userModel;
$this->unitTable = $unitTable;
$this->listOptionsTable = $listOptionsTable;
$this->typeTable = $typeTable;
$this->attributeTable = $attributeTable;
$this->zoneAttributeTable = $zoneAttributeTable;
$this->userValueTable = $userValueTable;
$this->userValueFloatTable = $userValueFloatTable;
$this->userValueIntTable = $userValueIntTable;
$this->userValueListTable = $userValueListTable;
$this->userValueStringTable = $userValueStringTable;
$this->valueTable = $valueTable;
$this->valueFloatTable = $valueFloatTable;
$this->valueIntTable = $valueIntTable;
$this->valueListTable = $valueListTable;
$this->valueStringTable = $valueStringTable;
}
private function loadUnits(): void
{
if (! isset($this->units)) {
$units = [];
foreach ($this->unitTable->select([]) as $unit) {
$id = (int) $unit['id'];
$units[$id] = [
'id' => $id,
'name' => $unit['name'],
'abbr' => $unit['abbr'],
];
}
$this->units = $units;
}
}
public function getUnit(int $id): ?array
{
$this->loadUnits();
return $this->units[$id] ?? null;
}
public function getZoneIdByCarTypeId(int $itemTypeId, array $vehicleTypeIds): int
{
if ($itemTypeId === Item::ENGINE) {
return self::ENGINE_ZONE_ID;
}
$zoneId = self::DEFAULT_ZONE_ID;
if (array_intersect($vehicleTypeIds, self::BUS_VEHICLE_TYPES)) {
$zoneId = self::BUS_ZONE_ID;
}
return $zoneId;
}
private function loadListOptions(array $attributeIds): void
{
$ids = array_diff($attributeIds, array_keys($this->listOptions));
if (count($ids)) {
$select = new Sql\Select($this->listOptionsTable->getTable());
$select
->where(new Sql\Predicate\In('attribute_id', $ids))
->order('position');
$rows = $this->listOptionsTable->selectWith($select);
foreach ($rows as $row) {
$aid = (int) $row['attribute_id'];
$id = (int) $row['id'];
$pid = (int) $row['parent_id'];
if (! isset($this->listOptions[$aid])) {
$this->listOptions[$aid] = [];
}
$this->listOptions[$aid][$id] = $row['name'];
if (! isset($this->listOptionsChilds[$aid][$pid])) {
$this->listOptionsChilds[$aid][$pid] = [$id];
} else {
$this->listOptionsChilds[$aid][$pid][] = $id;
}
}
}
}
public function getListOptionsArray(int $attributeId): array
{
$this->loadListOptions([$attributeId]);
if (! isset($this->listOptions[$attributeId])) {
return [];
}
return $this->getListOptionsArrayRecursive($attributeId, 0);
}
private function getListOptionsArrayRecursive(int $aid, int $parentId): array
{
$result = [];
if (isset($this->listOptionsChilds[$aid][$parentId])) {
foreach ($this->listOptionsChilds[$aid][$parentId] as $childId) {
$result[] = [
'id' => (int) $childId,
'name' => $this->translator->translate($this->listOptions[$aid][$childId]),
];
$childOptions = $this->getListOptionsArrayRecursive($aid, $childId);
foreach ($childOptions as &$value) {
$value['name'] = '… ' . $this->translator->translate($value['name']);
}
unset($value); // prevent future bugs
$result = array_merge($result, $childOptions);
}
}
return $result;
}
private function getListsOptions(array $attributeIds): array
{
$this->loadListOptions($attributeIds);
$result = [];
foreach ($attributeIds as $aid) {
if (isset($this->listOptions[$aid])) {
$result[$aid] = $this->getListOptions($aid, 0);
}
}
return $result;
}
private function getListOptions(int $aid, int $parentId): array
{
$result = [];
if (isset($this->listOptionsChilds[$aid][$parentId])) {
foreach ($this->listOptionsChilds[$aid][$parentId] as $childId) {
$result[(int) $childId] = $this->translator->translate($this->listOptions[$aid][$childId]);
$childOptions = $this->getListOptions($aid, $childId);
foreach ($childOptions as &$value) {
$value = '…' . $this->translator->translate($value);
}
unset($value); // prevent future bugs
$result = array_replace($result, $childOptions);
}
}
return $result;
}
/**
* @throws Exception
*/
private function getListOptionsText(int $attributeId, int $id): string
{
$this->loadListOptions([$attributeId]);
if (! isset($this->listOptions[$attributeId][$id])) {
throw new Exception("list option `$id` not found");
}
return $this->translator->translate($this->listOptions[$attributeId][$id], 'default');
}
/**
* @throws Exception
*/
public function getFilterSpec(int $attributeId): ?array
{
$filters = [];
$validators = [];
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
return null;
}
$type = null;
if ($attribute['typeId']) {
$type = $this->getType($attribute['typeId']);
}
if (! $type) {
return null;
}
$multioptions = $this->getListsOptions([$attributeId]);
$maxlength = null;
if ($type['maxlength']) {
$maxlength = $type['maxlength'];
}
$inputType = Input::class;
switch ($type['id']) {
case 1: // string
$filters = [['name' => 'StringTrim']];
if ($maxlength) {
$validators[] = [
'name' => 'StringLength',
'options' => [
'max' => $type['maxlength'],
],
];
}
break;
case 2: // int
$filters = [['name' => 'StringTrim']];
$validators = [
[
'name' => IsIntOrNull::class,
'options' => ['locale' => 'en_US'],
],
];
break;
case 3: // float
$filters = [['name' => 'StringTrim']];
$validators = [
[
'name' => IsFloatOrNull::class,
'options' => ['locale' => 'en_US'],
],
];
break;
case 4: // textarea
$filters = [['name' => 'StringTrim']];
break;
case 5: // checkbox
$validators = [
[
'name' => 'InArray',
'options' => [
'haystack' => [
'',
'-',
'0',
'1',
],
],
],
];
break;
case 6: // select
case 7: // treeselect
$haystack = [
'',
'-',
];
if (isset($multioptions[$attribute['id']])) {
$haystack = array_merge(
$haystack,
array_keys($multioptions[$attribute['id']])
);
}
$validators = [
[
'name' => 'InArray',
'options' => [
'haystack' => $haystack,
],
],
];
if ($attribute['isMultiple']) {
$inputType = ArrayInput::class;
}
break;
}
return [
'type' => $inputType,
'required' => false,
'filters' => $filters,
'validators' => $validators,
];
}
private function loadZone(int $id): array
{
if (isset($this->zoneAttrs[$id])) {
return $this->zoneAttrs[$id];
}
$select = new Sql\Select($this->zoneAttributeTable->getTable());
$select->columns(['attribute_id'])
->where(['zone_id' => $id])
->order('position');
$result = [];
foreach ($this->zoneAttributeTable->selectWith($select) as $row) {
$result[] = (int) $row['attribute_id'];
}
$this->zoneAttrs[$id] = $result;
return $result;
}
private function loadAttributes(): self
{
if (! isset($this->attributes)) {
$array = [];
$childs = [];
$select = new Sql\Select($this->attributeTable->getTable());
$select->order('position');
foreach ($this->attributeTable->selectWith($select) as $row) {
$id = (int) $row['id'];
$pid = (int) $row['parent_id'];
$array[$id] = [
'id' => $id,
'name' => $row['name'],
'description' => $row['description'],
'typeId' => (int) $row['type_id'],
'unitId' => (int) $row['unit_id'],
'isMultiple' => $row['multiple'],
'precision' => $row['precision'],
'parentId' => $pid ? $pid : null,
];
if (! isset($childs[$id])) {
$childs[$id] = [];
}
if (! isset($childs[$pid])) {
$childs[$pid] = [$id];
} else {
$childs[$pid][] = $id;
}
}
$this->attributes = $array;
$this->childs = $childs;
}
return $this;
}
public function getAttribute(int $id): ?array
{
$this->loadAttributes();
return $this->attributes[$id] ?? null;
}
/**
* @param mixed $value
* @throws Exception
*/
public function setUserValue2(int $uid, int $attributeId, int $itemId, $value, bool $empty): void
{
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("attribute `$attributeId` not found");
}
$somethingChanged = false;
$userValueDataTable = $this->getUserValueDataTable($attribute['typeId']);
$userValuePrimaryKey = [
'attribute_id' => $attribute['id'],
'item_id' => $itemId,
'user_id' => $uid,
];
if ($attribute['isMultiple']) {
// remove value descriptors
$this->userValueTable->delete([
'attribute_id = ?' => $attribute['id'],
'item_id = ?' => $itemId,
'user_id = ?' => $uid,
]);
// remove values
$userValueDataTable->delete($userValuePrimaryKey);
if ($value) {
if ($value === [null]) {
$value = [];
}
if ($empty) {
$value = [null];
}
if (count($value)) {
// insert new descriptors and values
/** @var Adapter $adapter */
$adapter = $this->userValueTable->getAdapter();
$adapter->query('
INSERT INTO attrs_user_values (attribute_id, item_id, user_id, add_date, update_date)
VALUES (:attribute_id, :item_id, :user_id, NOW(), NOW())
ON DUPLICATE KEY UPDATE update_date = VALUES(update_date)
', $userValuePrimaryKey);
$ordering = 1;
foreach ($value as $oneValue) {
$params = array_replace($userValuePrimaryKey, [
'ordering' => $ordering,
'value' => $oneValue,
]);
/** @var Adapter $adapter */
$adapter = $userValueDataTable->getAdapter();
$adapter->query('
INSERT INTO `' . $userValueDataTable->getTable() . '`
(attribute_id, item_id, user_id, ordering, value)
VALUES (:attribute_id, :item_id, :user_id, :ordering, :value)
ON DUPLICATE KEY UPDATE ordering = VALUES(ordering), value = VALUES(value)
', $params);
$ordering++;
}
}
}
$somethingChanged = $this->updateAttributeActualValue($attribute, $itemId);
} else {
if (strlen($value) > 0 || $empty) {
// insert/update value descriptor
$userValue = currentFromResultSetInterface($this->userValueTable->select($userValuePrimaryKey));
// insert update value
$userValueData = currentFromResultSetInterface($userValueDataTable->select($userValuePrimaryKey));
if ($empty) {
$value = null;
}
if ($userValueData) {
$valueChanged = $value === null
? $userValueData['value'] !== null
: $userValueData['value'] !== $value;
} else {
$valueChanged = true;
}
if (! $userValue || $valueChanged) {
/** @var Adapter $adapter */
$adapter = $this->userValueTable->getAdapter();
$adapter->query('
INSERT INTO attrs_user_values (attribute_id, item_id, user_id, add_date, update_date)
VALUES (:attribute_id, :item_id, :user_id, NOW(), NOW())
ON DUPLICATE KEY UPDATE update_date = VALUES(update_date)
', $userValuePrimaryKey);
$params = array_replace($userValuePrimaryKey, [
'value' => $value,
]);
/** @var Adapter $adapter */
$adapter = $userValueDataTable->getAdapter();
$adapter->query('
INSERT INTO `' . $userValueDataTable->getTable() . '` (attribute_id, item_id, user_id, value)
VALUES (:attribute_id, :item_id, :user_id, :value)
ON DUPLICATE KEY UPDATE value = VALUES(value)
', $params);
$somethingChanged = $this->updateAttributeActualValue($attribute, $itemId);
}
} else {
// delete value descriptor
$affected = $this->userValueTable->delete($userValuePrimaryKey);
// remove value
$affected += $userValueDataTable->delete($userValuePrimaryKey);
if ($affected > 0) {
$somethingChanged = $this->updateAttributeActualValue($attribute, $itemId);
}
}
}
if ($somethingChanged) {
$this->propagateInheritance($attribute, $itemId);
$this->propageteEngine($attribute, $itemId);
$this->refreshConflictFlag($attribute['id'], $itemId);
}
}
/**
* @param mixed $value
* @throws Exception
*/
public function setUserValue(int $uid, int $attributeId, int $itemId, $value): void
{
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("attribute `$attributeId` not found");
}
$somethingChanged = false;
$userValueDataTable = $this->getUserValueDataTable($attribute['typeId']);
$userValuePrimaryKey = [
'attribute_id' => $attribute['id'],
'item_id' => $itemId,
'user_id' => $uid,
];
if ($attribute['isMultiple']) {
// remove value descriptiors
$this->userValueTable->delete([
'attribute_id = ?' => $attribute['id'],
'item_id = ?' => $itemId,
'user_id = ?' => $uid,
]);
// remove values
$userValueDataTable->delete($userValuePrimaryKey);
if ($value) {
$empty = true;
$valueNot = false;
foreach ($value as $oneValue) {
if ($oneValue) {
$empty = false;
}
if ($oneValue === self::NULL_VALUE_STR) {
$valueNot = true;
}
}
if (! $empty) {
// insert new descriptiors and values
$this->userValueTable->insert(array_replace([
'add_date' => new Sql\Expression('NOW()'),
'update_date' => new Sql\Expression('NOW()'),
], $userValuePrimaryKey));
$ordering = 1;
if ($valueNot) {
$value = [null];
}
foreach ($value as $oneValue) {
$userValueDataTable->insert(array_replace([
'ordering' => $ordering,
'value' => $oneValue,
], $userValuePrimaryKey));
$ordering++;
}
}
}
$somethingChanged = $this->updateAttributeActualValue($attribute, $itemId);
} else {
if (strlen($value) > 0) {
// insert/update value decsriptor
$userValue = currentFromResultSetInterface($this->userValueTable->select($userValuePrimaryKey));
// insert update value
$userValueData = currentFromResultSetInterface($userValueDataTable->select($userValuePrimaryKey));
if ($value === self::NULL_VALUE_STR) {
$value = null;
}
if ($userValueData) {
$valueChanged = $value === null
? $userValueData['value'] !== null
: $userValueData['value'] !== $value;
} else {
$valueChanged = true;
}
if (! $userValue || $valueChanged) {
if (! $userValue) {
$this->userValueTable->insert(array_replace([
'add_date' => new Sql\Expression('NOW()'),
'update_date' => new Sql\Expression('NOW()'),
], $userValuePrimaryKey));
} else {
$this->userValueTable->update([
'update_date' => new Sql\Expression('NOW()'),
], $userValuePrimaryKey);
}
$set = ['value' => $value];
if ($userValueData) {
$userValueDataTable->update($set, $userValuePrimaryKey);
} else {
$userValueDataTable->insert(array_merge($set, $userValuePrimaryKey));
}
$somethingChanged = $this->updateAttributeActualValue($attribute, $itemId);
}
} else {
// delete value descriptor
$affected = $this->userValueTable->delete($userValuePrimaryKey);
// remove value
$affected += $userValueDataTable->delete($userValuePrimaryKey);
if ($affected > 0) {
$somethingChanged = $this->updateAttributeActualValue($attribute, $itemId);
}
}
}
if ($somethingChanged) {
$this->propagateInheritance($attribute, $itemId);
$this->propageteEngine($attribute, $itemId);
$this->refreshConflictFlag($attribute['id'], $itemId);
}
}
private function getEngineAttributeIds(): array
{
if (isset($this->engineAttributes)) {
return $this->engineAttributes;
}
$select = new Sql\Select($this->attributeTable->getTable());
$select->columns(['id'])
->join('attrs_zone_attributes', 'attrs_attributes.id = attrs_zone_attributes.attribute_id', [])
->where(['attrs_zone_attributes.zone_id' => self::ENGINE_ZONE_ID]);
$result = [];
foreach ($this->attributeTable->selectWith($select) as $row) {
$result[] = (int) $row['id'];
}
$this->engineAttributes = $result;
return $result;
}
/**
* @throws Exception
*/
private function propageteEngine(array $attribute, int $itemId): void
{
if (! $this->isEngineAttributeId($attribute['id'])) {
return;
}
if (! $attribute['typeId']) {
return;
}
$vehicles = $this->itemModel->getTable()->select([
'engine_item_id' => $itemId,
]);
foreach ($vehicles as $vehicle) {
$this->updateAttributeActualValue($attribute, $vehicle['id']);
}
}
/**
* @return array|ArrayAccess
*/
private function getChildCarIds(int $parentId)
{
if (! isset($this->carChildsCache[$parentId])) {
$this->carChildsCache[$parentId] = $this->itemParent->getChildItemsIds($parentId);
}
return $this->carChildsCache[$parentId];
}
private function haveOwnAttributeValue(int $attributeId, int $itemId): bool
{
return (bool) currentFromResultSetInterface($this->userValueTable->select([
'attribute_id' => $attributeId,
'item_id' => $itemId,
]));
}
/**
* @throws Exception
*/
private function propagateInheritance(array $attribute, int $itemId): void
{
$childIds = $this->getChildCarIds($itemId);
foreach ($childIds as $childId) {
// update only if row use inheritance
$haveValue = $this->haveOwnAttributeValue($attribute['id'], $childId);
if (! $haveValue) {
$value = $this->calcInheritedValue($attribute, $childId);
$changed = $this->setActualValue($attribute, $childId, $value);
if ($changed) {
$this->propagateInheritance($attribute, $childId);
$this->propageteEngine($attribute, $childId);
}
}
}
}
/**
* @param array|ArrayAccess $car
* @return array|ArrayObject|null
* @throws Exception
*/
private function specPicture($car, ?array $perspectives)
{
$order = [];
if ($perspectives) {
foreach ($perspectives as $pid) {
$order[] = new Sql\Expression('picture_item.perspective_id = ? DESC', [$pid]);
}
} else {
$order[] = 'pictures.id desc';
}
return $this->picture->getRow([
'status' => Picture::STATUS_ACCEPTED,
'item' => [
'ancestor_or_self' => $car['id'],
],
'order' => $order,
'group' => ['picture_item.perspective_id'],
]);
}
public function getAttributes(array $options = []): array
{
$defaults = [
'zone' => null,
'parent' => null,
'recursive' => false,
];
$options = array_merge($defaults, $options);
$zone = $options['zone'];
$parent = $options['parent'];
$recursive = $options['recursive'];
$this->loadAttributes();
if ($zone) {
$this->loadZone($zone);
}
$attributes = [];
if ($recursive) {
$ids = [];
if ($zone) {
if (isset($this->childs[$parent])) {
$ids = array_intersect($this->zoneAttrs[$zone], $this->childs[$parent]);
}
} else {
if (isset($this->childs[$parent])) {
$ids = $this->childs[$parent];
}
}
foreach ($ids as $id) {
$attributes[] = $this->attributes[$id];
}
} else {
if ($zone) {
$attributes = [];
if ($parent !== null) {
$ids = [];
if (isset($this->childs[$parent])) {
$ids = array_intersect($this->zoneAttrs[$zone], $this->childs[$parent]);
}
} else {
$ids = $this->zoneAttrs[$zone];
}
foreach ($ids as $id) {
$attributes[] = $this->attributes[$id];
}
} else {
if ($parent !== null) {
foreach ($this->childs[$parent] as $id) {
$attributes[] = $this->attributes[$id];
}
} else {
$attributes = $this->attributes;
}
}
}
if ($recursive) {
foreach ($attributes as &$attr) {
$attr['childs'] = $this->getAttributes([
'zone' => $zone,
'parent' => $attr['id'],
'recursive' => $recursive,
]);
}
}
return $attributes;
}
/**
* @throws Exception
* @return null|mixed
*/
public function getActualValue(int $attributeId, int $itemId)
{
if (! $itemId) {
throw new Exception("item_id not set");
}
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("attribute `$attributeId` not found");
}
$valuesTable = $this->getValueDataTable($attribute['typeId']);
$select = new Sql\Select($valuesTable->getTable());
$select->columns(['value'])
->where([
'attribute_id' => $attribute['id'],
'item_id' => $itemId,
]);
if ($attribute['isMultiple']) {
$select->order('ordering');
$rows = $valuesTable->selectWith($select);
$values = [];
foreach ($rows as $row) {
$values[] = $row['value'];
}
if (count($values)) {
return $values;
}
} else {
$row = currentFromResultSetInterface($valuesTable->selectWith($select));
if ($row) {
return $row['value'];
}
}
return null;
}
/**
* @throws Exception
*/
public function deleteUserValue(int $attributeId, int $itemId, int $userId): void
{
if (! $itemId) {
throw new Exception("item_id not set");
}
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("attribute not found");
}
$valueDataTable = $this->getUserValueDataTable($attribute['typeId']);
$valueDataTable->delete([
'attribute_id' => $attributeId,
'item_id' => $itemId,
'user_id' => $userId,
]);
$this->userValueTable->delete([
'attribute_id' => $attributeId,
'item_id' => $itemId,
'user_id' => $userId,
]);
$this->updateActualValue($attribute['id'], $itemId);
}
/**
* @throws Exception
*/
public function specifications(array $cars, array $options): CarSpecTable
{
$options = array_merge([
'contextCarId' => null,
'language' => 'en',
], $options);
$language = $options['language'];
$contextCarId = (int) $options['contextCarId'];
$ids = [];
foreach ($cars as $car) {
$ids[] = $car['id'];
}
$result = [];
$zoneIds = [];
foreach ($cars as $car) {
$vehicleTypeIds = $this->vehicleType->getVehicleTypes($car['id']);
$zoneId = $this->getZoneIdByCarTypeId($car['item_type_id'], $vehicleTypeIds);
$zoneIds[$zoneId] = true;
}
$zoneMixed = count($zoneIds) > 1;
if ($zoneMixed) {
$specsZoneId = null;
} else {
$keys = array_keys($zoneIds);
$specsZoneId = reset($keys);
}
$attributes = $this->getAttributes([
'zone' => $specsZoneId,
'recursive' => true,
'parent' => 0,
]);
$engineNameAttr = 100;
$carIds = [];
foreach ($cars as $car) {
$carIds[] = $car['id'];
}
if ($specsZoneId) {
$this->loadListOptions($this->zoneAttrs[$specsZoneId]);
$actualValues = $this->getZoneItemsActualValues($specsZoneId, $carIds);
} else {
$actualValues = $this->getItemsActualValues($carIds);
}
foreach ($actualValues as &$itemActualValues) {
foreach ($itemActualValues as $attributeId => &$value) {
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("Attribute `$attributeId` not found");
}
$value = $this->valueToText($attribute, $value, $language);
}
unset($value); // prevent future bugs
}
unset($itemActualValues); // prevent future bugs
foreach ($cars as $car) {
$itemId = (int) $car['id'];
//$values = $this->loadValues($attributes, $itemId);
$values = $actualValues[$itemId] ?? [];
// append engine name
if (! (isset($values[$engineNameAttr]) && $values[$engineNameAttr]) && $car['engine_item_id']) {
$engineRow = currentFromResultSetInterface(
$this->itemModel->getTable()->select(['id' => (int) $car['engine_item_id']])
);
if ($engineRow) {
$values[$engineNameAttr] = $engineRow['name'];
}
}
$name = null;
if ($contextCarId) {
$name = $this->itemParent->getNamePreferLanguage($contextCarId, $car['id'], $language);
}
if (! $name) {
$name = $this->itemNameFormatter->format($this->itemModel->getNameData($car, $language), $language);
}
$topPicture = $this->specPicture($car, self::TOP_PERSPECTIVES);
$topPictureRequest = null;
if ($topPicture) {
$topPictureRequest = $topPicture['image_id'];
}
$bottomPicture = $this->specPicture($car, self::BOTTOM_PERSPECTIVES);
$bottomPictureRequest = null;
if ($bottomPicture) {
$bottomPictureRequest = $bottomPicture['image_id'];
}
$result[] = [
'id' => $itemId,
'name' => $name,
'beginYear' => $car['begin_year'],
'endYear' => $car['end_year'],
'produced' => $car['produced'],
'produced_exactly' => $car['produced_exactly'],
'topPicture' => $topPicture,
'topPictureRequest' => $topPictureRequest,
'bottomPicture' => $bottomPicture,
'bottomPictureRequest' => $bottomPictureRequest,
'carType' => null,
'values' => $values,
];
}
// remove empty attributes
$this->removeEmpty($attributes, $result);
// load units
$this->addUnitsToAttributes($attributes);
return new CarSpecTable($result, $attributes);
}
private function addUnitsToAttributes(array &$attributes): void
{
foreach ($attributes as &$attribute) {
if ($attribute['unitId']) {
$attribute['unit'] = $this->getUnit($attribute['unitId']);
}
$this->addUnitsToAttributes($attribute['childs']);
}
}
private function removeEmpty(array &$attributes, array $cars): void
{
foreach ($attributes as $idx => &$attribute) {
$this->removeEmpty($attribute['childs'], $cars);
if (count($attribute['childs']) > 0) {
$haveValue = true;
} else {
$id = $attribute['id'];
$haveValue = false;
foreach ($cars as $car) {
if (isset($car['values'][$id])) {
$haveValue = true;
break;
}
}
}
if (! $haveValue) {
unset($attributes[$idx]);
}
}
}
public function getValueTable(): TableGateway
{
return $this->valueTable;
}
/**
* @throws Exception
*/
public function getValueDataTable(int $type): TableGateway
{
switch ($type) {
case 1: // string
return $this->valueStringTable;
case 2: // int
case 5: // checkbox
return $this->valueIntTable;
case 3: // float
return $this->valueFloatTable;
case 6: // select
case 7: // select
return $this->valueListTable;
}
throw new Exception("Unexpected type `$type`");
}
/**
* @throws Exception
*/
public function getUserValueDataTable(int $type): TableGateway
{
switch ($type) {
case 1: // string
return $this->userValueStringTable;
case 2: // int
case 5: // checkbox
return $this->userValueIntTable;
case 3: // float
return $this->userValueFloatTable;
case 6: // select
case 7: // select
return $this->userValueListTable;
}
throw new Exception("Unexpected type `$type`");
}
/**
* @param mixed $value
* @return mixed|null
* @throws Exception
*/
private function valueToText(array $attribute, $value, string $language)
{
if ($value === null) {
return null;
}
switch ($attribute['typeId']) {
case 1: // string
return $value;
case 2: // int
$formatter = new NumberFormatter($language, NumberFormatter::DECIMAL);
return $formatter->format($value);
case 3: // float
$formatter = new NumberFormatter($language, NumberFormatter::DECIMAL);
if ($attribute['precision']) {
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $attribute['precision']);
}
return $formatter->format($value, NumberFormatter::TYPE_DOUBLE);
case 4: // textarea
return $value;
case 5: // checkbox
return $value ? 'да' : 'нет';
case 6: // select
case 7: // select
if ($value) {
if (is_array($value)) {
$text = [];
$nullText = false;
foreach ($value as $v) {
if ($v === null) {
$text[] = null;
$nullText = true;
} else {
$text[] = $this->getListOptionsText($attribute['id'], $v);
}
}
return $nullText ? null : implode(', ', $text);
} else {
return $this->getListOptionsText($attribute['id'], $value);
}
}
break;
}
return null;
}
/**
* @throws Exception
*/
private function calcAvgUserValue(array $attribute, int $itemId): array
{
$userValuesDataTable = $this->getUserValueDataTable($attribute['typeId']);
$userValueDataRows = $userValuesDataTable->select([
'attribute_id' => $attribute['id'],
'item_id' => $itemId,
]);
// group by users
$data = [];
foreach ($userValueDataRows as $userValueDataRow) {
$uid = $userValueDataRow['user_id'];
if (! isset($data[$uid])) {
$data[$uid] = [];
}
$data[$uid][] = $userValueDataRow;
}
if (count($data) <= 0) {
return [
'value' => null,
'empty' => true,
];
}
$idx = 0;
$registry = $freshness = $ratios = [];
foreach ($data as $uid => $valueRows) {
if ($attribute['isMultiple']) {
$value = [];
foreach ($valueRows as $valueRow) {
$value[$valueRow['ordering']] = $valueRow['value'];
}
} else {
$value = null;
foreach ($valueRows as $valueRow) {
$value = $valueRow['value'];
}
}
$row = currentFromResultSetInterface($this->userValueTable->select([
'attribute_id' => $attribute['id'],
'item_id' => $itemId,
'user_id' => $uid,
]));
if (! $row) {
throw new Exception('Row(rows) without descriptors');
}
// look for same value
$matchRegIdx = null;
foreach ($registry as $regIdx => $regVal) {
if ($regVal === $value) {
$matchRegIdx = $regIdx;
}
}
if ($matchRegIdx === null) {
$registry[$idx] = $value;
$matchRegIdx = $idx;
$idx++;
}
if (! isset($ratios[$matchRegIdx])) {
$ratios[$matchRegIdx] = 0;
$freshness[$matchRegIdx] = null;
}
$ratios[$matchRegIdx] += $this->getUserValueWeight($uid);
if ($freshness[$matchRegIdx] < $row['update_date']) {
$freshness[$matchRegIdx] = $row['update_date'];
}
//$idx++;
}
// select max
$maxValueRatio = 0;
$maxValueIdx = null;
foreach ($ratios as $idx => $ratio) {
if ($maxValueIdx === null || $maxValueRatio <= $ratio) {
$maxValueIdx = $idx;
$maxValueRatio = $ratio;
}
}
$actualValue = $registry[$maxValueIdx];
$empty = false;
return [
'value' => $actualValue,
'empty' => $empty,
];
}
private function isEngineAttributeId(int $attrId): bool
{
return in_array($attrId, $this->getEngineAttributeIds());
}
/**
* @param array|ArrayAccess $attribute
* @throws Exception
*/
private function calcEngineValue($attribute, int $itemId): array
{
if (! $this->isEngineAttributeId($attribute['id'])) {
return [
'empty' => true,
'value' => null,
];
}
$carRow = currentFromResultSetInterface($this->itemModel->getTable()->select([
'id' => $itemId,
]));
if (! $carRow) {
return [
'empty' => true,
'value' => null,
];
}
if (! $carRow['engine_item_id']) {
return [
'empty' => true,
'value' => null,
];
}
$valueDataTable = $this->getValueDataTable($attribute['typeId']);
if (! $attribute['isMultiple']) {
$valueDataRow = currentFromResultSetInterface($valueDataTable->select([
'attribute_id' => $attribute['id'],
'item_id' => $carRow['engine_item_id'],
'value IS NOT NULL',
]));
if ($valueDataRow) {
return [
'empty' => false,
'value' => $valueDataRow['value'],
];
} else {
return [
'empty' => true,
'value' => null,
];
}
} else {
$valueDataRows = $valueDataTable->select([
'attribute_id' => $attribute['id'],
'item_id' => $carRow['engine_item_id'],
'value IS NOT NULL',
]);
if (count($valueDataRows)) {
$value = [];
foreach ($valueDataRows as $valueDataRow) {
$value[] = $valueDataRow['value'];
}
return [
'empty' => false,
'value' => $value,
];
} else {
return [
'empty' => true,
'value' => null,
];
}
}
}
/**
* @param array|ArrayAccess $attribute
* @throws Exception
*/
private function calcInheritedValue($attribute, int $itemId): array
{
$actualValue = [
'empty' => true,
'value' => null,
];
$parentIds = $this->itemParent->getParentIds($itemId);
$valueDataTable = $this->getValueDataTable($attribute['typeId']);
if (count($parentIds) > 0) {
if (! $attribute['isMultiple']) {
$idx = 0;
$registry = [];
$ratios = [];
$valueDataRows = $valueDataTable->select([
'attribute_id' => $attribute['id'],
new Sql\Predicate\In('item_id', $parentIds),
]);
foreach ($valueDataRows as $valueDataRow) {
$value = $valueDataRow['value'];
// look for same value
$matchRegIdx = null;
foreach ($registry as $regIdx => $regVal) {
if ($regVal === $value) {
$matchRegIdx = $regIdx;
}
}
if ($matchRegIdx === null) {
$registry[$idx] = $value;
$matchRegIdx = $idx;
$idx++;
}
if (! isset($ratios[$matchRegIdx])) {
$ratios[$matchRegIdx] = 0;
}
$ratios[$matchRegIdx] += 1;
}
// select max
$maxValueRatio = 0;
$maxValueIdx = null;
foreach ($ratios as $idx => $ratio) {
if ($maxValueIdx === null || $maxValueRatio <= $ratio) {
$maxValueIdx = $idx;
$maxValueRatio = $ratio;
}
}
if ($maxValueIdx !== null) {
$actualValue = [
'empty' => false,
'value' => $registry[$maxValueIdx],
];
}
}
// TODO: multiple attr inheritance
}
return $actualValue;
}
/**
* @throws Exception
*/
private function setActualValue(array $attribute, int $itemId, array $actualValue): bool
{
$valueDataTable = $this->getValueDataTable($attribute['typeId']);
$somethingChanges = false;
if ($actualValue['empty']) {
// descriptor
$affected = $this->valueTable->delete([
'attribute_id = ?' => $attribute['id'],
'item_id = ?' => $itemId,
]);
// value
$affected += $valueDataTable->delete([
'attribute_id = ?' => $attribute['id'],
'item_id = ?' => $itemId,
]);
if ($affected > 0) {
$somethingChanges = true;
}
} else {
$primaryKey = [
'attribute_id' => $attribute['id'],
'item_id' => $itemId,
];
// descriptor
/** @var Adapter $adapter */
$adapter = $this->valueTable->getAdapter();
$adapter->query('
INSERT INTO attrs_values (attribute_id, item_id, update_date)
VALUES (:attribute_id, :item_id, NOW())
ON DUPLICATE KEY UPDATE update_date = VALUES(update_date)
', $primaryKey);
// value
if ($attribute['isMultiple']) {
$affected = $valueDataTable->delete($primaryKey);
if ($affected > 0) {
$somethingChanges = true;
}
/** @var Adapter $adapter */
$adapter = $valueDataTable->getAdapter();
$stmt = $adapter->createStatement('
INSERT INTO `' . $valueDataTable->getTable() . '` (attribute_id, item_id, ordering, value)
VALUES (:attribute_id, :item_id, :ordering, :value)
ON DUPLICATE KEY UPDATE ordering = VALUES(ordering), value = VALUES(value)
');
foreach ($actualValue['value'] as $ordering => $value) {
$result = $stmt->execute(array_replace([
'ordering' => $ordering,
'value' => $value,
], $primaryKey));
if ($result->getAffectedRows() > 0) {
$somethingChanges = true;
}
}
} else {
$params = [
'value' => $actualValue['value'],
'attribute_id' => $attribute['id'],
'item_id' => $itemId,
];
/** @var Adapter $adapter */
$adapter = $valueDataTable->getAdapter();
$stmt = $adapter->createStatement('
INSERT INTO `' . $valueDataTable->getTable() . '` (attribute_id, item_id, value)
VALUES (:attribute_id, :item_id, :value)
ON DUPLICATE KEY UPDATE value = VALUES(value)
');
$result = $stmt->execute($params);
if ($result->getAffectedRows() > 0) {
$somethingChanges = true;
}
}
if ($somethingChanges) {
$this->valueTable->update([
'update_date' => new Sql\Expression('now()'),
], $primaryKey);
}
}
return $somethingChanges;
}
/**
* @throws Exception
*/
public function updateActualValue(int $attributeId, int $itemId): bool
{
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("attribute `$attributeId` not found");
}
return $this->updateAttributeActualValue($attribute, $itemId);
}
/**
* @throws Exception
*/
private function updateAttributeActualValue(array $attribute, int $itemId): bool
{
$actualValue = $this->calcAvgUserValue($attribute, $itemId);
if ($actualValue['empty']) {
$actualValue = $this->calcEngineValue($attribute, $itemId);
}
if ($actualValue['empty']) {
$actualValue = $this->calcInheritedValue($attribute, $itemId);
}
return $this->setActualValue($attribute, $itemId, $actualValue);
}
/**
* @param int|array $itemId
* @return bool|array
* @throws Exception
*/
public function hasSpecs($itemId)
{
$select = new Sql\Select($this->valueTable->getTable());
$select->columns(['item_id']);
if (is_array($itemId)) {
if (count($itemId) <= 0) {
return false;
}
$select->quantifier($select::QUANTIFIER_DISTINCT)
->where([new Sql\Predicate\In('item_id', $itemId)]);
$result = [];
foreach ($itemId as $id) {
$result[(int) $id] = false;
}
foreach ($this->valueTable->selectWith($select) as $row) {
$result[(int) $row['item_id']] = true;
}
return $result;
}
$select->where(['item_id' => (int) $itemId])
->limit(1);
return (bool) currentFromResultSetInterface($this->valueTable->selectWith($select));
}
/**
* @throws Exception
*/
public function getSpecsCount(int $itemId): int
{
$select = new Sql\Select($this->valueTable->getTable());
$select->columns(['count' => new Sql\Expression('count(1)')])
->where(['item_id' => $itemId]);
$row = currentFromResultSetInterface($this->valueTable->selectWith($select));
return $row ? (int) $row['count'] : 0;
}
/**
* @param int|array $itemId
* @return bool|array
* @throws Exception
*/
public function hasChildSpecs($itemId)
{
$select = new Sql\Select($this->valueTable->getTable());
$select->columns([])
->join('item_parent', 'attrs_values.item_id = item_parent.item_id', ['parent_id']);
if (is_array($itemId)) {
if (count($itemId) <= 0) {
return [];
}
$select->quantifier($select::QUANTIFIER_DISTINCT)
->where([new Sql\Predicate\In('item_parent.parent_id', $itemId)]);
$result = [];
foreach ($itemId as $id) {
$result[(int) $id] = false;
}
foreach ($this->valueTable->selectWith($select) as $row) {
$result[(int) $row['parent_id']] = true;
}
return $result;
}
$select->where(['item_parent.parent_id' => $itemId]);
return (bool) currentFromResultSetInterface($this->valueTable->selectWith($select));
}
/**
* @throws Exception
*/
public function updateAllActualValues(): void
{
$attributes = $this->getAttributes();
$select = $this->userValueTable->getSql()->select();
$select->columns(['item_id'])
->quantifier($select::QUANTIFIER_DISTINCT);
foreach ($this->userValueTable->selectWith($select) as $row) {
foreach ($attributes as $attribute) {
if ($attribute['typeId']) {
$this->updateAttributeActualValue($attribute, $row['item_id']);
}
}
}
}
/**
* @throws Exception
*/
public function updateActualValues(int $itemId): void
{
foreach ($this->getAttributes() as $attribute) {
if ($attribute['typeId']) {
$this->updateAttributeActualValue($attribute, $itemId);
}
}
}
/**
* @throws Exception
*/
public function updateInheritedValues(int $itemId): void
{
foreach ($this->getAttributes() as $attribute) {
if ($attribute['typeId']) {
$haveValue = $this->haveOwnAttributeValue($attribute['id'], $itemId);
if (! $haveValue) {
$this->updateAttributeActualValue($attribute, $itemId);
}
}
}
}
public function getContributors(int $itemId): array
{
if (! $itemId) {
return [];
}
$select = new Sql\Select($this->userValueTable->getTable());
$select->columns(['user_id', 'c' => new Sql\Expression('COUNT(1)')])
->where([new Sql\Predicate\In('item_id', (array) $itemId)])
->group('user_id')
->order('c desc');
$result = [];
foreach ($this->userValueTable->selectWith($select) as $row) {
$result[(int) $row['user_id']] = (int) $row['c'];
}
return $result;
}
/**
* @param mixed $value
* @return mixed|null
*/
private function prepareValue(int $typeId, $value)
{
switch ($typeId) {
case 1: // string
return $value;
case 2: // int
return $value;
case 3: // float
return $value;
case 4: // textarea
return $value;
case 5: // checkbox
return $value === null ? null : ($value ? 1 : 0);
case 6: // select
case 7: // tree select
return $value === null ? null : (int) $value;
}
return null;
}
/**
* @return mixed|null
* @throws Exception
*/
public function getUserValue(int $attributeId, int $itemId, int $userId)
{
if (! $itemId) {
throw new Exception("item_id not set");
}
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("attribute not found");
}
$valuesTable = $this->getUserValueDataTable($attribute['typeId']);
$select = new Sql\Select($valuesTable->getTable());
$select->columns(['value'])
->where([
'attribute_id' => (int) $attribute['id'],
'item_id' => $itemId,
'user_id' => $userId,
]);
if ($attribute['isMultiple']) {
$select->order('ordering');
}
$values = [];
foreach ($valuesTable->selectWith($select) as $row) {
$values[] = $this->prepareValue($attribute['typeId'], $row['value']);
}
if (count($values) <= 0) {
return null;
}
return $attribute['isMultiple'] ? $values : $values[0];
}
/**
* @throws Exception
*/
public function getUserValue2(int $attributeId, int $itemId, int $userId): array
{
if (! $itemId) {
throw new Exception("item_id not set");
}
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("attribute not found");
}
$valuesTable = $this->getUserValueDataTable($attribute['typeId']);
$select = new Sql\Select($valuesTable->getTable());
$select->columns(['value'])
->where([
'attribute_id' => (int) $attribute['id'],
'item_id' => $itemId,
'user_id' => $userId,
]);
if ($attribute['isMultiple']) {
$select->order('ordering');
}
$values = [];
foreach ($valuesTable->selectWith($select) as $row) {
$values[] = $this->prepareValue($attribute['typeId'], $row['value']);
}
if (count($values) <= 0) {
return [
'value' => null,
'empty' => false,
];
}
if ($attribute['isMultiple']) {
return [
'value' => $values,
'empty' => $values === [null],
];
}
return [
'value' => $values[0],
'empty' => $values[0] === null,
];
}
/**
* @throws Exception
*/
public function getUserValueText(int $attributeId, int $itemId, int $userId, string $language): ?string
{
if (! $itemId) {
throw new Exception("item_id not set");
}
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("attribute not found");
}
$valuesTable = $this->getUserValueDataTable($attribute['typeId']);
$select = new Sql\Select($valuesTable->getTable());
$select->columns(['value'])
->where([
'attribute_id' => (int) $attribute['id'],
'item_id' => $itemId,
'user_id' => $userId,
]);
if ($attribute['isMultiple']) {
$select->order('ordering');
}
$values = [];
foreach ($valuesTable->selectWith($select) as $row) {
$values[] = $this->valueToText($attribute, $row['value'], $language);
}
if (count($values) > 1) {
return implode(', ', $values);
} elseif (count($values) === 1) {
if ($values[0] === null) {
return null;
} else {
return $values[0];
}
}
return null;
}
/**
* @throws Exception
*/
public function getActualValueText(int $attributeId, int $itemId, string $language): ?string
{
if (! $itemId) {
throw new Exception("item_id not set");
}
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("attribute not found");
}
$value = $this->getActualValue($attribute['id'], $itemId);
if ($attribute['isMultiple'] && is_array($value)) {
$text = [];
foreach ($value as $v) {
$text[] = $this->valueToText($attribute, $v, $language);
}
return implode(', ', $text);
} else {
return $this->valueToText($attribute, $value, $language);
}
}
/**
* @throws Exception
*/
private function getItemsActualValues(array $itemIds): array
{
if (count($itemIds) <= 0) {
return [];
}
$requests = [
1 => false,
2 => false, /* , 5*/
3 => false,
//4 => [false],
6 => true, /* , 7 */
];
$values = [];
foreach ($requests as $typeId => $isMultiple) {
$valueDataTable = $this->getValueDataTable($typeId);
$select = new Sql\Select($valueDataTable->getTable());
$select->where([new Sql\Predicate\In('item_id', $itemIds)]);
if ($isMultiple) {
$select->order('ordering');
}
foreach ($valueDataTable->selectWith($select) as $row) {
$aid = (int) $row['attribute_id'];
$id = (int) $row['item_id'];
$value = $this->prepareValue($typeId, $row['value']);
if (! isset($values[$id])) {
$values[$id] = [];
}
$attribute = $this->getAttribute($aid);
if (! $attribute) {
throw new Exception("attribute `$aid` not found");
}
if ($attribute['isMultiple']) {
if (! isset($values[$id][$aid])) {
$values[$id][$aid] = [];
}
$values[$id][$aid][] = $value;
} else {
$values[$id][$aid] = $value;
}
}
}
return $values;
}
/**
* @throws Exception
*/
private function getZoneItemsActualValues(int $zoneId, array $itemIds): array
{
if (count($itemIds) <= 0) {
return [];
}
$this->loadZone($zoneId);
$attributes = $this->getAttributes([
'zone' => $zoneId,
'parent' => null,
]);
$requests = [];
foreach ($attributes as $attribute) {
$typeId = $attribute['typeId'];
$isMultiple = $attribute['isMultiple'] ? 1 : 0;
if ($typeId) {
if (! isset($requests[$typeId][$isMultiple])) {
$requests[$typeId][$isMultiple] = [];
}
$requests[$typeId][$isMultiple][] = $attribute['id'];
}
}
$values = [];
foreach ($requests as $typeId => $multiples) {
$valueDataTable = $this->getValueDataTable($typeId);
foreach ($multiples as $isMultiple => $ids) {
$select = new Sql\Select($valueDataTable->getTable());
$select->where([
new Sql\Predicate\In('attribute_id', $ids),
new Sql\Predicate\In('item_id', $itemIds),
]);
if ($isMultiple) {
$select->order('ordering');
}
foreach ($valueDataTable->selectWith($select) as $row) {
$aid = (int) $row['attribute_id'];
$id = (int) $row['item_id'];
$value = $this->prepareValue($typeId, $row['value']);
if (! isset($values[$id])) {
$values[$id] = [];
}
if ($isMultiple) {
if (! isset($values[$id][$aid])) {
$values[$id][$aid] = [];
}
$values[$id][$aid][] = $value;
} else {
$values[$id][$aid] = $value;
}
}
}
}
return $values;
}
/**
* @throws Exception
*/
public function getType(int $typeId): array
{
if (! isset($this->types)) {
$this->types = [];
foreach ($this->typeTable->select() as $row) {
$this->types[(int) $row['id']] = [
'id' => (int) $row['id'],
'name' => $row['name'],
'element' => $row['element'],
'maxlength' => $row['maxlength'],
'size' => $row['size'],
];
}
}
if (! isset($this->types[$typeId])) {
throw new Exception("Type `$typeId` not found");
}
return $this->types[$typeId];
}
/**
* @throws Exception
*/
public function refreshConflictFlag(int $attributeId, int $itemId): void
{
if (! $attributeId) {
throw new Exception("attributeId not provided");
}
if (! $itemId) {
throw new Exception("itemId not provided");
}
$attribute = $this->getAttribute($attributeId);
if (! $attribute) {
throw new Exception("Attribute not found");
}
$userValueRows = $this->userValueTable->select([
'attribute_id = ?' => $attribute['id'],
'item_id = ?' => $itemId,
]);
$userValues = [];
$uniqueValues = [];
foreach ($userValueRows as $userValueRow) {
$v = $this->getUserValue(
$attribute['id'],
$itemId,
$userValueRow['user_id']
);
$serializedValue = serialize($v);
$uniqueValues[] = $serializedValue;
$userValues[$userValueRow['user_id']] = [
'value' => $serializedValue,
'date' => $userValueRow['update_date'],
];
}
$uniqueValues = array_unique($uniqueValues);
$hasConflict = count($uniqueValues) > 1;
$valueRow = currentFromResultSetInterface($this->valueTable->select([
'attribute_id' => $attribute['id'],
'item_id' => $itemId,
]));
if (! $valueRow) {
return;
//throw new Exception("Value row not found");
}
$this->valueTable->update([
'conflict' => $hasConflict ? 1 : 0,
], [
'attribute_id' => $attribute['id'],
'item_id' => $itemId,
]);
$affectedUserIds = [];
if ($hasConflict) {
$actualValue = serialize($this->getActualValue($attributeId, $itemId));
$minDate = null; // min date of actual value
$actualValueVoters = 0;
foreach ($userValues as $userId => $userValue) {
if ($userValue['value'] === $actualValue) {
$actualValueVoters++;
if ($minDate === null || $minDate > $userValue['date']) {
$minDate = $userValue['date'];
}
}
}
foreach ($userValues as $userId => $userValue) {
$matchActual = $userValue['value'] === $actualValue;
$conflict = $matchActual ? -1 : 1;
if ($actualValueVoters > 1) {
if ($matchActual) {
$isFirstMatchActual = $userValue['date'] === $minDate;
$weight = $isFirstMatchActual
? self::WEIGHT_FIRST_ACTUAL : self::WEIGHT_SECOND_ACTUAL;
} else {
$weight = self::WEIGHT_WRONG;
}
} else {
$weight = self::WEIGHT_NONE;
}
$affectedRows = $this->userValueTable->update([
'conflict' => $conflict,
'weight' => $weight,
], [
'user_id = ?' => $userId,
'attribute_id = ?' => $attributeId,
'item_id = ?' => $itemId,
]);
if ($affectedRows) {
$affectedUserIds[] = $userId;
}
}
} else {
$affectedRows = $this->userValueTable->update([
'conflict' => 0,
'weight' => self::WEIGHT_NONE,
], [
'attribute_id = ?' => $attributeId,
'item_id = ?' => $itemId,
]);
if ($affectedRows) {
$affectedUserIds = array_keys($userValues);
}
}
$this->refreshUserConflicts($affectedUserIds);
}
/**
* @param int|int[] $userId
*/
public function refreshUserConflicts($userId): void
{
$userId = (array) $userId;
if (count($userId)) {
$pSelect = 'SELECT sum(weight) FROM attrs_user_values WHERE user_id = users.id AND weight > 0';
$nSelect = 'SELECT abs(sum(weight)) FROM attrs_user_values WHERE user_id = users.id AND weight < 0';
$expr = new Sql\Expression(
'1.5 * ((1 + IFNULL((' . $pSelect . '), 0)) / (1 + IFNULL((' . $nSelect . '), 0)))'
);
$this->userModel->getTable()->update([
'specs_weight' => $expr,
], [
new Sql\Predicate\In('id', $userId),
]);
}
}
/**
* @throws Exception
*/
public function refreshConflictFlags(): void
{
$select = new Sql\Select($this->valueTable->getTable());
$select
->quantifier($select::QUANTIFIER_DISTINCT)
->join(
'attrs_user_values',
'attrs_values.attribute_id = attrs_user_values.attribute_id '
. 'and attrs_values.item_id = attrs_user_values.item_id',
[]
)
->where(['attrs_user_values.conflict']);
foreach ($this->valueTable->selectWith($select) as $valueRow) {
$this->refreshConflictFlag($valueRow['attribute_id'], $valueRow['item_id']);
}
}
/**
* @throws Exception
*/
public function refreshItemConflictFlags(int $itemId): void
{
foreach ($this->userValueTable->select(['item_id' => $itemId]) as $valueRow) {
$this->refreshConflictFlag($valueRow['attribute_id'], $valueRow['item_id']);
}
}
/**
* @param mixed $filter
* @throws Exception
*/
public function getConflicts(int $userId, $filter, int $page, int $perPage): array
{
$userId = (int) $userId;
$select = new Sql\Select($this->valueTable->getTable());
$select
->join(
'attrs_user_values',
'attrs_values.attribute_id = attrs_user_values.attribute_id '
. 'and attrs_values.item_id = attrs_user_values.item_id',
[]
)
->where(['attrs_user_values.user_id' => $userId])
->order('attrs_values.update_date desc');
if ($filter === 'minus-weight') {
$select->where(['attrs_user_values.weight < 0']);
} elseif ($filter === 0) {
$select->where(['attrs_values.conflict']);
} elseif ($filter > 0) {
$select->where(['attrs_user_values.conflict > 0']);
} elseif ($filter < 0) {
$select->where(['attrs_user_values.conflict < 0']);
}
/** @var Adapter $adapter */
$adapter = $this->valueTable->getAdapter();
$paginator = new Paginator\Paginator(
new Paginator\Adapter\LaminasDb\DbSelect($select, $adapter)
);
$paginator
->setItemCountPerPage($perPage)
->setCurrentPageNumber($page);
$conflicts = [];
foreach ($paginator->getCurrentItems() as $valueRow) {
$attribute = $this->getAttribute($valueRow['attribute_id']);
if (! $attribute) {
throw new Exception("attribute `{$valueRow['attribute_id']}` not found");
}
$unit = null;
if ($attribute['unitId']) {
$unit = $this->getUnit($attribute['unitId']);
}
$attributeName = [];
$cAttr = $attribute;
do {
$attributeName[] = $this->translator->translate($cAttr['name']);
$cAttr = $this->getAttribute((int) $cAttr['parentId']);
} while ($cAttr);
$conflicts[] = [
'attribute_id' => (int) $valueRow['attribute_id'],
'item_id' => (int) $valueRow['item_id'],
'attribute' => implode(' / ', array_reverse($attributeName)),
'unit' => $unit,
];
}
return [
'conflicts' => $conflicts,
'paginator' => $paginator,
];
}
public function refreshUserConflictsStat(): void
{
$select = new Sql\Select($this->userValueTable->getTable());
$select->columns(['user_id']);
$userIds = [];
foreach ($this->userValueTable->selectWith($select) as $row) {
$userIds[] = (int) $row['user_id'];
}
$this->refreshUserConflicts($userIds);
}
public function refreshUsersConflictsStat(): void
{
$pSelect = 'SELECT sum(weight) FROM attrs_user_values WHERE user_id = users.id AND weight > 0';
$nSelect = 'SELECT abs(sum(weight)) FROM attrs_user_values WHERE user_id = users.id AND weight < 0';
$expr = new Sql\Expression(
'1.5 * ((1 + IFNULL((' . $pSelect . '), 0)) / (1 + IFNULL((' . $nSelect . '), 0)))'
);
$this->userModel->getTable()->update([
'specs_weight' => $expr,
], []);
}
/**
* @throws Exception
*/
private function getUserValueWeight(int $userId): float
{
if (! array_key_exists($userId, $this->valueWeights)) {
$userRow = $this->userModel->getRow($userId);
if ($userRow) {
$this->valueWeights[$userId] = $userRow['specs_weight'];
} else {
$this->valueWeights[$userId] = 1;
}
}
return $this->valueWeights[$userId];
}
public function getUserValueTable(): TableGateway
{
return $this->userValueTable;
}
public function getAttributeTable(): TableGateway
{
return $this->attributeTable;
}
}