Dev cookbook

Rychlý reference pro běžné operace. Když si nepamatuješ, koukni sem.

Setup: native docker-ce na Linuxu (Ubuntu 25.10, Docker Engine 29.x). Žádný Docker Desktop. Předpokládá se, že tvoje shell session je členem docker group (jinak každý docker command volá permission denied — fix: sudo usermod -aG docker $USER + logout/login, jednorázově).

Containery (Docker compose)

Service Host port Účel
apache 8000 (pevný) hlavní web
database dynamický Postgres 16 (new app DB), interně 5432
mysql legacy MySQL (jen interní 3306)
adminer 8080 (pevný) DB UI
mailer dynamický Mailpit (interní UI 8025, SMTP 1025)
php PHP-FPM (jen interní 9000)
docker compose ps                              # status
docker compose up -d                           # start (detached)
docker compose down                            # stop (volumes zachovány)
docker compose down -v                         # stop + smaž volumes (ztratíš DB!)
docker compose logs -f apache                  # tail logs konkrétní service
docker compose port database 5432              # zjisti aktuální host port pro Postgres
docker compose port mailer 8025                # ... pro Mailpit UI

Foto galerie — upload adresáře

PHP-FPM v php containeru jede jako www-data. Bind-mountnuté adresáře pro upload (public/uploads/) a imagine cache (public/media/cache/) tvoří host user (panda, uid 1000), takže www-data tam defaultně nemá write. Po fresh checkoutu / po docker compose down -v:

mkdir -p public/uploads public/media/cache
chmod 777 public/uploads public/media/cache

Oba adresáře jsou v .gitignore, takže permisivní mód nikoho neuráží. Symptom při chybějícím write: Warning: mkdir(): Permission denied při uploadu fotky.

Upload pipeline (user uploady i legacy import): každá fotka projde App\Service\PhotoNormalizer → vytáhne datum+GPS přes exiftool, převede přes ImageMagick (magick) na WebP master (auto-orient, ≤ 2560 px, q85, strip metadat), HEIC z iPhonů včetně. Oboje je v php containeru (docker/php/Dockerfile: imagemagick imagemagick-heic imagemagick-webp exiftool) — po editaci Dockerfile nezapomeň docker compose build php, jinak magick/exiftool chybí a upload spadne. Upload limit je v conf.d/uploads.ini (32M) kvůli mobil fotkám — default php.ini-development má jen 2M. Pipeline zapisuje i pixel rozměry masteru do line_photo.width/height (poměr stran pro justified grid na /galerie); starší řádky bez rozměrů dorovná bin/console app:photo:backfill-dimensions. Detail pipeline v architecture.md § Foto galerie — upload pipeline.

Symfony console

Vždy přes php container:

docker compose exec -T php bin/console <cmd>

Make targety (zkratky)

Lokální (Docker compose):

Target Co dělá
make dcSetup composer install
make dcCheckRequirements symfony check:requirements v php containeru (preflight prostředí)
make dcInitDb doctrine:database:create --if-not-exists + migrate
make dcDropdb doctrine:database:drop --force --if-exists (reset před fresh importem — viz migration.md)
make dcClearCache cache:clear --no-warmup (dle aktuálního APP_ENV v containeru / .env.local)
make dcAssetMapCompile bin/console asset-map:compile — generuje public/assets/manifest.json. NUTNÉ v prod — bez něj 500
make dcClearCacheProd cache:clear s vynuceným APP_ENV=prod (i kdyby .env.local měl APP_ENV=dev)
make loadLegacyDump jednorázový load slackcz_44953.sql po fresh docker compose down -v
make legacyImport jednorázový import do čerstvého schématu (highlines/users/crossings --truncate); předpokládá nahraný legacy dump
make stageLegacyPhotos rsync legacy foto stromu z ../old-slack-cz do var/legacy-import/ (mountnuté do php containeru, gitignored)
make importLegacyPhotos stage + app:import:line-photos --truncate — cover + galerie do WebP masterů. Orphany / fotky smazaných lajn → var/legacy-orphan-photos/. Až po legacyImport (potřebuje highlines).

