Symfonysymfonyphp

Những điểm mới của Symfony Console 7.4

Symfony Console đã trải qua nhiều cải tiến qua các phiên bản. Trước đây, lập trình viên thường phải kế thừa lớp Command, định nghĩa cấu hình trong configure() và viết logic chính trong execute(). Cách này ổn định nhưng khá dài dòng.

Dong Nguyen

February 26, 2026

Những điểm mới của Symfony Console 7.4

Symfony Console đã trải qua nhiều cải tiến qua các phiên bản. Trước đây, lập trình viên thường phải kế thừa lớp Command, định nghĩa cấu hình trong configure() và viết logic chính trong execute(). Cách này ổn định nhưng khá dài dòng.

Từ Symfony 5–6, khái niệm lệnh invokable xuất hiện: dùng phương thức __invoke() làm điểm vào chính, kết hợp attribute #[AsCommand] để giảm bớt boilerplate. Tuy nhiên, vẫn còn bất tiện khi phải lấy argument/option thủ công qua $input->getArgument().

Symfony 7.4 (phát hành tháng 11/2025) đã nâng cấp mạnh mẽ Console component, đưa lệnh console gần giống với controller HTTP hơn: hỗ trợ Backed Enum tự động, DTO cho input qua #[MapInput], tương tác khai báo với #[Ask] và #[Interact]. Kết quả là code ngắn gọn, type-safe, dễ bảo trì và kiểm thử hơn.

Hướng dẫn này sẽ đi qua các tính năng mới, refactor một lệnh cũ sang phong cách 7.4, kèm ví dụ thực tế.

1. Chuẩn bị môi trường

Yêu cầu:

  • PHP ≥ 8.2 (khuyến nghị 8.4)
  • Composer

Tạo project mới:

composer create-project symfony/skeleton:"^7.4" my_cli_project
cd my_cli_project
composer require symfony/console:"^7.4"

Hoặc nâng cấp project cũ:

