True Dependency Injection ด้วยส่วนประกอบ Symfony
เผยแพร่แล้ว: 2022-03-11Symfony2 ซึ่งเป็นเฟรมเวิร์ก PHP ที่มีประสิทธิภาพสูง ใช้รูปแบบ Dependency Injection Container โดยที่ส่วนประกอบจัดเตรียมอินเทอร์เฟซการฉีดการขึ้นต่อกันสำหรับคอนเทนเนอร์ DI ซึ่งช่วยให้แต่ละองค์ประกอบไม่สนใจการขึ้นต่อกันอื่นๆ คลาส 'เคอร์เนล' จะเริ่มต้นคอนเทนเนอร์ DI และแทรกลงในส่วนประกอบต่างๆ แต่นี่หมายความว่าคอนเทนเนอร์ DI สามารถใช้เป็นตัวระบุตำแหน่งบริการได้
Symfony2 ยังมีคลาส 'ContainerAware' สำหรับสิ่งนั้น หลายคนมีความเห็นว่า Service Locator เป็นรูปแบบต่อต้านใน Symfony2 ส่วนตัวผมไม่เห็นด้วย เป็นรูปแบบที่เรียบง่ายกว่าเมื่อเทียบกับ DI และเหมาะสำหรับโครงการทั่วไป แต่รูปแบบ Service Locator และรูปแบบ DI-container ที่รวมอยู่ในโปรเจ็กต์เดียวนั้นเป็นแบบที่ต่อต้านอย่างแน่นอน
ในบทความนี้ เราจะพยายามสร้างแอปพลิเคชัน Symfony2 โดยไม่ใช้รูปแบบตัวระบุตำแหน่งบริการ เราจะปฏิบัติตามกฎง่ายๆ ข้อหนึ่ง: มีเพียงผู้สร้างคอนเทนเนอร์ DI เท่านั้นที่สามารถรู้เกี่ยวกับคอนเทนเนอร์ DI
DI-คอนเทนเนอร์
ในรูปแบบการพึ่งพาการฉีด DI-container นิยามการขึ้นต่อกันของบริการ และบริการสามารถให้อินเทอร์เฟซสำหรับการฉีดเท่านั้น มีบทความมากมายเกี่ยวกับ Dependency Injection และคุณอาจอ่านทั้งหมดแล้ว ดังนั้นอย่ามุ่งความสนใจไปที่ทฤษฎีและมองที่แนวคิดพื้นฐาน DI สามารถเป็น 3 ประเภท:
ใน Symfony โครงสร้างการฉีดสามารถกำหนดได้โดยใช้ไฟล์การกำหนดค่าอย่างง่าย นี่คือวิธีการกำหนดค่าการฉีด 3 ประเภทเหล่านี้:
services: my_service: class: MyClass constructor_injection_service: class: SomeClass1 arguments: ["@my_service"] method_injection_service: class: SomeClass2 calls: - [ setProperty, "@my_service" ] property_injection_service: class: SomeClass3 properties: property: "@my_service"
โครงการ Bootstrapping
มาสร้างโครงสร้างแอปพลิเคชันพื้นฐานของเรากัน ขณะที่เราดำเนินการ เราจะติดตั้งส่วนประกอบ Symfony DI-container
$ mkdir trueDI $ cd trueDI $ composer init $ composer require symfony/dependency-injection $ composer require symfony/config $ composer require symfony/yaml $ mkdir config $ mkdir www $ mkdir src
เพื่อให้ตัวโหลดอัตโนมัติค้นหาคลาสของเราเองในโฟลเดอร์ src เราสามารถเพิ่มคุณสมบัติ 'autoloader' ในไฟล์ composer.json:
{ // ... "autoload": { "psr-4": { "": "src/" } } }
เรามาสร้างตัวสร้างคอนเทนเนอร์ของเราและห้ามฉีดคอนเทนเนอร์กัน
// in src/TrueContainer.php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\ContainerInterface; class TrueContainer extends ContainerBuilder { public static function buildContainer($rootPath) { $container = new self(); $container->setParameter('app_root', $rootPath); $loader = new YamlFileLoader( $container, new FileLocator($rootPath . '/config') ); $loader->load('services.yml'); $container->compile(); return $container; } public function get( $id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE ) { if (strtolower($id) == 'service_container') { if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior ) { return; } throw new InvalidArgumentException( 'The service definition "service_container" does not exist.' ); } return parent::get($id, $invalidBehavior); } }
ที่นี่เราใช้ส่วนประกอบ Config และ Yaml symfony คุณสามารถดูรายละเอียดในเอกสารอย่างเป็นทางการได้ที่นี่ นอกจากนี้เรายังกำหนดพารามิเตอร์เส้นทางราก 'app_root' ในกรณี เมธอด get โอเวอร์โหลดการรับค่าดีฟอลต์ของคลาสพาเรนต์ และป้องกันไม่ให้คอนเทนเนอร์ส่งคืน “service_container”
ต่อไป เราต้องการจุดเริ่มต้นสำหรับการสมัคร
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__));
อันนี้มีขึ้นเพื่อจัดการคำขอ http เราสามารถมีจุดเข้าใช้งานเพิ่มเติมสำหรับคำสั่งคอนโซล งาน cron และอื่นๆ จุดเริ่มต้นแต่ละจุดควรได้รับบริการบางอย่างและควรรู้เกี่ยวกับโครงสร้างคอนเทนเนอร์ DI นี่เป็นที่เดียวที่เราสามารถขอรับบริการจากคอนเทนเนอร์ได้ จากนี้ไป เราจะพยายามสร้างแอปพลิเคชันนี้โดยใช้ไฟล์การกำหนดค่าคอนเทนเนอร์ DI เท่านั้น
HttpKernel
HttpKernel (ไม่ใช่เคอร์เนลเฟรมเวิร์กที่มีปัญหาตัวระบุตำแหน่งบริการ) จะเป็นองค์ประกอบพื้นฐานของเราสำหรับ web part ของแอปพลิเคชัน นี่คือเวิร์กโฟลว์ HttpKernel ทั่วไป:
สี่เหลี่ยมสีเขียวคือเหตุการณ์
HttpKernel ใช้องค์ประกอบ HttpFoundation สำหรับอ็อบเจ็กต์คำขอและการตอบสนอง และส่วนประกอบ EventDispatcher สำหรับระบบเหตุการณ์ ไม่มีปัญหาในการเริ่มต้นใช้งานด้วยไฟล์คอนฟิกูเรชัน DI-container ต้องเริ่มต้น HttpKernel ด้วย EventDispatcher, ControllerResolver และเลือกใช้บริการ RequestStack (สำหรับคำขอย่อย)
นี่คือการกำหนดค่าคอนเทนเนอร์สำหรับมัน:
# in config/events.yml services: dispatcher: class: Symfony\Component\EventDispatcher\EventDispatcher
# in config/kernel.yml services: request: class: Symfony\Component\HttpFoundation\Request factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ] request_stack: class: Symfony\Component\HttpFoundation\RequestStack resolver: class: Symfony\Component\HttpKernel\Controller\ControllerResolver http_kernel: class: Symfony\Component\HttpKernel\HttpKernel arguments: ["@dispatcher", "@resolver", "@request_stack"]
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' }
อย่างที่คุณเห็น เราใช้คุณสมบัติ 'โรงงาน' เพื่อสร้างบริการคำขอ บริการ HttpKernel รับเฉพาะวัตถุคำขอและส่งคืนวัตถุตอบกลับ สามารถทำได้ในตัวควบคุมด้านหน้า
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__)); $HTTPKernel = $container->get('http_kernel'); $request = $container->get('request'); $response = $HTTPKernel->handle($request); $response->send();
หรือการตอบสนองสามารถกำหนดเป็นบริการในการกำหนดค่าโดยใช้คุณสมบัติ 'โรงงาน'
# in config/kernel.yml # ... response: class: Symfony\Component\HttpFoundation\Response factory: [ "@http_kernel", handle] arguments: ["@request"]
แล้วเราก็เอามันไปที่คอนโทรลเลอร์ด้านหน้า
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__)); $response = $container->get('response'); $response->send();
บริการตัวแก้ไขตัวควบคุมได้รับคุณสมบัติ '_controller' จากแอตทริบิวต์ของบริการคำขอเพื่อแก้ไขตัวควบคุม แอตทริบิวต์เหล่านี้สามารถกำหนดได้ในการกำหนดค่าคอนเทนเนอร์ แต่อาจดูยุ่งยากกว่าเล็กน้อย เนื่องจากเราต้องใช้อ็อบเจ็กต์ ParameterBag แทนอาร์เรย์ธรรมดา
# in config/kernel.yml # ... request_attributes: class: \Symfony\Component\HttpFoundation\ParameterBag calls: - [ set, [ _controller, \App\Controller\DefaultController::defaultAction ]] request: class: Symfony\Component\HttpFoundation\Request factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ] properties: attributes: "@request_attributes" # ...
และนี่คือคลาส DefaultController ด้วยวิธี defaultAction
// in src/App/Controller/DefaultController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; class DefaultController { function defaultAction() { return new Response("Hello cruel world"); } }
ด้วยสิ่งเหล่านี้ เราควรมีแอปพลิเคชันที่ใช้งานได้
คอนโทรลเลอร์นี้ค่อนข้างไร้ประโยชน์เพราะไม่มีการเข้าถึงบริการใดๆ ในกรอบงาน Symfony ปัญหานี้แก้ไขได้ด้วยการฉีดคอนเทนเนอร์ DI ในคอนโทรลเลอร์และใช้เป็นตัวระบุตำแหน่งบริการ เราจะไม่ทำอย่างนั้น ลองกำหนดคอนโทรลเลอร์เป็นบริการและใส่คำขอบริการเข้าไป นี่คือการกำหนดค่า:
# in config/controllers.yml services: controller.default: class: App\Controller\DefaultController arguments: [ "@request"]
# in config/kernel.yml # ... request_attributes: class: \Symfony\Component\HttpFoundation\ParameterBag calls: - [ set, [ _controller, ["@controller.default", defaultAction ]]] request: class: Symfony\Component\HttpFoundation\Request factory: [ Symfony\Component\HttpFoundation\Request, createFromGlobals ] properties: attributes: "@request_attributes" # ...
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' }
และรหัสควบคุม:
// in src/App/Controller/DefaultController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class DefaultController { /** @var Request */ protected $request; function __construct(Request $request) { $this->request = $request; } function defaultAction() { $name = $this->request->get('name'); return new Response("Hello $name"); } }
ตอนนี้ผู้ควบคุมสามารถเข้าถึงบริการร้องขอได้ อย่างที่คุณเห็น โครงการนี้มีการอ้างอิงแบบวงกลม ใช้งานได้เนื่องจากคอนเทนเนอร์ DI แชร์บริการหลังการสร้างและก่อนการฉีดวิธีการและคุณสมบัติ ดังนั้นเมื่อมีการสร้างบริการตัวควบคุม บริการคำขอก็มีอยู่แล้ว
นี่คือวิธีการทำงาน:
แต่สิ่งนี้ใช้ได้เพราะบริการคำขอถูกสร้างขึ้นก่อนเท่านั้น เมื่อเราได้รับบริการตอบกลับในตัวควบคุมด้านหน้า บริการคำขอจะเป็นการพึ่งพาเริ่มต้นครั้งแรก หากเราพยายามรับบริการคอนโทรลเลอร์ก่อน จะทำให้เกิดข้อผิดพลาดในการพึ่งพาแบบวงกลม สามารถแก้ไขได้โดยใช้วิธีการหรือการฉีดคุณสมบัติ
แต่มีปัญหาอื่น DI-container จะเริ่มต้นคอนโทรลเลอร์แต่ละตัวด้วยการพึ่งพา ดังนั้นจะเริ่มต้นบริการที่มีอยู่ทั้งหมดแม้ว่าจะไม่จำเป็นก็ตาม โชคดีที่คอนเทนเนอร์มีฟังก์ชันการโหลดแบบ Lazy Loading Symfony DI-component ใช้ 'ocramius/proxy-manager' สำหรับคลาสพร็อกซี เราต้องติดตั้งสะพานเชื่อมระหว่างกัน
$ composer require symfony/proxy-manager-bridge
และกำหนดไว้ที่ขั้นตอนการสร้างตู้คอนเทนเนอร์:
// in src/TrueContainer.php //... use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; // ... $container = new self(); $container->setProxyInstantiator(new RuntimeInstantiator()); // ...
ตอนนี้เราสามารถกำหนดบริการที่ขี้เกียจได้
# in config/controllers.yml services: controller.default: lazy: true class: App\Controller\DefaultController arguments: [ "@request" ]
ดังนั้นผู้ควบคุมจะทำให้เกิดการเริ่มต้นของบริการที่ขึ้นต่อกันเมื่อมีการเรียกวิธีการจริงเท่านั้น นอกจากนี้ยังหลีกเลี่ยงข้อผิดพลาดการพึ่งพาแบบวงกลมเนื่องจากบริการคอนโทรลเลอร์จะถูกแชร์ก่อนการเริ่มต้นจริง แม้ว่าเรายังคงต้องหลีกเลี่ยงการอ้างอิงแบบวงกลม ในกรณีนี้ เราไม่ควรใส่บริการตัวควบคุมในบริการร้องขอหรือบริการร้องขอลงในบริการควบคุม เห็นได้ชัดว่าเราต้องการบริการร้องขอในตัวควบคุม ดังนั้น ให้หลีกเลี่ยงการฉีดในบริการร้องขอในขั้นตอนการเริ่มต้นคอนเทนเนอร์ HttpKernel มีระบบกิจกรรมเพื่อการนี้
การกำหนดเส้นทาง
เห็นได้ชัดว่าเราต้องการให้มีตัวควบคุมที่แตกต่างกันสำหรับคำขอที่แตกต่างกัน เราจึงต้องมีระบบการกำหนดเส้นทาง มาติดตั้งองค์ประกอบการกำหนดเส้นทาง symfony

