Przejdź do treści
Backend

Symfony - Architektura enterprise w PHP na najwyższym poziomie

Opublikowano:
·5 min czytania·Autor: MDS Software Solutions Group

Symfony Architektura enterprise

backend

Symfony - Architektura enterprise w PHP na najwyższym poziomie

Symfony to jeden z najbardziej dojrzałych i wszechstronnych frameworków PHP, który od ponad dekady wyznacza standardy w budowie aplikacji klasy enterprise. Wykorzystywany przez gigantów takich jak BlaBlaCar, Spotify (mikroserwisy) czy Trivago, Symfony oferuje solidne fundamenty architektoniczne, które pozwalają tworzyć skalowalne, testowalne i łatwe w utrzymaniu aplikacje. W tym artykule szczegółowo omówimy kluczowe elementy architektury Symfony i pokażemy, dlaczego jest to najlepszy wybór dla wymagających projektów PHP.

Czym jest Symfony i jaka jest jego filozofia?#

Symfony to framework PHP stworzony przez firmę SensioLabs w 2005 roku. W odróżnieniu od wielu frameworków, które stawiają na prostotę kosztem elastyczności, Symfony opiera się na kilku kluczowych zasadach:

  • Standardy ponad konwencje - Symfony ściśle przestrzega standardów PHP-FIG (PSR-4, PSR-7, PSR-11, PSR-15)
  • Komponentowość - framework składa się z ponad 50 niezależnych komponentów wielokrotnego użytku
  • Stabilność i przewidywalność - jasna polityka wersjonowania (LTS co 2 lata), gwarancja kompatybilności wstecznej
  • Wydajność - zaawansowane mechanizmy cache'owania i kompilacji kontenera
  • Testowalność - architektura ułatwiająca pisanie testów jednostkowych i integracyjnych

Symfony nie narzuca jedynego słusznego sposobu rozwiązywania problemów. Zamiast tego dostarcza potężne narzędzia, które programista może komponować zgodnie z wymaganiami projektu.

System Bundle - modularność i reużywalność#

Bundle to podstawowa jednostka organizacji kodu w Symfony. Każdy bundle jest samodzielnym pakietem zawierającym kontrolery, serwisy, szablony, konfigurację i zasoby statyczne.

Tworzenie własnego Bundle#

// src/Invoice/InvoiceBundle.php
namespace App\Invoice;

use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class InvoiceBundle extends AbstractBundle
{
    public function loadExtension(
        array $config,
        ContainerConfigurator $container,
        ContainerBuilder $builder
    ): void {
        $container->import('../config/services.yaml');

        $container->services()
            ->set(InvoiceGenerator::class)
            ->arg('$vatRate', $config['vat_rate'] ?? 23)
            ->tag('app.invoice_generator');
    }

    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->rootNode()
            ->children()
                ->floatNode('vat_rate')->defaultValue(23)->end()
                ->scalarNode('template_dir')->defaultValue('invoices')->end()
            ->end();
    }
}

Bundle można łatwo przenosić między projektami i publikować jako pakiety Composera. Społeczność Symfony utrzymuje tysiące bundle'i do każdego zastosowania - od generowania PDF-ów po integrację z systemami płatności.

Dependency Injection Container - serce Symfony#

Kontener Dependency Injection (DIC) to absolutne serce architektury Symfony. To on zarządza tworzeniem, konfiguracją i wstrzykiwaniem zależności we wszystkich serwisach aplikacji.

Autowiring i konfiguracja serwisów#

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    App\Service\PaymentProcessor:
        arguments:
            $apiKey: '%env(PAYMENT_API_KEY)%'
            $sandbox: '%env(bool:PAYMENT_SANDBOX)%'
        tags:
            - { name: 'app.payment', priority: 10 }

Zaawansowane serwisy z interfejsami#

// src/Service/NotificationService.php
namespace App\Service;

use App\Contract\NotifierInterface;
use Psr\Log\LoggerInterface;

class NotificationService
{
    public function __construct(
        private readonly iterable $notifiers, // autowired tagged services
        private readonly LoggerInterface $logger,
        private readonly string $defaultChannel = 'email',
    ) {}