Produkce (SSH na beta.slack.cz):

Target Co dělá
make setupServer ssh deploy@HOST 'bash -s' < scripts/setup-server.sh — idempotentní provisioning. Doinstaluje apt packages, vytvoří chybějící adresáře + ACL. Pro fresh louku spusť skript ručně jako root, viz deploy.md.
make checkServerEnv Preflight skript přes SSH — verifikuje že server má vše co lokální HEAD potřebuje (PHP ext, FS perms, .env.local klíče, …). Exit 1 zablokuje make deploy. Pusť i ručně před git push pro fast feedback.
make checkCaddy Drift check infra/Caddyfile (repo) vs /etc/caddy/Caddyfile (server). Exit 1 = drift, make deploy zablokovaný. Volá se taky implicitně z make deploy jako preflight gate.
make deployCaddy Push infra/Caddyfile na server: scp → caddy validate → atomic cp → systemctl restart caddy + smoke test. Spusť kdykoliv změníš infra/Caddyfile.
make deploy Závisí na checkServerEnv + checkCaddy. Po úspěšných preflightech: ssh deploy@HOST 'bash -s' < scripts/deploy.sh + post-deploy smoke test.
make syncBetaFromLocal pg_dump lokál → scp → psql restore + cache:clear na beta.slack.cz (destruktivní, viz deploy.md)
make syncBetaPhotos rsync WebP masterů public/uploads/line/ na betu (mimo pg_dump → samostatně). Páruje se se syncBetaFromLocal.

Detail flow + script ecosystem v deploy.md sekce „Infrastruktura na první pohled".

Časté konzole příkazy

# vyčistit cache (NUTNÉ po změnách Twig templates / config)
docker compose exec -T php bin/console cache:clear --env=dev

# vyčistit konkrétní cache pool (např. feed cache)
docker compose exec -T php bin/console cache:pool:clear cache.app

# debug routes / parameters / DI / mapping
docker compose exec -T php bin/console debug:router
docker compose exec -T php bin/console debug:container --parameter=...
docker compose exec -T php bin/console doctrine:mapping:info --em=default

# importmap (asset mapper)
docker compose exec -T php bin/console importmap:require <package>

# logy
docker compose exec -T php tail -n 30 var/log/dev.log
docker compose exec -T php tail -n 30 var/log/dev.log | grep CRITICAL

Databáze

Postgres (nová app)

docker compose exec -T database psql -U app -d app -c "\dt"
docker compose exec -T database psql -U app -d app -c "SELECT COUNT(*) FROM line;"

V Adminer: http://localhost:8080/?pgsql=database&username=app&db=app (heslo password).

MySQL (legacy)

docker compose exec -T mysql mysql -uroot -proot old --default-character-set=utf8mb4 -e "SHOW TABLES;"
docker compose exec -T mysql mysql -uroot -proot old --default-character-set=utf8mb4 -e "SELECT COUNT(*) FROM highline;"

V Adminer: http://localhost:8080/?server=mysql&username=root&db=old (heslo root).

⚠ MySQL klient neumí Postgres syntax — COUNT(*) FILTER (WHERE ...) nefunguje, použít SUM(condition).

Migrace + import

Doctrine migrations

docker compose exec -T php bin/console make:migration --no-interaction      # generování
docker compose exec -T php bin/console doctrine:migrations:migrate -n        # spuštění
docker compose exec -T php bin/console doctrine:migrations:status            # přehled

Legacy import — fresh end-to-end

Při úplně čistém startu (např. po docker compose down -v nebo po reinstalu Dockeru) je MySQL volume prázdný — legacy dump se musí jednorázově nahrát, jinak importy padnou na "table doesn't exist". Compose neudělá nic auto-loadu.

