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
dockergroup (jinak každýdockercommand 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žítSUM(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 -vnebo 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-passwordreálně zapíšeResetPasswordRequestdo DB (token s lifetime z bundlu) — token je jednorázový a vyprší stejně jako kdyby přišel mailem. Workflow přesSymfonyCasts\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.xmlmáfailOnDeprecation/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 (/registraceapod. —RegistrationFormmá array-option constraints; vizroadmap.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řebujetwig/intl-extra) — místo toho použij|date('j. n. Y')|u.truncate(...)(potřebujetwig/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 usemap_controller; pro slim single-line view (např. detail) jeline_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), pakmake 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 metodyfindRecent()afindRecentForJson()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ý) |