    public function notify(User $user, string $message): void
    {
        foreach ($this->notifiers as $notifier) {
            if ($notifier->supports($this->defaultChannel)) {
                try {
                    $notifier->send($user, $message);
                    $this->logger->info('Notification sent', [
                        'channel' => $notifier->getChannel(),
                        'user' => $user->getId(),
                    ]);
                } catch (\Throwable $e) {
                    $this->logger->error('Notification failed', [
                        'error' => $e->getMessage(),
                    ]);
                }
            }
        }
    }
}

Compiler Pass dla zaawansowanej konfiguracji#

// src/DependencyInjection/Compiler/NotifierPass.php
namespace App\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class NotifierPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        if (!$container->has(NotificationService::class)) {
            return;
        }

        $definition = $container->findDefinition(NotificationService::class);
        $taggedServices = $container->findTaggedServiceIds('app.notifier');

        foreach ($taggedServices as $id => $tags) {
            $definition->addMethodCall('addNotifier', [new Reference($id)]);
        }
    }
}

Kontener DI Symfony jest kompilowany do czystego kodu PHP w środowisku produkcyjnym, co sprawia, że overhead wstrzykiwania zależności jest praktycznie zerowy.

Doctrine ORM - zaawansowane zarządzanie danymi#

Doctrine ORM to domyślna warstwa persystencji w Symfony, oferująca pełnoprawne mapowanie obiektowo-relacyjne z obsługą migracji, cache'owania i zaawansowanych zapytań.

Definiowanie encji z atrybutami PHP 8#

// src/Entity/Order.php
namespace App\Entity;

use App\Repository\OrderRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity(repositoryClass: OrderRepository::class)]
#[ORM\Table(name: 'orders')]
#[ORM\Index(columns: ['status', 'created_at'], name: 'idx_order_status_date')]
#[ORM\HasLifecycleCallbacks]
class Order
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'orders')]
    #[ORM\JoinColumn(nullable: false)]
    private Customer $customer;

    #[ORM\OneToMany(
        mappedBy: 'order',
        targetEntity: OrderItem::class,
        cascade: ['persist', 'remove'],
        orphanRemoval: true
    )]
    #[Assert\Count(min: 1, minMessage: 'Order must have at least one item')]
    private Collection $items;

    #[ORM\Column(length: 20)]
    #[Assert\Choice(choices: ['new', 'paid', 'shipped', 'delivered', 'cancelled'])]
    private string $status = 'new';

    #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2)]
    private string $totalAmount = '0.00';

    #[ORM\Column]
    private \DateTimeImmutable $createdAt;

    #[ORM\Column(nullable: true)]
    private ?\DateTimeImmutable $paidAt = null;

    public function __construct()
    {
        $this->items = new ArrayCollection();
        $this->createdAt = new \DateTimeImmutable();
    }

    public function addItem(OrderItem $item): self
    {
        if (!$this->items->contains($item)) {
            $this->items->add($item);
            $item->setOrder($this);
            $this->recalculateTotal();
        }
        return $this;
    }

    #[ORM\PreUpdate]
    public function onPreUpdate(): void
    {
        $this->recalculateTotal();
    }

    private function recalculateTotal(): void
    {
        $total = '0.00';
        foreach ($this->items as $item) {
            $total = bcadd($total, $item->getSubtotal(), 2);
        }
        $this->totalAmount = $total;
    }
}

Repozytorium z QueryBuilder#

// src/Repository/OrderRepository.php
namespace App\Repository;

use App\Entity\Order;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;

class OrderRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Order::class);
    }

    public function findRecentByCustomer(
        int $customerId,
        int $limit = 10
    ): array {
        return $this->createQueryBuilder('o')
            ->andWhere('o.customer = :customerId')
            ->andWhere('o.status != :cancelled')
            ->setParameter('customerId', $customerId)
            ->setParameter('cancelled', 'cancelled')
            ->orderBy('o.createdAt', 'DESC')
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

    public function getMonthlyRevenue(\DateTimeImmutable $month): string
    {
        $start = $month->modify('first day of this month midnight');
        $end = $month->modify('last day of this month 23:59:59');

        $result = $this->createQueryBuilder('o')
            ->select('SUM(o.totalAmount) as revenue')
            ->andWhere('o.paidAt BETWEEN :start AND :end')
            ->andWhere('o.status IN (:statuses)')
            ->setParameter('start', $start)
            ->setParameter('end', $end)
            ->setParameter('statuses', ['paid', 'shipped', 'delivered'])
            ->getQuery()
            ->getSingleScalarResult();

        return $result ?? '0.00';
    }
}