# 0) Pokud je legacy MySQL prázdný, nahraj dump:
docker compose exec -T mysql sh -c "mysql -uroot -proot old --default-character-set=utf8mb4" < slackcz_44953.sql

# 1) Nová DB:
docker compose exec -T php bin/console doctrine:database:drop --force --if-exists
docker compose exec -T php bin/console doctrine:database:create
docker compose exec -T php bin/console doctrine:migrations:migrate -n

# 2) Importy v tomhle pořadí (kvůli FK):
docker compose exec -T php bin/console app:import:lines --truncate
docker compose exec -T php bin/console app:import:users --truncate
docker compose exec -T php bin/console app:import:line-crossings --truncate
docker compose exec -T php bin/console app:import:longline-crossings --truncate

Krok 2 (jen importy) je zabalený v make legacyImport. Pro zopakování celého: dropni novou DB (krok 1), nahraj legacy dump (krok 0), pak make legacyImport.

Fotky jsou separátní krok (potřebují legacy soubory z ../old-slack-cz, ne jen DB dump):

make importLegacyPhotos   # rsync ../old-slack-cz → var/legacy-import + app:import:line-photos --truncate

Importuje cover (foto.jpg) + galerii (highline_foto) jako WebP mastery do public/uploads/line/<id>/. Orphany (soubory na disku bez DB řádku) a fotky smazaných lajn se neimportují — vyexpedují se do var/legacy-orphan-photos/ a reportnou. Detail modelu (cover, datum, GPS, normalizace) v migration.md § Line photos a architecture.md § Foto galerie — upload pipeline. Běž až po make legacyImport (fotka se váže na highline přes legacyId).

Účty / správa uživatelů

# Seznam všech uživatelů (id, email, nick, verified, active)
docker compose exec -T php bin/console app:user:list

# Filtr substringem na email/nick
docker compose exec -T php bin/console app:user:list -s pepa

# Jen neaktivovaní (isVerified=false)
docker compose exec -T php bin/console app:user:list --unverified

# Vygenerovat password-reset URL pro usera (email nebo id).
# Použij když mailer nejede (dev = Mailpit místo reálných mailů, prod beta = null://null).
# Vrátí absolutní URL — domain se bere z framework.router.default_uri (= DEFAULT_URI env).
docker compose exec -T php bin/console app:user:reset-password panda@example.com
docker compose exec -T php bin/console app:user:reset-password 42

# Grant/revoke ROLE_ADMIN
docker compose exec -T php bin/console app:admin:grant <email>
docker compose exec -T php bin/console app:admin:grant <email> --revoke

app:user:reset-password reálně zapíše ResetPasswordRequest do DB (token s lifetime z bundlu) — token je jednorázový a vyprší stejně jako kdyby přišel mailem. Workflow přes SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface, žádný hack.

Testy (PHPUnit)

tests/Controller/PublicPagesSmokeTest.php — smoke testy DB-free veřejných rout (kernel nabootuje, routing + security + Twig fungují). Běží na SQLite bez schématu, stejně jako CI workflow .github/workflows/symfony.yml — nepotřebují Postgres ani síť.

# Lokálně 1:1 jako CI (SQLite, test env):
docker compose exec -T php sh -c '
  mkdir -p data && touch data/database.sqlite
  DATABASE_URL="sqlite:///%kernel.project_dir%/data/database.sqlite" APP_ENV=test \
  vendor/bin/phpunit
'

phpunit.dist.xmlfailOnDeprecation/Notice/Warning=true → test, který spustí přímou (naši) deprecation, shodí build. Proto se do smoke testů zatím nedávají form-rendering routy (/registrace apod. — RegistrationForm má array-option constraints; viz roadmap.md). Vendor (indirect) deprecations se ignorují (ignoreIndirectDeprecations="true").

Smoke testy v terminálu

# stránka odpovídá?
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/mapa

# vidět konkrétní content
curl -s http://localhost:8000/mapa | grep -oE 'line-feed|map-panel' | sort -u

