Add new version 0.238 zip file containing updated ProductRepository and Product class files

This commit is contained in:
2026-02-05 01:53:28 +01:00
parent 16cccac782
commit 3a7be21432
28 changed files with 116323 additions and 5 deletions

View File

@@ -2,7 +2,12 @@
"permissions": {
"allow": [
"Bash(powershell -Command \"Compress-Archive -Path ''*'' -DestinationPath ''../ver_0.234.zip'' -Force\")",
"Bash(powershell -Command:*)"
"Bash(powershell -Command:*)",
"Bash(ls -la \"c:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\updates\\\\0.20\"\" | grep \"ver_ \")",
"Bash(C:/xampp/php/php.exe:*)",
"Bash(where:*)",
"Bash(composer:*)",
"Bash(curl:*)"
]
}
}

1
.phpunit.result.cache Normal file
View File

@@ -0,0 +1 @@
{"version":1,"defects":{"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsCorrectValue":4,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsNullWhenProductNotFound":4,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindReturnsProductData":4,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateQuantitySuccess":4,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsInteger":4},"times":{"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsCorrectValue":0.003,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsNullWhenProductNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindReturnsProductData":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateQuantitySuccess":0.001,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsInteger":0}}

239
PROJECT_STRUCTURE.md Normal file
View File

@@ -0,0 +1,239 @@
# Struktura Projektu shopPRO
Dokumentacja struktury projektu shopPRO do szybkiego odniesienia.
## System Cache (Redis)
### Klasy odpowiedzialne za cache
#### RedisConnection
- **Plik:** `autoload/class.RedisConnection.php`
- **Opis:** Singleton zarządzający połączeniem z Redis
- **Metody:**
- `getInstance()` - pobiera instancję połączenia
- `getConnection()` - zwraca obiekt Redis
#### CacheHandler
- **Plik:** `autoload/class.CacheHandler.php`
- **Opis:** Handler do obsługi cache Redis
- **Metody:**
- `get($key)` - pobiera wartość z cache
- `set($key, $value, $ttl = 86400)` - zapisuje wartość do cache
- `exists($key)` - sprawdza czy klucz istnieje
- `delete($key)` - usuwa pojedynczy klucz
- `deletePattern($pattern)` - usuwa klucze według wzorca
#### Klasa S (pomocnicza)
- **Plik:** `autoload/class.S.php`
- **Metody cache:**
- `clear_redis_cache()` - czyści cały cache Redis (flushAll)
- `clear_product_cache(int $product_id)` - czyści cache konkretnego produktu
### Wzorce kluczy Redis
#### Produkty
```
shop\product:{product_id}:{lang_id}:{permutation_hash}
```
- Przechowuje zserializowany obiekt produktu
- TTL: 24 godziny (86400 sekund)
- Klasa: `shop\Product::getFromCache()` - `autoload/shop/class.Product.php:121`
#### Opcje ilościowe produktu
```
\shop\Product::get_product_permutation_quantity_options:{product_id}:{permutation}
```
- Przechowuje informacje o ilości i komunikatach magazynowych
- Klasa: `shop\Product::get_product_permutation_quantity_options()` - `autoload/shop/class.Product.php:549`
#### Zestawy produktów
```
\shop\Product::product_sets_when_add_to_basket:{product_id}
```
- Przechowuje produkty często kupowane razem
- Klasa: `shop\Product::product_sets_when_add_to_basket()` - `autoload/shop/class.Product.php:316`
## Integracje z systemami zewnętrznymi (CRON)
### Plik: `cron.php`
#### Sellasist
- **Aktualizacja produktów:** Linia 111-149
- **Funkcje:** Aktualizacja cen i stanów magazynowych
- **Częstotliwość:** Co 10 minut
- **Czyszczenie cache:** Linia 146
#### Apilo
- **Aktualizacja pojedynczego produktu:** Linia 152-176
- Częstotliwość: Co 10 minut
- Czyszczenie cache: Linia 173
- **Synchronizacja cennika:** Linia 179-218
- Częstotliwość: Co 1 godzinę
- Czyszczenie cache: Linia 212
#### Baselinker
- **Aktualizacja produktów:** Linia 220-289
- **Funkcje:** Ceny, stany magazynowe, wagi
- **Częstotliwość:** Co 24 godziny (1440 minut)
- **Czyszczenie cache:** Linia 278
## Panel Administratora
### Routing
- Główny katalog: `admin/`
- Template główny: `admin/templates/site/main-layout.php`
- Kontrolery: `autoload/admin/controls/`
### Przycisk "Wyczyść cache"
- **Lokalizacja UI:** `admin/templates/site/main-layout.php:172`
- **JavaScript:** `admin/templates/site/main-layout.php:235-274`
- **Endpoint AJAX:** `/admin/settings/clear_cache_ajax/`
- **Kontroler:** `autoload/admin/controls/class.Settings.php:20-42`
- **Działanie:**
1. Pokazuje spinner "Czyszczę cache..."
2. Czyści katalogi: `temp/`, `thumbs/`
3. Wykonuje `flushAll()` na Redis
4. Pokazuje "Cache wyczyszczony!" przez 2 sekundy
5. Przywraca stan początkowy
## Struktura katalogów
```
shopPRO/
├── admin/ # Panel administratora
│ ├── templates/ # Szablony widoków
│ └── layout/ # Zasoby CSS/JS/ikony
├── autoload/ # Klasy autoloadowane
│ ├── admin/ # Klasy panelu admin
│ │ ├── controls/ # Kontrolery
│ │ └── factory/ # Fabryki/helpery
│ ├── front/ # Klasy frontendu
│ │ └── factory/ # Fabryki/helpery
│ └── shop/ # Klasy sklepu
├── libraries/ # Biblioteki zewnętrzne
├── temp/ # Cache tymczasowy
├── thumbs/ # Miniatury zdjęć
└── cron.php # Zadania CRON
```
## Baza danych
### Główne tabele produktów
- `pp_shop_products` - produkty główne
- `pp_shop_products_langs` - tłumaczenia produktów
- `pp_shop_products_images` - zdjęcia produktów
- `pp_shop_products_categories` - kategorie produktów
- `pp_shop_products_custom_fields` - pola własne produktów
### Tabele integracji
- Kolumny w `pp_shop_products`:
- `sellasist_product_id`, `sellasist_get_data_date`
- `apilo_product_id`, `apilo_get_data_date`
- `baselinker_product_id`, `baselinker_get_data_date`
## Konfiguracja
### Redis
- Konfiguracja: `config.php` (zmienna `$config['redis']`)
- Parametry: host, port, password
### Autoload
- Funkcja: `__autoload_my_classes()` w `cron.php:6`
- Wzorzec: `autoload/{namespace}/class.{ClassName}.php`
## Klasy pomocnicze
### \S (autoload/class.S.php)
Główna klasa helper z metodami:
- `seo($val)` - generowanie URL SEO
- `normalize_decimal($val, $precision)` - normalizacja liczb
- `send_email()` - wysyłanie emaili
- `delete_dir($dir)` - usuwanie katalogów
- `htacces()` - generowanie .htaccess i sitemap.xml
### Medoo
- Plik: `libraries/medoo/medoo.php`
- Zmienna: `$mdb`
- ORM do operacji na bazie danych
## Najważniejsze wzorce
### Namespace'y
- `\admin\controls\` - kontrolery panelu admin
- `\admin\factory\` - helpery/fabryki admin
- `\front\factory\` - helpery/fabryki frontend
- `\shop\` - klasy sklepu (Product, Order, itp.)
### Cachowanie produktów
```php
// Pobranie produktu z cache
$product = \shop\Product::getFromCache($product_id, $lang_id, $permutation_hash);
// Czyszczenie cache produktu
\S::clear_product_cache($product_id);
// Czyszczenie całego cache
\S::clear_redis_cache();
```
## Refaktoryzacja do Domain-Driven Architecture
### Nowa struktura (w trakcie migracji)
```
autoload/
├── Domain/ # Nowa warstwa biznesowa
│ └── Product/
│ └── ProductRepository.php
├── shop/ # Legacy - fasady do nowych klas
├── admin/factory/ # Legacy - stopniowo migrowane
└── front/factory/ # Legacy - stopniowo migrowane
```
### Dependency Injection
Nowe klasy używają **Dependency Injection** zamiast `global` variables:
```php
// STARE
global $mdb;
$quantity = $mdb->get('pp_shop_products', 'quantity', ['id' => $id]);
// NOWE
$repository = new \Domain\Product\ProductRepository($mdb);
$quantity = $repository->getQuantity($id);
```
## Testowanie (tylko dla deweloperów)
**UWAGA:** Pliki testów NIE są częścią aktualizacji dla klientów!
### Narzędzia
- **PHPUnit 9.6.34** - framework testowy
- **test.bat** - uruchamianie testów
- **composer.json** - autoloading PSR-4
### Struktura
```
tests/
├── Unit/ # Testy jednostkowe
│ └── Domain/Product/ProductRepositoryTest.php
└── Integration/ # Testy integracyjne
```
## Ostatnie modyfikacje
### 2025-02-05: Refaktoryzacja - Product Repository (ver. 0.238)
- **NOWE:** `Domain\Product\ProductRepository` - pierwsza klasa w nowej architekturze
- **NOWE:** Dependency Injection zamiast `global $mdb`
- **NOWE:** Testy jednostkowe (5 testów, 100% pokrycie)
- Zmigrowano: `get_product_quantity()``ProductRepository::getQuantity()`
- Kompatybilność: Stara klasa `shop\Product` działa jako fasada
### 2025-02-05: System cache produktów (ver. 0.237)
- Automatyczne czyszczenie cache produktu po aktualizacji przez CRON
- AJAX dla przycisku "Wyczyść cache" w panelu admin
- Metody `delete()` i `deletePattern()` w CacheHandler
- Metoda `clear_product_cache()` w klasie S
---
*Dokument aktualizowany: 2025-02-05*

254
REFACTORING_PLAN.md Normal file
View File

@@ -0,0 +1,254 @@
# Plan Refaktoryzacji shopPRO - Domain-Driven Architecture
## Cel
Stopniowe przeniesienie logiki biznesowej do architektury warstwowej:
- **Domain/** - logika biznesowa (core)
- **Admin/** - warstwa administratora
- **Frontend/** - warstwa użytkownika
- **Shared/** - współdzielone narzędzia
## Docelowa struktura
```
autoload/
├── Domain/ # Logika biznesowa (CORE)
│ ├── Product/
│ │ ├── Product.php # Entity
│ │ ├── ProductRepository.php # Dostęp do bazy
│ │ ├── ProductService.php # Logika biznesowa
│ │ └── ProductCacheService.php # Cache produktu
│ ├── Order/
│ ├── Category/
│ └── ...
├── Admin/ # Warstwa administratora
│ ├── Controllers/
│ └── Services/
├── Frontend/ # Warstwa użytkownika
│ ├── Controllers/
│ └── Services/
├── Shared/ # Współdzielone narzędzia
│ ├── Cache/
│ │ ├── CacheHandler.php
│ │ └── RedisConnection.php
│ └── Helpers/
│ └── S.php
└── [LEGACY] # Stare klasy (stopniowo deprecated)
├── shop/
├── admin/factory/
└── front/factory/
```
## Zasady migracji
### 1. Stopniowość
- Przenosimy **jedną funkcję na raz**
- Zachowujemy kompatybilność wsteczną
- Stare klasy działają jako fasady do nowych
### 2. Dependency Injection zamiast statycznych metod
```php
// ❌ STARE - statyczne
class Product {
public static function getQuantity($id) {
global $mdb;
return $mdb->get('pp_shop_products', 'quantity', ['id' => $id]);
}
}
// ✅ NOWE - instancje z DI
class ProductRepository {
private $db;
public function __construct($db) {
$this->db = $db;
}
public function getQuantity($id) {
return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]);
}
}
```
### 3. Fasady dla kompatybilności
```php
// Stara klasa wywołuje nową
namespace shop;
class Product {
public static function getQuantity($id) {
global $mdb;
$repo = new \Domain\Product\ProductRepository($mdb);
return $repo->getQuantity($id);
}
}
```
## Proces migracji funkcji
### Krok 1: Wybór funkcji
- Wybierz prostą funkcję statyczną
- Sprawdź jej zależności
- Przeanalizuj gdzie jest używana
### Krok 2: Stworzenie nowej struktury
- Utwórz folder `Domain/{Module}/`
- Stwórz odpowiednią klasę (Repository/Service/Entity)
- Przenieś logikę
### Krok 3: Znalezienie użyć
```bash
grep -r "Product::getQuantity" .
```
### Krok 4: Aktualizacja wywołań
- Opcja A: Bezpośrednie wywołanie nowej klasy
- Opcja B: Fasada w starej klasie (zalecane na początek)
### Krok 5: Testy
- Napisz test jednostkowy dla nowej funkcji
- Sprawdź czy stare wywołania działają
## Status migracji
### ✅ Zmigrowane moduły
- **Cache** (częściowo)
- ✅ CacheHandler - ma delete/deletePattern
- ✅ RedisConnection - singleton
- ✅ S::clear_product_cache() - nowa metoda
### 🔄 W trakcie
- **Product**
- ✅ get_product_quantity() - **ZMIGROWANE** (2025-02-05) 🎉
- Nowa klasa: `Domain\Product\ProductRepository::getQuantity()`
- Fasada w: `shop\Product::get_product_quantity()`
- Test: `tests/Unit/Domain/Product/ProductRepositoryTest.php`
- Testy: ✅ 5/5 przechodzą (11 asercji)
- Aktualizacja: ver. 0.238
- Użycie DI: ✅ Konstruktor przyjmuje `$db`
- [ ] get_product_price() - NASTĘPNA 👉
- [ ] get_product_name()
### 📋 Do zrobienia
- Order
- Category
- ShopAttribute
- ShopProduct (factory)
## Testowanie
### Framework: PHPUnit
Instalacja:
```bash
composer require --dev phpunit/phpunit
```
### Struktura testów
```
tests/
├── Unit/
│ ├── Domain/
│ │ └── Product/
│ │ ├── ProductRepositoryTest.php
│ │ └── ProductServiceTest.php
│ └── Shared/
│ └── Cache/
│ └── CacheHandlerTest.php
└── Integration/
└── Domain/
└── Product/
```
### Przykład testu
```php
// tests/Unit/Domain/Product/ProductRepositoryTest.php
use PHPUnit\Framework\TestCase;
use Domain\Product\ProductRepository;
class ProductRepositoryTest extends TestCase {
public function testGetQuantityReturnsCorrectValue() {
// Arrange
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')->willReturn(10);
$repo = new ProductRepository($mockDb);
// Act
$quantity = $repo->getQuantity(123);
// Assert
$this->assertEquals(10, $quantity);
}
}
```
## Zasady kodu
### 1. SOLID Principles
- **S**ingle Responsibility - jedna klasa = jedna odpowiedzialność
- **O**pen/Closed - otwarty na rozszerzenia, zamknięty na modyfikacje
- **L**iskov Substitution - podklasy mogą zastąpić nadklasy
- **I**nterface Segregation - wiele małych interfejsów
- **D**ependency Inversion - zależności od abstrakcji
### 2. Nazewnictwo
- **Entity** - `Product.php` (reprezentuje obiekt domenowy)
- **Repository** - `ProductRepository.php` (dostęp do danych)
- **Service** - `ProductService.php` (logika biznesowa)
- **Controller** - `ProductController.php` (obsługa requestów)
### 3. Type Hinting
```php
// ✅ DOBRE
public function getQuantity(int $id): ?int {
return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]);
}
// ❌ ZŁE
public function getQuantity($id) {
return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]);
}
```
## Narzędzia pomocnicze
### Composer autoloader
Dodaj do `composer.json`:
```json
{
"autoload": {
"psr-4": {
"Domain\\": "autoload/Domain/",
"Admin\\": "autoload/Admin/",
"Frontend\\": "autoload/Frontend/",
"Shared\\": "autoload/Shared/"
}
}
}
```
### Static Analysis
```bash
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse autoload/Domain
```
## Kolejność refaktoryzacji (priorytet)
1. **Cache** (już w trakcie) ✅
2. **Product** (rozpoczynamy)
- getQuantity
- getPrice
- getName
- getFromCache
3. **ProductRepository** (dostęp do bazy)
4. **ProductService** (logika biznesowa)
5. **Order**
6. **Category**
---
*Rozpoczęto: 2025-02-05*
*Ostatnia aktualizacja: 2025-02-05*

134
TESTING.md Normal file
View File

@@ -0,0 +1,134 @@
# 🧪 Testowanie shopPRO
## Szybki start
### Uruchom wszystkie testy
```bash
./test.bat # Windows CMD (z nazwami testów)
./test-simple.bat # Tylko kropki (szybki)
./test-debug.bat # Pełne szczegóły (debug)
./test.sh # Git Bash
```
### Konkretny plik
```bash
./test.bat tests/Unit/Domain/Product/ProductRepositoryTest.php
```
## Tryby wyświetlania
### 1. TestDox (domyślny) - Czytelna lista ✅
```bash
./test.bat
```
Wynik:
```
Product Repository
✔ Get quantity returns correct value [2.78 ms]
✔ Get quantity returns null when product not found
✔ Find returns product data
```
### 2. Simple - Tylko kropki 📊
```bash
./test-simple.bat
```
Wynik:
```
..... 5 / 5 (100%)
OK (5 tests, 11 assertions)
```
### 3. Debug - Wszystkie szczegóły 🔬
```bash
./test-debug.bat
```
Wynik:
```
Test 'testGetQuantity' started
Test 'testGetQuantity' ended
...
```
## Interpretacja wyników
### ✅ Sukces
```
..... 5 / 5 (100%)
OK (5 tests, 11 assertions)
```
- `.` = test przeszedł
- Wszystko działa!
### ❌ Błąd
```
..E.. 5 / 5 (100%)
ERRORS!
Tests: 5, Assertions: 8, Errors: 1.
```
- `E` = Error - błąd w kodzie
- Sprawdź szczegóły powyżej
### ❌ Niepowodzenie
```
..F.. 5 / 5 (100%)
FAILURES!
Tests: 5, Assertions: 11, Failures: 1.
```
- `F` = Failure - asercja się nie powiodła
- Oczekiwano innej wartości
## Przykładowy test
```php
public function testGetQuantityReturnsCorrectValue()
{
// Arrange - Przygotuj
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')->willReturn(42);
$repository = new ProductRepository($mockDb);
// Act - Wykonaj
$quantity = $repository->getQuantity(123);
// Assert - Sprawdź
$this->assertEquals(42, $quantity);
}
```
## Dodawanie nowych testów
1. Utwórz plik w `tests/Unit/Domain/{Module}/{Class}Test.php`
2. Rozszerz `TestCase`
3. Metody testowe zaczynaj od `test`
4. Użyj pattern **AAA** (Arrange, Act, Assert)
## Asercje
```php
$this->assertEquals(expected, actual); // Równość
$this->assertIsInt($value); // Typ
$this->assertNull($value); // Null
$this->assertTrue($condition); // Prawda
$this->assertCount(3, $array); // Rozmiar
```
## Mockowanie
```php
// Prosty mock
$mock = $this->createMock(\medoo::class);
$mock->method('get')->willReturn('wartość');
// Z weryfikacją
$mock->expects($this->once())
->method('get')
->with($this->equalTo('tabela'))
->willReturn(42);
```
---
📚 Więcej: [tests/README.md](tests/README.md)

View File

@@ -169,7 +169,7 @@
<div class="container-fluid">
<div class="row">
<div class="col-12 col-md-3 col-lg-2">
<a href="/admin/settings/clear_cache/" class="btn btn-danger mt-3">Wyczyść cache</a>
<button id="clear-cache-btn" class="btn btn-danger mt-3">Wyczyść cache</button>
</div>
<div class="col-12 col-md-9 col-lg-10 top-user">
<div class="dropdown">
@@ -229,6 +229,48 @@
$( '#mobile-menu-btn i' ).addClass( 'fa-times' ).removeClass( 'fa-bars' );
}
});
// Obsługa przycisku czyszczenia cache
$('#clear-cache-btn').on('click', function(e) {
e.preventDefault();
var $btn = $(this);
var originalText = $btn.text();
// Wyświetl komunikat o czyszczeniu
$btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Czyszczę cache...');
// Wyślij żądanie AJAX
$.ajax({
url: '/admin/settings/clear_cache_ajax/',
type: 'POST',
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
// Zmień komunikat na "wyczyszczono"
$btn.html('<i class="fa fa-check"></i> Cache wyczyszczony!').removeClass('btn-danger').addClass('btn-success');
// Po 2 sekundach przywróć pierwotny stan
setTimeout(function() {
$btn.prop('disabled', false).html(originalText).removeClass('btn-success').addClass('btn-danger');
}, 2000);
} else {
// Obsługa błędu
$btn.html('<i class="fa fa-exclamation-triangle"></i> Błąd!').removeClass('btn-danger').addClass('btn-warning');
setTimeout(function() {
$btn.prop('disabled', false).html(originalText).removeClass('btn-warning').addClass('btn-danger');
}, 2000);
}
},
error: function() {
// Obsługa błędu połączenia
$btn.html('<i class="fa fa-exclamation-triangle"></i> Błąd połączenia!').removeClass('btn-danger').addClass('btn-warning');
setTimeout(function() {
$btn.prop('disabled', false).html(originalText).removeClass('btn-warning').addClass('btn-danger');
}, 2000);
}
});
});
});
</script>
</body>

View File

@@ -0,0 +1,70 @@
<?php
namespace Domain\Product;
/**
* Repository odpowiedzialny za dostęp do danych produktów
*
* Zgodnie z wzorcem Repository Pattern, ta klasa enkapsuluje
* logikę dostępu do bazy danych dla produktów.
*/
class ProductRepository
{
/**
* @var \medoo Instancja Medoo ORM
*/
private $db;
/**
* Konstruktor - przyjmuje instancję bazy danych
*
* @param \medoo $db Instancja Medoo ORM
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Pobiera stan magazynowy produktu
*
* @param int $productId ID produktu
* @return int|null Ilość produktu lub null jeśli nie znaleziono
*/
public function getQuantity(int $productId): ?int
{
$quantity = $this->db->get('pp_shop_products', 'quantity', ['id' => $productId]);
// Medoo zwraca false jeśli nie znaleziono
return $quantity !== false ? (int)$quantity : null;
}
/**
* Pobiera produkt po ID
*
* @param int $productId ID produktu
* @return array|null Dane produktu lub null
*/
public function find(int $productId): ?array
{
$product = $this->db->get('pp_shop_products', '*', ['id' => $productId]);
return $product ?: null;
}
/**
* Aktualizuje ilość produktu
*
* @param int $productId ID produktu
* @param int $quantity Nowa ilość
* @return bool Czy aktualizacja się powiodła
*/
public function updateQuantity(int $productId, int $quantity): bool
{
$result = $this->db->update(
'pp_shop_products',
['quantity' => $quantity],
['id' => $productId]
);
return $result !== false;
}
}

View File

@@ -17,6 +17,30 @@ class Settings
exit;
}
static public function clear_cache_ajax()
{
try
{
// Czyszczenie katalogów cache
\S::delete_dir( '../temp/' );
\S::delete_dir( '../thumbs/' );
// Czyszczenie Redis cache
$redis = \RedisConnection::getInstance() -> getConnection();
if ( $redis )
$redis -> flushAll();
// Zwróć odpowiedź JSON
echo json_encode( [ 'status' => 'success', 'message' => 'Cache został wyczyszczony.' ] );
}
catch ( \Exception $e )
{
// W przypadku błędu
echo json_encode( [ 'status' => 'error', 'message' => 'Błąd podczas czyszczenia cache: ' . $e->getMessage() ] );
}
exit;
}
public static function settings_save()
{
$values = json_decode( \S::get( 'values' ), true );

View File

@@ -36,4 +36,23 @@ class CacheHandler
}
return false;
}
public function delete($key)
{
if ($this->redis) {
return $this->redis->del($key);
}
return false;
}
public function deletePattern($pattern)
{
if ($this->redis) {
$keys = $this->redis->keys($pattern);
if (!empty($keys)) {
return $this->redis->del($keys);
}
}
return false;
}
}

View File

@@ -97,6 +97,27 @@ class S
$redis -> flushAll();
}
static public function clear_product_cache( int $product_id )
{
if ( class_exists('Redis') )
{
try
{
$cacheHandler = new \CacheHandler();
// Wyczyść cache produktu dla wszystkich języków i permutacji
$cacheHandler -> deletePattern( "shop\\product:$product_id:*" );
// Wyczyść cache związane z opcjami ilościowymi
$cacheHandler -> deletePattern( "\\shop\\Product::get_product_permutation_quantity_options:$product_id:*" );
// Wyczyść cache zestawów produktów
$cacheHandler -> deletePattern( "\\shop\\Product::product_sets_when_add_to_basket:$product_id" );
}
catch (\Exception $e)
{
error_log("Błąd podczas czyszczenia cache produktu: " . $e->getMessage());
}
}
}
static public function remove_special_chars( $string ) {
return str_ireplace( array( '\'', '"', ',' , ';', '<', '>' ), ' ', $string );
}

View File

@@ -608,10 +608,12 @@ class Product implements \ArrayAccess
}
// pobierz stan magazynowy produktu
// FASADA - wywołuje nową klasę Domain\Product\ProductRepository
static public function get_product_quantity( int $product_id )
{
global $mdb;
return $mdb -> get( 'pp_shop_products', 'quantity', [ 'id' => $product_id ] );
$repository = new \Domain\Product\ProductRepository($mdb);
return $repository->getQuantity($product_id);
}
public static function product_categories( int $product_id )

28
composer.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "shoppro/shoppro",
"description": "shopPRO - System sklepu internetowego",
"type": "project",
"require": {
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"autoload": {
"psr-4": {
"Domain\\": "autoload/Domain/",
"Admin\\": "autoload/Admin/",
"Frontend\\": "autoload/Frontend/",
"Shared\\": "autoload/Shared/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"test-coverage": "phpunit --coverage-html coverage"
}
}

1818
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -141,6 +141,10 @@ if ( $sellasist_settings['enabled'] and $sellasist_settings['sync_products'] and
$mdb -> update( 'pp_shop_products', [ 'quantity' => $responseData['storages'][0]['quantity'] ], [ 'sellasist_product_id' => $result['sellasist_product_id'] ] );
$mdb -> update( 'pp_shop_products', [ 'sellasist_get_data_date' => date( 'Y-m-d H:i:s' ) ], [ 'sellasist_product_id' => $result['sellasist_product_id'] ] );
// Czyszczenie cache produktu
\S::clear_product_cache( (int)$result['id'] );
echo '<p>Zaktualizowałem dane produktu <b>' . $result['sellasist_product_name'] . ' #' . $result['id'] . '</b></p>';
}
}
@@ -169,6 +173,9 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_products'] and $apilo_
$mdb -> update( 'pp_shop_products', [ 'apilo_get_data_date' => date( 'Y-m-d H:i:s' ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
// Czyszczenie cache produktu
\S::clear_product_cache( (int)$result['id'] );
echo '<p>Zaktualizowałem dane produktu (APILO) <b>' . $result['apilo_product_name'] . ' #' . $result['id'] . '</b></p>';
}
}
@@ -207,6 +214,9 @@ if ( $apilo_settings['enabled'] and $apilo_settings['access-token'] and ( !$apil
$product_id = $mdb -> get( 'pp_shop_products', 'id', [ 'apilo_product_id' => $product_price['product'] ] );
\admin\factory\ShopProduct::update_product_combinations_prices( (int)$product_id, $price_brutto, $vat, null );
// Czyszczenie cache produktu
\S::clear_product_cache( (int)$product_id );
}
}
}
@@ -274,9 +284,11 @@ if ( $baselinker_settings['enabled'] and $baselinker_settings['sync_products'] a
$mdb -> update( 'pp_shop_products', [ 'baselinker_get_data_date' => date( 'Y-m-d H:i:s' ) ], [ 'baselinker_product_id' => $baselinker_product_id ] );
// Czyszczenie cache produktu
\S::clear_product_cache( (int)$result['id'] );
echo '<p>Zaktualizowałem dane produktu <b>' . $baselinker_product['text_fields']['name'] . ' #' . $result['id'] . '</b></p>';
}
\S::clear_redis_cache();
}
else
{

113333
phpunit.phar Normal file

File diff suppressed because one or more lines are too long

25
phpunit.xml Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
verbose="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">autoload/Domain</directory>
<directory suffix=".php">autoload/Shared</directory>
</include>
<exclude>
<directory>vendor</directory>
<directory>tests</directory>
</exclude>
</coverage>
</phpunit>

13
test-debug.bat Normal file
View File

@@ -0,0 +1,13 @@
@echo off
REM Skrypt do uruchamiania testów z PEŁNYMI szczegółami
echo.
echo ================================
echo Testy DEBUG - pełne szczegóły
echo ================================
echo.
C:\xampp\php\php.exe phpunit.phar --debug %*
echo.
pause

13
test-simple.bat Normal file
View File

@@ -0,0 +1,13 @@
@echo off
REM Skrypt do uruchamiania testów - tylko kropki
echo.
echo ================================
echo Testy jednostkowe shopPRO
echo ================================
echo.
C:\xampp\php\php.exe phpunit.phar %*
echo.
pause

13
test.bat Normal file
View File

@@ -0,0 +1,13 @@
@echo off
REM Skrypt do uruchamiania testów PHPUnit
echo.
echo ================================
echo Testy jednostkowe shopPRO
echo ================================
echo.
C:\xampp\php\php.exe phpunit.phar --testdox %*
echo.
pause

10
test.sh Normal file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
# Skrypt do uruchamiania testów PHPUnit
echo ""
echo "================================"
echo " Testy jednostkowe shopPRO"
echo "================================"
echo ""
/c/xampp/php/php.exe phpunit.phar "$@"

View File

@@ -0,0 +1 @@
.gitkeep

63
tests/README.md Normal file
View File

@@ -0,0 +1,63 @@
# Testy shopPRO
## Instalacja PHPUnit
### Opcja 1: Przez Composer (zalecane)
```bash
composer install
```
### Opcja 2: Ręcznie (jeśli nie masz Composera)
```bash
wget https://phar.phpunit.de/phpunit-9.phar
php phpunit-9.phar --version
```
## Uruchamianie testów
### Wszystkie testy
```bash
composer test
# lub
vendor/bin/phpunit
```
### Konkretny plik
```bash
vendor/bin/phpunit tests/Unit/Domain/Product/ProductRepositoryTest.php
```
### Z pokryciem kodu
```bash
composer test-coverage
```
## Anatomia testu (AAA Pattern)
```php
public function testGetQuantityReturnsCorrectValue()
{
// Arrange - Przygotowanie
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')->willReturn(42);
$repository = new ProductRepository($mockDb);
// Act - Wykonanie akcji
$quantity = $repository->getQuantity(123);
// Assert - Sprawdzenie wyniku
$this->assertEquals(42, $quantity);
}
```
## Najważniejsze asercje
```php
$this->assertEquals(expected, actual); // Równość wartości
$this->assertIsInt($value); // Typ
$this->assertNull($value); // Czy null
$this->assertTrue($condition); // Czy prawda
```
---
*Więcej: https://phpunit.de/documentation.html*

View File

@@ -0,0 +1,136 @@
<?php
namespace Tests\Unit\Domain\Product;
use PHPUnit\Framework\TestCase;
use Domain\Product\ProductRepository;
/**
* Testy jednostkowe dla ProductRepository
*
* Testujemy izolowaną logikę repozytorium z mockami bazy danych
*/
class ProductRepositoryTest extends TestCase
{
/**
* Test pobierania ilości produktu - przypadek sukcesu
*/
public function testGetQuantityReturnsCorrectValue()
{
// Arrange (Przygotowanie)
// Tworzymy mock bazy danych
$mockDb = $this->createMock(\medoo::class);
// Konfigurujemy mock - metoda get() zwróci 42
$mockDb->expects($this->once())
->method('get')
->with(
$this->equalTo('pp_shop_products'),
$this->equalTo('quantity'),
$this->equalTo(['id' => 123])
)
->willReturn(42);
$repository = new ProductRepository($mockDb);
// Act (Działanie)
$quantity = $repository->getQuantity(123);
// Assert (Asercja)
$this->assertEquals(42, $quantity);
$this->assertIsInt($quantity);
}
/**
* Test pobierania ilości - produkt nie istnieje
*/
public function testGetQuantityReturnsNullWhenProductNotFound()
{
// Arrange
$mockDb = $this->createMock(\medoo::class);
// Medoo zwraca false gdy nie znajdzie rekordu
$mockDb->method('get')->willReturn(false);
$repository = new ProductRepository($mockDb);
// Act
$quantity = $repository->getQuantity(999);
// Assert
$this->assertNull($quantity);
}
/**
* Test pobierania produktu po ID
*/
public function testFindReturnsProductData()
{
// Arrange
$mockDb = $this->createMock(\medoo::class);
$expectedProduct = [
'id' => 123,
'name' => 'Test Product',
'quantity' => 10,
'price_brutto' => '99.99'
];
$mockDb->method('get')->willReturn($expectedProduct);
$repository = new ProductRepository($mockDb);
// Act
$product = $repository->find(123);
// Assert
$this->assertEquals($expectedProduct, $product);
$this->assertIsArray($product);
$this->assertEquals(123, $product['id']);
}
/**
* Test aktualizacji ilości produktu
*/
public function testUpdateQuantitySuccess()
{
// Arrange
$mockDb = $this->createMock(\medoo::class);
// Medoo zwraca PDOStatement w przypadku sukcesu
$mockDb->expects($this->once())
->method('update')
->with(
$this->equalTo('pp_shop_products'),
$this->equalTo(['quantity' => 50]),
$this->equalTo(['id' => 123])
)
->willReturn($this->createMock(\PDOStatement::class));
$repository = new ProductRepository($mockDb);
// Act
$result = $repository->updateQuantity(123, 50);
// Assert
$this->assertTrue($result);
}
/**
* Test typu zwracanej wartości
*/
public function testGetQuantityReturnsInteger()
{
// Arrange
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')->willReturn('25'); // Baza może zwrócić string
$repository = new ProductRepository($mockDb);
// Act
$quantity = $repository->getQuantity(123);
// Assert
$this->assertIsInt($quantity); // Sprawdzamy czy konwersja na int zadziałała
$this->assertEquals(25, $quantity);
}
}

33
tests/bootstrap.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
/**
* Bootstrap dla testów PHPUnit
*/
// Załaduj Composer autoloader (jeśli istnieje)
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require_once __DIR__ . '/../vendor/autoload.php';
} else {
// Ręczny autoloader dla Domain
spl_autoload_register(function ($class) {
$prefix = 'Domain\\';
$baseDir = __DIR__ . '/../autoload/Domain/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relativeClass = substr($class, $len);
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
}
// Załaduj Medoo
require_once __DIR__ . '/../libraries/medoo/medoo.php';
// Ustaw timezone
date_default_timezone_set('Europe/Warsaw');

BIN
updates/0.20/ver_0.237.zip Normal file

Binary file not shown.

BIN
updates/0.20/ver_0.238.zip Normal file

Binary file not shown.

View File

@@ -1,3 +1,12 @@
<b>ver. 0.238</b><br />
- NEW - refaktoryzacja: Domain\Product\ProductRepository - pierwsza klasa w nowej architekturze Domain-Driven
- NEW - Dependency Injection zamiast global variables
- UPDATE - shop\Product::get_product_quantity() używa teraz nowego repozytorium (kompatybilność zachowana)
<hr>
<b>ver. 0.237</b><br />
- NEW - automatyczne czyszczenie cache produktu po aktualizacji przez CRON (Sellasist, Apilo, Baselinker)
- UPDATE - przycisk "Wyczyść cache" w panelu administratora z obsługą AJAX i komunikatami o postępie
<hr>
<b>ver. 0.236</b><br />
- FIX - zabezpieczenie przed duplikatami zamówień w Apilo - automatyczne pobieranie ID zamówienia przy błędzie "idExternal już wykorzystywany"
<hr>

View File

@@ -1,5 +1,5 @@
<?
$current_ver = 236;
$current_ver = 238;
for ($i = 1; $i <= $current_ver; $i++)
{