Migracje bazy danych#

# Generowanie migracji na podstawie zmian w encjach
php bin/console doctrine:migrations:diff

# Wykonanie migracji
php bin/console doctrine:migrations:migrate

# Wycofanie ostatniej migracji
php bin/console doctrine:migrations:migrate prev
// migrations/Version20250228120000.php
declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20250228120000 extends AbstractMigration
{
    public function up(Schema $schema): void
    {
        $this->addSql('CREATE TABLE orders (
            id INT AUTO_INCREMENT NOT NULL,
            customer_id INT NOT NULL,
            status VARCHAR(20) NOT NULL DEFAULT "new",
            total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
            created_at DATETIME NOT NULL COMMENT "(DC2Type:datetime_immutable)",
            paid_at DATETIME DEFAULT NULL COMMENT "(DC2Type:datetime_immutable)",
            INDEX idx_order_status_date (status, created_at),
            PRIMARY KEY(id)
        ) DEFAULT CHARACTER SET utf8mb4');
    }

    public function down(Schema $schema): void
    {
        $this->addSql('DROP TABLE orders');
    }
}

Symfony Flex i system receptur#

Symfony Flex to wtyczka Composera, która automatyzuje konfigurację pakietów. Kiedy instalujesz bundle, Flex automatycznie:

  • Tworzy pliki konfiguracyjne
  • Dodaje wpisy do bundles.php
  • Generuje zmienne środowiskowe w .env
  • Tworzy potrzebne katalogi
# Instalacja z automatyczną konfiguracją
composer require doctrine/orm
composer require security
composer require messenger
composer require api-platform/api-pack

# Aliasy Flex
composer require mailer    # symfony/mailer
composer require twig      # symfony/twig-bundle
composer require debug     # symfony/debug-bundle

Receptury (recipes) są przechowywane w oficjalnym repozytorium i mogą być tworzone przez społeczność. System ten eliminuje potrzebę ręcznej konfiguracji i znacząco przyspiesza start nowego projektu.

System zdarzeń i listenery#

Symfony implementuje wzorzec Mediator poprzez potężny system zdarzeń. Pozwala to na luźne powiązanie komponentów i łatwe rozszerzanie funkcjonalności.

Definiowanie i nasłuchiwanie zdarzeń#

// src/Event/OrderPlacedEvent.php
namespace App\Event;

use App\Entity\Order;
use Symfony\Contracts\EventDispatcher\Event;

class OrderPlacedEvent extends Event
{
    public const NAME = 'order.placed';

    public function __construct(
        private readonly Order $order
    ) {}

    public function getOrder(): Order
    {
        return $this->order;
    }
}
// src/EventListener/OrderNotificationListener.php
namespace App\EventListener;

use App\Event\OrderPlacedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

#[AsEventListener(event: OrderPlacedEvent::NAME, priority: 10)]
class OrderNotificationListener
{
    public function __construct(
        private readonly MailerInterface $mailer,
    ) {}

    public function __invoke(OrderPlacedEvent $event): void
    {
        $order = $event->getOrder();

        $email = (new Email())
            ->to($order->getCustomer()->getEmail())
            ->subject(sprintf('Order #%d confirmed', $order->getId()))
            ->html($this->renderTemplate($order));

        $this->mailer->send($email);
    }
}
// src/EventListener/OrderInventoryListener.php
namespace App\EventListener;

use App\Event\OrderPlacedEvent;
use App\Service\InventoryService;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: OrderPlacedEvent::NAME, priority: 5)]
class OrderInventoryListener
{
    public function __construct(
        private readonly InventoryService $inventory,
    ) {}

    public function __invoke(OrderPlacedEvent $event): void
    {
        foreach ($event->getOrder()->getItems() as $item) {
            $this->inventory->decreaseStock(
                $item->getProduct(),
                $item->getQuantity()
            );
        }
    }
}

Zdarzenia Symfony obsługują priorytety, propagację i asynchroniczne przetwarzanie w połączeniu z komponentem Messenger.

Komponent Security - uwierzytelnianie i autoryzacja#

System bezpieczeństwa Symfony to jeden z najbardziej rozbudowanych i elastycznych systemów uwierzytelniania i autoryzacji w ekosystemie PHP.

