1

Dzień 1 z 18

Uruchomienie projektu

Opublikowany

Zaczynamy od zera. Skonfigurujemy kompletne środowisko deweloperskie oparte na Dockerze z PHP 8.4-FPM, Nginx, PostgreSQL 16 i Mailpit. Zainstalujemy Symfony 8 z pełnym pakietem webapp i uruchomimy projekt w przeglądarce.

Czego się nauczysz

  • Instalacja Symfony CLI i wymagania systemowe
  • Utworzenie projektu przez `symfony new --webapp`
  • Struktura projektu Symfony 8 — co i gdzie
  • Docker: Dockerfile (multi-stage), Nginx, PostgreSQL 16, Mailpit
  • Plik .env i konfiguracja środowisk (dev/prod/test)
  • Polecenia Makefile: start, fast, stop, db-init
  • Pierwsze uruchomienie — http://localhost:8090
  • Pierwsza strona: kontroler i trasa #[Route]
  • Web Debug Toolbar, Profiler i konsola bin/console

Co zmieniło się w Symfony 8 vs 4.2

  • PHP 8.4 zamiast PHP 7.2 — wymagane przez Symfony 8
  • PostgreSQL 16 zamiast MySQL 5.7
  • Mailpit zamiast MailHog — nowocześniejszy interfejs testowania maili
  • Symfony CLI (`symfony new`) zamiast `composer create-project`
  • AssetMapper zamiast Webpack Encore — brak Node.js
  • Atrybut #[Route] zamiast adnotacji DocBlock i konfiguracji YAML
  • Docker Compose v2 (brak myślnika w poleceniach: `docker compose`)

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żywamy docker 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-skeleton wewnątrz kontenera. Dziś symfony new --webapp jest 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 Dockerfile na 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 .env jest 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 app z Dockerfile (warstwa dev),
  • 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 środowisku dev pokazuje 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 samo php bin/console ... bez przedrostka docker 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ę.

Narzędzia / paczki

Symfony CLI Docker Compose v2 PHP 8.4-FPM PostgreSQL 16 Mailpit MakerBundle AssetMapper

Spis treści