MidТеория4 min

Best Practices и PSR

Symfony best practices, PSR стандарты, Backward Compatibility promise

На экзамене проверяют знание Symfony best practices, PSR-стандартов и backward compatibility promise. Это теоретические вопросы, которые нельзя угадать — нужно знать точно.

Symfony Best Practices

Контроллеры

<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

// ✅ BEST PRACTICE: thin controllers, extend AbstractController
#[Route('/products', name: 'product_')]
class ProductController extends AbstractController
{
    // ✅ Use constructor injection
    public function __construct(
        private readonly ProductService $productService,
    ) {
    }

    // ✅ Keep actions small — delegate to services
    #[Route('', name: 'list')]
    public function list(): Response
    {
        $products = $this->productService->getAll();

        return $this->render('product/list.html.twig', [
            'products' => $products,
        ]);
    }

    // ❌ BAD: business logic in controller
    // ✅ GOOD: delegate to service
}

Конфигурация

Best Practice Описание
Атрибуты для routing #[Route] вместо YAML
YAML для services config/services.yaml
Env variables Для секретов и инфраструктуры
Parameters Для значений приложения
Constants Для бизнес-логики
# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

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

Подвох экзамена: Symfony рекомендует использовать атрибуты для routing и YAML для сервисов (не наоборот). Env variables — для инфраструктурных настроек (DATABASE_URL), Parameters — для настроек приложения (items_per_page). Не путайте.

Сервисы

<?php

declare(strict_types=1);

// ✅ BEST PRACTICE: use constructor injection
class OrderService
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly MailerInterface $mailer,
        private readonly LoggerInterface $logger,
    ) {
    }
}

// ❌ BAD: service locator pattern
class BadService
{
    public function doSomething(ContainerInterface $container): void
    {
        $em = $container->get('doctrine.orm.entity_manager');
    }
}

PSR стандарты

PSR-4: Autoloading

// composer.json
{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}
Namespace Путь к файлу
App\Controller\ProductController src/Controller/ProductController.php
App\Entity\Product src/Entity/Product.php
App\Service\OrderService src/Service/OrderService.php

Правила PSR-4:

  • Один класс = один файл
  • Имя файла = имя класса + .php
  • Namespace маппится на директорию

PSR-7: HTTP Message Interface

<?php

declare(strict_types=1);

// PSR-7 defines interfaces for HTTP messages
// Symfony does NOT use PSR-7 internally (uses HttpFoundation)
// But provides a bridge:

use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;

// Convert Symfony Request to PSR-7
$psr7Factory = new PsrHttpFactory(/* ... */);
$psrRequest = $psr7Factory->createRequest($symfonyRequest);

// Convert PSR-7 to Symfony Request
$httpFoundationFactory = new HttpFoundationFactory();
$symfonyRequest = $httpFoundationFactory->createRequest($psrRequest);

Подвох экзамена: Symfony НЕ использует PSR-7 напрямую. HttpFoundation — собственная реализация Symfony, появившаяся до PSR-7. Symfony предоставляет Bridge для конвертации между форматами. Если спросят "реализует ли Symfony PSR-7?" — ответ: нет, но предоставляет bridge.

PSR-11: Container Interface

<?php

declare(strict_types=1);

use Psr\Container\ContainerInterface;

// Symfony DI Container implements PSR-11
// ContainerInterface defines two methods:

interface ContainerInterface
{
    public function get(string $id): mixed;
    public function has(string $id): bool;
}

// In Symfony, service locator is a mini-container
// AbstractController uses it internally:
class AbstractController
{
    // $this->container is a ServiceLocator (PSR-11)
    public static function getSubscribedServices(): array
    {
        return [
            'router' => RouterInterface::class,
            'twig' => Environment::class,
            // ...
        ];
    }
}

PSR-12: Extended Coding Style

Правило Пример
Открывающая скобка класса На новой строке
Открывающая скобка метода На новой строке
Открывающая скобка if/for На той же строке
Видимость Обязательна для всех методов
Отступы 4 пробела (не табы)
Длина строки Мягкий лимит 120 символов
<?php

