db = $db; } /** * @return array{items: array>, total: int} */ public function listForAdmin( array $filters, string $sortColumn = 'o', string $sortDir = 'ASC', int $page = 1, int $perPage = 15 ): array { $allowedSortColumns = [ 'id' => 'sa.id', 'o' => 'sa.o', 'name' => 'name_for_sort', 'type' => 'sa.type', 'status' => 'sa.status', 'values_count' => 'values_count', ]; $sortSql = $allowedSortColumns[$sortColumn] ?? 'sa.o'; $sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC'; $page = max(1, $page); $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); $offset = ($page - 1) * $perPage; $whereData = $this->buildAdminWhere($filters); $whereSql = $whereData['sql']; $params = $whereData['params']; $params[':default_lang_id'] = $this->defaultLanguageId(); $sqlCount = " SELECT COUNT(0) FROM pp_shop_attributes AS sa WHERE {$whereSql} "; $stmtCount = $this->db->query($sqlCount, $whereData['params']); $countRows = $stmtCount ? $stmtCount->fetchAll() : []; $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; $sql = " SELECT sa.id, sa.status, sa.type, sa.o, ( SELECT sal.name FROM pp_shop_attributes_langs AS sal WHERE sal.attribute_id = sa.id AND sal.lang_id = :default_lang_id LIMIT 1 ) AS name_default, ( SELECT sal2.name FROM pp_shop_attributes_langs AS sal2 INNER JOIN pp_langs AS pl2 ON pl2.id = sal2.lang_id WHERE sal2.attribute_id = sa.id AND sal2.name <> '' ORDER BY pl2.o ASC LIMIT 1 ) AS name_any, ( SELECT COUNT(0) FROM pp_shop_attributes_values AS sav WHERE sav.attribute_id = sa.id ) AS values_count, COALESCE( ( SELECT sal3.name FROM pp_shop_attributes_langs AS sal3 WHERE sal3.attribute_id = sa.id AND sal3.lang_id = :default_lang_id LIMIT 1 ), ( SELECT sal4.name FROM pp_shop_attributes_langs AS sal4 INNER JOIN pp_langs AS pl4 ON pl4.id = sal4.lang_id WHERE sal4.attribute_id = sa.id AND sal4.name <> '' ORDER BY pl4.o ASC LIMIT 1 ), '' ) AS name_for_sort FROM pp_shop_attributes AS sa WHERE {$whereSql} ORDER BY {$sortSql} {$sortDir}, sa.id ASC LIMIT {$perPage} OFFSET {$offset} "; $stmt = $this->db->query($sql, $params); $items = $stmt ? $stmt->fetchAll() : []; if (!is_array($items)) { $items = []; } foreach ($items as &$item) { $nameDefault = trim((string)($item['name_default'] ?? '')); $nameAny = trim((string)($item['name_any'] ?? '')); $item['id'] = (int)($item['id'] ?? 0); $item['status'] = $this->toSwitchValue($item['status'] ?? 0); $item['type'] = (int)($item['type'] ?? 0); $item['o'] = (int)($item['o'] ?? 0); $item['name'] = $nameDefault !== '' ? $nameDefault : $nameAny; $item['values_count'] = (int)($item['values_count'] ?? 0); unset($item['name_default'], $item['name_any'], $item['name_for_sort']); } unset($item); return [ 'items' => $items, 'total' => $total, ]; } public function findAttribute(int $attributeId): array { if ($attributeId <= 0) { return $this->defaultAttribute(); } $attribute = $this->db->get('pp_shop_attributes', '*', ['id' => $attributeId]); if (!is_array($attribute)) { return $this->defaultAttribute(); } $attribute['id'] = (int)($attribute['id'] ?? 0); $attribute['status'] = $this->toSwitchValue($attribute['status'] ?? 0); $attribute['type'] = (int)($attribute['type'] ?? 0); $attribute['o'] = (int)($attribute['o'] ?? 0); $attribute['languages'] = []; $translations = $this->db->select( 'pp_shop_attributes_langs', ['lang_id', 'name'], ['attribute_id' => $attribute['id']] ); if (is_array($translations)) { foreach ($translations as $translation) { $langId = (string)($translation['lang_id'] ?? ''); if ($langId !== '') { $attribute['languages'][$langId] = $translation; } } } return $attribute; } public function saveAttribute(array $data): ?int { $attributeId = (int)($data['id'] ?? 0); $row = [ 'status' => $this->toSwitchValue($data['status'] ?? 0), 'type' => $this->toTypeValue($data['type'] ?? 0), 'o' => max(0, (int)($data['o'] ?? 0)), ]; if ($attributeId <= 0) { $this->db->insert('pp_shop_attributes', $row); $attributeId = (int)$this->db->id(); if ($attributeId <= 0) { return null; } } else { $this->db->update('pp_shop_attributes', $row, ['id' => $attributeId]); } $names = []; if (isset($data['name']) && is_array($data['name'])) { $names = $data['name']; } $this->saveAttributeTranslations($attributeId, $names); $this->clearTempAndCache(); $this->clearFrontCache($attributeId, 'frontAttributeDetails'); return $attributeId; } public function deleteAttribute(int $attributeId): bool { if ($attributeId <= 0) { return false; } $deleted = (bool)$this->db->delete('pp_shop_attributes', ['id' => $attributeId]); if ($deleted) { $this->clearTempAndCache(); $this->clearFrontCache($attributeId, 'frontAttributeDetails'); } return $deleted; } /** * @return array> */ public function findValues(int $attributeId): array { if ($attributeId <= 0) { return []; } $rows = $this->db->select( 'pp_shop_attributes_values', ['id', 'is_default', 'impact_on_the_price'], [ 'attribute_id' => $attributeId, 'ORDER' => ['id' => 'ASC'], ] ); if (!is_array($rows)) { return []; } $values = []; foreach ($rows as $row) { $valueId = (int)($row['id'] ?? 0); if ($valueId <= 0) { continue; } $value = [ 'id' => $valueId, 'is_default' => $this->toSwitchValue($row['is_default'] ?? 0), 'impact_on_the_price' => $this->toNullableNumeric($row['impact_on_the_price'] ?? null), 'languages' => [], ]; $translations = $this->db->select( 'pp_shop_attributes_values_langs', ['lang_id', 'name', 'value'], ['value_id' => $valueId] ); if (is_array($translations)) { foreach ($translations as $translation) { $langId = (string)($translation['lang_id'] ?? ''); if ($langId !== '') { $value['languages'][$langId] = $translation; } } } $values[] = $value; } return $values; } /** * @param array $payload */ public function saveValues(int $attributeId, array $payload): bool { if ($attributeId <= 0) { return false; } $rowsRaw = []; if (isset($payload['rows']) && is_array($payload['rows'])) { $rowsRaw = $payload['rows']; } elseif (array_key_exists(0, $payload)) { $rowsRaw = $payload; } $rows = $this->normalizeValueRows($rowsRaw); $currentIds = $this->db->select( 'pp_shop_attributes_values', 'id', ['attribute_id' => $attributeId] ); if (!is_array($currentIds)) { $currentIds = []; } $currentIds = array_values(array_unique(array_map('intval', $currentIds))); $incomingIds = []; foreach ($rows as $row) { $rowId = (int)($row['id'] ?? 0); if ($rowId > 0) { $incomingIds[$rowId] = $rowId; } } $incomingIds = array_values($incomingIds); $deleteIds = array_diff($currentIds, $incomingIds); foreach ($deleteIds as $deleteId) { $this->clearFrontCache((int)$deleteId, 'frontValueDetails'); $this->db->delete('pp_shop_attributes_values_langs', ['value_id' => (int)$deleteId]); $this->db->delete('pp_shop_attributes_values', ['id' => (int)$deleteId]); } $defaultValueId = 0; foreach ($rows as $row) { $rowId = (int)($row['id'] ?? 0); if ($rowId > 0 && !$this->valueBelongsToAttribute($rowId, $attributeId)) { $rowId = 0; } $impactOnPrice = $this->toNullableNumeric($row['impact_on_the_price'] ?? null); if ($rowId <= 0) { $this->db->insert('pp_shop_attributes_values', [ 'attribute_id' => $attributeId, 'impact_on_the_price' => $impactOnPrice, 'is_default' => 0, ]); $rowId = (int)$this->db->id(); if ($rowId <= 0) { return false; } } else { $this->db->update('pp_shop_attributes_values', [ 'impact_on_the_price' => $impactOnPrice, ], [ 'id' => $rowId, ]); } $translations = is_array($row['translations'] ?? null) ? $row['translations'] : []; $this->saveValueTranslations($rowId, $translations); $this->refreshCombinationPricesForValue($rowId, $impactOnPrice); $this->clearFrontCache($rowId, 'frontValueDetails'); if (!empty($row['is_default'])) { $defaultValueId = $rowId; } } $this->db->update( 'pp_shop_attributes_values', ['is_default' => 0], ['attribute_id' => $attributeId] ); if ($defaultValueId > 0) { $this->db->update( 'pp_shop_attributes_values', ['is_default' => 1], ['id' => $defaultValueId] ); } $this->clearTempAndCache(); return true; } /** * Legacy compatibility for old payload shape. * * @param array> $names * @param array> $values * @param array> $ids * @param mixed $defaultValue * @param array $impactOnThePrice */ public function saveLegacyValues( int $attributeId, array $names, array $values, array $ids, $defaultValue = '', array $impactOnThePrice = [] ): ?int { if ($attributeId <= 0) { return null; } $mainLanguage = $this->defaultLanguageId(); $mainLanguageNames = $names[$mainLanguage] ?? []; if (!is_array($mainLanguageNames)) { $mainLanguageNames = []; } $rows = []; $count = count($mainLanguageNames); for ($i = 0; $i < $count; ++$i) { $rowId = 0; if (isset($ids[$mainLanguage]) && is_array($ids[$mainLanguage])) { $rowId = (int)($ids[$mainLanguage][$i] ?? 0); } $translations = []; foreach ($names as $langId => $langNames) { if (!is_string($langId) || !is_array($langNames)) { continue; } $translations[$langId] = [ 'name' => (string)($langNames[$i] ?? ''), 'value' => isset($values[$langId]) && is_array($values[$langId]) ? (string)($values[$langId][$i] ?? '') : '', ]; } $rows[] = [ 'id' => $rowId, 'impact_on_the_price' => $impactOnThePrice[$i] ?? null, 'is_default' => ((string)$defaultValue === (string)$i), 'translations' => $translations, ]; } $saved = $this->saveValues($attributeId, ['rows' => $rows]); return $saved ? $attributeId : null; } public function valueDetails(int $valueId): array { if ($valueId <= 0) { return []; } $value = $this->db->get('pp_shop_attributes_values', '*', ['id' => $valueId]); if (!is_array($value)) { return []; } $value['id'] = (int)($value['id'] ?? 0); $value['attribute_id'] = (int)($value['attribute_id'] ?? 0); $value['is_default'] = $this->toSwitchValue($value['is_default'] ?? 0); $value['impact_on_the_price'] = $this->toNullableNumeric($value['impact_on_the_price'] ?? null); $value['languages'] = []; $translations = $this->db->select( 'pp_shop_attributes_values_langs', ['lang_id', 'name', 'value'], ['value_id' => $valueId] ); if (is_array($translations)) { foreach ($translations as $translation) { $langId = (string)($translation['lang_id'] ?? ''); if ($langId !== '') { $value['languages'][$langId] = $translation; } } } return $value; } public function getAttributeNameById(int $attributeId, ?string $langId = null): string { if ($attributeId <= 0) { return ''; } $languageId = $langId !== null && trim($langId) !== '' ? trim($langId) : $this->defaultLanguageId(); return (string)$this->db->get( 'pp_shop_attributes_langs', 'name', [ 'AND' => [ 'attribute_id' => $attributeId, 'lang_id' => $languageId, ], ] ); } public function getAttributeValueById(int $valueId, ?string $langId = null): string { if ($valueId <= 0) { return ''; } $languageId = $langId !== null && trim($langId) !== '' ? trim($langId) : $this->defaultLanguageId(); $cacheHandler = new \Shared\Cache\CacheHandler(); $cacheKey = "AttributeRepository::getAttributeValueById:{$valueId}:{$languageId}"; $cached = $cacheHandler->get($cacheKey); if ($cached !== false && $cached !== null) { return (string)unserialize($cached); } $name = (string)$this->db->get( 'pp_shop_attributes_values_langs', 'name', [ 'AND' => [ 'value_id' => $valueId, 'lang_id' => $languageId, ], ] ); $cacheHandler->set($cacheKey, $name); return $name; } /** * @return array> */ public function getAttributesListForCombinations(): array { $rows = $this->db->select('pp_shop_attributes', '*', ['ORDER' => ['o' => 'ASC']]); if (!is_array($rows)) { return []; } $attributes = []; foreach ($rows as $row) { $attributeId = (int)($row['id'] ?? 0); if ($attributeId <= 0) { continue; } $attribute = $this->findAttribute($attributeId); $attribute['values'] = $this->findValues($attributeId); $attributes[] = $attribute; } return $attributes; } /** * Zwraca aktywne atrybuty z wartościami i wielojęzycznymi nazwami dla REST API. * * @return array> */ public function listForApi(): array { // 1. Get all active attribute IDs (1 query) $rows = $this->db->select('pp_shop_attributes', ['id', 'type', 'status'], [ 'status' => 1, 'ORDER' => ['o' => 'ASC'], ]); if (!is_array($rows) || empty($rows)) { return []; } $attrIds = []; foreach ($rows as $row) { $id = (int)($row['id'] ?? 0); if ($id > 0) { $attrIds[] = $id; } } if (empty($attrIds)) { return []; } // 2. Batch load ALL attribute translations (1 query) $allAttrTranslations = $this->db->select( 'pp_shop_attributes_langs', ['attribute_id', 'lang_id', 'name'], ['attribute_id' => $attrIds] ); $attrNamesMap = []; if (is_array($allAttrTranslations)) { foreach ($allAttrTranslations as $t) { $aId = (int)($t['attribute_id'] ?? 0); $langId = (string)($t['lang_id'] ?? ''); if ($aId > 0 && $langId !== '') { $attrNamesMap[$aId][$langId] = (string)($t['name'] ?? ''); } } } // 3. Batch load ALL values for those attribute IDs (1 query) $allValueRows = $this->db->select( 'pp_shop_attributes_values', ['id', 'attribute_id', 'is_default', 'impact_on_the_price'], [ 'attribute_id' => $attrIds, 'ORDER' => ['id' => 'ASC'], ] ); $valuesByAttr = []; $allValueIds = []; if (is_array($allValueRows)) { foreach ($allValueRows as $vRow) { $valueId = (int)($vRow['id'] ?? 0); $attrId = (int)($vRow['attribute_id'] ?? 0); if ($valueId > 0 && $attrId > 0) { $valuesByAttr[$attrId][] = $vRow; $allValueIds[] = $valueId; } } } // 4. Batch load ALL value translations (1 query) $valueNamesMap = []; if (!empty($allValueIds)) { $allValueTranslations = $this->db->select( 'pp_shop_attributes_values_langs', ['value_id', 'lang_id', 'name'], ['value_id' => $allValueIds] ); if (is_array($allValueTranslations)) { foreach ($allValueTranslations as $vt) { $vId = (int)($vt['value_id'] ?? 0); $langId = (string)($vt['lang_id'] ?? ''); if ($vId > 0 && $langId !== '') { $valueNamesMap[$vId][$langId] = (string)($vt['name'] ?? ''); } } } } // 5. Assemble result in-memory $result = []; foreach ($rows as $row) { $attributeId = (int)($row['id'] ?? 0); if ($attributeId <= 0) { continue; } $names = isset($attrNamesMap[$attributeId]) ? $attrNamesMap[$attributeId] : []; $values = []; if (isset($valuesByAttr[$attributeId])) { foreach ($valuesByAttr[$attributeId] as $vRow) { $valueId = (int)$vRow['id']; $impact = $vRow['impact_on_the_price']; $values[] = [ 'id' => $valueId, 'names' => isset($valueNamesMap[$valueId]) ? $valueNamesMap[$valueId] : [], 'is_default' => (int)($vRow['is_default'] ?? 0), 'impact_on_the_price' => ($impact !== null && $impact !== '') ? (float)$impact : null, ]; } } $result[] = [ 'id' => $attributeId, 'type' => (int)($row['type'] ?? 0), 'status' => (int)($row['status'] ?? 0), 'names' => $names, 'values' => $values, ]; } return $result; } /** * Find existing attribute by name/type or create a new one for API integration. * * @return array{id:int,created:bool}|null */ public function ensureAttributeForApi(string $name, int $type = 0, string $langId = 'pl'): ?array { $normalizedName = trim($name); $normalizedLangId = trim($langId) !== '' ? trim($langId) : 'pl'; $normalizedType = $this->toTypeValue($type); if ($normalizedName === '') { return null; } $existingId = $this->findAttributeIdByNameAndType($normalizedName, $normalizedType); if ($existingId > 0) { return ['id' => $existingId, 'created' => false]; } $this->db->insert('pp_shop_attributes', [ 'status' => 1, 'type' => $normalizedType, 'o' => $this->nextOrder(), ]); $attributeId = (int) $this->db->id(); if ($attributeId <= 0) { return null; } $this->db->insert('pp_shop_attributes_langs', [ 'attribute_id' => $attributeId, 'lang_id' => $normalizedLangId, 'name' => $normalizedName, ]); $this->clearTempAndCache(); $this->clearFrontCache($attributeId, 'frontAttributeDetails'); return ['id' => $attributeId, 'created' => true]; } /** * Find existing value by name within attribute or create a new one for API integration. * * @return array{id:int,created:bool}|null */ public function ensureAttributeValueForApi(int $attributeId, string $name, string $langId = 'pl'): ?array { $normalizedName = trim($name); $normalizedLangId = trim($langId) !== '' ? trim($langId) : 'pl'; $attributeId = max(0, $attributeId); if ($attributeId <= 0 || $normalizedName === '') { return null; } $attributeExists = (int) $this->db->count('pp_shop_attributes', ['id' => $attributeId]) > 0; if (!$attributeExists) { return null; } $existingId = $this->findAttributeValueIdByName($attributeId, $normalizedName); if ($existingId > 0) { return ['id' => $existingId, 'created' => false]; } $this->db->insert('pp_shop_attributes_values', [ 'attribute_id' => $attributeId, 'impact_on_the_price' => null, 'is_default' => 0, ]); $valueId = (int) $this->db->id(); if ($valueId <= 0) { return null; } $this->db->insert('pp_shop_attributes_values_langs', [ 'value_id' => $valueId, 'lang_id' => $normalizedLangId, 'name' => $normalizedName, 'value' => null, ]); $this->clearTempAndCache(); $this->clearFrontCache($valueId, 'frontValueDetails'); return ['id' => $valueId, 'created' => true]; } /** * @return array{sql: string, params: array} */ private function buildAdminWhere(array $filters): array { $where = ['1 = 1']; $params = []; $name = trim((string)($filters['name'] ?? '')); if ($name !== '') { if (strlen($name) > 255) { $name = substr($name, 0, 255); } $where[] = 'EXISTS ( SELECT 1 FROM pp_shop_attributes_langs AS sal_filter WHERE sal_filter.attribute_id = sa.id AND sal_filter.name LIKE :name )'; $params[':name'] = '%' . $name . '%'; } $status = trim((string)($filters['status'] ?? '')); if ($status === '0' || $status === '1') { $where[] = 'sa.status = :status'; $params[':status'] = (int)$status; } return [ 'sql' => implode(' AND ', $where), 'params' => $params, ]; } /** * @param array $names */ private function saveAttributeTranslations(int $attributeId, array $names): void { foreach ($names as $langId => $name) { if (!is_string($langId) || trim($langId) === '') { continue; } $translationName = trim((string)$name); $where = [ 'AND' => [ 'attribute_id' => $attributeId, 'lang_id' => $langId, ], ]; $translationId = $this->db->get('pp_shop_attributes_langs', 'id', $where); if ($translationId) { $this->db->update('pp_shop_attributes_langs', [ 'name' => $translationName, ], [ 'id' => (int)$translationId, ]); } else { $this->db->insert('pp_shop_attributes_langs', [ 'attribute_id' => $attributeId, 'lang_id' => $langId, 'name' => $translationName, ]); } } } /** * @param array $translations */ private function saveValueTranslations(int $valueId, array $translations): void { foreach ($translations as $langId => $translationData) { if (!is_string($langId) || trim($langId) === '') { continue; } $name = ''; $value = null; if (is_array($translationData)) { $name = trim((string)($translationData['name'] ?? '')); $rawValue = trim((string)($translationData['value'] ?? '')); $value = $rawValue !== '' ? $rawValue : null; } else { $name = trim((string)$translationData); } $where = [ 'AND' => [ 'value_id' => $valueId, 'lang_id' => $langId, ], ]; $translationId = $this->db->get('pp_shop_attributes_values_langs', 'id', $where); if ($name === '') { if ($translationId) { $this->db->delete('pp_shop_attributes_values_langs', ['id' => (int)$translationId]); } continue; } if ($translationId) { $this->db->update('pp_shop_attributes_values_langs', [ 'name' => $name, 'value' => $value, ], [ 'id' => (int)$translationId, ]); } else { $this->db->insert('pp_shop_attributes_values_langs', [ 'value_id' => $valueId, 'lang_id' => $langId, 'name' => $name, 'value' => $value, ]); } } } private function valueBelongsToAttribute(int $valueId, int $attributeId): bool { if ($valueId <= 0 || $attributeId <= 0) { return false; } return (bool)$this->db->count('pp_shop_attributes_values', [ 'AND' => [ 'id' => $valueId, 'attribute_id' => $attributeId, ], ]); } /** * @param array> $rows * @return array> */ private function normalizeValueRows(array $rows): array { $normalizedRows = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } $translations = []; if (isset($row['translations']) && is_array($row['translations'])) { $translations = $row['translations']; } $normalizedRows[] = [ 'id' => (int)($row['id'] ?? 0), 'impact_on_the_price' => $row['impact_on_the_price'] ?? null, 'is_default' => !empty($row['is_default']), 'translations' => $translations, ]; } return $normalizedRows; } private function refreshCombinationPricesForValue(int $valueId, ?string $impactOnThePrice): void { if ($valueId <= 0 || $impactOnThePrice === null) { return; } $impact = (float)$impactOnThePrice; $products = $this->db->select('pp_shop_products_attributes', ['product_id'], ['value_id' => $valueId]); if (!is_array($products)) { return; } foreach ($products as $row) { $productId = (int)($row['product_id'] ?? 0); if ($productId <= 0) { continue; } $parentId = (int)$this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]); if ($parentId <= 0) { continue; } $parentProduct = $this->db->get('pp_shop_products', '*', ['id' => $parentId]); if (!is_array($parentProduct)) { continue; } $parentPriceBrutto = (float)($parentProduct['price_brutto'] ?? 0); $parentVat = (float)($parentProduct['vat'] ?? 0); $parentPriceBruttoPromo = $parentProduct['price_brutto_promo']; $parentPriceNettoPromo = $parentProduct['price_netto_promo']; if ($impact > 0) { $priceBrutto = $parentPriceBrutto + $impact; $priceNetto = $this->normalizeDecimal($priceBrutto / (1 + ($parentVat / 100)), 2); if ($parentPriceNettoPromo !== null && $parentPriceBruttoPromo !== null) { $priceBruttoPromo = (float)$parentPriceBruttoPromo + $impact; $priceNettoPromo = $this->normalizeDecimal($priceBruttoPromo / (1 + ($parentVat / 100)), 2); } else { $priceBruttoPromo = null; $priceNettoPromo = null; } $this->db->update('pp_shop_products', [ 'price_netto' => $priceNetto, 'price_brutto' => $priceBrutto, 'price_netto_promo' => $priceNettoPromo, 'price_brutto_promo' => $priceBruttoPromo, 'date_modify' => date('Y-m-d H:i:s'), ], [ 'id' => $productId, ]); continue; } if (abs($impact) < 0.000001) { $this->db->update('pp_shop_products', [ 'price_netto' => null, 'price_brutto' => null, 'price_netto_promo' => null, 'price_brutto_promo' => null, 'quantity' => null, 'stock_0_buy' => null, 'date_modify' => date('Y-m-d H:i:s'), ], [ 'id' => $productId, ]); } } } private function toSwitchValue($value): int { if (is_bool($value)) { return $value ? 1 : 0; } if (is_numeric($value)) { return ((int)$value) === 1 ? 1 : 0; } if (is_string($value)) { $normalized = strtolower(trim($value)); return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0; } return 0; } private function toTypeValue($value): int { $type = (int)$value; if ($type < 0 || $type > 2) { return 0; } return $type; } private function toNullableNumeric($value): ?string { if ($value === null) { return null; } $stringValue = trim((string)$value); if ($stringValue === '') { return null; } return str_replace(',', '.', $stringValue); } private function defaultAttribute(): array { return [ 'id' => 0, 'status' => 1, 'type' => 0, 'o' => $this->nextOrder(), 'languages' => [], ]; } private function nextOrder(): int { $maxOrder = $this->db->max('pp_shop_attributes', 'o'); return max(0, (int)$maxOrder + 1); } private function defaultLanguageId(): string { if ($this->defaultLangId !== null) { return $this->defaultLangId; } $rows = $this->db->select('pp_langs', ['id', 'start', 'o'], [ 'status' => 1, 'ORDER' => ['start' => 'DESC', 'o' => 'ASC'], ]); if (is_array($rows) && !empty($rows)) { $this->defaultLangId = (string)($rows[0]['id'] ?? ''); } else { $this->defaultLangId = ''; } return $this->defaultLangId; } private function findAttributeIdByNameAndType(string $name, int $type): int { $statement = $this->db->query( 'SELECT sa.id FROM pp_shop_attributes sa INNER JOIN pp_shop_attributes_langs sal ON sal.attribute_id = sa.id WHERE sa.type = :type AND LOWER(TRIM(sal.name)) = LOWER(TRIM(:name)) ORDER BY sa.id ASC LIMIT 1', [ ':type' => $type, ':name' => $name, ] ); if (!$statement) { return 0; } $id = $statement->fetchColumn(); return $id === false ? 0 : (int) $id; } private function findAttributeValueIdByName(int $attributeId, string $name): int { $statement = $this->db->query( 'SELECT sav.id FROM pp_shop_attributes_values sav INNER JOIN pp_shop_attributes_values_langs savl ON savl.value_id = sav.id WHERE sav.attribute_id = :attribute_id AND LOWER(TRIM(savl.name)) = LOWER(TRIM(:name)) ORDER BY sav.id ASC LIMIT 1', [ ':attribute_id' => $attributeId, ':name' => $name, ] ); if (!$statement) { return 0; } $id = $statement->fetchColumn(); return $id === false ? 0 : (int) $id; } // ── Frontend methods ────────────────────────────────────────── public function frontAttributeDetails(int $attributeId, string $langId): array { $cacheHandler = new \Shared\Cache\CacheHandler(); $cacheKey = "AttributeRepository::frontAttributeDetails:$attributeId:$langId"; $objectData = $cacheHandler->get($cacheKey); if ($objectData) { $cached = @unserialize($objectData); if (is_array($cached)) { return $cached; } $cacheHandler->delete($cacheKey); } $attribute = $this->db->get('pp_shop_attributes', '*', ['id' => (int)$attributeId]); if (!is_array($attribute)) { $attribute = ['id' => $attributeId, 'status' => 0, 'type' => 0]; } $attribute['language'] = $this->db->get('pp_shop_attributes_langs', ['lang_id', 'name'], [ 'AND' => [ 'attribute_id' => (int)$attributeId, 'lang_id' => $langId, ], ]); if (!is_array($attribute['language'])) { $attribute['language'] = ['lang_id' => $langId, 'name' => '']; } $cacheHandler->set($cacheKey, $attribute); return $attribute; } public function frontValueDetails(int $valueId, string $langId): array { $cacheHandler = new \Shared\Cache\CacheHandler(); $cacheKey = "AttributeRepository::frontValueDetails:$valueId:$langId"; $objectData = $cacheHandler->get($cacheKey); if ($objectData) { $cached = @unserialize($objectData); if (is_array($cached)) { return $cached; } $cacheHandler->delete($cacheKey); } $value = $this->db->get('pp_shop_attributes_values', '*', ['id' => (int)$valueId]); if (!is_array($value)) { $value = ['id' => $valueId]; } $value['language'] = $this->db->get('pp_shop_attributes_values_langs', ['lang_id', 'name'], [ 'AND' => [ 'value_id' => (int)$valueId, 'lang_id' => $langId, ], ]); if (!is_array($value['language'])) { $value['language'] = ['lang_id' => $langId, 'name' => '']; } $cacheHandler->set($cacheKey, $value); return $value; } private function clearFrontCache(int $id, string $type): void { if ($id <= 0 || !class_exists('\Shared\Cache\CacheHandler')) { return; } $cacheHandler = new \Shared\Cache\CacheHandler(); $langs = $this->db->select('pp_langs', 'id', ['status' => 1]); if (!is_array($langs)) { return; } foreach ($langs as $langId) { $cacheHandler->delete("AttributeRepository::$type:$id:$langId"); } } private function clearTempAndCache(): void { \Shared\Helpers\Helpers::delete_dir('../temp/'); \Shared\Helpers\Helpers::delete_cache(); } private function normalizeDecimal(float $value, int $precision = 2): float { return round($value, $precision); } public function isValueDefault(int $valueId) { if ($valueId <= 0) { return null; } $cacheHandler = new \Shared\Cache\CacheHandler(); $cacheKey = "attr_value_default:{$valueId}"; $cached = $cacheHandler->get($cacheKey); if ($cached) { return unserialize($cached); } $isDefault = $this->db->get('pp_shop_attributes_values', 'is_default', ['id' => $valueId]); $cacheHandler->set($cacheKey, $isDefault); return $isDefault; } public function getAttributeOrder(int $attributeId) { if ($attributeId <= 0) { return null; } $cacheHandler = new \Shared\Cache\CacheHandler(); $cacheKey = "attr_order:{$attributeId}"; $cached = $cacheHandler->get($cacheKey); if ($cached) { return unserialize($cached); } $order = $this->db->get('pp_shop_attributes', 'o', ['id' => $attributeId]); $cacheHandler->set($cacheKey, $order); return $order; } public function getAttributeNameByValue(int $valueId, string $langId) { if ($valueId <= 0) { return null; } $stmt = $this->db->query( 'SELECT name FROM pp_shop_attributes_langs AS psal ' . 'INNER JOIN pp_shop_attributes_values AS psav ON psal.attribute_id = psav.attribute_id ' . 'WHERE psav.id = :value_id AND lang_id = :lang_id', [':value_id' => $valueId, ':lang_id' => $langId] ); if (!$stmt) { return null; } $row = $stmt->fetch(\PDO::FETCH_ASSOC); return is_array($row) ? ($row['name'] ?? null) : null; } }