Konfiguracja Firewalla#

# config/packages/security.yaml
security:
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
            algorithm: auto
            cost: 13

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        api:
            pattern: ^/api
            stateless: true
            jwt: ~
        main:
            lazy: true
            provider: app_user_provider
            custom_authenticator: App\Security\LoginFormAuthenticator
            login_throttling:
                max_attempts: 5
                interval: '15 minutes'
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 604800
            logout:
                path: app_logout

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/api/admin, roles: ROLE_API_ADMIN }
        - { path: ^/api, roles: ROLE_API_USER }
        - { path: ^/profile, roles: ROLE_USER }

Custom Voter dla autoryzacji#

// src/Security/Voter/OrderVoter.php
namespace App\Security\Voter;

use App\Entity\Order;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class OrderVoter extends Voter
{
    public const VIEW = 'ORDER_VIEW';
    public const EDIT = 'ORDER_EDIT';
    public const CANCEL = 'ORDER_CANCEL';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::VIEW, self::EDIT, self::CANCEL])
            && $subject instanceof Order;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token
    ): bool {
        $user = $token->getUser();
        if (!$user instanceof User) {
            return false;
        }

        /** @var Order $order */
        $order = $subject;

        return match($attribute) {
            self::VIEW => $this->canView($order, $user),
            self::EDIT => $this->canEdit($order, $user),
            self::CANCEL => $this->canCancel($order, $user),
            default => false,
        };
    }

    private function canView(Order $order, User $user): bool
    {
        return $order->getCustomer() === $user
            || in_array('ROLE_ADMIN', $user->getRoles());
    }

    private function canEdit(Order $order, User $user): bool
    {
        return $order->getCustomer() === $user
            && $order->getStatus() === 'new';
    }

    private function canCancel(Order $order, User $user): bool
    {
        return $order->getCustomer() === $user
            && in_array($order->getStatus(), ['new', 'paid']);
    }
}

Użycie w kontrolerze:

#[Route('/order/{id}/cancel', methods: ['POST'])]
public function cancel(Order $order): Response
{
    $this->denyAccessUnlessGranted(OrderVoter::CANCEL, $order);

    $order->setStatus('cancelled');
    $this->entityManager->flush();

    return $this->redirectToRoute('order_list');
}

Symfony Messenger - asynchroniczne przetwarzanie#

Messenger to komponent Symfony do obsługi wiadomości asynchronicznych. Obsługuje transporty takie jak RabbitMQ, Amazon SQS, Redis i Doctrine.

Definiowanie wiadomości i handlera#

// src/Message/GenerateInvoice.php
namespace App\Message;

class GenerateInvoice
{
    public function __construct(
        private readonly int $orderId,
        private readonly string $format = 'pdf',
    ) {}

    public function getOrderId(): int
    {
        return $this->orderId;
    }

    public function getFormat(): string
    {
        return $this->format;
    }
}
// src/MessageHandler/GenerateInvoiceHandler.php
namespace App\MessageHandler;

use App\Message\GenerateInvoice;
use App\Service\InvoiceGenerator;
use App\Repository\OrderRepository;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class GenerateInvoiceHandler
{
    public function __construct(
        private readonly OrderRepository $orderRepository,
        private readonly InvoiceGenerator $invoiceGenerator,
    ) {}

    public function __invoke(GenerateInvoice $message): void
    {
        $order = $this->orderRepository->find($message->getOrderId());

        if (!$order) {
            throw new \RuntimeException(
                sprintf('Order #%d not found', $message->getOrderId())
            );
        }

        $this->invoiceGenerator->generate($order, $message->getFormat());
    }
}

Konfiguracja transportów#

# config/packages/messenger.yaml
framework:
    messenger:
        failure_transport: failed

        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                retry_strategy:
                    max_retries: 3
                    delay: 1000
                    multiplier: 2

            failed:
                dsn: 'doctrine://default?queue_name=failed'

            async_priority_high:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queues:
                        high_priority: ~

        routing:
            App\Message\GenerateInvoice: async
            App\Message\SendNotification: async_priority_high
            App\Message\ProcessPayment: async_priority_high
# Uruchamianie konsumenta
php bin/console messenger:consume async async_priority_high -vv

# Obsługa nieudanych wiadomości
php bin/console messenger:failed:show
php bin/console messenger:failed:retry

