fix: linki produktow z permutacja atrybutow w feedzie Google (v0.350)

Separator URL miedzy parami attr-val zmieniony z "/" na "_" w generatorze
feedu (ProductRepository::appendCombinationToXml). Wzorzec routingu
pp_routes rozszerzony do [0-9_-]+ w Helpers::htacces (oba warianty:
seo_link i fallback p-id-name). LayoutEngine konwertuje "_" -> "|"
przed wywolaniem ProductRepository::findCached — format DB pozostaje "|".
Partial product-attribute.php preselectuje wartosc z permutation_hash
URL (forced_value_id), co poprawia UX wejscia z linka feedu.

Suita: 834 -> 841 testow (+7), 2330 assertions.

Wymagane akcje na produkcji po deployu: regeneracja pp_routes
(Helpers::htacces), wyczyszczenie klucza pp_routes:all w Redis,
regeneracja google-feed.xml, resubmit feedu w GMC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 01:58:29 +02:00
parent 0de47f4e62
commit fba215b372
15 changed files with 765 additions and 29 deletions

View File

@@ -14,7 +14,7 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online -
|-----------|-------|
| Version | 0.333 |
| Status | Production |
| Last Updated | 2026-04-20 |
| Last Updated | 2026-04-30 |
## Requirements
@@ -32,6 +32,7 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online -
- [x] Domain-Driven Architecture (migracja z legacy zakończona)
- [x] Szybka edycja custom_label_0..4 na liscie produktow admina (toggle sesyjny + autocomplete)
- [x] Poprawna kalkulacja kosztu transportu na /koszyk-podsumowanie (fix delivery_free bez uwzglednienia progu)
- [x] Linki produktów z permutacją w feedzie Google działają (separator `_` w URL, konwersja `_``|` w warstwie front, regex `[0-9_-]+` w pp_routes)
### Active (In Progress)
@@ -83,12 +84,13 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online -
| `id` w tabbed FormEdit przez `hiddenFields` | Zapobiega insert zamiast update przy edycji encji | 2026-04-18 | Active |
| Inline custom labels w product list przez sesyjny toggle | Szybszy workflow dla Google XML bez wejscia w edycje produktu | 2026-04-19 | Active |
| Kalkulacja kosztu transportu na /koszyk-podsumowanie w kontrolerze (nie w szablonie) | Spojnosc logiki progu darmowej dostawy miedzy /koszyk i /koszyk-podsumowanie | 2026-04-20 | Active |
| Separator URL permutacji `_` zamiast `/` (DB pozostaje `|`) | Jeden segment URL dopasowywalny przez pp_routes; konwersja `_``|` w warstwie front | 2026-04-30 | Active |
## Success Metrics
| Metric | Target | Current | Status |
|--------|--------|---------|--------|
| Testy | >800 | 834 | On track |
| Testy | >800 | 841 | On track |
| Pokrycie architektury DDD | 100% | 100% | Achieved |
## Tech Stack
@@ -117,4 +119,4 @@ Quick Reference:
---
*PROJECT.md - Updated when requirements or context change*
*Last updated: 2026-04-20 after Phase 17*
*Last updated: 2026-04-30 after Phase 18*

View File

@@ -38,6 +38,7 @@ Status: Planning
| 9 | Apilo email notification + infinite retry | 1 | Done | 2026-03-19 |
| 15 | Scontainers edit saves as new record | 1 | Done | 2026-04-18 |
| 17 | Cart summary transport cost fix | 1 | Done | 2026-04-20 |
| 18 | Google feed permutation URL fix | 1 | Done | 2026-04-30 |
## Feature
@@ -125,5 +126,11 @@ Status: Planning
**Scope:** Przekazac z `ShopBasketController::summaryView()` do szablonu wyliczony `transport_cost_effective` i flage `free_delivery_applies` uwzgledniajaca prog. Zaktualizowac summary-view.php aby uzywal tych kluczy zamiast surowej flagi `delivery_free`. Test jednostkowy dla logiki wyliczenia.
### Phase 18 — Google feed permutation URL fix
**Problem:** URL produktu z permutacją atrybutów w feedzie Google miał format `/slug/20-170/21-175` (slash między parami). Wzorzec routingu `pp_routes` używa `[0-9-]+`, który nie obejmuje `/`, więc URL nie matchuje żadnej trasy i `index.php` ładuje stronę główną. Klienci z GMC trafiają na home zamiast na produkt z wybraną kombinacją.
**Scope:** Zmienić separator z `/` na `_` w generatorze feedu (`ProductRepository::appendCombinationToXml`), rozszerzyć regex routingu o `_` (`Helpers`), dodać konwersję `_``|` w warstwie front (`LayoutEngine`), preselekcja wartości atrybutu w partialu na podstawie `permutation_hash` z URL. Plus unit testy regex + generator linku.
---
*Last updated: 2026-04-20 (Phase 17 complete)*
*Last updated: 2026-04-30 (Phase 18 complete)*

View File

