07-01: Performance — N+1 subqueries, information_schema static, DB indexes 07-02: Stability — SSL verification (4 clients), cron throttle→DB, migration 000014b 07-03: UX — orderpro_to_allegro disable, orders list items 14-17 07-04: Tests — AllegroTokenManager + AllegroOrderImportService unit tests 07-05: InPost ShipmentProviderInterface (replaces allegro_wza workaround) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
221 lines
9.0 KiB
Markdown
221 lines
9.0 KiB
Markdown
---
|
|
phase: 07-pre-expansion-fixes
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- src/Modules/Orders/OrdersRepository.php
|
|
- database/migrations/20260313_000048_add_orders_performance_indexes.sql
|
|
autonomous: true
|
|
---
|
|
|
|
<objective>
|
|
## Goal
|
|
Wyeliminować dwa bottlenecki wydajnościowe w module Orders: 4 correlated subqueries na każdy wiersz listy zamówień oraz zapytanie do information_schema przy każdym żądaniu HTTP.
|
|
|
|
## Purpose
|
|
Lista zamówień jest głównym widokiem aplikacji. Przy 50 wierszach na stronę, correlated subqueries oznaczają 200+ dodatkowych zapytań per page load. `canResolveMappedMedia()` uderza w `information_schema` przy każdym żądaniu do OrdersRepository — notoriously slow na MySQL. Oba problemy narastają z liczbą zamówień.
|
|
|
|
## Output
|
|
- `OrdersRepository::buildListSql()` — 4 subqueries zastąpione aggregating LEFT JOIN
|
|
- `OrdersRepository::canResolveMappedMedia()` — wynik cachowany w `static` property
|
|
- Nowa migracja z brakującymi indeksami dla `orders` table
|
|
</objective>
|
|
|
|
<context>
|
|
## Project Context
|
|
@.paul/PROJECT.md
|
|
|
|
## Source Files
|
|
@src/Modules/Orders/OrdersRepository.php
|
|
</context>
|
|
|
|
<acceptance_criteria>
|
|
|
|
## AC-1: Brak correlated subqueries w liście zamówień
|
|
```gherkin
|
|
Given OrdersRepository::buildListSql() zawiera 4 correlated subqueries (items_count, items_qty, shipments_count, documents_count)
|
|
When metoda buduje SQL dla listy zamówień
|
|
Then SQL nie zawiera "(SELECT COUNT(*) FROM order_items" ani podobnych correlated subqueries
|
|
AND zwracane kolumny items_count, items_qty, shipments_count, documents_count nadal istnieją
|
|
AND logika transformOrderRow() w pełni działa (te pola nadal trafiają do wyniku)
|
|
```
|
|
|
|
## AC-2: information_schema nie jest odpytywany per-request
|
|
```gherkin
|
|
Given canResolveMappedMedia() używa instance property $this->supportsMappedMedia jako cache
|
|
When OrdersRepository jest instanciowany dwukrotnie w tej samej sesji PHP (np. dwa wywołania repository)
|
|
Then information_schema.COLUMNS jest zapytany co najwyżej raz na cykl PHP (static property)
|
|
```
|
|
|
|
## AC-3: Brakujące indeksy dodane migracją
|
|
```gherkin
|
|
Given tabela orders nie ma indeksów na kolumnach source, external_status_id, ordered_at
|
|
When tworzona jest nowa migracja i wykonana przez migrator
|
|
Then tabela orders ma indeksy na: source, external_status_id, ordered_at
|
|
AND migracja jest idempotentna (IF NOT EXISTS lub ADD INDEX IF NOT EXISTS lub ALTER IGNORE)
|
|
```
|
|
|
|
</acceptance_criteria>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Zamień 4 correlated subqueries na aggregating LEFT JOINs w buildListSql()</name>
|
|
<files>src/Modules/Orders/OrdersRepository.php</files>
|
|
<action>
|
|
W metodzie `buildListSql()` (linie ~135-171) zamień:
|
|
```sql
|
|
(SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS items_count,
|
|
(SELECT COALESCE(SUM(oi.quantity), 0) FROM order_items oi WHERE oi.order_id = o.id) AS items_qty,
|
|
(SELECT COUNT(*) FROM order_shipments sh WHERE sh.order_id = o.id) AS shipments_count,
|
|
(SELECT COUNT(*) FROM order_documents od WHERE od.order_id = o.id) AS documents_count
|
|
```
|
|
Na:
|
|
```sql
|
|
COALESCE(oi_agg.items_count, 0) AS items_count,
|
|
COALESCE(oi_agg.items_qty, 0) AS items_qty,
|
|
COALESCE(sh_agg.shipments_count, 0) AS shipments_count,
|
|
COALESCE(od_agg.documents_count, 0) AS documents_count
|
|
```
|
|
I dodaj do klauzuli FROM (po istniejących LEFT JOINach, przed WHERE):
|
|
```sql
|
|
LEFT JOIN (
|
|
SELECT order_id,
|
|
COUNT(*) AS items_count,
|
|
COALESCE(SUM(quantity), 0) AS items_qty
|
|
FROM order_items
|
|
GROUP BY order_id
|
|
) oi_agg ON oi_agg.order_id = o.id
|
|
LEFT JOIN (
|
|
SELECT order_id, COUNT(*) AS shipments_count
|
|
FROM order_shipments
|
|
GROUP BY order_id
|
|
) sh_agg ON sh_agg.order_id = o.id
|
|
LEFT JOIN (
|
|
SELECT order_id, COUNT(*) AS documents_count
|
|
FROM order_documents
|
|
GROUP BY order_id
|
|
) od_agg ON od_agg.order_id = o.id
|
|
```
|
|
UWAGA: buildListSql() generuje SQL jako string — upewnij się że nowe JOINy są wstawione
|
|
przed `$whereSql` (który jest konkatenowany na końcu z klauzulą WHERE).
|
|
|
|
Sprawdź jak wygląda $whereSql — czy zawiera "WHERE" czy tylko "AND ..."?
|
|
Jeśli WHERE jest w $whereSql, nowe JOINy idą bezpośrednio przed nim.
|
|
|
|
NIE zmieniaj metody transformOrderRow() — klucze tablicy pozostają te same.
|
|
</action>
|
|
<verify>
|
|
php -l src/Modules/Orders/OrdersRepository.php
|
|
grep -c "SELECT COUNT\|SELECT COALESCE\|subquery" src/Modules/Orders/OrdersRepository.php
|
|
# Powinno zwrócić 0 dla wzorców correlated subquery w buildListSql
|
|
</verify>
|
|
<done>AC-1 satisfied: buildListSql() używa LEFT JOIN zamiast correlated subqueries</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Zamień instance property na static w canResolveMappedMedia()</name>
|
|
<files>src/Modules/Orders/OrdersRepository.php</files>
|
|
<action>
|
|
Znajdź deklarację instance property (okolice klasy):
|
|
```php
|
|
private ?bool $supportsMappedMedia = null;
|
|
```
|
|
Zmień na:
|
|
```php
|
|
private static ?bool $supportsMappedMedia = null;
|
|
```
|
|
W metodzie `canResolveMappedMedia()` zmień referencje z `$this->supportsMappedMedia` na
|
|
`self::$supportsMappedMedia` (we wszystkich miejscach metody — przypisanie i odczyt).
|
|
|
|
To jedyna zmiana. Nie modyfikuj logiki zapytania ani żadnej innej metody.
|
|
</action>
|
|
<verify>
|
|
php -l src/Modules/Orders/OrdersRepository.php
|
|
grep -n "supportsMappedMedia" src/Modules/Orders/OrdersRepository.php
|
|
# Powinno pokazać: private static ?bool i self::$supportsMappedMedia
|
|
</verify>
|
|
<done>AC-2 satisfied: canResolveMappedMedia() używa static property</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Migracja z brakującymi indeksami dla tabeli orders</name>
|
|
<files>database/migrations/20260313_000048_add_orders_performance_indexes.sql</files>
|
|
<action>
|
|
Stwórz plik `database/migrations/20260313_000048_add_orders_performance_indexes.sql`.
|
|
|
|
Przed pisaniem migracji, sprawdź jakie indeksy już istnieją:
|
|
```bash
|
|
grep -n "INDEX\|KEY " database/migrations/20260302_000018_create_orders_tables_and_schedule.sql
|
|
grep -rn "ADD INDEX\|ADD KEY" database/migrations/ | grep -i "order"
|
|
```
|
|
|
|
Migracja powinna dodać brakujące indeksy:
|
|
```sql
|
|
-- Indeksy dla typowych filtrów i sortowań na liście zamówień
|
|
ALTER TABLE orders
|
|
ADD INDEX IF NOT EXISTS orders_source_idx (source),
|
|
ADD INDEX IF NOT EXISTS orders_external_status_idx (external_status_id),
|
|
ADD INDEX IF NOT EXISTS orders_ordered_at_idx (ordered_at),
|
|
ADD INDEX IF NOT EXISTS orders_source_status_idx (source, external_status_id);
|
|
|
|
-- Indeks dla allegro status mapping lookup (JOIN w buildListSql)
|
|
ALTER TABLE allegro_order_status_mappings
|
|
ADD INDEX IF NOT EXISTS allegro_status_code_idx (allegro_status_code)
|
|
-- tylko jeśli ten indeks nie istnieje (sprawdź migration 000038)
|
|
```
|
|
|
|
WAŻNE: Sprawdź aktualny schemat przed dodaniem indeksu — nie twórz duplikatów.
|
|
Użyj `IF NOT EXISTS` gdzie MySQL to obsługuje (MySQL 8.0+), albo sformułuj jako
|
|
osobne polecenia z IGNORE: `ALTER TABLE orders ADD IGNORE INDEX ...`
|
|
|
|
Sprawdź też `database/migrations/20260308_000038_ensure_order_status_mappings_table.sql`
|
|
aby wiedzieć jakie indeksy allegro_order_status_mappings już ma.
|
|
</action>
|
|
<verify>
|
|
php -l database/migrations/20260313_000048_add_orders_performance_indexes.sql 2>/dev/null || echo "SQL file - no php lint needed"
|
|
# Sprawdź czy plik istnieje i nie jest pusty
|
|
cat database/migrations/20260313_000048_add_orders_performance_indexes.sql
|
|
</verify>
|
|
<done>AC-3 satisfied: migracja z indeksami na source, external_status_id, ordered_at istnieje</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<boundaries>
|
|
|
|
## DO NOT CHANGE
|
|
- Logika filtrowania ($whereSql) — tylko zmiana mechanizmu agregacji, nie filtrów
|
|
- Metoda transformOrderRow() — zwracane klucze muszą pozostać identyczne
|
|
- Inne metody OrdersRepository poza buildListSql() i canResolveMappedMedia()
|
|
- Istniejące pliki migracji — tylko NOWY plik 000048
|
|
|
|
## SCOPE LIMITS
|
|
- Tylko optymalizacja zapytania list; nie zmieniamy zapytań detail/find
|
|
- Nie dodajemy nowych kolumn ani nie zmieniamy schematu tabel — tylko indeksy
|
|
- Nie implementujemy cache'owania na poziomie Redis/Memcached — tylko static property
|
|
|
|
</boundaries>
|
|
|
|
<verification>
|
|
Przed zamknięciem planu:
|
|
- [ ] php -l src/Modules/Orders/OrdersRepository.php — brak błędów
|
|
- [ ] grep buildListSql src/Modules/Orders/OrdersRepository.php — brak "SELECT COUNT(*) FROM order_items WHERE oi.order_id"
|
|
- [ ] grep "supportsMappedMedia" src/Modules/Orders/OrdersRepository.php — pokazuje "static"
|
|
- [ ] Plik migracji 000048 istnieje i jest nieopusty
|
|
- [ ] Lista zamówień ładuje się bez błędów PHP
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 4 correlated subqueries usunięte z buildListSql()
|
|
- canResolveMappedMedia() z static property
|
|
- Migracja 000048 z indeksami na source, external_status_id, ordered_at
|
|
- Zero błędów składniowych PHP
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-01-SUMMARY.md`
|
|
</output>
|