ver. 0.293: Code review fixes — 6 repositories, 16 fixes
- ArticleRepository: SQL injection fix (addslashes→parameterized), DRY refactor topArticles/newsListArticles
- AttributeRepository: dead class_exists('\S') blocking cache/temp clear
- CategoryRepository: dead class_exists('\S') blocking SEO link generation (critical)
- BannerRepository: parameterize $today in SQL + null guard on query()
- BasketCalculator: null guard checkProductQuantityInStock + optional DI params
- PromotionRepository: null guard on $basket (production fatal)
- OrderRepository/ShopBasketController/ajax.php: explicit DI in BasketCalculator callers
614 tests, 1821 assertions (+4 new)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
2
ajax.php
2
ajax.php
@@ -63,7 +63,7 @@ if ( $a == 'basket_change_transport' )
|
||||
\Shared\Helpers\Helpers::set_session( 'transport_id', \Shared\Helpers\Helpers::get( 'transport_id' ) );
|
||||
|
||||
$basket = \Shared\Helpers\Helpers::get_session( 'basket' );
|
||||
$basket_summary = \Domain\Basket\BasketCalculator::summaryPrice( $basket, null );
|
||||
$basket_summary = \Domain\Basket\BasketCalculator::summaryPrice( $basket, null, $lang_id );
|
||||
$transport_cost = ( new \Domain\Transport\TransportRepository( $mdb ) )->transportCostCached( \Shared\Helpers\Helpers::get( 'transport_id' ) );
|
||||
|
||||
echo json_encode( [ 'summary' => \Shared\Helpers\Helpers::decimal( $basket_summary + $transport_cost ) . ' zł' ] );
|
||||
|
||||
@@ -844,13 +844,14 @@ class ArticleRepository
|
||||
/**
|
||||
* Pobiera artykuly opublikowane w podanym zakresie dat.
|
||||
*/
|
||||
public function articlesByDateAdd( string $dateStart, string $dateEnd ): array
|
||||
public function articlesByDateAdd( string $dateStart, string $dateEnd, string $langId = 'pl' ): array
|
||||
{
|
||||
$stmt = $this->db->query(
|
||||
'SELECT id FROM pp_articles '
|
||||
. 'WHERE status = 1 '
|
||||
. 'AND date_add BETWEEN \'' . addslashes( $dateStart ) . '\' AND \'' . addslashes( $dateEnd ) . '\' '
|
||||
. 'ORDER BY date_add DESC'
|
||||
. 'AND date_add BETWEEN :date_start AND :date_end '
|
||||
. 'ORDER BY date_add DESC',
|
||||
[':date_start' => $dateStart, ':date_end' => $dateEnd]
|
||||
);
|
||||
|
||||
$articles = [];
|
||||
@@ -858,7 +859,7 @@ class ArticleRepository
|
||||
|
||||
if ( is_array( $rows ) ) {
|
||||
foreach ( $rows as $row ) {
|
||||
$articles[] = $this->articleDetailsFrontend( $row['id'], 'pl' );
|
||||
$articles[] = $this->articleDetailsFrontend( $row['id'], $langId );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,24 +890,18 @@ class ArticleRepository
|
||||
return null;
|
||||
}
|
||||
|
||||
$results = $this->db->select('pp_articles_langs', '*', [
|
||||
$langRow = $this->db->get('pp_articles_langs', '*', [
|
||||
'AND' => ['article_id' => $articleId, 'lang_id' => $langId]
|
||||
]);
|
||||
|
||||
if (is_array($results)) {
|
||||
foreach ($results as $row) {
|
||||
if ($row['copy_from']) {
|
||||
$results2 = $this->db->select('pp_articles_langs', '*', [
|
||||
'AND' => ['article_id' => $articleId, 'lang_id' => $row['copy_from']]
|
||||
]);
|
||||
if (is_array($results2)) {
|
||||
foreach ($results2 as $row2) {
|
||||
$article['language'] = $row2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$article['language'] = $row;
|
||||
}
|
||||
if ($langRow) {
|
||||
if ($langRow['copy_from']) {
|
||||
$copyRow = $this->db->get('pp_articles_langs', '*', [
|
||||
'AND' => ['article_id' => $articleId, 'lang_id' => $langRow['copy_from']]
|
||||
]);
|
||||
$article['language'] = $copyRow ? $copyRow : $langRow;
|
||||
} else {
|
||||
$article['language'] = $langRow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,7 +950,7 @@ class ArticleRepository
|
||||
return unserialize($objectData);
|
||||
}
|
||||
|
||||
$results = $this->db->query(
|
||||
$stmt = $this->db->query(
|
||||
'SELECT * FROM ( '
|
||||
. 'SELECT '
|
||||
. 'a.id, date_modify, date_add, o, '
|
||||
@@ -971,12 +966,14 @@ class ArticleRepository
|
||||
. 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
|
||||
. 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
|
||||
. 'WHERE '
|
||||
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = \'' . $langId . '\' '
|
||||
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = :lang_id '
|
||||
. ') AS q1 '
|
||||
. 'WHERE q1.title IS NOT NULL '
|
||||
. 'ORDER BY q1.' . $order . ' '
|
||||
. 'LIMIT ' . (int)$from . ',' . (int)$limit
|
||||
)->fetchAll();
|
||||
. 'LIMIT ' . (int)$from . ',' . (int)$limit,
|
||||
[':lang_id' => $langId]
|
||||
);
|
||||
$results = $stmt ? $stmt->fetchAll() : [];
|
||||
|
||||
if (is_array($results) && !empty($results)) {
|
||||
foreach ($results as $row) {
|
||||
@@ -1003,7 +1000,7 @@ class ArticleRepository
|
||||
return (int)unserialize($objectData);
|
||||
}
|
||||
|
||||
$results = $this->db->query(
|
||||
$stmt = $this->db->query(
|
||||
'SELECT COUNT(0) FROM ( '
|
||||
. 'SELECT '
|
||||
. 'a.id, '
|
||||
@@ -1019,10 +1016,12 @@ class ArticleRepository
|
||||
. 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
|
||||
. 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
|
||||
. 'WHERE '
|
||||
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = \'' . $langId . '\' '
|
||||
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = :lang_id '
|
||||
. ') AS q1 '
|
||||
. 'WHERE q1.title IS NOT NULL'
|
||||
)->fetchAll();
|
||||
. 'WHERE q1.title IS NOT NULL',
|
||||
[':lang_id' => $langId]
|
||||
);
|
||||
$results = $stmt ? $stmt->fetchAll() : [];
|
||||
|
||||
$count = isset($results[0][0]) ? (int)$results[0][0] : 0;
|
||||
|
||||
@@ -1106,14 +1105,30 @@ class ArticleRepository
|
||||
* Pobiera najpopularniejsze artykuly ze strony (wg views DESC, z Redis cache).
|
||||
*/
|
||||
public function topArticles(int $pageId, int $limit, string $langId): ?array
|
||||
{
|
||||
return $this->fetchArticlesByPage('topArticles', $pageId, $limit, $langId, 'views DESC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera najnowsze artykuly ze strony (wg date_add DESC, z Redis cache).
|
||||
*/
|
||||
public function newsListArticles(int $pageId, int $limit, string $langId): ?array
|
||||
{
|
||||
return $this->fetchArticlesByPage('newsListArticles', $pageId, $limit, $langId, 'date_add DESC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wspolna logika dla topArticles/newsListArticles (z Redis cache).
|
||||
*/
|
||||
private function fetchArticlesByPage(string $cachePrefix, int $pageId, int $limit, string $langId, string $orderBy): ?array
|
||||
{
|
||||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||||
$cacheKey = "ArticleRepository::topArticles:{$pageId}:{$limit}:{$langId}";
|
||||
$cacheKey = "ArticleRepository::{$cachePrefix}:{$pageId}:{$limit}:{$langId}";
|
||||
|
||||
$objectData = $cacheHandler->get($cacheKey);
|
||||
|
||||
if (!$objectData) {
|
||||
$articlesData = $this->db->query(
|
||||
$stmt = $this->db->query(
|
||||
'SELECT * FROM ( '
|
||||
. 'SELECT '
|
||||
. 'a.id, date_add, views, '
|
||||
@@ -1129,61 +1144,14 @@ class ArticleRepository
|
||||
. 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
|
||||
. 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
|
||||
. 'WHERE '
|
||||
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = \'' . $langId . '\' '
|
||||
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = :lang_id '
|
||||
. ') AS q1 '
|
||||
. 'WHERE q1.title IS NOT NULL '
|
||||
. 'ORDER BY q1.views DESC '
|
||||
. 'LIMIT 0, ' . (int)$limit
|
||||
)->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
$cacheHandler->set($cacheKey, $articlesData);
|
||||
} else {
|
||||
$articlesData = unserialize($objectData);
|
||||
}
|
||||
|
||||
$articles = null;
|
||||
if (\Shared\Helpers\Helpers::is_array_fix($articlesData)) {
|
||||
foreach ($articlesData as $row) {
|
||||
$articles[] = $this->articleDetailsFrontend((int)$row['id'], $langId);
|
||||
}
|
||||
}
|
||||
|
||||
return $articles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera najnowsze artykuly ze strony (wg date_add DESC, z Redis cache).
|
||||
*/
|
||||
public function newsListArticles(int $pageId, int $limit, string $langId): ?array
|
||||
{
|
||||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||||
$cacheKey = "ArticleRepository::newsListArticles:{$pageId}:{$limit}:{$langId}";
|
||||
|
||||
$objectData = $cacheHandler->get($cacheKey);
|
||||
|
||||
if (!$objectData) {
|
||||
$articlesData = $this->db->query(
|
||||
'SELECT * FROM ( '
|
||||
. 'SELECT '
|
||||
. 'a.id, date_add, '
|
||||
. '( CASE '
|
||||
. 'WHEN copy_from IS NULL THEN title '
|
||||
. 'WHEN copy_from IS NOT NULL THEN ( '
|
||||
. 'SELECT title FROM pp_articles_langs '
|
||||
. 'WHERE lang_id = al.copy_from AND article_id = a.id '
|
||||
. ') '
|
||||
. 'END ) AS title '
|
||||
. 'FROM '
|
||||
. 'pp_articles_pages AS ap '
|
||||
. 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
|
||||
. 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
|
||||
. 'WHERE '
|
||||
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = \'' . $langId . '\' '
|
||||
. ') AS q1 '
|
||||
. 'WHERE q1.title IS NOT NULL '
|
||||
. 'ORDER BY q1.date_add DESC '
|
||||
. 'LIMIT 0, ' . (int)$limit
|
||||
)->fetchAll(\PDO::FETCH_ASSOC);
|
||||
. 'ORDER BY q1.' . $orderBy . ' '
|
||||
. 'LIMIT 0, ' . (int)$limit,
|
||||
[':lang_id' => $langId]
|
||||
);
|
||||
$articlesData = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||||
|
||||
$cacheHandler->set($cacheKey, $articlesData);
|
||||
} else {
|
||||
|
||||
@@ -938,14 +938,8 @@ class AttributeRepository
|
||||
|
||||
private function clearTempAndCache(): void
|
||||
{
|
||||
if (class_exists('\S')) {
|
||||
if (method_exists('\S', 'delete_dir')) {
|
||||
\Shared\Helpers\Helpers::delete_dir('../temp/');
|
||||
}
|
||||
if (method_exists('\S', 'delete_cache')) {
|
||||
\Shared\Helpers\Helpers::delete_cache();
|
||||
}
|
||||
}
|
||||
\Shared\Helpers\Helpers::delete_dir('../temp/');
|
||||
\Shared\Helpers\Helpers::delete_cache();
|
||||
}
|
||||
|
||||
private function normalizeDecimal(float $value, int $precision = 2): float
|
||||
|
||||
@@ -331,13 +331,15 @@ class BannerRepository
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$results = $this->db->query(
|
||||
$stmt = $this->db->query(
|
||||
"SELECT id, name FROM pp_banners "
|
||||
. "WHERE status = 1 "
|
||||
. "AND (date_start <= '{$today}' OR date_start IS NULL) "
|
||||
. "AND (date_end >= '{$today}' OR date_end IS NULL) "
|
||||
. "AND home_page = 0"
|
||||
)->fetchAll();
|
||||
. "AND (date_start <= :today1 OR date_start IS NULL) "
|
||||
. "AND (date_end >= :today2 OR date_end IS NULL) "
|
||||
. "AND home_page = 0",
|
||||
[':today1' => $today, ':today2' => $today]
|
||||
);
|
||||
$results = $stmt ? $stmt->fetchAll() : [];
|
||||
|
||||
$banners = null;
|
||||
if (is_array($results) && !empty($results)) {
|
||||
@@ -370,15 +372,17 @@ class BannerRepository
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$results = $this->db->query(
|
||||
$stmt = $this->db->query(
|
||||
"SELECT * FROM pp_banners "
|
||||
. "WHERE status = 1 "
|
||||
. "AND (date_start <= '{$today}' OR date_start IS NULL) "
|
||||
. "AND (date_end >= '{$today}' OR date_end IS NULL) "
|
||||
. "AND (date_start <= :today1 OR date_start IS NULL) "
|
||||
. "AND (date_end >= :today2 OR date_end IS NULL) "
|
||||
. "AND home_page = 1 "
|
||||
. "ORDER BY date_end ASC "
|
||||
. "LIMIT 1"
|
||||
)->fetchAll();
|
||||
. "LIMIT 1",
|
||||
[':today1' => $today, ':today2' => $today]
|
||||
);
|
||||
$results = $stmt ? $stmt->fetchAll() : [];
|
||||
|
||||
$banner = null;
|
||||
if (is_array($results) && !empty($results)) {
|
||||
|
||||
@@ -26,22 +26,32 @@ class BasketCalculator
|
||||
return $count . ' produktów';
|
||||
}
|
||||
|
||||
public static function summaryPrice($basket, $coupon = null)
|
||||
/**
|
||||
* @param string|null $langId Language ID (falls back to global $lang_id if null)
|
||||
* @param \Domain\Product\ProductRepository|null $productRepo (falls back to $GLOBALS['mdb'] if null)
|
||||
*/
|
||||
public static function summaryPrice($basket, $coupon = null, $langId = null, $productRepo = null)
|
||||
{
|
||||
global $lang_id;
|
||||
if ($langId === null) {
|
||||
global $lang_id;
|
||||
$langId = $lang_id;
|
||||
}
|
||||
if ($productRepo === null) {
|
||||
$productRepo = new \Domain\Product\ProductRepository($GLOBALS['mdb']);
|
||||
}
|
||||
|
||||
$summary = 0;
|
||||
$productRepo = new \Domain\Product\ProductRepository($GLOBALS['mdb']);
|
||||
|
||||
if (is_array($basket)) {
|
||||
foreach ($basket as $position) {
|
||||
$product = $productRepo->findCached((int)$position['product-id'], $lang_id);
|
||||
$product = $productRepo->findCached((int)$position['product-id'], $langId);
|
||||
|
||||
$product_price_tmp = self::calculateBasketProductPrice(
|
||||
(float)($product['price_brutto_promo'] ?? 0),
|
||||
(float)($product['price_brutto'] ?? 0),
|
||||
$coupon,
|
||||
$position
|
||||
$position,
|
||||
$productRepo
|
||||
);
|
||||
$summary += $product_price_tmp['price_new'] * $position['quantity'];
|
||||
}
|
||||
@@ -71,6 +81,9 @@ class BasketCalculator
|
||||
|
||||
public static function checkProductQuantityInStock($basket, bool $message = false)
|
||||
{
|
||||
if ( !is_array( $basket ) || empty( $basket ) )
|
||||
return false;
|
||||
|
||||
$result = false;
|
||||
$productRepo = new \Domain\Product\ProductRepository($GLOBALS['mdb']);
|
||||
|
||||
@@ -115,9 +128,14 @@ class BasketCalculator
|
||||
* Calculate product price in basket (with coupon + promotion discounts).
|
||||
* Migrated from \shop\Product::calculate_basket_product_price()
|
||||
*/
|
||||
public static function calculateBasketProductPrice( float $price_brutto_promo, float $price_brutto, $coupon, $basket_position )
|
||||
/**
|
||||
* @param \Domain\Product\ProductRepository|null $productRepo (falls back to $GLOBALS['mdb'] if null)
|
||||
*/
|
||||
public static function calculateBasketProductPrice( float $price_brutto_promo, float $price_brutto, $coupon, $basket_position, $productRepo = null )
|
||||
{
|
||||
$productRepo = new \Domain\Product\ProductRepository($GLOBALS['mdb']);
|
||||
if ($productRepo === null) {
|
||||
$productRepo = new \Domain\Product\ProductRepository($GLOBALS['mdb']);
|
||||
}
|
||||
|
||||
// Produkty przecenione
|
||||
if ( $price_brutto_promo )
|
||||
|
||||
@@ -668,18 +668,12 @@ class CategoryRepository
|
||||
|
||||
private function refreshCategoryArtifacts(): void
|
||||
{
|
||||
if (class_exists('\\S')) {
|
||||
\Shared\Helpers\Helpers::htacces();
|
||||
\Shared\Helpers\Helpers::delete_dir('../temp/');
|
||||
}
|
||||
\Shared\Helpers\Helpers::htacces();
|
||||
\Shared\Helpers\Helpers::delete_dir('../temp/');
|
||||
}
|
||||
|
||||
private function normalizeSeoLink($value): ?string
|
||||
{
|
||||
if (!class_exists('\\S')) {
|
||||
return $this->toNullableString($value);
|
||||
}
|
||||
|
||||
$seo = \Shared\Helpers\Helpers::seo((string)$value);
|
||||
$seo = trim((string)$seo);
|
||||
|
||||
|
||||
@@ -560,7 +560,8 @@ class OrderRepository
|
||||
|
||||
$transport = ( new \Domain\Transport\TransportRepository( $this->db ) )->findActiveByIdCached( $transport_id );
|
||||
$payment_method = ( new \Domain\PaymentMethod\PaymentMethodRepository( $this->db ) )->findActiveById( (int)$payment_id );
|
||||
$basket_summary = \Domain\Basket\BasketCalculator::summaryPrice($basket, $coupon);
|
||||
$productRepo = new \Domain\Product\ProductRepository($this->db);
|
||||
$basket_summary = \Domain\Basket\BasketCalculator::summaryPrice($basket, $coupon, $lang_id, $productRepo);
|
||||
$order_number = $this->generateOrderNumber();
|
||||
$order_date = date('Y-m-d H:i:s');
|
||||
$hash = md5($order_number . time());
|
||||
@@ -619,7 +620,6 @@ class OrderRepository
|
||||
if (is_array($basket)) {
|
||||
foreach ($basket as $basket_position) {
|
||||
$attributes = '';
|
||||
$productRepo = new \Domain\Product\ProductRepository($this->db);
|
||||
$product = $productRepo->findCached($basket_position['product-id'], $lang_id);
|
||||
|
||||
if (is_array($basket_position['attributes'])) {
|
||||
@@ -649,7 +649,7 @@ class OrderRepository
|
||||
}
|
||||
}
|
||||
|
||||
$product_price_tmp = \Domain\Basket\BasketCalculator::calculateBasketProductPrice((float)$product['price_brutto_promo'], (float)$product['price_brutto'], $coupon, $basket_position);
|
||||
$product_price_tmp = \Domain\Basket\BasketCalculator::calculateBasketProductPrice((float)$product['price_brutto_promo'], (float)$product['price_brutto'], $coupon, $basket_position, $productRepo);
|
||||
|
||||
$this->db->insert('pp_shop_order_products', [
|
||||
'order_id' => $order_id,
|
||||
|
||||
@@ -453,6 +453,9 @@ class PromotionRepository
|
||||
|
||||
public function findPromotion( $basket )
|
||||
{
|
||||
if ( !is_array( $basket ) || empty( $basket ) )
|
||||
return is_array( $basket ) ? $basket : [];
|
||||
|
||||
foreach ( $basket as $key => $val )
|
||||
{
|
||||
unset( $basket[$key]['discount_type'] );
|
||||
|
||||
@@ -173,7 +173,7 @@ class ShopBasketController
|
||||
echo json_encode( [
|
||||
'result' => 'ok',
|
||||
'basket_mini_count' => \Domain\Basket\BasketCalculator::countProductsText( \Domain\Basket\BasketCalculator::countProducts( $basket ) ),
|
||||
'basket_mini_value' => \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ),
|
||||
'basket_mini_value' => \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon, $lang_id ),
|
||||
'product_sets' => ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->productSetsWhenAddToBasket( (int)$values['product-id'] )
|
||||
] );
|
||||
exit;
|
||||
@@ -393,7 +393,7 @@ class ShopBasketController
|
||||
'coupon' => $coupon
|
||||
] ),
|
||||
'basket_mini_count' => \Domain\Basket\BasketCalculator::countProductsText( \Domain\Basket\BasketCalculator::countProducts( $basket ) ),
|
||||
'basket_mini_value' => \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ),
|
||||
'basket_mini_value' => \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon, $lang_id ),
|
||||
'products_count' => count( $basket ),
|
||||
'transport_methods' => \Shared\Tpl\Tpl::view( 'shop-basket/basket-transport-methods', [
|
||||
'transports_methods' => ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->transportMethodsFront( $basket, $coupon ),
|
||||
|
||||
@@ -4,7 +4,34 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
|
||||
|
||||
---
|
||||
|
||||
## ver. 0.294 (2026-02-18) - Usuniecie autoload/shop/ — 12 legacy klas
|
||||
## ver. 0.293 (2026-02-19) - Code review: fixes ArticleRepository, AttributeRepository, BannerRepository, BasketCalculator, CategoryRepository, PromotionRepository
|
||||
|
||||
- **ArticleRepository** (7 fixes):
|
||||
- FIX: `articlesByDateAdd()` — SQL injection (addslashes→parameterized queries), dodano parametr `$langId`
|
||||
- FIX: `articleDetailsFrontend()` — uproszczono select()+foreach→get()
|
||||
- FIX: `articlesIds()`, `pageArticlesCount()` — parametryzacja `$langId`
|
||||
- FIX: `topArticles()`, `newsListArticles()` — DRY refactor via `fetchArticlesByPage()`, parametryzacja
|
||||
- **AttributeRepository** (1 fix):
|
||||
- FIX: `clearTempAndCache()` — martwy `class_exists('\S')` blokował czyszczenie cache/temp
|
||||
- **CategoryRepository** (2 fixes):
|
||||
- FIX: `refreshCategoryArtifacts()` — martwy `class_exists('\S')` blokował czyszczenie htaccess/temp
|
||||
- FIX: `normalizeSeoLink()` — **krytyczny bug** — linki SEO kategorii nigdy nie były generowane od usunięcia `\S`
|
||||
- **BannerRepository** (2 fixes):
|
||||
- FIX: `banners()`, `mainBanner()` — parametryzacja `$today` w SQL + null guard na `query()`
|
||||
- **BasketCalculator** (3 fixes):
|
||||
- FIX: `checkProductQuantityInStock()` — dodano `is_array()` guard (foreach na null→fatal)
|
||||
- FIX: `summaryPrice()` — dodano opcjonalne DI params `$langId`, `$productRepo` z fallbackiem do globals
|
||||
- FIX: `calculateBasketProductPrice()` — dodano opcjonalny `$productRepo` z fallbackiem do globals
|
||||
- **PromotionRepository** (1 fix):
|
||||
- FIX: `findPromotion()` — null guard na `$basket` (produkcyjny fatal error)
|
||||
- **OrderRepository** — zaktualizowano callery BasketCalculator (jawne DI zamiast globals), usunięto redundantne tworzenie ProductRepository w pętli
|
||||
- **ShopBasketController**, **ajax.php** — zaktualizowano callery summaryPrice (jawne `$lang_id`)
|
||||
- **CLASS_CATALOG.md** — zaktualizowano katalog dla 5 klas (rzeczywiste metody + znaczniki przeglądu)
|
||||
- Testy: 614 OK, 1821 asercji (+4 nowe testy BasketCalculator)
|
||||
|
||||
---
|
||||
|
||||
## ver. 0.292 (2026-02-18) - Usuniecie autoload/shop/ — 12 legacy klas
|
||||
|
||||
- **Faza 5.1: class.Order.php (~562 linii) USUNIETA**
|
||||
- Logika Apilo sync przeniesiona do `OrderAdminService::processApiloSyncQueue()`
|
||||
|
||||
@@ -17,101 +17,140 @@ Wygenerowano: 2026-02-18
|
||||
<a id="domain"></a>
|
||||
## 1. Domain/ — Warstwa domenowa
|
||||
|
||||
### Domain\Article\ArticleRepository
|
||||
### Domain\Article\ArticleRepository ✅ REVIEWED
|
||||
File: `autoload/Domain/Article/ArticleRepository.php`
|
||||
Properties:
|
||||
- `private $db`
|
||||
- `private const MAX_PER_PAGE = 100`
|
||||
|
||||
Methods:
|
||||
- `public function __construct($db)`
|
||||
- `public function listForAdmin(array $filters, string $sortColumn = 'id', string $sortDir = 'DESC', int $page = 1, int $perPage = 15): array`
|
||||
- `public function find(int $articleId): array`
|
||||
- `public function save(array $data): ?int`
|
||||
- `public function delete(int $articleId): bool`
|
||||
- `public function toggleStatus(int $articleId): bool`
|
||||
- `public function detailsForLanguage(int $articleId, string $langId): ?array`
|
||||
- `public function frontArticleDetails(int $articleId, string $langId): array`
|
||||
- `public function frontArticleDetailsBySeoLink(string $seoLink, string $langId): ?array`
|
||||
- `public function frontArticleList(string $langId, int $page = 1, int $perPage = 12): array`
|
||||
- `public function frontArticleListAll(string $langId): array`
|
||||
- `public function getFirstImageCached(int $articleId)`
|
||||
- `private function baseListSelect(): string`
|
||||
- `private function translationsMap(int $articleId): array`
|
||||
- `private function extractTranslations(array $data): array`
|
||||
- `private function toSwitchValue($value): int`
|
||||
- `private function defaultArticle(): array`
|
||||
- `private function clearFrontCache(int $articleId): void`
|
||||
- `private function saveSeoRedirects(int $articleId, string $langId, string $newSeoLink, string $currentSeoLink): void`
|
||||
Public Methods:
|
||||
- ✅ `public function __construct($db)`
|
||||
- ✅ `public function find(int $articleId): ?array` — try/catch na brak kolumny `o` (kompatybilność)
|
||||
- ✅ `public function save(int $articleId, array $data, int $userId): int`
|
||||
- ✅ `public function archive(int $articleId): bool`
|
||||
- ✅ `public function restore(int $articleId): bool`
|
||||
- ✅ `public function deletePermanently(int $articleId): bool`
|
||||
- ✅ `public function listForAdmin(array $filters, string $sortColumn = 'date_add', string $sortDir = 'DESC', int $page = 1, int $perPage = 15): array`
|
||||
- ✅ `public function listArchivedForAdmin(array $filters, string $sortColumn = 'date_add', string $sortDir = 'DESC', int $page = 1, int $perPage = 15): array`
|
||||
- ✅ `public function saveGalleryOrder(int $articleId, string $order): bool`
|
||||
- ✅ `public function saveFilesOrder(int $articleId, string $order): bool`
|
||||
- ✅ `public function pagesSummaryForArticles(array $articleIds): array`
|
||||
- ✅ `public function updateImageAlt(int $imageId, string $imageAlt): bool`
|
||||
- ✅ `public function updateFileName(int $fileId, string $fileName): bool`
|
||||
- ✅ `public function markFileToDelete(int $fileId): bool`
|
||||
- ✅ `public function markImageToDelete(int $imageId): bool`
|
||||
- ✅ `public function deleteNonassignedFiles(): void`
|
||||
- ✅ `public function deleteNonassignedImages(): void`
|
||||
- 🔧 `public function articlesByDateAdd(string $dateStart, string $dateEnd, string $langId = 'pl'): array` — naprawiono SQL injection (addslashes→parameterized), dodano parametr $langId
|
||||
- 🔧 `public function articleDetailsFrontend(int $articleId, string $langId): ?array` — select+foreach → get() (uproszczono)
|
||||
- 🔧 `public function articlesIds(int $pageId, string $langId, int $limit, int $sortType, int $from): ?array` — parametryzacja $langId
|
||||
- 🔧 `public function pageArticlesCount(int $pageId, string $langId): int` — parametryzacja $langId
|
||||
- ✅ `public function pageArticles(array $page, string $langId, int $bs): array`
|
||||
- ✅ `public function news(int $pageId, int $limit, string $langId): ?array`
|
||||
- ✅ `public function articleNoindex(int $articleId, string $langId): bool`
|
||||
- 🔧 `public function topArticles(int $pageId, int $limit, string $langId): ?array` — parametryzacja $langId + DRY via fetchArticlesByPage()
|
||||
- 🔧 `public function newsListArticles(int $pageId, int $limit, string $langId): ?array` — parametryzacja $langId + DRY via fetchArticlesByPage()
|
||||
|
||||
Private Methods:
|
||||
- ✅ `private function createArticle(array $data, int $userId): int`
|
||||
- ✅ `private function updateArticle(int $articleId, array $data, int $userId): int`
|
||||
- ✅ `private function buildArticleRow(array $data, int $userId, bool $isNew): array`
|
||||
- ✅ `private function buildLangRow($langId, array $data): array`
|
||||
- ✅ `private function applyGalleryOrderIfProvided(int $articleId, array $data): void`
|
||||
- ✅ `private function applyFilesOrderIfProvided(int $articleId, array $data): void`
|
||||
- ✅ `private function saveTranslations(int $articleId, array $data, bool $isNew): void`
|
||||
- ✅ `private function savePages(int $articleId, $pages, bool $isNew): void`
|
||||
- ✅ `private function assignTempFiles(int $articleId): void`
|
||||
- ✅ `private function assignTempImages(int $articleId): void`
|
||||
- ✅ `private function deleteMarkedImages(int $articleId): void`
|
||||
- ✅ `private function deleteMarkedFiles(int $articleId): void`
|
||||
- ✅ `private function maxPageOrder(): int`
|
||||
- ✅ `private function isCheckedValue($value): bool`
|
||||
- ✅ `private function appendDateRangeFilter(array &$where, array &$params, string $column, string $fromKey, string $toKey, array $filters): void`
|
||||
- 🔧 `private function fetchArticlesByPage(string $cachePrefix, int $pageId, int $limit, string $langId, string $orderBy): ?array` — NOWA, wspólna logika topArticles/newsListArticles
|
||||
|
||||
---
|
||||
|
||||
### Domain\Attribute\AttributeRepository
|
||||
### Domain\Attribute\AttributeRepository ✅ REVIEWED
|
||||
File: `autoload/Domain/Attribute/AttributeRepository.php`
|
||||
Properties:
|
||||
- `private $db`
|
||||
- `private ?string $defaultLangId = null`
|
||||
- `private const MAX_PER_PAGE = 100`
|
||||
|
||||
Methods:
|
||||
- `public function __construct($db)`
|
||||
- `public function listForAdmin(array $filters, string $sortColumn = 'id', string $sortDir = 'DESC', int $page = 1, int $perPage = 15): array`
|
||||
- `public function find(int $attributeId): array`
|
||||
- `public function save(array $data): ?int`
|
||||
- `public function delete(int $attributeId): bool`
|
||||
- `public function detailsForLanguage(int $attributeId, string $langId): ?array`
|
||||
- `public function getAttributeValues(int $attributeId): array`
|
||||
- `public function findValue(int $valueId): array`
|
||||
- `public function saveValue(array $data): ?int`
|
||||
- `public function deleteValue(int $valueId): bool`
|
||||
- `public function detailsForValueLanguage(int $valueId, string $langId): ?array`
|
||||
- `public function allForAdmin(): array`
|
||||
- `public function allValuesForAttribute(int $attributeId): array`
|
||||
- `public function productAttributes(int $productId): array`
|
||||
- `public function valueDetails(int $valueId): ?array`
|
||||
- `public function isValueDefault(int $valueId): int`
|
||||
- `public function getAttributeOrder(int $attributeId): int`
|
||||
- `public function getAttributeNameById(int $attributeId, string $langId): string`
|
||||
- `public function getAttributeValueById(int $valueId, string $langId): string`
|
||||
- `private function baseListSelect(): string`
|
||||
- `private function translationsMap(int $attributeId): array`
|
||||
- `private function extractTranslations(array $data): array`
|
||||
- `private function valueTranslationsMap(int $valueId): array`
|
||||
- `private function extractValueTranslations(array $data): array`
|
||||
- `private function toSwitchValue($value): int`
|
||||
- `private function defaultAttribute(): array`
|
||||
- `private function defaultValue(): array`
|
||||
Public Methods:
|
||||
- ✅ `public function __construct($db)`
|
||||
- ✅ `public function listForAdmin(array $filters, string $sortColumn = 'o', string $sortDir = 'ASC', int $page = 1, int $perPage = 15): array`
|
||||
- ✅ `public function findAttribute(int $attributeId): array`
|
||||
- ✅ `public function saveAttribute(array $data): ?int`
|
||||
- ✅ `public function deleteAttribute(int $attributeId): bool`
|
||||
- ✅ `public function findValues(int $attributeId): array`
|
||||
- ✅ `public function saveValues(int $attributeId, array $payload): bool`
|
||||
- ✅ `public function saveLegacyValues(int $attributeId, array $names, array $values, array $ids, $defaultValue, array $impactOnThePrice): ?int`
|
||||
- ✅ `public function valueDetails(int $valueId): array`
|
||||
- ✅ `public function getAttributeNameById(int $attributeId, ?string $langId = null): string`
|
||||
- ✅ `public function getAttributeValueById(int $valueId, ?string $langId = null): string` — z cache
|
||||
- ✅ `public function getAttributesListForCombinations(): array`
|
||||
- ✅ `public function frontAttributeDetails(int $attributeId, string $langId): array` — z cache
|
||||
- ✅ `public function frontValueDetails(int $valueId, string $langId): array` — z cache
|
||||
- ✅ `public function isValueDefault(int $valueId)`
|
||||
- ✅ `public function getAttributeOrder(int $attributeId)`
|
||||
- ✅ `public function getAttributeNameByValue(int $valueId, string $langId)` — parametryzowane query
|
||||
|
||||
Private Methods:
|
||||
- ✅ `private function buildAdminWhere(array $filters): array`
|
||||
- ✅ `private function saveAttributeTranslations(int $attributeId, array $names): void`
|
||||
- ✅ `private function saveValueTranslations(int $valueId, array $translations): void`
|
||||
- ✅ `private function valueBelongsToAttribute(int $valueId, int $attributeId): bool`
|
||||
- ✅ `private function normalizeValueRows(array $rows): array`
|
||||
- ✅ `private function refreshCombinationPricesForValue(int $valueId, ?string $impactOnThePrice): void`
|
||||
- ✅ `private function toSwitchValue($value): int`
|
||||
- ✅ `private function toTypeValue($value): int`
|
||||
- ✅ `private function toNullableNumeric($value): ?string`
|
||||
- ✅ `private function defaultAttribute(): array`
|
||||
- ✅ `private function nextOrder(): int`
|
||||
- ✅ `private function defaultLanguageId(): string`
|
||||
- ✅ `private function clearFrontCache(int $id, string $type): void`
|
||||
- 🔧 `private function clearTempAndCache(): void` — usunięto martwy check `class_exists('\S')`
|
||||
- ✅ `private function normalizeDecimal(float $value, int $precision = 2): float`
|
||||
|
||||
---
|
||||
|
||||
### Domain\Banner\BannerRepository
|
||||
### Domain\Banner\BannerRepository ✅ REVIEWED
|
||||
File: `autoload/Domain/Banner/BannerRepository.php`
|
||||
Properties:
|
||||
- `private $db`
|
||||
- `private const MAX_PER_PAGE = 100`
|
||||
|
||||
Methods:
|
||||
- `public function __construct($db)`
|
||||
- `public function listForAdmin(array $filters = [], string $sortColumn = 'id', string $sortDir = 'DESC', int $page = 1, int $perPage = 15): array`
|
||||
- `public function find(int $bannerId): ?array`
|
||||
- `public function save(array $data): ?int`
|
||||
- `public function delete(int $bannerId): bool`
|
||||
- `public function allActiveCached(): array`
|
||||
- `private function toSwitchValue($value): int`
|
||||
Public Methods:
|
||||
- ✅ `public function __construct($db)`
|
||||
- ✅ `public function find(int $bannerId): ?array` — pobiera baner + tłumaczenia
|
||||
- ✅ `public function delete(int $bannerId): bool`
|
||||
- ✅ `public function save(array $data)` — insert/update, obsługuje nowy i legacy format
|
||||
- ✅ `public function listForAdmin(array $filters, string $sortColumn = 'name', string $sortDir = 'ASC', int $page = 1, int $perPage = 15): array` — z thumbnailami
|
||||
- 🔧 `public function banners(string $langId): ?array` — parametryzacja $today + null guard na query()
|
||||
- 🔧 `public function mainBanner(string $langId): ?array` — parametryzacja $today + null guard na query()
|
||||
|
||||
Private Methods:
|
||||
- ✅ `private function fetchThumbnailsByBannerIds(array $bannerIds): array` — parametryzowane IN
|
||||
- ✅ `private function saveTranslations(int $bannerId, array $src, array $url, array $html, array $text): void` — legacy format
|
||||
- ✅ `private function saveTranslationsFromArray(int $bannerId, array $translations): void` — nowy format
|
||||
- ✅ `private function upsertTranslation(int $bannerId, $langId, array $fields): void` — count+insert/update
|
||||
|
||||
---
|
||||
|
||||
### Domain\Basket\BasketCalculator
|
||||
### Domain\Basket\BasketCalculator ✅ REVIEWED
|
||||
File: `autoload/Domain/Basket/BasketCalculator.php`
|
||||
Properties: (brak)
|
||||
Properties: (brak — klasa statyczna)
|
||||
|
||||
Methods:
|
||||
- `public static function summaryPrice($basket, $coupon): float`
|
||||
- `public static function summaryPriceWithTransport($basket, $coupon, $transport_cost): float`
|
||||
- `public static function summaryQty($basket): int`
|
||||
- `public static function summaryWp($basket): float`
|
||||
- `public static function summaryCoupon($basket, $coupon): float`
|
||||
- `public static function summaryNetto($basket): float`
|
||||
- `public static function summaryBrutto($basket): float`
|
||||
- ✅ `public static function summaryWp($basket)` — suma wag, is_array guard
|
||||
- ✅ `public static function countProductsText($count)` — polska pluralizacja
|
||||
- 🔧 `public static function summaryPrice($basket, $coupon = null, $langId = null, $productRepo = null)` — dodano opcjonalne DI params z fallbackiem do globals
|
||||
- ✅ `public static function countProducts($basket)` — suma ilości, is_array guard
|
||||
- ✅ `public static function validateBasket($basket)` — null guard
|
||||
- 🔧 `public static function checkProductQuantityInStock($basket, bool $message = false)` — dodano is_array guard (bug: foreach na null)
|
||||
- 🔧 `public static function calculateBasketProductPrice(float $price_brutto_promo, float $price_brutto, $coupon, $basket_position, $productRepo = null)` — dodano opcjonalny $productRepo z fallbackiem do globals
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ composer test # standard
|
||||
## Aktualny stan
|
||||
|
||||
```text
|
||||
OK (610 tests, 1817 assertions)
|
||||
OK (614 tests, 1821 assertions)
|
||||
```
|
||||
|
||||
Zweryfikowano: 2026-02-18 (ver. 0.294)
|
||||
Zweryfikowano: 2026-02-19 (ver. 0.293)
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
|
||||
@@ -18,16 +18,16 @@ Aktualizacje znajdują się w folderze `updates/0.XX/` gdzie XX oznacza dziesią
|
||||
|
||||
## Procedura tworzenia nowej aktualizacji
|
||||
|
||||
## Status biezacej aktualizacji (ver. 0.292)
|
||||
## Status biezacej aktualizacji (ver. 0.293)
|
||||
|
||||
- Wersja udostepniona: `0.292` (data: 2026-02-18).
|
||||
- Wersja udostepniona: `0.293` (data: 2026-02-19).
|
||||
- Pliki publikacyjne:
|
||||
- `updates/0.20/ver_0.292.zip`, `ver_0.292_files.txt`
|
||||
- `updates/0.20/ver_0.293.zip`
|
||||
- Pliki metadanych aktualizacji:
|
||||
- `updates/changelog.php` (skonsolidowany wpis `ver. 0.292` z 0.292+0.293+0.294)
|
||||
- `updates/versions.php` (`$current_ver = 292`)
|
||||
- `updates/changelog.php`
|
||||
- `updates/versions.php` (`$current_ver = 293`)
|
||||
- Weryfikacja testow przed publikacja:
|
||||
- `OK (610 tests, 1817 assertions)`
|
||||
- `OK (614 tests, 1821 assertions)`
|
||||
|
||||
### 1. Określ numer wersji
|
||||
Sprawdź ostatnią wersję w `updates/` i zwiększ o 1.
|
||||
|
||||
@@ -691,15 +691,16 @@ class ArticleRepositoryTest extends TestCase
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->expects($this->once())
|
||||
$mockDb->expects($this->exactly(2))
|
||||
->method('get')
|
||||
->with('pp_articles', '*', ['id' => 5])
|
||||
->willReturn(['id' => 5, 'status' => 1, 'show_title' => 1]);
|
||||
->willReturnOnConsecutiveCalls(
|
||||
['id' => 5, 'status' => 1, 'show_title' => 1],
|
||||
['lang_id' => 'pl', 'title' => 'Testowy', 'copy_from' => null]
|
||||
);
|
||||
|
||||
$mockDb->expects($this->exactly(4))
|
||||
$mockDb->expects($this->exactly(3))
|
||||
->method('select')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
[['lang_id' => 'pl', 'title' => 'Testowy', 'copy_from' => null]],
|
||||
[['id' => 10, 'src' => '/img/a.jpg']],
|
||||
[['id' => 20, 'src' => '/files/a.pdf']],
|
||||
[1, 2]
|
||||
@@ -732,20 +733,17 @@ class ArticleRepositoryTest extends TestCase
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')
|
||||
->willReturn(['id' => 7, 'status' => 1]);
|
||||
->willReturnOnConsecutiveCalls(
|
||||
['id' => 7, 'status' => 1],
|
||||
['lang_id' => 'en', 'title' => 'English', 'copy_from' => 'pl'],
|
||||
['lang_id' => 'pl', 'title' => 'Polski']
|
||||
);
|
||||
|
||||
$mockDb->expects($this->exactly(5))
|
||||
$mockDb->expects($this->exactly(3))
|
||||
->method('select')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
// First call: langs with copy_from
|
||||
[['lang_id' => 'en', 'title' => 'English', 'copy_from' => 'pl']],
|
||||
// Second call: copy_from fallback
|
||||
[['lang_id' => 'pl', 'title' => 'Polski']],
|
||||
// images
|
||||
[],
|
||||
// files
|
||||
[],
|
||||
// pages
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -917,11 +915,13 @@ class ArticleRepositoryTest extends TestCase
|
||||
});
|
||||
|
||||
$mockDb->method('get')
|
||||
->willReturn(['id' => 5, 'status' => 1]);
|
||||
->willReturnOnConsecutiveCalls(
|
||||
['id' => 5, 'status' => 1],
|
||||
['lang_id' => 'pl', 'title' => 'Popular', 'copy_from' => null]
|
||||
);
|
||||
|
||||
$mockDb->method('select')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
[['lang_id' => 'pl', 'title' => 'Popular', 'copy_from' => null]],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
@@ -952,11 +952,13 @@ class ArticleRepositoryTest extends TestCase
|
||||
});
|
||||
|
||||
$mockDb->method('get')
|
||||
->willReturn(['id' => 8, 'status' => 1]);
|
||||
->willReturnOnConsecutiveCalls(
|
||||
['id' => 8, 'status' => 1],
|
||||
['lang_id' => 'pl', 'title' => 'Newest', 'copy_from' => null]
|
||||
);
|
||||
|
||||
$mockDb->method('select')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
[['lang_id' => 'pl', 'title' => 'Newest', 'copy_from' => null]],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
|
||||
@@ -64,4 +64,25 @@ class BasketCalculatorTest extends TestCase
|
||||
$this->assertSame('3 produkty', BasketCalculator::countProductsText('3'));
|
||||
$this->assertSame('0 produktów', BasketCalculator::countProductsText('abc'));
|
||||
}
|
||||
|
||||
public function testCheckProductQuantityInStockReturnsFalseOnNullBasket(): void
|
||||
{
|
||||
$this->assertFalse(BasketCalculator::checkProductQuantityInStock(null));
|
||||
}
|
||||
|
||||
public function testCheckProductQuantityInStockReturnsFalseOnEmptyBasket(): void
|
||||
{
|
||||
$this->assertFalse(BasketCalculator::checkProductQuantityInStock([]));
|
||||
}
|
||||
|
||||
public function testValidateBasketReturnsEmptyArrayOnNull(): void
|
||||
{
|
||||
$this->assertSame([], BasketCalculator::validateBasket(null));
|
||||
}
|
||||
|
||||
public function testValidateBasketReturnsBasketArrayAsIs(): void
|
||||
{
|
||||
$basket = [['product-id' => 1, 'quantity' => 2]];
|
||||
$this->assertSame($basket, BasketCalculator::validateBasket($basket));
|
||||
}
|
||||
}
|
||||
|
||||
BIN
updates/0.20/ver_0.293.zip
Normal file
BIN
updates/0.20/ver_0.293.zip
Normal file
Binary file not shown.
@@ -1,3 +1,12 @@
|
||||
<b>ver. 0.293 - 19.02.2026</b><br />
|
||||
- FIX - ArticleRepository: SQL injection fix (addslashes→parameterized), uproszczenie articleDetailsFrontend
|
||||
- FIX - AttributeRepository: martwy class_exists('\S') blokowal czyszczenie cache/temp
|
||||
- FIX - CategoryRepository: martwy class_exists('\S') blokowal generowanie linkow SEO kategorii
|
||||
- FIX - BannerRepository: parametryzacja dat w SQL + null guard na query()
|
||||
- FIX - BasketCalculator: null guard checkProductQuantityInStock + opcjonalne DI params summaryPrice/calculateBasketProductPrice
|
||||
- FIX - PromotionRepository: null guard na $basket (produkcyjny fatal error)
|
||||
- UPDATE - OrderRepository, ShopBasketController, ajax.php: jawne DI zamiast globals w callerach BasketCalculator
|
||||
<hr>
|
||||
<b>ver. 0.292 - 18.02.2026</b><br />
|
||||
- UPDATE - pelna migracja front\factory\ do Domain (5 ostatnich klas: ShopProduct, ShopPaymentMethod, ShopPromotion, ShopStatuses, ShopTransport)
|
||||
- UPDATE - ProductRepository: ~20 nowych metod frontendowych (cache Redis, lazy loading, SKU/EAN fallback)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?
|
||||
$current_ver = 292;
|
||||
$current_ver = 293;
|
||||
|
||||
for ($i = 1; $i <= $current_ver; $i++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user