Symfony 8 in 2026: New Features, PHP 8.4 Lazy Objects and Interview Questions

Symfony 8 introduces native multi-step forms, invokable commands, PHP 8.4 lazy objects, and new JSON components. A deep dive into the features that matter for production and technical interviews.

Symfony 8 new features and PHP 8.4 lazy objects deep dive

Symfony 8, released in November 2025, removes two years of deprecations and ships the same feature set as Symfony 7.4 — but with a hard floor of PHP 8.4. That requirement unlocks native lazy objects, property hooks, and the new HTML5 parser, three language-level additions the framework now relies on internally. This article breaks down the features that change daily development, impact interview preparation, and deserve attention in production upgrades.

Symfony 8 at a Glance

Symfony 8.0 requires PHP 8.4+, ships native multi-step forms (AbstractFlowType), invokable console commands with #[Argument] and #[Option] attributes, three new JSON/ObjectMapper components, and replaces proxy code generation with PHP 8.4 native lazy objects across DependencyInjection and Doctrine.

Native Lazy Objects Replace Proxy Code Generation

PHP 8.4 introduces lazy objects at the engine level through ReflectionClass::newLazyGhost() and ReflectionClass::newLazyProxy(). Symfony 8 leverages this directly: the DependencyInjection component no longer generates proxy classes for lazy services. Instead, it creates ghost objects that initialize on first property access.

The practical consequence is significant. final and readonly classes — previously incompatible with Symfony's proxy-based lazy loading — now work without workarounds. Doctrine benefits equally: entity proxies no longer require code generation, removing a maintenance burden that persisted for over a decade.

src/Service/HeavyReportGenerator.phpphp
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Lazy;

#[Lazy]
final readonly class HeavyReportGenerator
{
    public function __construct(
        private DatabaseConnection $db,
        private PdfEngine $pdf,
        private CacheInterface $cache,
    ) {
        // Constructor runs only when a method is actually called
    }

    public function generate(int $reportId): string
    {
        $data = $this->db->fetchReport($reportId);
        return $this->pdf->render($data);
    }
}

The #[Lazy] attribute marks the service for ghost-object instantiation. The container injects a shell that looks identical to HeavyReportGenerator. The constructor — and the three injected dependencies — only execute when generate() is called. For services used conditionally (report generation triggered by a specific admin action, for example), this eliminates unnecessary database connections and memory allocation on every request.

The #[Autowire(lazy: true)] variant allows lazy injection at the call site without modifying the service class itself:

src/Controller/DashboardController.phpphp
namespace App\Controller;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

class DashboardController
{
    public function __construct(
        #[Autowire(lazy: true)]
        private HeavyReportGenerator $reportGenerator,
    ) {}
}

In interview settings, expect questions about the difference between ghost objects and virtual proxies. Ghost objects initialize the original instance in place. Virtual proxies delegate to a separate fully-initialized instance. Symfony defaults to ghosts unless a factory is involved, in which case it switches to proxies automatically.

Multi-Step Forms with AbstractFlowType

Before Symfony 8, multi-step forms required third-party bundles like CraueFormFlowBundle. The framework now provides AbstractFlowType — a native form flow engine with per-step validation, conditional branching, and built-in navigation.

src/Form/UserSignUpType.phpphp
namespace App\Form;

use Symfony\Component\Form\Flow\AbstractFlowType;
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class UserSignUpType extends AbstractFlowType
{
    public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void
    {
        $builder->addStep('personal', UserPersonalType::class);
        $builder->addStep('professional', UserProfessionalType::class);
        $builder->addStep('account', UserAccountType::class);

        $builder->add('navigator', NavigatorFlowType::class);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => UserSignUp::class,
            'step_property_path' => 'currentStep',
        ]);
    }
}

Each step name doubles as a validation group. The UserSignUp data class uses #[Valid] with group constraints to validate only the active step:

src/DTO/UserSignUp.phpphp
namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