"require": {
    "symfony/console": "^7.4",
    "symfony/framework-bundle": "^7.4"
}
composer update symfony/*

Kiểm tra phiên bản:

php bin/console --version
# Nên hiển thị Symfony 7.4.x

2. Hỗ trợ Enum tự động (Backed Enum)

Trước 7.4: Phải validate thủ công.

Ví dụ cũ:

#[AsCommand(name: 'app:old-region')]
class OldRegionCommand extends Command
{
    protected function configure(): void
    {
        $this->addArgument('zone', InputArgument::REQUIRED, 'Vùng (asia, europe)');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $zone = $input->getArgument('zone');
        if (!in_array($zone, ['asia', 'europe'])) {
            $output->writeln('<error>Vùng không hợp lệ!</error>');
            return Command::FAILURE;
        }
        // Xử lý...
        return Command::SUCCESS;
    }
}

Symfony 7.4: Type-hint trực tiếp bằng Enum → framework tự validate và báo lỗi chi tiết.

Định nghĩa Enum:

namespace App\Enum;

enum CloudZone: string
{
    case ASIA = 'ap-southeast-1';
    case EUROPE = 'eu-west-1';
    case AMERICA = 'us-west-2';
}

Lệnh mới:

#[AsCommand(name: 'app:launch-instance', description: 'Khởi tạo máy chủ ở vùng chỉ định')]
class LaunchInstanceCommand extends Command
{
    public function __invoke(
        OutputInterface $output,
        #[Argument] CloudZone $zone
    ): int {
        $output->writeln("Khởi tạo instance tại vùng: {$zone->value}");
        return Command::SUCCESS;
    }
}

Chạy sai:

php bin/console app:launch-instance invalid

Kết quả tự động:

[ERROR] Giá trị "invalid" không hợp lệ cho đối số "zone".
Các giá trị cho phép: "ap-southeast-1", "eu-west-1", "us-west-2".

3. Sử dụng DTO cho input với #[MapInput]

Khi lệnh có nhiều tham số, __invoke() dễ trở nên dài. Symfony 7.4 cho phép map tất cả vào một DTO.

Tạo DTO:

namespace App\Dto;

use App\Enum\CloudZone;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Validator\Constraints as Assert;

class InstanceConfig
{
    #[Argument(description: 'Vùng triển khai')]
    public CloudZone $zone;

    #[Option(short: 's', description: 'Kích cỡ máy chủ')]
    #[Assert\Choice(['micro', 'small', 'large'])]
    public string $size = 'small';

    #[Option(name: 'dry-run', description: 'Chạy thử, không tạo thật')]
    public bool $dryRun = false;
}

Lệnh sử dụng DTO:

#[AsCommand(name: 'app:deploy-instance')]
class DeployInstanceCommand extends Command
{
    public function __invoke(
        OutputInterface $output,
        #[MapInput] InstanceConfig $config
    ): int {
        if ($config->dryRun) {
            $output->writeln('<info>Chế độ thử nghiệm: không thay đổi thực tế.</info>');
        }

        $output->writeln(sprintf(
            'Triển khai instance kích cỡ %s tại %s...',
            $config->size,
            $config->zone->value
        ));

        return Command::SUCCESS;
    }
}

Lợi ích:

  • Tách biệt cấu hình input khỏi logic.
  • Tái sử dụng DTO ở nơi khác.
  • Tự động validate nếu cài symfony/validator.

4. Tương tác khai báo (Interactive) với #[Ask] và #[Interact]

Trước đây phải override interact(). Giờ dùng attribute.

Ví dụ #[Ask] đơn giản:

#[AsCommand(name: 'app:greet')]
class GreetCommand extends Command
{
    public function __invoke(
        OutputInterface $output,
        #[Argument, Ask(question: 'Bạn tên là gì?')] 
        string $username
    ): int {
        $output->writeln("Xin chào, $username!");
        return Command::SUCCESS;
    }
}

Nếu chạy không truyền argument → tự hỏi. Truyền rồi → bỏ qua prompt.

Với #[Interact] cho logic phức tạp:

#[AsCommand(name: 'app:setup')]
class SetupCommand extends Command
{
    #[Interact]
    public function handleInteraction(InputInterface $input, SymfonyStyle $io): void
    {
        if (null === $input->getArgument('token')) {
            $token = $io->askHidden('Nhập API token bí mật');
            $input->setArgument('token', $token);
        }
    }

    public function __invoke(OutputInterface $output, string $token): int
    {
        $output->writeln('Token đã nhận (ẩn): ' . substr($token, 0, 4) . '...');
        return Command::SUCCESS;
    }
}

5. Kiểm thử lệnh invokable

CommandTester vẫn hoạt động bình thường, hỗ trợ Enum và DTO.

Ví dụ test:

namespace App\Tests\Command;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Console\Application;

class DeployInstanceCommandTest extends KernelTestCase
{
    public function testDeploySuccess(): void
    {
        $kernel = self::bootKernel();
        $app = new Application($kernel);
        $command = $app->find('app:deploy-instance');
        $tester = new CommandTester($command);

        $tester->execute([
            'zone' => 'ap-southeast-1',
            '--size' => 'large',
            '--dry-run' => true,
        ]);

        $tester->assertCommandIsSuccessful();
        $display = $tester->getDisplay();
        $this->assertStringContainsString('Chế độ thử nghiệm', $display);
        $this->assertStringContainsString('large instance tại ap-southeast-1', $display);
    }

    public function testInvalidZone(): void
    {
        // ... kiểm tra lỗi khi truyền zone sai
    }
}

6. Ví dụ thực tế: Lệnh tạo báo cáo phức tạp

Kết hợp tất cả:

Enum:

enum ReportType: string { case PDF = 'pdf'; case EXCEL = 'xlsx'; }
enum TimeRange: string { case DAY = 'day'; case MONTH = 'month'; }

DTO:

class ReportRequest
{
    #[Argument] public ReportType $format;
    #[Argument] public TimeRange $range;
    #[Option] public bool $sendEmail = false;
    // ...
}

Lệnh đầy đủ sẽ dùng Enum, DTO, prompt nếu thiếu, và logic tạo báo cáo.

Kết luận

Symfony 7.4 biến việc viết lệnh console thành trải nghiệm hiện đại, gần với lập trình web: type-safe, ít boilerplate, dễ validate và test. Các tính năng này giúp code sạch hơn, dễ bảo trì hơn, đặc biệt với dự án lớn.

Nếu bạn đang dùng Symfony cũ, đây là lý do tốt để nâng cấp lên 7.4. Thử áp dụng vào dự án của bạn và cảm nhận sự khác biệt!

Support My Work

If you found this article helpful, consider supporting my work. It helps me keep creating free content for the developer community.

Buy Me a Coffee

Need Help With Your Project?

I'm available for freelance work. Whether you need a full-stack application, API development, or technical consulting, I'd love to help.

View My Services