declare(strict_types=1);

namespace App\Service;

use App\Entity\Product;
use App\Repository\ProductRepository;

// PSR-12 compliant code
class ProductService
{                                          // ← New line for class
    public function __construct(
        private readonly ProductRepository $repository,
    ) {                                    // ← New line for method
    }

    public function findExpensive(float $minPrice): array
    {                                      // ← New line for method
        if ($minPrice < 0) {               // ← Same line for if
            throw new \InvalidArgumentException('Price must be positive');
        }

        return $this->repository->findByMinPrice($minPrice);
    }
}

PSR-15: HTTP Server Request Handlers

<?php

declare(strict_types=1);

// PSR-15 defines middleware interfaces
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

// Symfony uses its own event system instead of PSR-15 middleware
// But the concepts are similar:
// PSR-15 Middleware ≈ Symfony Event Listener on kernel.request/kernel.response

Сводная таблица PSR

PSR Название Symfony использует
PSR-1 Basic Coding Standard Да
PSR-3 Logger Interface Да (LoggerInterface)
PSR-4 Autoloading Да (через Composer)
PSR-6 Caching Interface Да (Cache component)
PSR-7 HTTP Message Через Bridge
PSR-11 Container Interface Да (DI Container)
PSR-12 Extended Coding Style Рекомендуется
PSR-14 Event Dispatcher Да (EventDispatcher)
PSR-15 HTTP Handlers Через Bridge
PSR-16 Simple Cache Да (Cache component)
PSR-17 HTTP Factories Через Bridge
PSR-18 HTTP Client Да (HttpClient)

Подвох экзамена: Symfony реализует PSR-3 (Logger), PSR-6 и PSR-16 (Cache), PSR-11 (Container), PSR-14 (EventDispatcher), PSR-18 (HttpClient) нативно. PSR-7 и PSR-15 — только через Bridge. На экзамене часто спрашивают, какие PSR поддерживаются нативно.

Backward Compatibility Promise

Что гарантирует Symfony

Внутри major-версии (например 7.0 → 7.4):

<?php

declare(strict_types=1);

// ✅ Гарантируется стабильным:
// Public API — public methods и их сигнатуры
// Интерфейсы — для type-hinting (использования)
// Конфигурация — формат YAML/XML/PHP
// Console commands — их существование и аргументы

// ❌ НЕ гарантируется:
// @internal классы и методы
// Private/protected методы
// Добавление методов в интерфейсы
// Экспериментальные фичи (@experimental)
// Формат вывода (текст ошибок, debug output)

Deprecation Process

<?php

declare(strict_types=1);

// Symfony deprecation lifecycle:
// Version 7.0: Feature X available, Feature Y introduced
// Version 7.1-7.4: Feature X triggers deprecation notice
// Version 8.0: Feature X removed, Feature Y is the standard

// Example: trigger_deprecation()
trigger_deprecation(
    'symfony/framework-bundle',
    '7.2',
    'Method "%s" is deprecated, use "%s" instead.',
    'oldMethod',
    'newMethod',
);
Стадия Что происходит
Версия N.0 Фича доступна, работает
Версия N.x Фича deprecated (warning)
Версия N+1.0 Фича удалена

Подвох экзамена: Deprecation в Symfony — это 2-step process. Сначала deprecation notice (можно обнаружить в тестах через PHPUnit Bridge), потом удаление в следующей major-версии. Symfony НИКОГДА не удаляет фичу без предварительного deprecation.

Symfony 8.0: Все фичи, deprecated в Symfony 7.x, удалены в 8.0. При миграции с 7.4 на 8.0 нужно сначала исправить все deprecation notices. PHPUnit Bridge помогает обнаружить их в тестах.

Semantic Versioning

Symfony следует SemVer: MAJOR.MINOR.PATCH

Тип Когда Пример BC breaks
PATCH Bug fixes 7.4.1 → 7.4.2 Нет
MINOR New features 7.3 → 7.4 Нет
MAJOR Breaking changes 7.4 → 8.0 Да

Symfony выпускает minor-версию каждые 6 месяцев и major-версию каждые 2 года.