class UserSignUp
{
    public function __construct(
        #[Assert\Valid(groups: ['personal'])]
        public Personal $personal = new Personal(),

        #[Assert\Valid(groups: ['professional'])]
        public Professional $professional = new Professional(),

        #[Assert\Valid(groups: ['account'])]
        public Account $account = new Account(),

        public string $currentStep = 'personal',
    ) {}
}

The controller mirrors standard Symfony form handling. isFinished() returns true only after the final step passes validation:

src/Controller/SignUpController.phpphp
#[Route('/signup')]
public function __invoke(Request $request): Response
{
    $flow = $this->createForm(UserSignUpType::class, new UserSignUp())
        ->handleRequest($request);

    if ($flow->isSubmitted() && $flow->isValid() && $flow->isFinished()) {
        $this->userService->register($flow->getData());
        return $this->redirectToRoute('app_signup_success');
    }

    return $this->render('signup/flow.html.twig', [
        'form' => $flow->getStepForm(),
    ]);
}

Navigation buttons (NextFlowType, PreviousFlowType, FinishFlowType, ResetFlowType) support conditional rendering via include_if, step skipping with skip, and direct navigation with back_to. This covers most real-world wizard patterns without custom JavaScript state management.

Ready to ace your Symfony interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Invokable Commands with Input Attributes

Console commands no longer require extending the Command base class or overriding configure(). The __invoke() method receives arguments and options directly through PHP attributes.

src/Command/CreateUserCommand.phpphp
namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'app:create-user',
    description: 'Create a new user account',
)]
class CreateUserCommand
{
    public function __invoke(
        SymfonyStyle $io,
        #[Argument(description: 'The username for the new account')]
        string $username,
        #[Argument(description: 'The email address')]
        string $email,
        #[Option(shortcut: 'r', description: 'Role to assign')]
        string $role = 'ROLE_USER',
        #[Option(description: 'Activate immediately')]
        bool $activate = false,
    ): int {
        // User creation logic
        $io->success(sprintf('User "%s" created with role %s.', $username, $role));
        return Command::SUCCESS;
    }
}

Symfony 8 adds backed enum support for arguments and options. String input converts to the matching enum case automatically:

src/Enum/CloudRegion.phpphp
enum CloudRegion: string {
    case UsEast = 'us-east';
    case UsWest = 'us-west';
    case EuWest = 'eu-west';
}

// In the command
public function __invoke(
    SymfonyStyle $io,
    #[Argument] CloudRegion $region,
    #[Option] ?ServerSize $size = null,
): int {
    $io->info(sprintf('Deploying to %s', $region->value));
    return Command::SUCCESS;
}

For commands with many parameters, #[MapInput] maps input to a DTO:

src/Command/Input/DeployInput.phpphp
class DeployInput
{
    #[Argument]
    public CloudRegion $region;

    #[Option]
    public string $branch = 'main';

    #[Option(shortcut: 'f')]
    public bool $force = false;
}

// src/Command/DeployCommand.php
#[AsCommand(name: 'app:deploy')]
class DeployCommand
{
    public function __invoke(
        SymfonyStyle $io,
        #[MapInput] DeployInput $input,
    ): int {
        // Access $input->region, $input->branch, $input->force
        return Command::SUCCESS;
    }
}

This pattern eliminates the ceremony of configure() + execute() and makes commands testable by constructing the DTO directly.

JSON Streamer, JSON Path, and ObjectMapper Components

Three new standalone components ship with Symfony 8:

JsonStreamer processes large JSON payloads without loading the entire document into memory. For APIs handling megabyte-scale responses — bulk data exports, analytics feeds, log ingestion — this avoids the memory ceiling of json_decode().

JsonPath implements a subset of RFC 9535 for navigating nested JSON structures. Instead of decoding an entire response and traversing arrays manually, a path expression extracts the target data directly.

ObjectMapper converts between DTOs and arrays/JSON using PHP attributes, replacing hand-written hydration logic. Combined with the Serializer component, it covers the full lifecycle of API request/response mapping.

Interview Relevance