# JSON endpoint — všechny highlines pro markery
curl -s http://localhost:8000/mapa/data | head -c 500

# JSON endpoint — N posledních přechodů (tab Přechody + emoji markery na mapě)
curl -s http://localhost:8000/mapa/feed | head -c 500

# JSON endpoint — time-travel okno (-7 dní od daného data)
curl -s "http://localhost:8000/mapa/feed?date=2025-09-01&days=7" | head -c 500

# asset existuje (po importmap:require / asset()):
curl -s http://localhost:8000/$(curl -s http://localhost:8000/ | grep -oE 'assets/images/[^"]+' | head -1) -o /dev/null -w "%{http_code}\n"

Asset Mapper / Twig — známé bolístky

  • Po editaci Twig template v dev mode se občas drží stará verze v var/cache. Když se chová divně → cache:clear --env=dev.
  • Twig filtry, které NEjsou nainstalované (a budou házet 500):
    • format_datetime (potřebuje twig/intl-extra) — místo toho použij |date('j. n. Y')
    • |u.truncate(...) (potřebuje twig/string-extra) — místo toho použij CSS -webkit-line-clamp
  • Leaflet markery mají v Asset Mapperu rozbité default ikony → musí se URL předat přes Stimulus values, viz assets/controllers/map_controller.js + templates/pages/map.html.twig. Pro plnohodnotnou mapu se 254 lajnami + time-travel use map_controller; pro slim single-line view (např. detail) je line_detail_map_controller.
  • Legacy import je jednorázová lokální věc na čisté schéma. Spouští se při nahazování projektu (make legacyImport), kdy je DB prázdná — proto FK z crossings → highline nevadí (žádné crossings ještě nejsou). Když to chceš zopakovat: dropni novou DB, znovu nahraj legacy dump (adminer / make loadLegacyDump), pak make legacyImport.
  • Asset URL hashe se mění při edit → curl test musí parsovat URL z HTML, ne hardcodovat.
  • Limit posledních přechodů je centralizovaný v App\Repository\LineCrossingRepository::RECENT_LIMIT. Měň jen tam — propíše se na index page stripe, mapové emoji markery i tab Přechody na /mapa. Repo metody findRecent() a findRecentForJson() ho přebírají defaultem.

Feed (slackTV)

Prezentace: teaser panel na / + dedikovaná stránka /tv (app_tv). /tv má click-to-play (tv_controller.js, youtube-nocookie embed na klik).

# vyčistit jen feed cache (nedělej cache:clear jen kvůli feedu — to drahá operace)
docker compose exec -T php bin/console cache:pool:clear cache.app

# změnit channels/queries: edituj config/packages/feed.yaml + cache:clear
# řadit zdroj od nejstarších: místo holého id dej `{ id: '@@x', sort: asc }` (default desc)
# přidat / vyměnit YOUTUBE_API_KEY: edituj .env.local + cache:pool:clear cache.app

Quota math: každá search query = 100 units / fetch. Default cache TTL = 21600 s (6 h) (feed.cache.ttl_seconds ve feed.yaml) ⇒ 4 misses/den ⇒ 400 units/den/query. Free limit 10 000/den, takže aktuální config (1 query, 0 kanálů) jede s ~25× rezervou — prostor pro víc queries i kanálů (channel = 2 units). Detailní vzorec + historie přepalu kvóty v architecture.md § Quota economics.

Známé containery / služby co máš pod prsty

Co URL
App http://localhost:8000/
Mapa http://localhost:8000/mapa
Detail lajny http://localhost:8000/lajna/{slug} (např. cimburi-cimbuline)
Deník uživatele http://localhost:8000/denik/{id}
O projektu http://localhost:8000/o-projektu
Adminer (PG) http://localhost:8080/?pgsql=database&username=app&db=app
Adminer (MySQL legacy) http://localhost:8080/?server=mysql&username=root&db=old
Mailpit UI docker compose port mailer 8025 → otevři ten port (host port je dynamický)