@@ -2,22 +2,23 @@
## Project Reference
See: .paul/PROJECT.md (updated 2026-04-18)
See: .paul/PROJECT.md (updated 2026-04-30)
**Core value:** Właściciel sklepu ma pełną kontrolę nad sprzedażą online w jednym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
**Current focus:** Phase 17 complete - loop closed
**Current focus:** Phase 18 complete - loop closed
## Current Position
Milestone: Hotfix
Phase: 17 of 17 (Cart summary transport cost fix) - Complete
Plan: 17-01 complete
Status: UNIFY complete, ready for next PLAN loop (transition-phase pending)
Last activity: 2026-04-20 - Closed loop for .paul/phases/17-cart-summary-transport-cost-fix/17-01-PLAN.md
Milestone: Hotfix
Phase: 18 of 18 (Google feed permutation URL fix) - Complete
Plan: 18-01 complete
Status: UNIFY complete, ready for next PLAN loop (transition-phase git commit pending)
Last activity: 2026-04-30 - Closed loop for .paul/phases/18-google-feed-permutation-url-fix/18-01-PLAN.md
Progress:
- Milestone: [##########] 100%
- Phase 17: [##########] 100%
- Milestone: [##########] 100% (Hotfix rolling)
- Phase 18: [##########] 100%
## Loop Position
@@ -43,10 +44,17 @@ Phase 14: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-16]
Phase 15: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-18]
Phase 16: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-19]
Phase 17: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-20]
Phase 18: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-30]
```
## Accumulated Context
### Decisions
- 2026-04-30: Phase 18 loop closed with SUMMARY at .paul/phases/18-google-feed-permutation-url-fix/18-01-SUMMARY.md
- 2026-04-30: Transition-phase git commit for Phase 18 not executed in this UNIFY run (deferred — pattern z faz 15/16/17)
- 2026-04-30: Phase 18 APPLY complete — 4 pliki silnika + 2 nowe pliki testów (HelpersRoutingTest 4 testy, ProductFeedLinkTest 3 testy); suita 841 zielona
- 2026-04-30: Created Phase 18 plan at .paul/phases/18-google-feed-permutation-url-fix/18-01-PLAN.md
- 2026-04-30: Phase 18 — separator URL permutacji `/``_`; konwersja `_``|` w warstwie front; regex `[0-9_-]+` w pp_routes
- 2026-04-30: Phase 18 — override /feature-dev (hotfix z konkretną instrukcją), brak redirectów 301, brak automatycznych akcji post-deploy
- 2026-04-20: Phase 17 loop closed with SUMMARY at .paul/phases/17-cart-summary-transport-cost-fix/17-01-SUMMARY.md
- 2026-04-20: Transition-phase git commit for Phase 17 not executed in this UNIFY run (deferred)
- 2026-04-20: Phase 17 APPLY complete - human-verify checkpoint approved, 834 testow zielonych (6 nowych)
@@ -89,17 +97,17 @@ None.
### Blockers/Concerns
None.
### Skill Audit (Phase 16)
### Skill Audit (Phase 18)
| Expected | Invoked | Notes |
|----------|---------|-------|
| /feature-dev | ○ | User-approved override during APPLY |
| /koniec-pracy | ○ | Marked as available by user; execute on session close workflow |
| /feature-dev | ○ | User-approved override (hotfix z konkretną instrukcją) |
| /koniec-pracy | ○ | Pending — uruchomić przy zakończeniu sesji jeśli release wchodzi do update package |
## Session Continuity
Last session: 2026-04-20
Stopped at: Phase 17 complete, loop closed
Next action: Start next milestone or create next phase plan (transition-phase commit pending)
Resume file: .paul/phases/17-cart-summary-transport-cost-fix/17-01-SUMMARY.md
Last session: 2026-04-30
Stopped at: Phase 18 complete, loop closed
Next action: Start next phase plan (transition-phase git commit pending), lub uruchomić /koniec-pracy jeśli zamykamy sesję
Resume file: .paul/phases/18-google-feed-permutation-url-fix/18-01-SUMMARY.md
---
*STATE.md — Updated after every significant action*

View File

@@ -0,0 +1,25 @@
# 2026-04-30
## Co zrobiono
- [Phase 18, Plan 01] Fix linków produktów z permutacją atrybutów w feedzie Google
- Separator URL między parami `attr-val` zmieniony z `/` na `_` w `ProductRepository::appendCombinationToXml`
- Wzorzec routingu `pp_routes` rozszerzony o `_` (`[0-9-]+``[0-9_-]+`) w `Helpers::htacces`
- Konwersja `_``|` w `LayoutEngine` przed wywołaniem `ProductRepository::findCached`
- Preselekcja wartości atrybutu na podstawie `permutation_hash` z URL w partialu `product-attribute.php`
- 2 nowe pliki testów: `HelpersRoutingTest` (4 testy) + `ProductFeedLinkTest` (3 testy via Reflection)
- Suita PHPUnit: 834 → 841 zielonych
## Zmienione pliki
- `autoload/Domain/Product/ProductRepository.php`
- `autoload/Shared/Helpers/Helpers.php`
- `autoload/front/LayoutEngine.php`
- `templates/shop-product/_partial/product-attribute.php`
- `tests/Unit/Shared/Helpers/HelpersRoutingTest.php`
- `tests/Unit/Domain/Product/ProductFeedLinkTest.php`
- `.paul/STATE.md`
- `.paul/PROJECT.md`
- `.paul/ROADMAP.md`
- `.paul/phases/18-google-feed-permutation-url-fix/18-01-PLAN.md`
- `.paul/phases/18-google-feed-permutation-url-fix/18-01-SUMMARY.md`

View File

@@ -4,8 +4,8 @@
| Metric | Value |
|--------|-------|
| Total tests | **828** |
| Total assertions | **2306** |
| Total tests | **841** |
| Total assertions | **2330** |
| Framework | PHPUnit 9.6 (`phpunit.phar`) |
| Bootstrap | `tests/bootstrap.php` |
| Config | `phpunit.xml` |

View File

@@ -2,6 +2,16 @@
> Chronologiczny log zmian technicznych — co i dlaczego.
## v0.350 (2026-04-30)
- Naprawiono linki produktow z permutacja atrybutow w feedzie Google: separator par `attr-val` w URL zmieniony z `/` na `_`. Stary format `/slug/20-170/21-175` nie matchowal sie w `pp_routes` (regex `[0-9-]+` nie obejmuje `/`), wiec klienci z GMC ladowali na strone glowna zamiast na produkt.
- `ProductRepository::appendCombinationToXml`: `str_replace('|', '/', ...)` -> `str_replace('|', '_', ...)` w obu galeziach (z `seo_link` i fallback `p-id-name`).
- `Helpers::htacces`: regex routingu produktow z permutacja rozszerzony do `/([0-9_-]+)$` w obu wariantach.
- `LayoutEngine.php` (// PRODUKT): konwersja `_` -> `|` przed wywolaniem `ProductRepository::findCached` — format DB pozostaje bez zmian (`attr-val|attr-val`).
- `templates/shop-product/_partial/product-attribute.php`: preselekcja wartosci atrybutu na podstawie `permutation_hash` z URL (`$forced_value_id`); wartosc `is_default` uzywana tylko gdy URL nie wymusza wyboru. Dotyczy `checked` na inpucie i emisji bloku `fradio_label_click(...)`.
- Dodano 7 testow jednostkowych: `HelpersRoutingTest` (4 testy regex + assercje na zawartosci pliku) i `ProductFeedLinkTest` (3 testy `appendCombinationToXml` via `ReflectionMethod` z mockiem `TransportRepository`). Suita: 841 testow / 2330 assertions.
- Wymagane akcje na produkcji po deployu: regeneracja `pp_routes` (`Helpers::htacces()`), wyczyszczenie klucza `pp_routes:all` w Redis, regeneracja `google-feed.xml`, resubmit feedu w GMC.
## v0.349 (2026-04-20)
- Naprawiono wyswietlanie kosztu transportu na /koszyk-podsumowanie: transporty z `delivery_free=1` pokazuja teraz rzeczywisty koszt ponizej progu `settings.free_delivery`, a 0,00 zl dopiero po osiagnieciu progu (spojnie z lista na /koszyk).

View File

@@ -3688,3 +3688,6 @@ Dodać możliwość ustawienia limitu znaków w wiadomościach do produktu
## SonarQube - v0.349 - brak nowych issues
## SonarQube - v0.350 - brak nowych issues

View File

@@ -0,0 +1,258 @@
---
phase: 18-google-feed-permutation-url-fix
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/Domain/Product/ProductRepository.php
- autoload/Shared/Helpers/Helpers.php
- autoload/front/LayoutEngine.php
- templates/shop-product/_partial/product-attribute.php
- tests/Unit/Domain/Product/ProductRepositoryTest.php
- tests/Unit/Shared/Helpers/HelpersTest.php
autonomous: true
delegation: off
---
<objective>
## Goal
Naprawić linki produktów z permutacją atrybutów w feedzie Google: zamienić separator `/` na `_` między parami `attr-val`, dopasować routing `pp_routes`, konwersję `_``|` w warstwie front oraz preselekcję wartości atrybutów na podstawie `permutation_hash` z URL.
## Purpose
URL z formatu `/slug/20-170/21-175` nie matchował się w `pp_routes` (wzorzec `[0-9-]+` nie obejmuje `/`), więc Google Merchant Center prowadził klientów na stronę główną zamiast na produkt z wybraną kombinacją atrybutów. Strata ruchu komercyjnego z feedu.
## Output
- 4 pliki silnika z nowym separatorem `_`
- Unit testy: regex routingu (Helpers) + generator linku (ProductRepository::appendCombinationToXml)
- SUMMARY z listą akcji post-deploy do wykonania ręcznie na produkcji
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/codebase/architecture.md
## Source Files
@autoload/Domain/Product/ProductRepository.php
@autoload/Shared/Helpers/Helpers.php
@autoload/front/LayoutEngine.php
@templates/shop-product/_partial/product-attribute.php
<clarifications>
- **Testy** — Czy dodać unit testy dla zmian?
→ Odpowiedź: Tak — pełne pokrycie (Helpers regex + ProductRepository::appendCombinationToXml)
- **Post-deploy** — Czy wykonać regenerację routes/cache/feedu w ramach fazy?
→ Odpowiedź: Nic — tylko kod; akcje produkcyjne udokumentowane w SUMMARY
- **Redirect 301** — Czy dodać redirecty ze starych URL-i?
→ Odpowiedź: Nie — Google sam zaktualizuje linki z feedu
- **Skills** — /feature-dev required w SPECIAL-FLOWS?
→ Odpowiedź: Override — pomiń (hotfix z konkretną instrukcją, jak w fazach 15/16/17)
</clarifications>
</context>
<acceptance_criteria>
## AC-1: Generator linku w feedzie używa `_`
```gherkin
Given produkt z permutacją atrybutów (permutation_hash = "20-170|21-175")
When wywołany jest ProductRepository::appendCombinationToXml dla feedu Google
Then wygenerowany URL zawiera segment `20-170_21-175` (jeden segment, separator `_`)
And nie zawiera `/` między parami atrybutów
And dotyczy obu gałęzi (z seo_link i fallback p-id-name)
```
## AC-2: Routing `pp_routes` matchuje URL z `_`
```gherkin
Given wzorzec routingu wygenerowany przez Helpers dla produktu z permutacją
When URI to `slug-produktu/20-170_21-175`
Then regex `[0-9_-]+` dopasowuje cały segment permutacji
And `permutation_hash` w wynikowych GET to `20-170_21-175`
And dotyczy obu wariantów (z seo_link i fallback p-id-name)
```
## AC-3: Front konwertuje `_` z URL na `|` przed zapytaniem do bazy
```gherkin
Given GET['permutation_hash'] = "20-170_21-175"
When LayoutEngine renderuje blok PRODUKT
Then ProductRepository::findCached otrzymuje argument "20-170|21-175"
And gdy GET['permutation_hash'] nie istnieje, findCached otrzymuje null
```
## AC-4: Partial atrybutu preselectuje wartość z URL
```gherkin
Given URL produktu z permutation_hash zawierającym parę dla bieżącego atrybutu
When renderuje się templates/shop-product/_partial/product-attribute.php
Then aktywna (checked) jest wartość z URL, nie z is_default
And gdy atrybut nie występuje w hashu, zachowane jest stare zachowanie (is_default)
And blok <script> z fradio_label_click() emitowany jest dla wartości z URL
```
## AC-5: Pełna suita testów zielona
```gherkin
Given wprowadzone zmiany w 4 plikach + 2 nowe/zaktualizowane testy
When uruchomiony jest ./test.ps1
Then wszystkie testy przechodzą (>=836 834 obecnych + 2 nowe)
And brak warningów PHP
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Zmiana separatora w generatorze feedu i routingu</name>
<files>autoload/Domain/Product/ProductRepository.php, autoload/Shared/Helpers/Helpers.php, autoload/front/LayoutEngine.php</files>
<action>
1. **autoload/Domain/Product/ProductRepository.php** — w `appendCombinationToXml` (~linie 2372 i 2374):
- Zamienić `str_replace('|', '/', $combination['permutation_hash'])` na `str_replace('|', '_', $combination['permutation_hash'])`
- Dotyczy OBU gałęzi (seo_link i fallback `p-id-name`)
- Najpierw przeczytać metodę i potwierdzić obie wystąpienia przed edycją
2. **autoload/Shared/Helpers/Helpers.php** — w generatorze tras (~linie 694 i 699):
- Rozszerzyć regex z `'^' . ... . '/([0-9-]+)$'` na `'^' . ... . '/([0-9_-]+)$'`
- Dotyczy OBU wariantów (seo_link i fallback `p-id-name`)
3. **autoload/front/LayoutEngine.php** — w bloku `// PRODUKT` (~linia 196):
- Wyciągnąć `permutation_hash` do zmiennej z konwersją `_``|`:
```php
$permutation_hash = isset($_GET['permutation_hash']) ? str_replace('_', '|', $_GET['permutation_hash']) : null;
```
- Przekazać `$permutation_hash` do `findCached()` zamiast inline `$_GET['permutation_hash'] ?? null`
Avoid:
- Zmian w `findCached()` lub `permutation_hash` w bazie — separator w DB pozostaje `|`
- Modyfikacji innych metod ProductRepository
- PHP 8.0+ syntaxu (`match`, named args)
</action>
<verify>
- `grep -n "str_replace.*'|'.*'/'" autoload/Domain/Product/ProductRepository.php` — brak wyników (0 wystąpień)
- `grep -n "str_replace.*'|'.*'_'" autoload/Domain/Product/ProductRepository.php` — 2 wystąpienia
- `grep -n "\[0-9_-\]+" autoload/Shared/Helpers/Helpers.php` — 2 wystąpienia
- `grep -n "\[0-9-\]+\\\$" autoload/Shared/Helpers/Helpers.php` — brak (stary wzorzec usunięty z generatora produktów z permutacją)
- `grep -n "permutation_hash" autoload/front/LayoutEngine.php` — zmienna wyciągnięta przed `findCached`
</verify>
<done>AC-1, AC-2, AC-3 spełnione</done>
</task>
<task type="auto">
<name>Task 2: Preselekcja atrybutu z permutation_hash w partialu</name>
<files>templates/shop-product/_partial/product-attribute.php</files>
<action>
Najpierw przeczytać cały plik partiala (mały, ~kilkadziesiąt linii) i zlokalizować pętlę `foreach` po `values` oraz miejsca używające `$value['is_default']`.
Na początku partiala (przed pętlą po values) dodać:
```php
$forced_value_id = null;
if ( isset( $_GET['permutation_hash'] ) && $_GET['permutation_hash'] !== '' )
{
$pairs = explode( '|', str_replace( '_', '|', $_GET['permutation_hash'] ) );
foreach ( $pairs as $pair )
{
$parts = explode( '-', $pair );
if ( count( $parts ) == 2 && (int)$parts[0] === (int)$this -> attribute['id'] )
{
$forced_value_id = (int)$parts[1];
break;
}
}
}
```
W pętli foreach po values, przed użyciem flagi `is_default`, policzyć:
```php
$is_active = $forced_value_id !== null
? ( (int)$value['id'] === $forced_value_id )
: (bool)$value['is_default'];
```
Zastąpić WSZYSTKIE użycia `$value['is_default']` w kontekście aktywności (checked, fradio_label_click) zmienną `$is_active`. Nie ruszać `is_default` jeśli używane gdzie indziej semantycznie (np. atrybut metadata).
Avoid:
- Modyfikacji `templates_user/` (potwierdzono: nie istnieje w tym repo)
- Zmian struktury HTML / klas CSS
- PHP 8.0+ syntaxu
</action>
<verify>
- `grep -n "forced_value_id" templates/shop-product/_partial/product-attribute.php` — co najmniej 4 wystąpienia (deklaracja, set, użycie w `$is_active`, użycie w `$is_active`)
- `grep -n "is_active" templates/shop-product/_partial/product-attribute.php` — co najmniej 2 wystąpienia (deklaracja + użycie w checked/script)
- Manualnie potwierdzić: `checked="checked"` używa `$is_active`, `fradio_label_click(...)` script gate'owany przez `$is_active`
</verify>
<done>AC-4 spełnione</done>
</task>
<task type="auto">
<name>Task 3: Unit testy dla regex routingu i generatora linku</name>
<files>tests/Unit/Shared/Helpers/HelpersTest.php, tests/Unit/Domain/Product/ProductRepositoryTest.php</files>
<action>
Najpierw sprawdzić strukturę istniejących testów (szczególnie czy `HelpersTest.php` istnieje — jeśli nie, utworzyć z bootstrapem zgodnym z innymi testami w `tests/Unit/Shared/`).
1. **Helpers — test regex routingu z `_`:**
- Wywołać generator tras dla produktu z permutacją (jeśli metoda jest publiczna; w przeciwnym razie test integracyjny z mockiem `$mdb` zwracającym permutacje produktu)
- Zweryfikować że wygenerowany pattern zawiera `[0-9_-]+` zamiast `[0-9-]+`
- Test: `preg_match` na patternie z URI `slug/20-170_21-175` zwraca true i wyciąga `20-170_21-175` jako capture group
- Test negatywny: pattern NIE matchuje `slug/20-170/21-175` (stary format ze slashem — chcemy 404, nie przypadkowy match)
2. **ProductRepository::appendCombinationToXml — test separatora `_`:**
- Może być nieosiągalna metoda private/protected. Strategia A (preferowana): jeśli private, użyć ReflectionMethod do wywołania na instancji z mockiem `$mdb`. Strategia B: jeśli zbyt skomplikowane, dodać minimalny test który wywołuje publiczną metodę feedu z mockiem i sprawdza wygenerowany XML.
- Mock combination z `permutation_hash = '20-170|21-175'`, `seo_link = 'jakas-fraza'`
- Asercja: w wygenerowanym XML link zawiera `jakas-fraza/20-170_21-175`, NIE zawiera `20-170/21-175`
- Drugi test: gałąź fallback (brak `seo_link`) — link `p-{id}-{name}/20-170_21-175`
Trzymać się konwencji: AAA, mock Medoo (`$this->createMock(\medoo::class)`), namespace tests jak w istniejących plikach. Brak PHP 8.0+ syntaxu. Nazwy metod z `test` prefiksem.
</action>
<verify>
- `./test.ps1 tests/Unit/Shared/Helpers/HelpersTest.php` — przechodzi
- `./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php` — przechodzi (wszystkie testy, łącznie z nowymi)
- `./test.ps1` — pełna suita zielona, count >= 836
</verify>
<done>AC-5 spełnione (testy zielone, ≥2 nowe testy pokrywające AC-1 i AC-2)</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Format `permutation_hash` w bazie (kolumna `pp_shop_product_combinations.permutation_hash` pozostaje z separatorem `|`)
- Sygnatura `ProductRepository::findCached()` — przyjmuje hash z `|`
- Inne metody ProductRepository / inne kontrolery / inne partiale
- Plików `templates_user/` (nie istnieje w tym repo, akcja po stronie klientów)
- Schemat bazy danych
- `.htaccess` w roocie (redirecty 301 wykluczone z scope)
## SCOPE LIMITS
- Tylko 4 pliki silnika + 2 pliki testów (lista w `files_modified`)
- Brak automatycznej regeneracji `pp_routes` — udokumentowane w SUMMARY jako akcja deploy
- Brak czyszczenia cache `pp_routes:all` w fazie — akcja deploy
- Brak regeneracji `google-feed.xml` w fazie — akcja deploy
- Brak redirectów 301 ze starych URL-i
</boundaries>
<verification>
- [ ] `./test.ps1` — pełna suita zielona (≥836 testów)
- [ ] Brak `str_replace('|', '/', ...)` w ProductRepository (grep)
- [ ] `[0-9_-]+` w obu wzorcach Helpers (grep)
- [ ] `permutation_hash` wyciągnięte do zmiennej w LayoutEngine z konwersją `_`→`|`
- [ ] Partial używa `$is_active` (forced_value_id || is_default) zamiast surowego `is_default`
- [ ] Wszystkie 5 AC spełnione
</verification>
<success_criteria>
- 4 pliki silnika zmienione zgodnie z instrukcją
- 2 nowe / zaktualizowane testy: routing regex + generator XML linku
- Pełna suita testów zielona
- Brak regresji w istniejących testach (834 → ≥836)
- SUMMARY zawiera dokładną listę akcji post-deploy (regen pp_routes, clear cache, regen feedu, resubmit GMC)
</success_criteria>
<output>
After completion, create `.paul/phases/18-google-feed-permutation-url-fix/18-01-SUMMARY.md` containing:
- Co zmienione (lista plików + diff highlights)
- Akcje post-deploy do wykonania ręcznie na produkcji (kolejność: regen pp_routes → clear cache pp_routes:all → regen google-feed.xml → resubmit GMC)
- Test count delta
- Decyzje (override /feature-dev, brak redirectów 301)
</output>