Symfony interview questions in 2026 frequently cover lazy objects (ghost vs. proxy), the AbstractFlowType lifecycle, and the shift from configure() to invokable commands. Understanding these patterns signals familiarity with the current framework version, not just legacy knowledge.

Security and DX Improvements Worth Noting

CSRF protection no longer requires server-side sessions. The new stateless CSRF implementation works with HTTP caching, making it viable for API-first architectures and CDN-cached pages.

The Messenger component supports message signing — cryptographic signatures on payloads prevent tampering between producer and consumer. For systems where message integrity matters (payment processing, audit trails), this replaces custom signing middleware.

New validation constraints cover charsets, MAC addresses, ISO week numbers, word counts, YAML syntax, slugs, Twig templates, and video files. The #[Slug] constraint alone eliminates a common custom validator.

Security voters now explain their decisions in the profiler. Debugging authorization logic — previously a process of guessing which voter denied access — becomes a direct lookup in the Symfony profiler toolbar.

Upgrade Path

Symfony 8.0 drops all deprecations from the 7.x cycle, removing 13,202 lines of backward-compatibility code. The recommended approach: upgrade to Symfony 7.4 first, run php bin/phpunit --display-deprecations, fix every warning, then switch to 8.0. Skipping 7.4 risks runtime errors from removed APIs.

PHP 8.4 Property Hooks in Symfony Context

Property hooks (get and set defined directly on class properties) integrate naturally with Symfony's ecosystem. Doctrine entities can use hooks for computed properties. Form types can leverage hooks for data transformation. Validation constraints work on hooked properties without modification.

src/Entity/Product.phpphp
class Product
{
    public string $name {
        set(string $value) {
            $this->name = trim($value);
        }
    }

    public float $price {
        set(float $value) {
            if ($value < 0) {
                throw new \InvalidArgumentException('Price cannot be negative');
            }
            $this->price = $value;
        }
    }

    public float $priceWithTax {
        get => $this->price * 1.20;
    }
}

Property hooks reduce the need for getter/setter pairs and move invariant enforcement into the property definition. In Symfony forms, this means less transformer boilerplate. In Doctrine entities, computed values stay close to the data they depend on.

Interview Questions to Prepare For

Symfony 8 shifts what interviewers test. The following topics appear regularly in Symfony interview preparations:

  • Lazy objects: Explain the difference between ghost objects and virtual proxies. When does Symfony choose one over the other? Why does PHP 8.4 make final readonly services lazy-loadable?
  • Multi-step forms: How does AbstractFlowType handle per-step validation? What is the role of step_property_path? How do conditional steps work?
  • Invokable commands: What replaces configure() in invokable commands? How does #[MapInput] work? What are the rules for #[Option] defaults?
  • New components: When would JsonStreamer be preferred over json_decode()? What problem does ObjectMapper solve that the Serializer does not?
  • Upgrade strategy: Why is upgrading to 7.4 before 8.0 recommended? What does the removal of 13,202 lines of deprecation code imply for backward compatibility?

Practice these with real code. The Symfony console commands module and the Doctrine advanced module cover the most frequently tested areas.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Conclusion

  • Symfony 8 requires PHP 8.4 and fully adopts native lazy objects, eliminating proxy code generation for both the DI container and Doctrine ORM
  • AbstractFlowType replaces third-party bundles for multi-step forms, with per-step validation groups and built-in navigation buttons
  • Invokable commands with #[Argument], #[Option], and #[MapInput] remove boilerplate and support backed enums and DTOs
  • Three new components (JsonStreamer, JsonPath, ObjectMapper) address JSON processing and object mapping without external dependencies
  • Stateless CSRF, message signing, security voter debugging, and 20+ new validation constraints strengthen the security and DX surface
  • Property hooks from PHP 8.4 integrate transparently with Doctrine entities, Symfony forms, and validation constraints
  • The upgrade path runs through Symfony 7.4: fix all deprecations there before switching to 8.0

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#symfony
#php
#symfony-8
#php-8.4
#lazy-objects
#web-framework

Share

Related articles