# Dependency Injection ของ Symfony: Service Container, Autowiring และ Tag ในปี 2026 > เจาะลึก dependency injection ของ Symfony ครอบคลุม service container, autowiring, แอตทริบิวต์ #[Autowire], การผูกอินเทอร์เฟซ, tagged service ด้วย #[AutowireIterator], compiler pass และ lazy service ใน Symfony 7.4 และ 8.0 - Published: 2026-07-06 - Updated: 2026-07-07 - Author: SharpSkill - Tags: symfony, dependency injection, service container, autowiring, php, di - Reading time: 11 min --- Dependency injection คือกระดูกสันหลังของทุกแอปพลิเคชัน Symfony: service container จะสร้างอินสแตนซ์ กำหนดค่า และเชื่อมโยงออบเจ็กต์เข้าด้วยกัน เพื่อให้คลาสเพียงแค่ประกาศสิ่งที่ต้องการ แทนที่จะต้องสร้างมันขึ้นมาเอง ใน Symfony 7.4 และ 8.0 autowiring, แอตทริบิวต์ของ PHP และ tagged service ได้ลดการตั้งค่าแบบชัดแจ้งลงจนแทบเป็นศูนย์ ในขณะที่ยังคงทำให้ container ถูกแก้ไขค่าอย่างสมบูรณ์และคาดเดาได้ตั้งแต่ตอนคอมไพล์ การเข้าใจว่ากลไกการแก้ไขค่านั้นทำงานอย่างไรคือเส้นแบ่งระหว่างนักพัฒนาที่ต้องต่อสู้กับ container กับผู้ที่ปล่อยให้ container ทำงานแทนตน > **Dependency injection ใน Symfony คืออะไร** > > Dependency injection ใน Symfony คือรูปแบบที่ service container ส่งผ่าน dependency ของออบเจ็กต์เข้ามาทาง constructor แทนที่จะปล่อยให้ออบเจ็กต์สร้างมันขึ้นมาเอง autowiring จะแก้ไขค่าอาร์กิวเมนต์แต่ละตัวที่มี type-hint ให้ชี้ไปยัง service ที่ตรงกันโดยอัตโนมัติ ดังนั้นคลาสส่วนใหญ่จึงไม่ต้องการการตั้งค่าด้วยมือเลย ## Service Container ของ Symfony แก้ไขค่า Dependency อย่างไร Service container คือคลาส PHP ที่ถูกคอมไพล์แล้ว ซึ่งรู้วิธีสร้างทุก service ในแอปพลิเคชัน ระหว่างขั้นตอนการคอมไพล์ container นั้น Symfony จะสแกนนิยามของคลาส อ่านลายเซ็นของ constructor และผลิต factory ที่ถูกแคชไว้ ในตอนรันไทม์ การร้องขอ service จะได้อินสแตนซ์ที่ใช้ร่วมกันซึ่งถูกสร้างแบบ lazy กลับมา เนื่องจากการเชื่อมโยงเกิดขึ้นตอนคอมไพล์ dependency ที่ตั้งค่าผิดจะล้มเหลวตอนที่แคชถูกสร้าง ไม่ใช่ตอนที่ request มาถึง production service เป็นเพียงคลาสที่ทำงานอย่างหนึ่งและถูกจัดการโดย container ไฟล์เริ่มต้น `config/services.yaml` จะลงทะเบียนทุกสิ่งภายใต้ `src/` ให้เป็น service โดยเปิด autowiring และ autoconfiguration ไว้ นี่จึงเป็นเหตุผลที่คลาสธรรมดาทำงานได้โดยไม่ต้องตั้งค่าเพิ่ม ```yaml # 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' ``` เมื่อมีค่าเริ่มต้นเหล่านั้นอยู่ คลาสก็ขอผู้ร่วมงานของมันผ่าน constructor และ container เป็นผู้จัดหาให้ ไม่มี factory ไม่มี `new` ไม่มีรายการอาร์กิวเมนต์ที่ทำด้วยมือ ```php // src/Notification/SlackNotifier.php 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 สร้างอินสแตนซ์ `SlackNotifier` เพียงครั้งเดียว inject HTTP client และ logger ที่ใช้ร่วมกันเข้าไป แล้วนำอินสแตนซ์นั้นกลับมาใช้ซ้ำในทุกที่ที่มีการร้องขอ พฤติกรรม shared-by-default นี้เองที่ทำให้ container ทั้งเร็วและประหยัดหน่วยความจำ โมเดลฉบับเต็มถูกบันทึกไว้ใน[คู่มือ service container อย่างเป็นทางการของ Symfony](https://symfony.com/doc/current/service_container.html) ซึ่งต่อยอดจากมาตรฐาน[อินเทอร์เฟซ container แบบ PSR-11](https://www.php-fig.org/psr/psr-11/) ที่ใช้ร่วมกันทั่วทั้งระบบนิเวศ PHP การใช้ร่วมกันสามารถปิดได้เมื่อต้องการอินสแตนซ์ใหม่ต่อหนึ่ง request เช่น builder ที่มีสถานะและสะสมข้อมูลไปเรื่อย ๆ การตั้ง `shared: false` บนนิยามหนึ่งจะเปลี่ยนมันให้กลายเป็น factory ที่คืนออบเจ็กต์ใหม่ทุกครั้งที่ถูก inject ในทางปฏิบัติสิ่งนี้เกิดขึ้นไม่บ่อย เพราะ service ส่วนใหญ่เป็นผู้ร่วมงานที่ไม่มีสถานะ แต่การรู้ว่าค่าเริ่มต้นนี้มีอยู่และรู้วิธีเขียนทับมันจึงมีความสำคัญ เมื่อบั๊กหนึ่งถูกสืบย้อนไปพบว่าผู้ใช้สองรายเผลอแก้ไขออบเจ็กต์ตัวเดียวกัน ## Autowiring ของ Symfony: Type Hint และแอตทริบิวต์ Autowire Autowiring ของ Symfony แก้ไขค่าอาร์กิวเมนต์ของ constructor จากการประกาศชนิดของมัน เมื่ออาร์กิวเมนต์ถูก type-hint ด้วยคลาสหรืออินเทอร์เฟซที่แมปตรงไปยัง service เพียงตัวเดียว container ก็ inject มันได้โดยไม่ต้องตั้งค่าใด ๆ autowiring จะติดขัดเฉพาะกับค่าที่ไม่ใช่ service เท่านั้น ได้แก่ ค่า scalar, environment variable และ parameter แอตทริบิวต์ `#[Autowire]` เข้ามาอุดช่องว่างนั้นบนตัวอาร์กิวเมนต์โดยตรง โดยวางการเชื่อมโยงไว้ติดกับโค้ดที่ใช้งานมัน ```php // src/Service/PaymentGateway.php 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, ) {} } ``` อาร์กิวเมนต์แต่ละตัวประกาศแหล่งที่มาของตนเอง ไม่ว่าจะเป็น parameter, environment variable หรือ service ที่มีชื่อ แอตทริบิวต์นี้ยังรับอาร์กิวเมนต์ `expression:` สำหรับค่าแบบ Expression Language ซึ่งมีประโยชน์เมื่อ dependency มาจากการคำนวณตอนรันไทม์แทนที่จะเป็นนิยามแบบคงที่ การเก็บ metadata นี้ไว้บน constructor หมายความว่าผู้อ่านเห็นได้ทันทีว่าแต่ละค่ามาจากที่ใด โดยไม่ต้องเปิดไฟล์ YAML แยกต่างหาก เอกสารอ้างอิงของแอตทริบิวต์อยู่ใน[เอกสาร autowiring ของ Symfony](https://symfony.com/doc/current/service_container/autowiring.html) > **จง type-hint อินเทอร์เฟซ ไม่ใช่คลาสรูปธรรม** > > การ autowire คลาสรูปธรรมอย่าง `S3Storage` จะผูกทุกผู้ใช้เข้ากับการอิมพลีเมนต์นั้น และทำให้การสลับมันในเทสต์เป็นเรื่องเจ็บปวด จง type-hint ที่ตัวอินเทอร์เฟซแทน แล้วปล่อยให้ `#[AsAlias]` หรือ `#[Autowire(service: ...)]` เป็นผู้ตัดสินว่า service รูปธรรมตัวไหน การ inject ทั้ง container เข้ามาเพื่อดึง service ด้วยมือจะทำลายการตรวจสอบตอนคอมไพล์ และคือแอนตี้แพตเทิร์นที่ autowiring มีขึ้นเพื่อกำจัดทิ้ง ## การผูกอินเทอร์เฟซเมื่อมีหลายการอิมพลีเมนต์ Autowiring ที่อิงกับอินเทอร์เฟซทำงานได้อย่างราบรื่นจนกระทั่งมีสองคลาสอิมพลีเมนต์มัน ณ จุดนั้น container ไม่อาจเดาได้ว่าจะ inject ตัวใดและจะโยนข้อผิดพลาดเรื่องความกำกวมออกมาตอนคอมไพล์ สองแอตทริบิวต์ช่วยแก้ปัญหานี้ได้ `#[AsAlias]` ทำเครื่องหมายว่าการอิมพลีเมนต์หนึ่งเป็นค่าเริ่มต้นสำหรับอินเทอร์เฟซ ส่วน `#[Autowire(service: ...)]` ปักหมุดการอิมพลีเมนต์เฉพาะตัวลงบนอาร์กิวเมนต์แต่ละตัว ```php // src/Storage/StorageInterface.php namespace App\Storage; interface StorageInterface { public function put(string $key, string $contents): void; } ``` ```php // src/Storage/S3Storage.php 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 */ } } ``` ```php // src/Service/BackupService.php 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, ) {} } ``` แอตทริบิวต์ `#[Target]` เสนอทางเลือกที่สาม โดยอ้างอิงการอิมพลีเมนต์ด้วยชื่อ alias ของ autowiring ซึ่งแยกชื่อตัวแปรอาร์กิวเมนต์ออกจากการแก้ไขค่า การเลือกระหว่างวิธีเหล่านี้ขึ้นอยู่กับเจตนา ใช้ `#[AsAlias]` เมื่อการอิมพลีเมนต์หนึ่งเป็นค่าเริ่มต้นอย่างแท้จริงทั่วทั้งฐานโค้ด และใช้ `#[Autowire(service: ...)]` หรือ `#[Target]` เมื่อผู้ใช้เพียงรายเดียวต้องการรูปแบบที่ไม่ใช่ค่าเริ่มต้น แพตเทิร์นนี้ปรากฏอยู่ตลอดในแอปพลิเคชัน production และเป็นหัวข้อโปรดใน[โมดูลสัมภาษณ์ dependency injection ของ Symfony](/technologies/symfony/interview-questions/dependency-injection) ## Service Tag ของ Symfony และตัวรวบรวม AutowireIterator Tag คือวิธีที่ Symfony ใช้จัดกลุ่ม service ที่เกี่ยวข้องกัน เพื่อให้ service อื่นเรียกใช้ทั้งหมดได้ในคราวเดียว สิ่งนี้ขับเคลื่อนแพตเทิร์น strategy, chain-of-responsibility และ plugin ที่ปรากฏอยู่ทั่วตัว framework เอง ได้แก่ form type extension, Twig extension และ event subscriber ล้วนถูกขับเคลื่อนด้วย tag ทั้งสิ้น แทนที่จะติด tag ด้วยมือ การวาง `#[AutoconfigureTag]` ไว้บนอินเทอร์เฟซจะบอก container ให้ติด tag ให้ทุกคลาสที่อิมพลีเมนต์มันโดยอัตโนมัติ ```php // src/Export/ExporterInterface.php 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; } ``` การอิมพลีเมนต์แต่ละตัวควบคุมตำแหน่งของมันในคอลเลกชันได้ด้วย `#[AsTaggedItem]` ซึ่งกำหนดลำดับความสำคัญ (ค่าที่สูงกว่าจะทำงานก่อน) และดัชนีสตริงเสริมสำหรับการเข้าถึงแบบใช้คีย์ ```php // src/Export/CsvExporter.php 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 ผู้เรียกใช้จะรวบรวม exporter ที่ติด tag ทุกตัวเข้าไปใน `iterable` ที่มีลำดับด้วย `#[AutowireIterator]` จากนั้นมันจะไล่ไปตามคอลเลกชันและมอบหมายงานให้กลยุทธ์ตัวแรกที่รองรับรูปแบบที่ร้องขอ ```php // src/Export/ExportManager.php 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"); } } ``` เมื่อคอลเลกชันมีขนาดใหญ่หรือมีต้นทุนในการสร้างสูง `#[AutowireLocator]` คือทางเลือกแบบ lazy โดยมัน inject container แบบ PSR-11 ที่จะสร้างอินสแตนซ์ของ service ที่ติด tag เฉพาะตอนที่มันถูกดึงออกมาด้วยคีย์จริง ๆ เท่านั้น ความแตกต่างระหว่างการวนซ้ำแบบ eager กับการค้นหาแบบ lazy นั้นสำคัญสำหรับ service ที่ถือการเชื่อมต่อหรือสถานะที่หนักไว้ ## Autoconfiguration, Compiler Pass และ Lazy Service Autoconfiguration คือกลไกที่อ่านแอตทริบิวต์อย่าง `#[AutoconfigureTag]` และ `#[AsTaggedItem]` แล้วนำมาปรับใช้ระหว่างการคอมไพล์ นี่จึงเป็นเหตุผลที่การอิมพลีเมนต์ `EventSubscriberInterface` เพียงอย่างเดียวก็เพียงพอที่จะลงทะเบียน event subscriber โดยไม่ต้องตั้งค่าเลย สำหรับการแปลงที่แอตทริบิวต์ไม่อาจแสดงออกได้ compiler pass จะเกี่ยวเข้าไปในกระบวนการสร้าง container โดยตรงและแก้ไขนิยามต่าง ๆ ก่อนที่แคชจะถูกเขียน โค้ดต้นฉบับของคอมโพเนนต์ `DependencyInjection` บน [GitHub](https://github.com/symfony/dependency-injection) แสดงให้เห็นว่าตัว framework เองใช้ pass เหล่านี้นับสิบตัวภายใน กราฟที่อ่อนไหวต่อประสิทธิภาพจะได้ประโยชน์จาก lazy service การทำเครื่องหมายอาร์กิวเมนต์ด้วย `#[Autowire(lazy: true)]` จะเลื่อนการสร้างอินสแตนซ์ออกไปจนกว่าจะมีการเรียก dependency นั้นเป็นครั้งแรก โดยอาศัย native lazy object ของ PHP 8.4 แทนไลบรารี proxy ที่ถูกเจนเนอเรตขึ้น ```php // src/Service/ReportBuilder.php 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 มาพร้อมกับ PHP 8.4 และมีการอธิบายเชิงลึกในคู่มือเรื่อง [lazy object ของ Symfony 8 และ PHP 8.4](/blog/symfony/symfony-8-new-features-php-84-lazy-objects) ผลตอบแทนที่ได้คือ container ที่เพรียวอยู่เสมอ ผู้ร่วมงานที่หนักอย่าง PDF renderer, client SDK ของบุคคลที่สาม หรือตัวสร้างรายงาน จะไม่กินต้นทุนใด ๆ จนกว่าเส้นทางโค้ดที่ต้องการมันจะทำงานจริง ## คำถามและคำตอบสัมภาษณ์ DI ของ Symfony คำถามสัมภาษณ์ DI ของ Symfony ที่พบบ่อยคือการถามถึงความแตกต่างระหว่าง autowiring และ autoconfiguration autowiring แก้ไขค่าอาร์กิวเมนต์ของ constructor ของ service ตามชนิด ในขณะที่ autoconfiguration ปรับใช้ tag, การเรียก method และการตั้งค่าให้กับ service ตามอินเทอร์เฟซที่มันอิมพลีเมนต์ ทั้งสองเป็นสวิตช์ที่แยกจากกันโดยอิสระ service หนึ่งอาจถูก autowire โดยไม่ถูก autoconfigure ก็ได้ และในทางกลับกันเช่นกัน อีกคำถามที่พบบ่อยคือทำไม container จึงแยกระหว่าง service แบบ public และ private service เป็น private โดยค่าเริ่มต้น หมายความว่ามันไม่สามารถถูกดึงจาก container ได้โดยตรงด้วย `$container->get()` และทำได้เพียงถูก inject เท่านั้น service แบบ private สามารถถูก inline และถูกตัดออกในระหว่างการคอมไพล์เพื่อความเร็ว ส่วน service แบบ public ต้องคงความสามารถในการถูกดึงไว้ ตัว optimizer จึงปล่อยมันไว้อย่างเดิม การดึง service ด้วยมือจาก container ถูกมองว่าเป็นแอนตี้แพตเทิร์นก็เพราะมันทำลายการปรับให้เหมาะสมนี้และซ่อน dependency เอาไว้ Circular dependency คือกับดักคลาสสิกและเป็นคำถามต่อเนื่องที่พบบ่อย หาก service A ต้องการ B ใน constructor และ B ต้องการ A container จะสร้างทั้งคู่ไม่ได้และรายงานการอ้างอิงแบบวนซ้ำตอนคอมไพล์ วิธีแก้มาตรฐานคือตัดวงจรด้วยการแยกพฤติกรรมที่ใช้ร่วมกันออกไปเป็น service ตัวที่สาม เปลี่ยนฝั่งหนึ่งไปเป็นการ inject แบบ lazy ด้วย `#[Autowire(lazy: true)]` เพื่อให้ dependency ถูกแก้ไขค่าเฉพาะเมื่อถูกเรียกครั้งแรก หรือใช้ setter injection เพื่อให้ออบเจ็กต์มีอยู่ก่อนที่ผู้ร่วมงานของมันจะถูกเชื่อมเข้ามา การตระหนักว่าทำไมวงจรนี้จึงเป็นกลิ่นของการออกแบบที่ไม่ดี ไม่ใช่แค่รู้วิธีทำให้มันเงียบลง คือสิ่งที่ผู้สัมภาษณ์ตั้งใจฟังจริง ๆ ผู้สัมภาษณ์ยังหยั่งเชิงเรื่องจังหวะเวลาของการแก้ไขค่าใน container ด้วย เนื่องจาก Symfony คอมไพล์ container หนึ่งครั้งแล้วแคชไว้ dependency ที่แก้ไขค่าไม่ได้จะโผล่ออกมาตอน cache warmup แทนที่จะเป็นตอน request การรับประกันนั้นคือผลลัพธ์โดยตรงของโมเดลแบบตอนคอมไพล์ และเป็นเหตุผลหนึ่งที่ทำให้แอปพลิเคชัน Symfony ขนาดใหญ่ยังคงคาดเดาได้ภายใต้ภาระงาน สำหรับชุดคำถามที่กว้างขวางกว่านี้ [เส้นทางเทคโนโลยี Symfony](/technologies/symfony) จัดกลุ่มคำถามเหล่านั้นตามคอมโพเนนต์และระดับความยาก ## บทสรุป - ปล่อยให้ container ทำงาน ลงทะเบียนคลาสภายใต้ `src/` type-hint dependency ใน constructor และเลิกทำการเชื่อมโยง YAML ด้วยมือโดยสิ้นเชิง - หยิบ `#[Autowire]` มาใช้เฉพาะกับสิ่งที่ไม่ใช่ service, parameter, environment variable และ service id ที่เจาะจงเท่านั้น - แก้ความกำกวมของอินเทอร์เฟซด้วย `#[AsAlias]` สำหรับค่าเริ่มต้น และ `#[Autowire(service: ...)]` หรือ `#[Target]` สำหรับการเลือกอย่างชัดแจ้ง - ขับเคลื่อนแพตเทิร์น strategy และ plugin ด้วย `#[AutoconfigureTag]`, ลำดับความสำคัญของ `#[AsTaggedItem]` และ `#[AutowireIterator]` แล้วเปลี่ยนไปใช้ `#[AutowireLocator]` เมื่อการค้นหาแบบ lazy เหมาะกว่าการวนซ้ำแบบ eager - เลื่อนผู้ร่วมงานที่มีต้นทุนสูงออกไปด้วย `#[Autowire(lazy: true)]` ซึ่งใช้ native lazy object ของ PHP 8.4 - คง service ไว้เป็น private และให้ถูก inject แทนการถูกดึง เพื่อให้ compiler สามารถ inline และปรับกราฟให้เหมาะสมได้ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/symfony/symfony-dependency-injection-service-container-autowiring-tags