API Platform - budowa REST i GraphQL API#

API Platform to najszybszy sposób na zbudowanie profesjonalnego API w Symfony. Automatycznie generuje endpointy CRUD, dokumentację OpenAPI, paginację i filtrowanie.

Konfiguracja zasobu API#

// src/Entity/Product.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;

#[ORM\Entity]
#[ApiResource(
    operations: [
        new GetCollection(paginationItemsPerPage: 20),
        new Get(),
        new Post(security: "is_granted('ROLE_ADMIN')"),
        new Put(security: "is_granted('ROLE_ADMIN')"),
        new Delete(security: "is_granted('ROLE_SUPER_ADMIN')"),
    ],
    normalizationContext: ['groups' => ['product:read']],
    denormalizationContext: ['groups' => ['product:write']],
)]
#[ApiFilter(SearchFilter::class, properties: [
    'name' => 'partial',
    'category.name' => 'exact',
])]
#[ApiFilter(RangeFilter::class, properties: ['price'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'price', 'createdAt'])]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups(['product:read'])]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Groups(['product:read', 'product:write'])]
    #[Assert\NotBlank]
    #[Assert\Length(min: 3, max: 255)]
    private string $name;

    #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2)]
    #[Groups(['product:read', 'product:write'])]
    #[Assert\Positive]
    private string $price;

    #[ORM\Column(type: Types::TEXT, nullable: true)]
    #[Groups(['product:read', 'product:write'])]
    private ?string $description = null;
}

API Platform generuje automatycznie dokumentację Swagger/OpenAPI, obsługę JSON-LD, HAL, paginację i GraphQL. Wystarczy jedna komenda composer require api, aby uzyskać w pełni funkcjonalne API.

Twig - system szablonów#

Twig to szybki, bezpieczny i elastyczny silnik szablonów stworzony specjalnie dla Symfony. Automatycznie escapuje dane, zapobiegając atakom XSS.