View File

@@ -0,0 +1,168 @@
---
phase: 18-google-feed-permutation-url-fix
plan: 01
subsystem: feed/routing
tags: [google-merchant, pp_routes, permutation, regex, php74]
requires:
- phase: prior-architecture
provides: ProductRepository, Helpers::htacces, LayoutEngine, frontAttributePartial
provides:
- Separator URL permutacji `_` zamiast `/` w feedzie Google
- Wzorzec routingu pp_routes obejmujący `[0-9_-]+`
- Konwersja `_``|` po stronie front przed `findCached`
- Preselekcja wartości atrybutu z `permutation_hash` w partialu
affects: [google-feed, pp_routes, frontend-product-attributes]
tech-stack:
added: []
patterns:
- "URL feedu: jeden segment z `_` zamiast wielu segmentów ze `/`"
- "DB format `|`, URL format `_`, konwersja w warstwie front"
key-files:
created:
- tests/Unit/Shared/Helpers/HelpersRoutingTest.php
- tests/Unit/Domain/Product/ProductFeedLinkTest.php
modified:
- autoload/Domain/Product/ProductRepository.php
- autoload/Shared/Helpers/Helpers.php
- autoload/front/LayoutEngine.php
- templates/shop-product/_partial/product-attribute.php
key-decisions:
- "Separator URL `_` zamiast `/` (one segment dopasowywalny przez pp_routes)"
- "Konwersja `_``|` w LayoutEngine, format DB pozostaje `|`"
- "Brak redirectów 301 — Google sam zaktualizuje feed"
- "Brak automatycznych akcji post-deploy — udokumentowane jako manual steps"
- "Override /feature-dev (hotfix z konkretną instrukcją)"
patterns-established:
- "Forced value via URL parameters w partialach (preselekcja zamiast is_default)"
- "Reflection-based test prywatnych metod XML feedu"
duration: ~25min
completed: 2026-04-30
---
# Phase 18 Plan 01: Google feed permutation URL fix — Summary
**Linki produktów z permutacją w feedzie Google używają teraz `_` jako separatora par `attr-val`, routing `pp_routes` matchuje takie URL-e, a partial atrybutu preselectuje wartości na podstawie `permutation_hash` z URL.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~25 min |
| Started | 2026-04-30 |
| Completed | 2026-04-30 |
| Tasks | 3 / 3 |
| Files modified | 6 (4 silnik + 2 testy) |
| Tests | 834 → 841 (+7) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Generator linku w feedzie używa `_` | Pass | ProductFeedLinkTest.testCombinationLinkUsesUnderscoreInSeoLinkBranch + fallback + single-pair |
| AC-2: Routing `pp_routes` matchuje URL z `_` | Pass | HelpersRoutingTest weryfikuje obecność `[0-9_-]+` w generatorze + preg_match na nowym wzorcu |
| AC-3: Front konwertuje `_` z URL na `|` przed zapytaniem | Pass | LayoutEngine.php:196 — zmienna `$permutation_hash` z `str_replace('_','|',...)` |
| AC-4: Partial preselectuje wartość z URL | Pass | `$forced_value_id` + `$is_active` używane w `checked` i `<script>` |
| AC-5: Pełna suita testów zielona | Pass | PHPUnit: 841 tests, 2330 assertions, 0.764s |
## Accomplishments
- Naprawa krytycznego problemu komercyjnego: feed Google prowadził klientów na home zamiast na produkt
- Spójność stosu: separator URL (`_`) ↔ format DB (`|`) z jasnym punktem konwersji w warstwie front
- 7 nowych testów (4 routing + 3 generator linku) — pełne pokrycie zmiany
- Reflection-based test metody prywatnej `appendCombinationToXml` z mockiem Medoo i mockiem TransportRepository
- UI strony produktu wchodząc z linka feedu pokazuje wybraną kombinację atrybutów (zamiast `is_default`)
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/Domain/Product/ProductRepository.php` | Modified (×2) | `appendCombinationToXml`: separator `/``_` w obu gałęziach (seo_link i fallback) |
| `autoload/Shared/Helpers/Helpers.php` | Modified (×2) | Generator pp_routes: regex `[0-9-]+``[0-9_-]+` w obu wariantach |
| `autoload/front/LayoutEngine.php` | Modified | Wyciągnięcie `$permutation_hash` z konwersją `_``|` przed `findCached` |
| `templates/shop-product/_partial/product-attribute.php` | Modified | `$forced_value_id` z URL + `$is_active` w `checked`/`<script>` |
| `tests/Unit/Shared/Helpers/HelpersRoutingTest.php` | Created | 4 testy regex routingu (file content + preg_match dla `_` i odrzucenia `/`) |
| `tests/Unit/Domain/Product/ProductFeedLinkTest.php` | Created | 3 testy `appendCombinationToXml` via Reflection (seo_link, fallback, single-pair) |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Separator URL `_` zamiast `/` | `_` mieści się w jednym segmencie regex `[0-9_-]+`; `/` wymagałby zmiany struktury routingu | Czysty fix, minimalna zmiana w pp_routes |
| Format DB pozostaje `|` | Nie tykać zapisanych danych w `pp_shop_product_combinations.permutation_hash` | Zero migracji DB; konwersja tylko w warstwie I/O |
| Brak redirectów 301 | Stare URL-e z feedu wymarły gdy GMC zaciągnie nowy feed | Mniej kodu w `.htaccess`, brak długoterminowego balastu |
| Brak automatycznych akcji post-deploy | Hotfix dotyczy tylko silnika; regen routes/cache/feed są zależne od środowiska | Wymaga manualnego runbook'a (poniżej) |
| Override /feature-dev | Hotfix z konkretną instrukcją od użytkownika, jak fazy 15/16/17 | Skill audit logged |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 0 | — |
| Scope additions | 0 | — |
| Deferred | 0 | — |
**Total impact:** Zero. Plan wykonany dokładnie według instrukcji.
### Auto-fixed Issues
None.
### Deferred Items
None.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| `ProductRepository::appendCombinationToXml` jest `private` z zależnościami od `transportRepoForXml` i `AttributeRepository` (DB) | Test via `ReflectionMethod`; mock Medoo (`select``[]`, `get``null`); wstrzyknięty mock `TransportRepository::lowestTransportPrice` zwracający `0.0` na dynamicznej property `transportRepoForXml` |
| Brak istniejącego folderu `tests/Unit/Shared/Helpers/` | Utworzony nowy katalog + `HelpersRoutingTest.php` |
| `Helpers::htacces()` zbyt rozległe do testu E2E (DB writes, file I/O) | Test pośredni: assercje na zawartości pliku Helpers.php (file_get_contents) + standalone `preg_match` na sample patternie |
## Post-deploy runbook (manual, kolejność krytyczna)
Wymagane akcje na środowisku produkcyjnym po deployu kodu:
1. **Regeneracja `pp_routes`** — wywołać `Helpers::htacces()` (np. zapis ustawień w adminie lub regeneracja sitemap), żeby nowe wzorce z `_` trafiły do bazy. Bez tego stare wzorce `[0-9-]+` w `pp_routes` nadal nie zmatchują URL z `_`.
2. **Wyczyścić cache routingu** — skasować klucz `pp_routes:all` w Redis (`DEL pp_routes:all`) albo poczekać 24h na expiry. `index.php:63` cachuje routing.
3. **Regeneracja feedu Google** — uruchomić cron `cron/cron-xml.php` (`\admin\factory\ShopProduct::generate_google_feed_xml()`), żeby `google-feed.xml` zawierał nowe linki z `_`.
4. **Resubmit feedu w GMC** — automatycznie wg harmonogramu lub ręcznie "Fetch now".
5. **Stare URL-e w GMC** — same wypadną z indeksu po podmianie feedu (Google).
Walidacja po deployu:
- `https://domena/google-feed.xml` — tagi `<link>` zawierają `_` zamiast `/` między parami
- `https://domena/slug-produktu/20-170_21-175` — ładuje produkt z preselectowaną kombinacją (nie home)
- GMC: feed bez błędów "Landing page error"
## Skill Audit (Phase 18)
| Expected | Invoked | Notes |
|----------|---------|-------|
| /feature-dev | ○ | User-approved override (hotfix z konkretną instrukcją) |
| /koniec-pracy | ○ | Pending — uruchomić przy zakończeniu sesji jeśli release wchodzi do update package |
## Next Phase Readiness
**Ready:**
- Hotfix completed; pełna suita zielona
- Brak zmian w schemacie DB
- Wzorzec preselekcji partial z URL parameter dostępny dla innych partiali (jeśli pojawi się potrzeba)
**Concerns:**
- Akcje post-deploy (regen routes / clear cache / regen feed) wymagają manualnego wykonania — brak automatu
- Klienci sklepu mający własne nadpisane `templates_user/shop-product/_partial/product-attribute.php` muszą zaaplikować zmianę u siebie (Tpl::view priorytetuje `templates_user/`)
**Blockers:**
- None.
---
*Phase: 18-google-feed-permutation-url-fix, Plan: 01*
*Completed: 2026-04-30*

