Dependency Injection trong Symfony: Service Container, Autowiring và Tag năm 2026
Phân tích chuyên sâu dependency injection trong Symfony: service container, autowiring, attribute #[Autowire], ràng buộc interface, tagged service với #[AutowireIterator], compiler pass và lazy service trong Symfony 7.4 và 8.0.

Dependency injection là xương sống của mọi ứng dụng Symfony: service container khởi tạo, cấu hình và nối kết các đối tượng lại với nhau, để một lớp chỉ cần khai báo thứ nó cần thay vì tự dựng lấy. Ở Symfony 7.4 và 8.0, autowiring, các attribute của PHP và tagged service đã cắt giảm phần cấu hình tường minh xuống gần như bằng không, trong khi vẫn giữ container được phân giải trọn vẹn và có thể dự đoán ngay tại thời điểm biên dịch. Hiểu cách cơ chế phân giải đó vận hành chính là ranh giới phân tách những lập trình viên vật lộn với container khỏi những người để container làm việc thay mình.
Dependency injection trong Symfony là một mẫu thiết kế trong đó service container truyền các dependency của một đối tượng qua constructor của nó, thay vì để đối tượng tự tạo ra chúng. Autowiring tự động phân giải mỗi tham số có type-hint tới một service tương ứng, nên hầu hết các lớp hoàn toàn không cần cấu hình thủ công.
Cách Service Container của Symfony Phân Giải Dependency
Service container là một lớp PHP đã biên dịch, nắm rõ cách dựng mọi service trong ứng dụng. Trong giai đoạn biên dịch container, Symfony quét các định nghĩa lớp, đọc chữ ký constructor và tạo ra một factory đã được cache. Tại runtime, việc yêu cầu một service sẽ trả về một instance dùng chung được dựng theo kiểu lazy. Vì việc nối kết diễn ra tại thời điểm biên dịch, một dependency cấu hình sai sẽ hỏng ngay khi cache được dựng, chứ không phải khi một request chạm vào production.
Một service đơn thuần chỉ là một lớp thực hiện một nhiệm vụ và được container quản lý. Tệp config/services.yaml mặc định đăng ký mọi thứ dưới src/ thành service với autowiring và autoconfiguration được bật, đó là lý do một lớp bình thường chạy được mà không cần thiết lập thêm.
# config/services.yaml
services:
_defaults:
autowire: true # inject arguments by type-hint
autoconfigure: true # apply tags based on implemented interfaces
App\:
resource: '../src/'
exclude:
- '../src/Entity/'
- '../src/Kernel.php'Với những giá trị mặc định đó, một lớp yêu cầu các cộng tác viên của mình qua constructor và container cung cấp chúng. Không factory, không new, không danh sách tham số thủ công.
namespace App\Notification;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class SlackNotifier
{
// The container reads each type-hint and injects a matching service
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly LoggerInterface $logger,
) {}
public function send(string $channel, string $message): void
{
$this->httpClient->request('POST', 'https://slack.example/api/post', [
'json' => ['channel' => $channel, 'text' => $message],
]);
$this->logger->info('Slack message dispatched', ['channel' => $channel]);
}
}Container khởi tạo SlackNotifier một lần, inject HTTP client và logger dùng chung, rồi tái sử dụng đúng instance đó ở mọi nơi nó được yêu cầu. Chính hành vi shared-by-default này khiến container vừa nhanh vừa tiết kiệm bộ nhớ. Toàn bộ mô hình được tài liệu hóa trong hướng dẫn service container chính thức của Symfony, vốn xây dựng trên chuẩn interface container PSR-11 được dùng chung khắp hệ sinh thái PHP.
Có thể tắt việc dùng chung khi cần một instance mới cho mỗi request, chẳng hạn một builder có trạng thái tích lũy dữ liệu. Đặt shared: false trên một định nghĩa biến nó thành một factory trả về một đối tượng mới mỗi lần được inject. Trong thực tế điều này hiếm, vì đa số service là các cộng tác viên không trạng thái, nhưng biết rằng giá trị mặc định này tồn tại và cách ghi đè nó lại quan trọng khi một lỗi truy ngược về hai bên tiêu thụ vô tình cùng biến đổi một đối tượng.
Autowiring của Symfony: Type Hint và Attribute Autowire
Autowiring của Symfony phân giải các tham số constructor dựa trên khai báo kiểu của chúng. Khi một tham số được type-hint bằng một lớp hay interface ánh xạ tới đúng một service, container inject nó mà không cần cấu hình nào. Autowiring chỉ bó tay với các giá trị không phải service: scalar, environment variable và parameter. Attribute #[Autowire] khỏa lấp khoảng trống đó ngay trên tham số, giữ việc nối kết nằm sát với đoạn code sử dụng nó.
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class PaymentGateway
{
public function __construct(
// Inject a container parameter with the %...% syntax
#[Autowire('%kernel.environment%')]
private readonly string $environment,
// Inject an environment variable, resolved at runtime
#[Autowire(env: 'STRIPE_SECRET')]
private readonly string $apiKey,
// Bind a specific service when the type alone is ambiguous
#[Autowire(service: 'monolog.logger.payment')]
private readonly LoggerInterface $logger,
) {}
}Mỗi tham số tự khai báo nguồn của mình: một parameter, một environment variable, hay một service có tên. Attribute này còn nhận một tham số expression: cho các giá trị Expression Language, hữu dụng khi một dependency đến từ một phép tính runtime chứ không phải một định nghĩa tĩnh. Đặt phần metadata này ngay trên constructor nghĩa là người đọc thấy chính xác từng giá trị bắt nguồn từ đâu mà không phải mở một tệp YAML riêng. Tài liệu tham chiếu của attribute nằm trong tài liệu autowiring của Symfony.
Autowire một lớp cụ thể như S3Storage sẽ ràng buộc mọi bên tiêu thụ vào chính triển khai đó và khiến việc thay thế nó trong test trở nên khổ sở. Hãy type-hint interface và để #[AsAlias] hoặc #[Autowire(service: ...)] quyết định service cụ thể. Inject cả container để tự tay lấy service làm vô hiệu các kiểm tra tại thời điểm biên dịch, và đó chính là phản mẫu mà autowiring sinh ra để loại bỏ.
Ràng Buộc Interface Khi Tồn Tại Nhiều Triển Khai
Autowiring theo một interface hoạt động trơn tru cho đến khi có hai lớp cùng triển khai nó. Lúc đó container không thể đoán nên inject cái nào và ném ra lỗi nhập nhằng tại thời điểm biên dịch. Hai attribute giải quyết điều này: #[AsAlias] đánh dấu một triển khai là mặc định cho interface, còn #[Autowire(service: ...)] ghim một triển khai cụ thể trên một tham số riêng lẻ.
namespace App\Storage;
interface StorageInterface
{
public function put(string $key, string $contents): void;
}namespace App\Storage;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
// Becomes the default service returned for StorageInterface
#[AsAlias(StorageInterface::class)]
final class S3Storage implements StorageInterface
{
public function put(string $key, string $contents): void { /* upload to S3 */ }
}namespace App\Service;
use App\Storage\LocalStorage;
use App\Storage\StorageInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class BackupService
{
public function __construct(
// Resolves to the #[AsAlias] default (S3Storage)
private readonly StorageInterface $primary,
// Pins the concrete implementation explicitly by service id
#[Autowire(service: LocalStorage::class)]
private readonly StorageInterface $fallback,
) {}
}Attribute #[Target] cho một lựa chọn thứ ba: nó tham chiếu một triển khai theo tên alias autowiring của nó, tách tên biến tham số khỏi việc phân giải. Chọn giữa các cách này quy về ý định. Dùng #[AsAlias] khi một triển khai thực sự là mặc định trên toàn bộ mã nguồn, và #[Autowire(service: ...)] hoặc #[Target] khi một bên tiêu thụ duy nhất cần một biến thể không mặc định. Mẫu này xuất hiện liên tục trong các ứng dụng production, và là chủ đề ưa thích trong mô-đun phỏng vấn dependency injection Symfony.
Sẵn sàng chinh phục phỏng vấn Symfony?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Tag Service của Symfony và Bộ Thu Thập AutowireIterator
Tag là cách Symfony nhóm các service liên quan để một service khác có thể tiêu thụ tất cả cùng lúc. Điều này tiếp sức cho các mẫu strategy, chain-of-responsibility và plugin xuất hiện xuyên suốt chính framework: các form type extension, Twig extension và event subscriber đều được điều khiển bằng tag. Thay vì gắn tag thủ công, #[AutoconfigureTag] đặt trên một interface sẽ bảo container tự động gắn tag cho mọi lớp triển khai nó.
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
// Every class implementing this interface is tagged automatically
#[AutoconfigureTag('app.exporter')]
interface ExporterInterface
{
public function supports(string $format): bool;
public function export(array $rows): string;
}Từng triển khai kiểm soát vị trí của mình trong tập hợp bằng #[AsTaggedItem], thứ đặt một mức ưu tiên (giá trị cao hơn chạy trước) và một chỉ mục chuỗi tùy chọn để truy cập theo khóa.
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
// Higher priority services appear earlier in the injected iterable
#[AsTaggedItem(priority: 10)]
final class CsvExporter implements ExporterInterface
{
public function supports(string $format): bool
{
return $format === 'csv';
}
public function export(array $rows): string
{
return implode("\n", array_map(fn ($r) => implode(',', $r), $rows));
}
}Service tiêu thụ gom mọi exporter đã gắn tag vào một iterable có thứ tự bằng #[AutowireIterator]. Sau đó nó duyệt qua tập hợp và ủy thác cho chiến lược đầu tiên hỗ trợ định dạng được yêu cầu.
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
final class ExportManager
{
// Collects all services tagged 'app.exporter', ordered by priority
public function __construct(
#[AutowireIterator('app.exporter')]
private readonly iterable $exporters,
) {}
public function export(string $format, array $rows): string
{
foreach ($this->exporters as $exporter) {
if ($exporter->supports($format)) {
return $exporter->export($rows);
}
}
throw new \InvalidArgumentException("No exporter for format: $format");
}
}Khi tập hợp lớn hoặc tốn kém để dựng, #[AutowireLocator] là lựa chọn lazy thay thế: nó inject một container PSR-11 chỉ khởi tạo một service đã gắn tag khi service đó thực sự được lấy ra theo khóa. Sự khác biệt giữa lặp eager và tra cứu lazy đó rất quan trọng với các service giữ kết nối hoặc trạng thái nặng.
Autoconfiguration, Compiler Pass và Lazy Service
Autoconfiguration là cơ chế đọc các attribute như #[AutoconfigureTag] và #[AsTaggedItem] rồi áp dụng chúng trong lúc biên dịch. Đó là lý do chỉ cần triển khai EventSubscriberInterface là đủ để đăng ký một event subscriber với cấu hình bằng không. Với những biến đổi mà attribute không thể diễn đạt, một compiler pass móc thẳng vào quá trình dựng container và biến đổi các định nghĩa trước khi cache được ghi. Mã nguồn của thành phần DependencyInjection trên GitHub cho thấy chính framework dùng hàng chục pass như vậy ở tầng bên trong.
Các đồ thị nhạy cảm với hiệu năng hưởng lợi từ lazy service. Đánh dấu một tham số bằng #[Autowire(lazy: true)] trì hoãn việc khởi tạo cho tới khi dependency được gọi lần đầu, dựa trên native lazy object của PHP 8.4 thay vì một thư viện proxy được sinh ra.
namespace App\Service;
use App\Pdf\PdfRenderer;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class ReportBuilder
{
public function __construct(
// PdfRenderer is only instantiated on first actual use
#[Autowire(lazy: true)]
private readonly PdfRenderer $renderer,
) {}
public function buildMonthly(): string
{
// If no method touches $this->renderer, it is never constructed
return $this->renderer->render('monthly-report');
}
}Native lazy object xuất hiện cùng PHP 8.4 và được trình bày sâu trong hướng dẫn về lazy object của Symfony 8 và PHP 8.4. Thành quả là một container luôn gọn nhẹ: những cộng tác viên nặng như PDF renderer, client SDK bên thứ ba, hay bộ sinh báo cáo không tốn chi phí gì cho tới khi nhánh code cần đến chúng thực sự chạy.
Câu Hỏi và Câu Trả Lời Phỏng Vấn DI Symfony
Một câu hỏi phỏng vấn DI Symfony thường gặp là hỏi về khác biệt giữa autowiring và autoconfiguration. Autowiring phân giải các tham số constructor của một service theo kiểu, còn autoconfiguration áp dụng tag, lời gọi method và thiết lập cho một service dựa trên các interface nó triển khai. Chúng là hai công tắc độc lập: một service có thể được autowire mà không được autoconfigure, và ngược lại.
Một câu hỏi thường gặp khác là tại sao container phân biệt service public và private. Service mặc định là private, nghĩa là không thể lấy trực tiếp từ container bằng $container->get() mà chỉ có thể được inject. Service private có thể được inline và tối ưu hóa cho biến mất trong lúc biên dịch, nhanh hơn; service public phải giữ khả năng truy xuất, nên bộ tối ưu để nguyên chúng. Việc tự tay lấy service từ container bị xem là phản mẫu chính vì nó làm vô hiệu phần tối ưu này và che giấu các dependency.
Circular dependency là một cái bẫy kinh điển và cũng là câu hỏi nối tiếp thường gặp. Nếu service A cần B trong constructor và B cần A, container không thể dựng nổi cái nào và báo tham chiếu vòng tại thời điểm biên dịch. Các cách khắc phục tiêu chuẩn là phá vỡ vòng lặp bằng cách tách hành vi dùng chung ra một service thứ ba, chuyển một bên sang injection lazy với #[Autowire(lazy: true)] để dependency chỉ được phân giải khi gọi lần đầu, hoặc dùng setter injection để đối tượng tồn tại trước khi cộng tác viên của nó được nối vào. Nhận ra vì sao vòng lặp là một dấu hiệu thiết kế xấu, chứ không chỉ cách làm nó im tiếng, mới là điều người phỏng vấn thực sự lắng nghe.
Người phỏng vấn cũng dò về thời điểm phân giải của container. Vì Symfony biên dịch container một lần và cache nó, một dependency không thể phân giải sẽ lộ ra lúc cache warmup thay vì lúc request. Sự bảo đảm đó là hệ quả trực tiếp của mô hình tại thời điểm biên dịch và là một lý do khiến các ứng dụng Symfony lớn giữ được tính dự đoán dưới tải. Để có tập câu hỏi rộng hơn, lộ trình công nghệ Symfony nhóm chúng theo thành phần và độ khó.
Kết Luận
- Hãy để container làm việc: đăng ký các lớp dưới
src/, type-hint dependency trong constructor, và bỏ hẳn việc nối kết YAML thủ công. - Chỉ dùng đến
#[Autowire]cho các thứ không phải service, cho parameter, environment variable và service id cụ thể. - Giải quyết nhập nhằng interface bằng
#[AsAlias]cho mặc định và#[Autowire(service: ...)]hoặc#[Target]cho lựa chọn tường minh. - Điều khiển các mẫu strategy và plugin bằng
#[AutoconfigureTag], mức ưu tiên#[AsTaggedItem]và#[AutowireIterator]; chuyển sang#[AutowireLocator]khi tra cứu lazy đáng chuộng hơn lặp eager. - Trì hoãn các cộng tác viên tốn kém bằng
#[Autowire(lazy: true)], thứ dùng native lazy object của PHP 8.4. - Giữ service ở dạng private và được inject thay vì lấy ra, để compiler có thể inline và tối ưu hóa đồ thị.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

Kiểm Thử Symfony năm 2026: PHPUnit, KernelTestCase và Functional Tests
Hướng dẫn toàn diện về kiểm thử ứng dụng Symfony với PHPUnit 12, KernelTestCase cho integration testing, và WebTestCase cho functional tests theo best practices.

Symfony Live Components và UX 3.0: Ứng Dụng Phản Hồi Không Cần JavaScript Năm 2026
Symfony Live Components xây dựng giao diện phản hồi bằng PHP và Twig mà không cần JavaScript. Hướng dẫn chi tiết về LiveProp, LiveAction, form và deferred loading.

Doctrine ORM: Làm chủ các quan hệ trong Symfony
Hướng dẫn đầy đủ về quan hệ Doctrine ORM trong Symfony. OneToMany, ManyToMany, chiến lược tải và tối ưu hiệu năng kèm ví dụ thực tế.