{# templates/order/show.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}Order #{{ order.id }}{% endblock %}

{% block body %}
<div class="order-details">
    <h1>Order #{{ order.id }}</h1>
    <p class="status status--{{ order.status }}">
        {{ order.status|trans }}
    </p>

    <table class="items-table">
        <thead>
            <tr>
                <th>{{ 'product'|trans }}</th>
                <th>{{ 'quantity'|trans }}</th>
                <th>{{ 'price'|trans }}</th>
                <th>{{ 'subtotal'|trans }}</th>
            </tr>
        </thead>
        <tbody>
        {% for item in order.items %}
            <tr>
                <td>{{ item.product.name }}</td>
                <td>{{ item.quantity }}</td>
                <td>{{ item.price|format_currency('PLN') }}</td>
                <td>{{ item.subtotal|format_currency('PLN') }}</td>
            </tr>
        {% else %}
            <tr>
                <td colspan="4">{{ 'no_items'|trans }}</td>
            </tr>
        {% endfor %}
        </tbody>
        <tfoot>
            <tr>
                <td colspan="3"><strong>{{ 'total'|trans }}</strong></td>
                <td><strong>{{ order.totalAmount|format_currency('PLN') }}</strong></td>
            </tr>
        </tfoot>
    </table>

    {% if is_granted('ORDER_CANCEL', order) %}
        <form method="post" action="{{ path('order_cancel', {id: order.id}) }}">
            <input type="hidden" name="_token"
                   value="{{ csrf_token('cancel' ~ order.id) }}">
            <button type="submit" class="btn btn--danger">
                {{ 'cancel_order'|trans }}
            </button>
        </form>
    {% endif %}
</div>
{% endblock %}

Wydajność - HTTP Cache, Varnish i OPcache#

Symfony oferuje wbudowane mechanizmy optymalizacji wydajności na wielu poziomach.

HTTP Cache z ESI#

// src/Controller/ProductController.php
use Symfony\Component\HttpFoundation\Response;

#[Route('/products/{id}')]
public function show(Product $product): Response
{
    $response = $this->render('product/show.html.twig', [
        'product' => $product,
    ]);

    // Cache na 1 godzinę
    $response->setSharedMaxAge(3600);
    $response->headers->addCacheControlDirective('must-revalidate');

    // ETag dla warunkowych żądań
    $response->setEtag(md5($response->getContent()));
    $response->isNotModified($this->getRequest());

    return $response;
}

Konfiguracja Varnish#

# config/packages/framework.yaml
framework:
    http_cache:
        enabled: true
    esi:
        enabled: true
    fragments:
        path: /_fragment

# Twig ESI
# {{ render_esi(controller('App\\Controller\\SidebarController::popular')) }}

Optymalizacja produkcyjna#

# Wyczyść i rozgrzej cache
php bin/console cache:clear --env=prod
php bin/console cache:warmup --env=prod

# Kompiluj kontener DI
php bin/console container:dump --env=prod

# Dump autoloadera Composera
composer dump-autoload --optimize --classmap-authoritative

# OPcache preloading (php.ini)
# opcache.preload=/path/to/project/config/preload.php
# opcache.preload_user=www-data

Dzięki Symfony Reverse Proxy (lub Varnish), ESI fragments i prawidłowej konfiguracji OPcache, aplikacje Symfony mogą obsługiwać tysiące żądań na sekundę na pojedynczym serwerze.

Kiedy wybrać Symfony zamiast Laravel?#

To pytanie pojawia się regularnie w społeczności PHP. Oto kluczowe czynniki decyzyjne:

| Kryterium | Symfony | Laravel | |-----------|---------|---------| | Złożoność projektu | Enterprise, duże zespoły | MVP, małe/średnie projekty | | Elastyczność | Pełna kontrola nad architekturą | Konwencja ponad konfiguracją | | Krzywa uczenia | Stroma, ale solidne fundamenty | Łagodna, szybki start | | Wydajność | Wyższa po optymalizacji | Wystarczająca dla większości | | Ekosystem | Komponenty, standardy PSR | Bogaty ekosystem pakietów | | Long-term support | Gwarantowany LTS | Mniej formalny LTS | | Testowalność | Wbudowana w architekturę | Dobra, ale mniej rygorystyczna |

Wybierz Symfony, gdy:

  • Budujesz system enterprise z długim cyklem życia
  • Potrzebujesz pełnej kontroli nad architekturą
  • Pracujesz w dużym zespole z wymaganiami dotyczącymi standardów
  • Korzystasz z DDD lub heksagonalnej architektury
  • Wymagasz gwarantowanej kompatybilności wstecznej (LTS)
  • Budujesz mikroserwisy (komponenty Symfony doskonale się sprawdzają samodzielnie)

Wybierz Laravel, gdy:

  • Potrzebujesz szybkiego prototypowania
  • Budujesz MVP lub mniejszą aplikację
  • Priorytetem jest szybkość developmentu nad strukturą
  • Potrzebujesz dużo gotowych rozwiązań out-of-the-box

Warto dodać, że Laravel sam korzysta z wielu komponentów Symfony (HttpFoundation, Console, Routing, EventDispatcher), co świadczy o jakości architektury Symfony.

Podsumowanie#

Symfony to framework dla programistów, którzy cenią:

  • Architekturę - czyste separowanie odpowiedzialności, DI, SOLID
  • Stabilność - gwarantowana kompatybilność wsteczna, LTS
  • Wydajność - kompilowany kontener, HTTP cache, preloading
  • Testowalność - mockowanie serwisów, functional testing
  • Ekosystem - ponad 50 niezależnych komponentów, tysiące bundle'i
  • Standardy - pełna zgodność z PSR, interoperacyjność

Symfony udowadnia, że PHP jest w pełni zdolne do obsługi najbardziej wymagających aplikacji enterprise, dorównując frameworkom z ekosystemu Java czy .NET.

Potrzebujesz wsparcia z Symfony?#

W MDS Software Solutions Group specjalizujemy się w budowie i utrzymaniu aplikacji enterprise opartych na Symfony. Oferujemy:

  • Projektowanie architektury aplikacji Symfony od podstaw
  • Migrację starszych aplikacji PHP do Symfony
  • Implementację DDD i architektury heksagonalnej
  • Integrację z API Platform, Doctrine i Messenger
  • Optymalizację wydajności i audyty kodu
  • Wsparcie techniczne i szkolenia dla zespołów

Skontaktuj się z nami, aby omówić Twój projekt i odkryć, jak Symfony może wzmocnić Twój biznes!

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

Symfony - Architektura enterprise w PHP na najwyższym poziomie | MDS Software Solutions Group | MDS Software Solutions Group