Dzień 1: Uruchomienie projektu
Witaj w pierwszym rozdziale zaktualizowanej wersji legendarnego tutoriala Jobeet. Oryginalny kurs powstał dla Symfony 1.x, a jego najpopularniejsza wersja dla Symfony 4.2 — my przepisujemy go w całości na Symfony 8 / PHP 8.4, z PostgreSQL 16, Dockerem i współczesnymi dobrymi praktykami.
Dzisiaj zbudujemy kompletne środowisko deweloperskie, utworzymy projekt Symfony 8 i wyświetlimy pierwszą stronę aplikacji w przeglądarce. Na końcu będziesz mieć w pełni działający, uruchomiony projekt gotowy do dalszej rozbudowy.
Czego potrzebujesz? Całe środowisko uruchomimy w Dockerze, więc na komputerze nie musisz instalować PHP, PostgreSQL ani serwera WWW. Wystarczą cztery narzędzia opisane poniżej.
Wymagania wstępne
Zainstaluj na swoim komputerze:
| Narzędzie | Po co | Link |
|---|---|---|
| Docker + Docker Compose v2 | uruchomienie całego stacku (PHP, baza, serwer WWW) | https://www.docker.com/get-started |
| Git | kontrola wersji | https://git-scm.com/ |
| make | skróty do najczęstszych poleceń (domyślnie dostępny na macOS i Linux) | — |
| Symfony CLI | wygodne tworzenie i diagnozowanie projektów Symfony | https://symfony.com/download |
Sprawdź, czy Docker działa i czy masz Compose v2 (polecenie pisane bez myślnika — docker compose,
a nie docker-compose):
docker --version
docker compose version
Zmiana vs Symfony 4.2: w starym tutorialu używano
docker-compose(z myślnikiem, osobny program w Pythonie). Obecnie Compose jest wbudowany w Dockera jako podkomenda — używamydocker compose.
Krok 1 — Instalacja Symfony CLI
Symfony CLI to oficjalne narzędzie, które zastąpiło dawne composer create-project jako rekomendowany
sposób tworzenia nowych projektów. Instalacja zależy od systemu:
# macOS / Linux (Homebrew)
brew install symfony-cli/tap/symfony-cli
# Linux (skrypt instalacyjny)
curl -sS https://get.symfony.com/cli/installer | bash
# Windows (Scoop)
scoop install symfony-cli
Po instalacji sprawdź wersję oraz wymagania środowiska:
symfony version
symfony check:requirements
Krok 2 — Utworzenie projektu Symfony 8
Utwórz nowy projekt w katalogu jobeet, korzystając z pakietu --webapp (pełny zestaw zależności
do budowy aplikacji webowej: Twig, Doctrine, formularze, walidator, security, mailer, maker itd.):
symfony new jobeet --version="8.0.*" --webapp
cd jobeet
Zmiana vs Symfony 4.2: stary tutorial uruchamiał
composer create-project symfony/website-skeletonwewnątrz kontenera. Dziśsymfony new --webappjest oficjalną, prostszą metodą — a Symfony Flex automatycznie skonfiguruje za nas pakiety i wygeneruje pliki Dockera dla bazy i mailera.
Co właśnie się stało?
Symfony Flex podczas instalacji automatycznie dopisał do projektu fragmenty konfiguracji Dockera —
zauważysz w plikach compose.yaml i compose.override.yaml bloki oznaczone komentarzami:
###> doctrine/doctrine-bundle ###
# ... usługa bazy danych PostgreSQL
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
# ... usługa Mailpit (testowanie e-maili)
###< symfony/mailer ###
Tych bloków nie edytujemy ręcznie — zarządza nimi Flex. W kolejnych krokach dołożymy tylko własne
usługi (app, nginx) oraz Dockerfile, żeby cała aplikacja działała w kontenerach.
Krok 3 — Struktura projektu Symfony 8
Zanim ruszymy dalej, poznaj najważniejsze katalogi. Symfony 8 ma czystą, przewidywalną strukturę:
jobeet/
├── bin/ # bin/console — narzędzie CLI Symfony
├── config/ # konfiguracja (YAML) + paczki w config/packages/
│ ├── packages/
│ ├── routes/
│ └── services.yaml # konfiguracja kontenera DI
├── public/ # web root — tu jest jedyny publiczny plik: index.php
│ └── index.php
├── src/ # CAŁY Twój kod PHP (PSR-4, namespace App\)
│ ├── Controller/
│ ├── Entity/
│ └── Repository/
├── templates/ # szablony Twig
├── tests/ # testy PHPUnit
├── translations/ # pliki tłumaczeń
├── var/ # cache i logi (nie commituj)
├── vendor/ # zależności Composera (nie commituj)
├── .env # domyślne zmienne środowiskowe (commitowane)
└── composer.json
Najważniejsza zasada: Twój kod trafia do src/, a katalog public/ zawiera wyłącznie front
controller index.php. Reszta projektu jest niedostępna z przeglądarki.
Krok 4 — Środowisko Docker
Zbudujemy stack zbliżony do produkcyjnego: PHP 8.4-FPM + Nginx + PostgreSQL 16 + Mailpit.
4.1. Dockerfile
Utwórz w katalogu głównym plik Dockerfile. Użyjemy multi-stage build — wspólna baza (base)
oraz osobne warstwy dla deweloperki (dev) i produkcji (prod):
FROM php:8.4-fpm-alpine AS base
RUN apk add --no-cache \
postgresql-dev \
icu-dev \
libzip-dev \
git \
unzip \
&& docker-php-ext-install \
pdo_pgsql \
intl \
zip \
opcache
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# --- Środowisko deweloperskie ---
FROM base AS dev
COPY composer.json composer.lock symfony.lock ./
RUN composer install --no-scripts --no-autoloader --no-interaction
COPY . .
RUN composer dump-autoload \
&& composer run-script post-install-cmd --no-interaction
# --- Środowisko produkcyjne ---
FROM base AS prod
ENV APP_ENV=prod
COPY composer.json composer.lock symfony.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --no-interaction
COPY . .
RUN composer dump-autoload --optimize \
&& composer run-script post-install-cmd --no-interaction \
&& php bin/console cache:warmup
Zmiana vs Symfony 4.2: stary tutorial pobierał gotowy ZIP z konfiguracją z phpdocker.io (PHP 7.2, MySQL 5.7). Tutaj piszemy własny, minimalny
Dockerfilena PHP 8.4 — bez Node.js, bo do obsługi assetów Symfony używa dziś AssetMappera (o tym w kolejnych dniach).
4.2. Konfiguracja Nginx
Utwórz plik docker/nginx/default.conf:
server {
listen 80;
server_name localhost;
root /var/www/html/public;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass app:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location ~ \.php$ {
return 404;
}
}
Nginx serwuje statyczne pliki z public/, a żądania PHP przekazuje do kontenera app na porcie 9000.
4.3. compose.yaml
Plik compose.yaml już istnieje (Flex dopisał blok bazy danych). Uzupełnij go o usługi app i nginx,
tak aby całość wyglądała następująco:
services:
app:
build:
context: .
target: dev
environment:
DATABASE_URL: postgresql://app:!ChangeMe!@database:5432/app?serverVersion=16&charset=utf8
MAILER_DSN: smtp://mailer:1025
depends_on:
database:
condition: service_healthy
nginx:
image: nginx:alpine
depends_on:
- app
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
###> doctrine/doctrine-bundle ###
database:
image: postgres:${POSTGRES_VERSION:-16}-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
# Hasło zmień koniecznie na produkcji
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app}
healthcheck:
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
timeout: 5s
retries: 5
start_period: 60s
volumes:
- database_data:/var/lib/postgresql/data:rw
###< doctrine/doctrine-bundle ###
volumes:
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###
Zwróć uwagę na depends_on z condition: service_healthy — dzięki healthcheck aplikacja poczeka,
aż baza będzie naprawdę gotowa do przyjmowania połączeń.
4.4. compose.override.yaml
Plik nadpisujący ustawienia tylko dla deweloperki (mapowanie kodu jako volume, porty, Mailpit).
Flex dopisał tu już blok bazy i mailera — uzupełnij go o app i nginx:
services:
app:
volumes:
- .:/var/www/html
- /var/www/html/vendor
nginx:
ports:
- "8090:80"
volumes:
- ./public:/var/www/html/public:ro
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
###> doctrine/doctrine-bundle ###
database:
ports:
- "5432:5432"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
mailer:
image: axllent/mailpit
ports:
- "1025:1025"
- "8025:8025"
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
###< symfony/mailer ###
Wolumen .:/var/www/html montuje Twój kod do kontenera — zmiany w plikach są widoczne natychmiast,
bez przebudowy obrazu. Anonimowy wolumen /var/www/html/vendor chroni katalog vendor/ z kontenera
przed nadpisaniem przez (potencjalnie pusty) vendor/ z hosta.
Zmiana vs Symfony 4.2: zamiast MailHog używamy Mailpit — nowocześniejszego, aktywnie rozwijanego narzędzia do przechwytywania e-maili w środowisku dev.
4.5. .dockerignore
Aby nie kopiować do obrazu śmieci, utwórz plik .dockerignore:
/.git
/var
/vendor
/node_modules
.env.local
Krok 5 — Makefile ze skrótami
Codzienne polecenia warto schować za prostymi skrótami. Utwórz plik Makefile
(uwaga: wcięcia w Makefile muszą być znakami TAB, nie spacjami):
.DEFAULT_GOAL := help
.PHONY: help start fast stop db-init console check
help: ## Wyświetla dostępne polecenia
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
start: ## Buduje obrazy i uruchamia wszystkie kontenery
docker compose up -d --build
fast: ## Szybki start bez przebudowy obrazów
docker compose up -d --no-build
stop: ## Zatrzymuje kontenery i usuwa wolumeny (czyści bazę!)
docker compose down -v
db-init: ## Migracje + dane testowe (po make stop)
docker compose exec app php bin/console doctrine:migrations:migrate --no-interaction
console: ## Wejście do kontenera app z powłoką
docker compose exec app sh
Teraz zamiast długich poleceń wystarczy make start, make stop itd. Listę zobaczysz przez make help.
Krok 6 — Plik .env i środowiska Symfony
Symfony rozróżnia środowiska (ang. environments): dev, prod i test. Otwórz plik .env
w katalogu głównym — znajdziesz w nim m.in.:
APP_ENV=dev
APP_SECRET=...
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
MAILER_DSN=smtp://localhost:1025
dev— środowisko deweloperskie: wyświetla błędy, ostrzeżenia i pasek debugowania.prod— środowisko produkcyjne: zoptymalizowane, bez szczegółowych komunikatów o błędach.test— używane przy uruchamianiu testów automatycznych.
W naszym stacku Dockera zmienne DATABASE_URL i MAILER_DSN są nadpisywane w compose.yaml
(host database i mailer to nazwy usług w sieci Dockera, a nie 127.0.0.1).
Dobra praktyka: plik
.envjest commitowany i zawiera bezpieczne wartości domyślne. Lokalne nadpisania (np. inne hasło) umieszczaj w.env.local, którego Git ignoruje.
Krok 7 — Pierwsze uruchomienie
Zbuduj obrazy i wystartuj kontenery:
make start
To polecenie:
- buduje obraz
appzDockerfile(warstwadev), - pobiera obrazy Nginx, PostgreSQL i Mailpit,
- uruchamia wszystkie kontenery w tle (
-d= detached).
Sprawdź status — wszystkie usługi powinny mieć stan running (a baza healthy):
docker compose ps
Zainicjuj bazę danych (na razie nie mamy jeszcze migracji — polecenie po prostu przygotuje schemat migracji; encje dodamy w Dniu 3):
make db-init
Otwórz w przeglądarce http://localhost:8090. Zobaczysz domyślną stronę powitalną Symfony — to znaczy, że aplikacja działa! 🎉
Nie utworzyliśmy jeszcze żadnej trasy
/, więc Symfony w środowiskudevpokazuje przyjazną stronę powitalną. Zaraz zastąpimy ją własną stroną Jobeet.
Krok 8 — Pierwsza własna strona
Czas na namacalny efekt — własny kontroler i trasę. Wygeneruj szkielet kontrolera komendą MakerBundle
(dostępną dzięki pakietowi --webapp):
docker compose exec app php bin/console make:controller JobController
Maker utworzy src/Controller/JobController.php oraz szablon templates/job/index.html.twig.
Otwórz kontroler i zmień go tak, aby obsługiwał stronę główną (/):
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class JobController extends AbstractController
{
#[Route('/', name: 'job_list')]
public function list(): Response
{
return $this->render('job/list.html.twig');
}
}
Zmiana vs Symfony 4.2: trasy definiujemy dziś wyłącznie przez atrybuty PHP (
#[Route(...)]), a nie przez adnotacje w docblockach (/** @Route */) ani pliki YAML. Atrybut#[Route]jest natywną częścią PHP 8 — nie potrzebujemy jużSensioFrameworkExtraBundle.
Utwórz szablon templates/job/list.html.twig:
{% extends 'base.html.twig' %}
{% block title %}Jobeet — oferty pracy{% endblock %}
{% block body %}
<h1>Witaj w Jobeet!</h1>
<p>Twoja aplikacja Symfony 8 działa poprawnie.</p>
<p>W kolejnych dniach zbudujemy tu portal z ogłoszeniami o pracę.</p>
{% endblock %}
Szablon base.html.twig (z którego dziedziczymy przez extends) został utworzony automatycznie
przy instalacji Twiga. Odśwież http://localhost:8090 — zamiast strony powitalnej Symfony
zobaczysz teraz własną stronę „Witaj w Jobeet!”.
Krok 9 — Web Debug Toolbar i Profiler
W środowisku dev na dole strony pojawia się Web Debug Toolbar — najlepszy przyjaciel dewelopera.
Pokazuje czas wykonania, zużycie pamięci, liczbę zapytań SQL, trasę, zalogowanego użytkownika i wiele
więcej.
Kliknięcie dowolnej sekcji paska otwiera Profiler — szczegółowy podgląd żądania. Możesz go też otworzyć bezpośrednio: http://localhost:8090/_profiler.
Z paska skorzystasz na każdym kroku tutoriala — np. do diagnozowania zapytań do bazy.
Krok 10 — Konsola Symfony
Symfony dostarcza potężne narzędzie wiersza poleceń bin/console. Wszystkie komendy uruchamiamy
wewnątrz kontenera app:
# Lista wszystkich dostępnych komend
docker compose exec app php bin/console list
# Informacje o aplikacji i wersjach
docker compose exec app php bin/console about
# Wszystkie zarejestrowane trasy (znajdziesz tu naszą job_list)
docker compose exec app php bin/console debug:router
W wyniku debug:router zobaczysz swoją trasę:
------------ -------- -------- ------ -----
Name Method Scheme Host Path
------------ -------- -------- ------ -----
job_list ANY ANY ANY /
------------ -------- -------- ------ -----
Wskazówka: jeśli wykonujesz wiele komend, wejdź raz do kontenera (
make console) i wpisuj samophp bin/console ...bez przedrostkadocker compose exec app.
(Opcjonalnie) Narzędzia jakości kodu
Współczesny projekt Symfony pilnuje jakości kodu automatycznie. W kolejnych dniach będziemy korzystać z:
- PHPStan — statyczna analiza kodu (wykrywa błędy bez uruchamiania),
- PHP CS Fixer — automatyczne formatowanie zgodnie ze standardami Symfony / PER-CS,
- Rector — automatyczne refaktoryzacje i modernizacja kodu,
- PHPUnit — testy jednostkowe i funkcjonalne.
Na razie wystarczy wiedzieć, że istnieją — skonfigurujemy je, gdy pojawi się pierwszy realny kod do sprawdzenia.
Rozwiązywanie problemów
Port 8090 jest zajęty
Inna aplikacja używa tego portu. W compose.override.yaml zmień mapowanie usługi nginx, np. na
"8095:80", i otwórz aplikację pod http://localhost:8095.
Aplikacja nie łączy się z bazą
Upewnij się, że kontener bazy ma stan healthy (docker compose ps). Przy pierwszym starcie
PostgreSQL potrzebuje kilkudziesięciu sekund na inicjalizację — stąd start_period: 60s w healthcheck.
Zmiany w compose.yaml lub Dockerfile nie działają
Przebuduj obrazy i zrestartuj stack:
make stop && make start
Podsumowanie
Gratulacje! Masz w pełni działające, nowoczesne środowisko deweloperskie:
- ✅ stack Docker: PHP 8.4-FPM, Nginx, PostgreSQL 16, Mailpit,
- ✅ projekt Symfony 8 utworzony przez
symfony new --webapp, - ✅ aplikacja dostępna pod http://localhost:8090,
- ✅ pierwsza własna strona z kontrolerem i trasą opartą o atrybut
#[Route], - ✅ działający Web Debug Toolbar, Profiler i konsola Symfony.
W Dniu 2 zdefiniujemy, czym dokładnie jest Jobeet: poznamy aktorów systemu, user stories i zaprojektujemy model danych portalu z ogłoszeniami o pracę.