View File

@@ -2369,9 +2369,9 @@ class ProductRepository
$itemNode->appendChild( $doc->createElement( 'g:description', html_entity_decode( strip_tags( $desc ) ) ) );
if ( $product['language']['seo_link'] ) {
$link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product['language']['seo_link'] ) . '/' . str_replace( '|', '/', $combination['permutation_hash'] );
$link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product['language']['seo_link'] ) . '/' . str_replace( '|', '_', $combination['permutation_hash'] );
} else {
$link = $domainPrefix . '://' . $url . '/p-' . $product['id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] ) . '/' . str_replace( '|', '/', $combination['permutation_hash'] );
$link = $domainPrefix . '://' . $url . '/p-' . $product['id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] ) . '/' . str_replace( '|', '_', $combination['permutation_hash'] );
}
$itemNode->appendChild( $doc->createElement( 'link', $link ) );

View File

@@ -691,12 +691,12 @@ class Helpers
if ( $row2['seo_link'] )
{
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9_-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
}
else
{
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9_-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
}
}
}

View File

@@ -193,7 +193,8 @@ class LayoutEngine
//
if ( \Shared\Helpers\Helpers::get( 'product' ) )
{
$product = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCached( \Shared\Helpers\Helpers::get( 'product' ), $lang_id, $_GET['permutation_hash'] ?? null );
$permutation_hash = isset( $_GET['permutation_hash'] ) ? str_replace( '_', '|', $_GET['permutation_hash'] ) : null;
$product = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCached( \Shared\Helpers\Helpers::get( 'product' ), $lang_id, $permutation_hash );
if ( $product['language']['meta_title'] )
$page['language']['title'] = $product['language']['meta_title'];

View File

@@ -2,17 +2,37 @@
global $lang_id;
$attributeRepo = new \Domain\Attribute\AttributeRepository( $GLOBALS['mdb'] );
$attribute_details = $attributeRepo->frontAttributeDetails( (int)$this -> attribute['id'], $lang_id );
$forced_value_id = null;
if ( isset( $_GET['permutation_hash'] ) && $_GET['permutation_hash'] !== '' )
{
$pairs = explode( '|', str_replace( '_', '|', $_GET['permutation_hash'] ) );
foreach ( $pairs as $pair )
{
$parts = explode( '-', $pair );
if ( count( $parts ) == 2 && (int)$parts[0] === (int)$this -> attribute['id'] )
{
$forced_value_id = (int)$parts[1];
break;
}
}
}
if ( $attribute_details['type'] == 0 )
{
?>
<div class="attribute-container fradio-group attribute-<?= \Shared\Helpers\Helpers::seo( $attribute_details['language']['name'] );?>" level="<?= $this -> level;?>" order="<?= $this -> order;?>" attribute="<?= \Shared\Helpers\Helpers::seo( $attribute_details['language']['name'] );?>" attribute-name="<?= $attribute_details['language']['name'];?>">
<p class="attribute-label"><?= $attribute_details['language']['name'];?>:</p>
<? foreach ( $this -> attribute['values'] as $value ):?>
<? foreach ( $this -> attribute['values'] as $value ):
$is_active = $forced_value_id !== null
? ( (int)$value['id'] === $forced_value_id )
: (bool)$value['is_default'];
?>
<div class="fradio">
<input type="radio" id="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" <? if ( $value['is_default'] ):?>checked="checked"<? endif;?> require="true" value="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" name="<?= \Shared\Helpers\Helpers::seo( $attribute_details['language']['name'] );?>">
<input type="radio" id="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" <? if ( $is_active ):?>checked="checked"<? endif;?> require="true" value="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" name="<?= \Shared\Helpers\Helpers::seo( $attribute_details['language']['name'] );?>">
<label for="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" order="<?= $this -> order;?>"><?= ( new \Domain\Attribute\AttributeRepository( $GLOBALS['mdb'] ) )->getAttributeValueById( $value['id'], $lang_id );?></label>
</div>
<? if ( $value['is_default'] ):?>
<? if ( $is_active ):?>
<script class="footer" type="text/javascript">
$( function()
{

View File

@@ -0,0 +1,151 @@
<?php
namespace Tests\Unit\Domain\Product;
use PHPUnit\Framework\TestCase;
use Domain\Product\ProductRepository;
/**
* Phase 18 — testy generatora linku do feedu Google.
*
* ProductRepository::appendCombinationToXml buduje <link> dla pozycji
* feedu Google. permutation_hash w bazie ma format "attr-val|attr-val".
* W URL feedu separator między parami to "_" (nie "/"), żeby URL był
* jednym segmentem dopasowywalnym przez routing pp_routes.
*
* Test wywołuje prywatną metodę przez ReflectionMethod z minimalnymi
* danymi produktu i sprawdza zawartość wynikowego DOMDocument.
*/
class ProductFeedLinkTest extends TestCase
{
private function buildRepoWithMocks(): ProductRepository
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('select')->willReturn([]);
$mockDb->method('get')->willReturn(null);
$repo = new ProductRepository($mockDb);
// appendShippingToXml wywołuje $this->transportRepoForXml->lowestTransportPrice().
// Inicjalizacja w generateGoogleXmlFeed(); dla unit testu wstrzykujemy mock dynamicznie.
$transportMock = $this->getMockBuilder(\Domain\Transport\TransportRepository::class)
->disableOriginalConstructor()
->getMock();
$transportMock->method('lowestTransportPrice')->willReturn(0.0);
$repo->transportRepoForXml = $transportMock;
return $repo;
}
private function invokeAppendCombination(ProductRepository $repo, array $product, array $combination): string
{
$doc = new \DOMDocument('1.0', 'UTF-8');
$channelNode = $doc->appendChild($doc->createElement('channel'));
$method = new \ReflectionMethod(ProductRepository::class, 'appendCombinationToXml');
$method->setAccessible(true);
$method->invoke($repo, $doc, $channelNode, $product, $combination, 'https', 'shop.example.com');
return $doc->saveXML();
}
private function baseProduct(array $overrides = []): array
{
return array_merge([
'id' => 123,
'ean' => '5901234567890',
'language' => [
'name' => 'Produkt testowy',
'xml_name' => '',
'short_description' => 'Opis',
'meta_title' => '',
'seo_link' => 'sukienka-czerwona',
],
'price_brutto' => 100,
'price_brutto_promo' => 0,
'quantity' => 10,
'stock_0_buy' => 0,
'wp' => 1,
'images' => [],
], $overrides);
}
public function testCombinationLinkUsesUnderscoreInSeoLinkBranch()
{
$repo = $this->buildRepoWithMocks();
$product = $this->baseProduct();
$combination = [
'id' => 555,
'permutation_hash' => '20-170|21-175',
'price_brutto' => 120,
'price_brutto_promo' => 0,
'quantity' => 5,
'stock_0_buy' => 0,
];
$xml = $this->invokeAppendCombination($repo, $product, $combination);
$this->assertStringContainsString(
'<link>https://shop.example.com/sukienka-czerwona/20-170_21-175</link>',
$xml,
'Link feedu z seo_link musi używać "_" jako separatora par attr-val'
);
$this->assertStringNotContainsString(
'20-170/21-175',
$xml,
'Link feedu nie może zawierać starego separatora "/" między parami atrybutów'
);
}
public function testCombinationLinkUsesUnderscoreInFallbackBranch()
{
$repo = $this->buildRepoWithMocks();
$product = $this->baseProduct([
'language' => [
'name' => 'Sukienka czerwona',
'xml_name' => '',
'short_description' => 'Opis',
'meta_title' => '',
'seo_link' => '',
],
]);
$combination = [
'id' => 555,
'permutation_hash' => '20-170|21-175',
'price_brutto' => 120,
'price_brutto_promo' => 0,
'quantity' => 5,
'stock_0_buy' => 0,
];
$xml = $this->invokeAppendCombination($repo, $product, $combination);
// Fallback uses "p-{id}-{seo(name)}/...". Helpers::seo stub returns input unchanged.
$this->assertStringContainsString(
'<link>https://shop.example.com/p-123-Sukienka czerwona/20-170_21-175</link>',
$xml,
'Link fallback (bez seo_link) musi używać "_" jako separatora par attr-val'
);
}
public function testCombinationLinkWithSinglePair()
{
$repo = $this->buildRepoWithMocks();
$product = $this->baseProduct();
$combination = [
'id' => 555,
'permutation_hash' => '20-170',
'price_brutto' => 120,
'price_brutto_promo' => 0,
'quantity' => 5,
'stock_0_buy' => 0,
];
$xml = $this->invokeAppendCombination($repo, $product, $combination);
$this->assertStringContainsString(
'<link>https://shop.example.com/sukienka-czerwona/20-170</link>',
$xml,
'Pojedyncza para attr-val pozostaje bez zmian (str_replace nie ma co podmieniać)'
);
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Tests\Unit\Shared\Helpers;
use PHPUnit\Framework\TestCase;
/**
* Phase 18 — testy regex routingu pp_routes dla URL produktów z permutacją.
*
* Helpers::htacces() generuje pattern dla każdego produktu z permutacją.
* Pattern używa klasy znakowej [0-9_-]+, żeby dopasować segment "20-170_21-175"
* w jednym kawałku (separator pomiędzy parami atrybutów to "_", nie "/").
*
* Testy nie wywołują htacces() (zbyt duże zależności), tylko weryfikują:
* 1. Wzorzec literałem [0-9_-]+ występuje w generatorze pp_routes (file content)
* 2. Wzorzec przyjmuje URL z "_" i odrzuca wariant ze "/"
*/
class HelpersRoutingTest extends TestCase
{
private $helpersSource;
protected function setUp(): void
{
parent::setUp();
$this->helpersSource = file_get_contents(
__DIR__ . '/../../../../autoload/Shared/Helpers/Helpers.php'
);
}
public function testHelpersGeneratorUsesPermutationCharClassWithUnderscore()
{
// Liczba miejsc, gdzie pattern produktu z permutacją używa nowej klasy znaków.
$newPattern = substr_count($this->helpersSource, '/([0-9_-]+)$');
$this->assertGreaterThanOrEqual(
2,
$newPattern,
'Helpers.php musi zawierać dwa wystąpienia /([0-9_-]+)$ (gałąź seo_link i fallback p-id-name)'
);
// Stary wzorzec [0-9-]+ nie powinien już występować jako finalny segment URL.
$this->assertStringNotContainsString(
'/([0-9-]+)$',
$this->helpersSource,
'Stary wzorzec /([0-9-]+)$ został zastąpiony przez /([0-9_-]+)$ — nie powinno go już być w generatorze pp_routes'
);
}
public function testRegexMatchesUrlWithUnderscoreSeparator()
{
$pattern = '#^slug-produktu/([0-9_-]+)$#';
$matches = [];
$this->assertSame(
1,
preg_match($pattern, 'slug-produktu/20-170_21-175', $matches),
'Nowy wzorzec musi dopasować URL z "_" jako separatorem par atrybutów'
);
$this->assertSame('20-170_21-175', $matches[1]);
}
public function testRegexRejectsLegacyUrlWithSlashSeparator()
{
$pattern = '#^slug-produktu/([0-9_-]+)$#';
$this->assertSame(
0,
preg_match($pattern, 'slug-produktu/20-170/21-175'),
'Wzorzec NIE powinien dopasować starego URL ze "/" — taki URL ma trafiać do innego routingu lub 404'
);
}
public function testRegexMatchesSinglePairUrl()
{
$pattern = '#^slug-produktu/([0-9_-]+)$#';
$matches = [];
$this->assertSame(
1,
preg_match($pattern, 'slug-produktu/20-170', $matches),
'Wzorzec dopasowuje też URL z jedną parą attr-val'
);
$this->assertSame('20-170', $matches[1]);
}
}