$ composer require symfony/routing
องค์ประกอบการเราต์มีคลาสเราเตอร์ซึ่งสามารถใช้ไฟล์การกำหนดค่าการกำหนดเส้นทาง แต่การกำหนดค่าเหล่านี้เป็นเพียงพารามิเตอร์คีย์-ค่าสำหรับคลาสเส้นทาง กรอบงาน Symfony ใช้ตัวแก้ไขตัวควบคุมของตัวเองจาก FrameworkBundle ซึ่งบรรจุคอนเทนเนอร์ในตัวควบคุมด้วยอินเทอร์เฟซ 'ContainerAware' นี่คือสิ่งที่เรากำลังพยายามหลีกเลี่ยง ตัวแก้ไขตัวควบคุม HttpKernel ส่งคืนอ็อบเจ็กต์คลาสราวกับว่ามีอยู่แล้วในแอตทริบิวต์ '_controller' เป็นอาร์เรย์ที่มีอ็อบเจ็กต์ตัวควบคุมและสตริงวิธีดำเนินการ (อันที่จริง รีโซลเวอร์คอนโทรลเลอร์จะส่งคืนเหมือนว่าเป็นเพียงอาร์เรย์) ดังนั้นเราต้องกำหนดแต่ละเส้นทางเป็นบริการและใส่ตัวควบคุมเข้าไป มาเพิ่มบริการควบคุมอื่น ๆ เพื่อดูว่ามันทำงานอย่างไร
# in config/controllers.yml # ... controller.page: lazy: true class: App\Controller\PageController arguments: [ "@request"]
// in src/App/Controller/PageController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class PageController { /** @var Request */ protected $request; function __construct(Request $request) { $this->request = $request; } function defaultAction($id) { return new Response("Page $id doesn't exist"); } }
องค์ประกอบ HttpKernel มีคลาส RouteListener ซึ่งใช้เหตุการณ์ 'kernel.request' นี่คือการกำหนดค่าหนึ่งที่เป็นไปได้กับตัวควบคุมแบบสันหลังยาว:
# in config/routes/default.yml services: route.home: class: Symfony\Component\Routing\Route arguments: path: / defaults: _controller: ["@controller.default", 'defaultAction'] route.page: class: Symfony\Component\Routing\Route arguments: path: /page/{id} defaults: _controller: ["@controller.page", 'defaultAction']
# in config/routing.yml imports: - { resource: 'routes/default.yml' } services: route.collection: class: Symfony\Component\Routing\RouteCollection calls: - [ add, ["route_home", "@route.home"] ] - [ add, ["route_page", "@route.page"] ] router.request_context: class: Symfony\Component\Routing\RequestContext calls: - [ fromRequest, ["@request"] ] router.matcher: class: Symfony\Component\Routing\Matcher\UrlMatcher arguments: [ "@route.collection", "@router.request_context" ] router.listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener arguments: matcher: "@router.matcher" request_stack: "@request_stack" context: "@router.request_context"
# in config/events.yml service: dispatcher: class: Symfony\Component\EventDispatcher\EventDispatcher calls: - [ addSubscriber, ["@router.listener"]]
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' } - { resource: 'routing.yml' }
นอกจากนี้เรายังต้องการตัวสร้าง URL ในแอปพลิเคชันของเรา นี่คือ:
# in config/routing.yml # ... router.generator: class: Symfony\Component\Routing\Generator\UrlGenerator arguments: routes: "@route.collection" context: "@router.request_context"
ตัวสร้าง URL สามารถแทรกลงในคอนโทรลเลอร์และบริการแสดงผล ตอนนี้เรามีแอปพลิเคชันพื้นฐานแล้ว บริการอื่นๆ สามารถกำหนดได้ในลักษณะเดียวกับที่ไฟล์การกำหนดค่าถูกฉีดเข้าไปในคอนโทรลเลอร์บางตัวหรือโปรแกรมเลือกจ่ายงานเหตุการณ์ ตัวอย่างเช่น นี่คือการกำหนดค่าบางอย่างสำหรับทวิกและหลักคำสอน
Twig
Twig เป็นเอ็นจิ้นเทมเพลตเริ่มต้นในเฟรมเวิร์ก Symfony2 ส่วนประกอบ Symfony2 จำนวนมากสามารถใช้งานได้โดยไม่ต้องใช้อะแดปเตอร์ ดังนั้นจึงเป็นตัวเลือกที่ชัดเจนสำหรับการสมัครของเรา
$ composer require twig/twig $ mkdir src/App/View
# in config/twig.yml services: templating.twig_loader: class: Twig_Loader_Filesystem arguments: [ "%app_root%/src/App/View" ] templating.twig: class: Twig_Environment arguments: [ "@templating.twig_loader" ]
หลักคำสอน
Doctrine เป็น ORM ที่ใช้ในเฟรมเวิร์ก Symfony2 เราสามารถใช้ ORM อื่นๆ ได้ แต่ส่วนประกอบ Symfony2 สามารถใช้คุณสมบัติ Docrine ได้มากมายอยู่แล้ว
$ composer require doctrine/orm $ mkdir src/App/Entity
# in config/doctrine.yml parameters: doctrine.driver: "pdo_pgsql" doctrine.user: "postgres" doctrine.password: "postgres" doctrine.dbname: "true_di" doctrine.paths: ["%app_root%/src/App/Entity"] doctrine.is_dev: true services: doctrine.config: class: Doctrine\ORM\Configuration factory: [ Doctrine\ORM\Tools\Setup, createAnnotationMetadataConfiguration ] arguments: paths: "%doctrine.paths%" isDevMode: "%doctrine.is_dev%" doctrine.entity_manager: class: Doctrine\ORM\EntityManager factory: [ Doctrine\ORM\EntityManager, create ] arguments: conn: driver: "%doctrine.driver%" user: "%doctrine.user%" password: "%doctrine.password%" dbname: "%doctrine.dbname%" config: "@doctrine.config"
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' } - { resource: 'routing.yml' } - { resource: 'twig.yml' } - { resource: 'doctrine.yml' }
นอกจากนี้เรายังสามารถใช้ไฟล์การกำหนดค่าการแมป YML และ XML แทนคำอธิบายประกอบ เราแค่ต้องใช้เมธอด 'createYAMLMetadataConfiguration' และ 'createXMLMetadataConfiguration' และตั้งค่าพาธไปยังโฟลเดอร์ที่มีไฟล์ปรับแต่งเหล่านี้
การฉีดทุกบริการที่จำเป็นในคอนโทรลเลอร์แต่ละตัวแยกกันอาจกลายเป็นเรื่องน่ารำคาญได้อย่างรวดเร็ว เพื่อให้ส่วนประกอบ DI-container ดีขึ้นเล็กน้อยมีบริการที่เป็นนามธรรมและการสืบทอดบริการ ดังนั้นเราจึงสามารถกำหนดตัวควบคุมนามธรรมบางตัวได้:
# in config/controllers.yml services: controller.base_web: lazy: true abstract: true class: App\Controller\Base\WebController arguments: request: "@request" templating: "@templating.twig" entityManager: "@doctrine.entity_manager" urlGenerator: "@router.generator" controller.default: class: App\Controller\DefaultController parent: controller.base_web controller.page: class: App\Controller\PageController parent: controller.base_web
// in src/App/Controller/Base/WebController.php namespace App\Controller\Base; use Symfony\Component\HttpFoundation\Request; use Twig_Environment; use Doctrine\ORM\EntityManager; use Symfony\Component\Routing\Generator\UrlGenerator; abstract class WebController { /** @var Request */ protected $request; /** @var Twig_Environment */ protected $templating; /** @var EntityManager */ protected $entityManager; /** @var UrlGenerator */ protected $urlGenerator; function __construct( Request $request, Twig_Environment $templating, EntityManager $entityManager, UrlGenerator $urlGenerator ) { $this->request = $request; $this->templating = $templating; $this->entityManager = $entityManager; $this->urlGenerator = $urlGenerator; } } // in src/App/Controller/DefaultController // … class DefaultController extend WebController { // ... } // in src/App/Controller/PageController // … class PageController extend WebController { // ... }
มีส่วนประกอบ Symfony ที่มีประโยชน์อื่นๆ มากมาย เช่น แบบฟอร์ม คำสั่ง และสินทรัพย์ พวกเขาได้รับการพัฒนาเป็นส่วนประกอบอิสระ ดังนั้นการรวมโดยใช้ DI-container ไม่น่าจะมีปัญหา
แท็ก
DI-container ยังมีระบบแท็ก แท็กสามารถประมวลผลได้โดยคลาส Compiler Pass คอมโพเนนต์ Event Dispatcher มี Compiler Pass ของตัวเองเพื่อทำให้การสมัครรับข้อมูลเหตุการณ์ง่ายขึ้น แต่ใช้คลาส ContainerAwareEventDispatcher แทนคลาส EventDispatcher เราจึงใช้ไม่ได้ แต่เราสามารถใช้คอมไพเลอร์พาสของเราสำหรับเหตุการณ์ เส้นทาง การรักษาความปลอดภัย และวัตถุประสงค์อื่นๆ
ตัวอย่างเช่น มาติดตั้งแท็กสำหรับระบบกำหนดเส้นทาง ในการกำหนดเส้นทาง เราต้องกำหนดบริการเส้นทางในไฟล์กำหนดค่าเส้นทางในโฟลเดอร์ config/routes จากนั้นเพิ่มไปยังบริการรวบรวมเส้นทางในไฟล์ config/routing.yml มันดูไม่สอดคล้องกันเพราะเรากำหนดพารามิเตอร์เราเตอร์ในที่หนึ่งและชื่อเราเตอร์ในอีกที่หนึ่ง
ด้วยระบบแท็ก เราสามารถกำหนดชื่อเส้นทางในแท็กและเพิ่มบริการเส้นทางนี้ในการรวบรวมเส้นทางโดยใช้ชื่อแท็ก
คอมโพเนนต์ DI-container ใช้คลาสคอมไพเลอร์พาสเพื่อทำการปรับเปลี่ยนการกำหนดค่าคอนเทนเนอร์ก่อนการเริ่มต้นจริง ลองใช้คลาสคอมไพเลอร์พาสของเราสำหรับระบบแท็กเราเตอร์
// in src/CompilerPass/RouterTagCompilerPass.php namespace CompilerPass; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; class RouterTagCompilerPass implements CompilerPassInterface { /** * You can modify the container here before it is dumped to PHP code. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { $routeTags = $container->findTaggedServiceIds('route'); $collectionTags = $container->findTaggedServiceIds('route_collection'); /** @var Definition[] $routeCollections */ $routeCollections = array(); foreach ($collectionTags as $serviceName => $tagData) $routeCollections[] = $container->getDefinition($serviceName); foreach ($routeTags as $routeServiceName => $tagData) { $routeNames = array(); foreach ($tagData as $tag) if (isset($tag['route_name'])) $routeNames[] = $tag['route_name']; if (!$routeNames) continue; $routeReference = new Reference($routeServiceName); foreach ($routeCollections as $collection) foreach ($routeNames as $name) $collection->addMethodCall('add', array($name, $routeReference)); } } }
// in src/TrueContainer.php //... use CompilerPass\RouterTagCompilerPass; // ... $container = new self(); $container->addCompilerPass(new RouterTagCompilerPass()); // ...
ตอนนี้เราสามารถแก้ไขการกำหนดค่าของเรา:
# in config/routing.yml # … route.collection: class: Symfony\Component\Routing\RouteCollection tags: - { name: route_collection } # ...
# in config/routes/default.yml services: route.home: class: Symfony\Component\Routing\Route arguments: path: / defaults: _controller: ["@controller.default", 'defaultAction'] tags: - { name: route, route_name: 'route_home' } route.page: class: Symfony\Component\Routing\Route arguments: path: /page/{id} defaults: _controller: ["@controller.page", 'defaultAction'] tags: - { name: route, route_name: 'route_page' }
อย่างที่คุณเห็น เราได้รับการรวบรวมเส้นทางตามชื่อแท็กแทนที่จะเป็นชื่อบริการ ดังนั้นระบบแท็กเส้นทางของเราจึงไม่ขึ้นอยู่กับการกำหนดค่าจริง นอกจากนี้ คุณสามารถเพิ่มเส้นทางไปยังบริการรวบรวมด้วยวิธี 'เพิ่ม' ผู้ส่งผ่านคอมไพเลอร์สามารถลดความซับซ้อนของการกำหนดค่าการพึ่งพาได้อย่างมาก แต่สามารถเพิ่มลักษณะการทำงานที่ไม่คาดคิดให้กับคอนเทนเนอร์ DI ได้ ดังนั้นจึงเป็นการดีกว่าที่จะไม่แก้ไขตรรกะที่มีอยู่ เช่น การเปลี่ยนอาร์กิวเมนต์ การเรียกใช้เมธอด หรือชื่อคลาส เพียงเพิ่มอันใหม่ที่มีอยู่อย่างที่เราทำโดยใช้แท็ก
สรุป
ขณะนี้ เรามีแอปพลิเคชันที่ใช้เฉพาะรูปแบบคอนเทนเนอร์ DI และสร้างขึ้นโดยใช้ไฟล์การกำหนดค่าคอนเทนเนอร์ DI เท่านั้น อย่างที่คุณเห็น ไม่มีความท้าทายร้ายแรงในการสร้างแอปพลิเคชัน Symfony ด้วยวิธีนี้ และคุณสามารถเห็นภาพการพึ่งพาแอปพลิเคชันทั้งหมดของคุณได้ง่ายๆ เหตุผลเดียวที่ผู้คนใช้ DI-container เป็นตัวระบุตำแหน่งบริการคือ แนวคิดของตัวระบุตำแหน่งบริการเข้าใจง่ายขึ้น และฐานรหัสขนาดใหญ่ที่มีคอนเทนเนอร์ DI ที่ใช้เป็นตัวระบุตำแหน่งบริการน่าจะเป็นผลมาจากเหตุผลนั้น
คุณสามารถค้นหาซอร์สโค้ดของแอปพลิเคชันนี้บน GitHub