Symfony2, korkean suorituskyvyn PHP-kehys, käyttää Dependency Injection Container -mallia, jossa komponentit tarjoavat riippuvuusinjektiorajapinnan DI-säiliölle. Tämän ansiosta kukin komponentti ei välitä muista riippuvuuksista. Kernel-luokka alustaa DI-säiliön ja ruiskuttaa sen eri komponentteihin. Mutta tämä tarkoittaa, että DI-säiliötä voidaan käyttää huoltopalveluna.
Symfony2: lla on siihen jopa ContainerAware-luokka. Monien mielestä Service Locator on Symfony2: n anti-malli. Henkilökohtaisesti en ole samaa mieltä. Se on yksinkertaisempi malli verrattuna DI: hen ja se on hyvä yksinkertaisille projekteille. Mutta Service Locator -malli ja DI-konttikuvio yhdessä projektissa ovat ehdottomasti anti-malli.
Tässä artikkelissa yritämme rakentaa Symfony2-sovelluksen ottamatta käyttöön Service Locator -mallia. Noudatamme yhtä yksinkertaista sääntöä: vain DI-konttien valmistaja voi tietää DI-kontista.
Dependency Injection -mallissa DI-kontti määrittelee palveluriippuvuudet ja palvelut voivat antaa vain käyttöliittymän injektointia varten. Siitä on monia artikkeleita Riippuvuuden injektio , ja olet todennäköisesti lukenut ne kaikki. Älkäämme siis keskittykö teoriaan, vaan katsokaamme vain perusajatusta. DI voi olla 3 tyyppiä:
Symfony-ohjelmassa injektiorakenne voidaan määrittää yksinkertaisten kokoonpanotiedostojen avulla. Nämä 3 injektiotyyppiä voidaan määrittää seuraavasti:
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'
Luodaan perushakemuksen rakenne. Kun olemme siinä, asennamme Symfony DI -konttikomponentin.
$ 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
Jotta säveltäjän autoloader löytäisi omat luokat src-kansiosta, voimme lisätä ‘autoloader’ -ominaisuuden säveltäjä.json-tiedostoon:
{ // ... 'autoload': { 'psr-4': { '': 'src/' } } }
Luodaan konttien valmistaja ja kielletään konttien ruiskuttaminen.
// in src/TrueContainer.php use SymfonyComponentDependencyInjectionContainerBuilder; use SymfonyComponentConfigFileLocator; use SymfonyComponentDependencyInjectionLoaderYamlFileLoader; use SymfonyComponentDependencyInjectionContainerInterface; 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); } }
Tässä käytämme Config- ja Yaml symfony -komponentteja. Löydät yksityiskohdat virallisista asiakirjoista tässä . Määritimme myös juuripolun parametrin ”app_root” joka tapauksessa. Hankintamenetelmä ylikuormittaa yläluokan oletushakukäyttäytymistä ja estää säilöä palauttamasta 'service_container'.
Seuraavaksi tarvitsemme aloituskohdan sovellukselle.
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__));
Tämän on tarkoitus käsitellä http-pyyntöjä. Meillä voi olla enemmän lähtökohtia konsolikomennoille, cron-tehtäville ja muulle. Jokaisen lähtökohdan on tarkoitus saada tiettyjä palveluja, ja heidän tulisi tietää DI-konttien rakenteesta. Tämä on ainoa paikka, jossa voimme pyytää palveluja kontilta. Tästä hetkestä lähtien yritämme rakentaa tämän sovelluksen vain DI-konttien määritystiedostoilla.
HttpKernel (ei kehysydin palvelupaikannusongelmalla) on peruskomponenttimme sovelluksen verkko-osalle. Tässä on tyypillinen HttpKernel-työnkulku:
Vihreät neliöt ovat tapahtumia.
HttpKernel käyttää HttpFoundation-komponenttia pyyntö- ja vastausobjekteissa ja EventDispatcher-komponenttia tapahtumajärjestelmässä. Niiden alustamisessa DI-kontin määritystiedostoilla ei ole ongelmia. HttpKernel on alustettava EventDispatcher-, ControllerResolver- ja valinnaisesti RequestStack-palveluilla (alipyynnöille).
Tässä on sen säiliökokoonpano:
# in config/events.yml services: dispatcher: class: SymfonyComponentEventDispatcherEventDispatcher
# in config/kernel.yml services: request: class: SymfonyComponentHttpFoundationRequest factory: [ SymfonyComponentHttpFoundationRequest, createFromGlobals ] request_stack: class: SymfonyComponentHttpFoundationRequestStack resolver: class: SymfonyComponentHttpKernelControllerControllerResolver http_kernel: class: SymfonyComponentHttpKernelHttpKernel arguments: ['@dispatcher', '@resolver', '@request_stack']
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' }
Kuten näette, käytämme 'tehdas' -ominaisuutta pyyntöpalvelun luomiseen. HttpKernel-palvelu saa vain Request-objektin ja palauttaa Response-objektin. Se voidaan tehdä etuohjaimessa.
// 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();
Tai vastaus voidaan määritellä palveluna konfiguraatiossa käyttämällä ”tehdas” -ominaisuutta.
# in config/kernel.yml # ... response: class: SymfonyComponentHttpFoundationResponse factory: [ '@http_kernel', handle] arguments: ['@request']
Ja sitten saamme sen vain etuohjaimeen.
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__)); $response = $container->get('response'); $response->send();
Ohjaimen ratkaisupalvelu saa ominaisuuden _controller pyynnön palvelun määritteistä ohjaimen ratkaisemiseksi. Nämä määritteet voidaan määritellä säilön konfiguraatiossa, mutta se näyttää hieman hankalammalta, koska meidän on käytettävä ParameterBag-objektia yksinkertaisen taulukon sijaan.
# in config/kernel.yml # ... request_attributes: class: SymfonyComponentHttpFoundationParameterBag calls: - [ set, [ _controller, AppControllerDefaultController::defaultAction ]] request: class: SymfonyComponentHttpFoundationRequest factory: [ SymfonyComponentHttpFoundationRequest, createFromGlobals ] properties: attributes: '@request_attributes' # ...
Ja tässä on DefaultController-luokka defaultAction-menetelmällä.
// in src/App/Controller/DefaultController.php namespace AppController; use SymfonyComponentHttpFoundationResponse; class DefaultController { function defaultAction() { return new Response('Hello cruel world'); } }
Kun kaikki nämä ovat paikallaan, meillä pitäisi olla toimiva sovellus.
Tämä ohjain on melko hyödytön, koska sillä ei ole pääsyä mihinkään palveluun. Symfony-kehyksessä tämä ongelma ratkaistaan injektoimalla DI-kontti ohjaimeen ja käyttämällä sitä palvelunpaikantimena. Emme tee sitä. Joten määritellään ohjain palveluksi ja syötetään siihen pyyntöpalvelu. Tässä on kokoonpano:
# in config/controllers.yml services: controller.default: class: AppControllerDefaultController arguments: [ '@request']
# in config/kernel.yml # ... request_attributes: class: SymfonyComponentHttpFoundationParameterBag calls: - [ set, [ _controller, ['@controller.default', defaultAction ]]] request: class: SymfonyComponentHttpFoundationRequest factory: [ SymfonyComponentHttpFoundationRequest, createFromGlobals ] properties: attributes: '@request_attributes' # ...
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' }
Ja ohjaimen koodi:
// in src/App/Controller/DefaultController.php namespace AppController; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpFoundationResponse; 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'); } }
Nyt ohjaimella on pääsy pyyntöpalveluun. Kuten näette, tällä järjestelmällä on pyöreät riippuvuudet. Se toimii, koska DI-kontti jakaa palvelun luomisen jälkeen ja ennen menetelmien ja omaisuuden injektointia. Joten kun ohjainpalvelua luodaan, pyyntöpalvelu on jo olemassa.
Näin se toimii:
Mutta tämä toimii vain siksi, että pyyntöpalvelu luodaan ensin. Kun saamme vastauspalvelun etuohjaimessa, pyyntöpalvelu on ensimmäinen alustettu riippuvuus. Jos yritämme ensin saada ohjainpalvelun, se aiheuttaa pyöreän riippuvuusvirheen. Se voidaan korjata käyttämällä menetelmää tai omaisuusinjektioita.
Mutta on toinenkin ongelma. DI-kontti alustaa jokaisen ohjaimen riippuvuuksilla. Joten se alustaa kaikki olemassa olevat palvelut, vaikka niitä ei tarvita. Onneksi kontissa on laiska lastausominaisuus. Symfony DI-komponentti käyttää välityspalveluluokkiin ”ocramius / proxy-manager”. Meidän on asennettava silta niiden välille.
$ composer require symfony/proxy-manager-bridge
Ja määritä se konttien valmistusvaiheessa:
// in src/TrueContainer.php //... use SymfonyBridgeProxyManagerLazyProxyInstantiatorRuntimeInstantiator; // ... $container = new self(); $container->setProxyInstantiator(new RuntimeInstantiator()); // ...
Nyt voimme määritellä laiskat palvelut.
# in config/controllers.yml services: controller.default: lazy: true class: AppControllerDefaultController arguments: [ '@request' ]
Joten ohjaimet aiheuttavat riippuvien palvelujen alustamisen vain, kun todellinen menetelmä kutsutaan. Lisäksi se välttää pyöreät riippuvuusvirheet, koska ohjainpalvelu jaetaan ennen varsinaista alustusta; vaikka meidän on silti vältettävä kiertoviittauksia. Tässä tapauksessa emme saa injektoida ohjainpalvelua pyyntöpalveluun tai pyyntöpalvelua ohjainpalveluun. Tarvitsemme tietysti kyselypalvelua ohjaimissa, joten vältetään injektiota pyyntöpalveluun kontin aloitusvaiheessa. HttpKernelillä on tätä varten tapahtumajärjestelmä.
Ilmeisesti haluamme, että meillä on erilaiset ohjaimet eri pyyntöjä varten. Joten tarvitsemme reititysjärjestelmän. Asennetaan symfony-reitityskomponentti.
$ composer require symfony/routing
Reitityskomponentilla on luokan reititin, joka voi käyttää reitityksen määritystiedostoja. Mutta nämä kokoonpanot ovat vain avainarvoparametreja Route-luokalle. Symfony framework käyttää omaa ohjainresoluuteriaan FrameworkBundlesta, joka ruiskuttaa kontin ohjaimiin ContainerAware-käyttöliittymällä. Juuri tätä yritämme välttää. HttpKernel-ohjaimen resolveri palauttaa luokan objektin sellaisenaan, jos se on jo olemassa ’_controller’ -määritteessä matriisina ohjainobjektin ja toimintomenetelmämerkkijonon kanssa (itse asiassa ohjaimen resolveri palauttaa sen ikään kuin se olisi vain taulukko). Joten meidän on määriteltävä jokainen reitti palveluksi ja syötettävä siihen ohjain. Lisätään muu ohjainpalvelu nähdäksesi, miten se toimii.
# in config/controllers.yml # ... controller.page: lazy: true class: AppControllerPageController arguments: [ '@request']
// in src/App/Controller/PageController.php namespace AppController; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpFoundationResponse; 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-komponentilla on RouteListener-luokka, joka käyttää ”kernel.request” -tapahtumaa. Tässä on yksi mahdollinen kokoonpano laiska-ohjaimilla:
# in config/routes/default.yml services: route.home: class: SymfonyComponentRoutingRoute arguments: path: / defaults: _controller: ['@controller.default', 'defaultAction'] route.page: class: SymfonyComponentRoutingRoute arguments: path: /page/{id} defaults: _controller: ['@controller.page', 'defaultAction']
# in config/routing.yml imports: - { resource: ’routes/default.yml' } services: route.collection: class: SymfonyComponentRoutingRouteCollection calls: - [ add, ['route_home', '@route.home'] ] - [ add, ['route_page', '@route.page'] ] router.request_context: class: SymfonyComponentRoutingRequestContext calls: - [ fromRequest, ['@request'] ] router.matcher: class: SymfonyComponentRoutingMatcherUrlMatcher arguments: [ '@route.collection', '@router.request_context' ] router.listener: class: SymfonyComponentHttpKernelEventListenerRouterListener arguments: matcher: '@router.matcher' request_stack: '@request_stack' context: '@router.request_context'
# in config/events.yml service: dispatcher: class: SymfonyComponentEventDispatcherEventDispatcher calls: - [ addSubscriber, ['@router.listener']]
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' } - { resource: 'routing.yml' }
Tarvitsemme myös URL-generaattorin sovelluksemme. Tässä se on:
# in config/routing.yml # ... router.generator: class: SymfonyComponentRoutingGeneratorUrlGenerator arguments: routes: '@route.collection' context: '@router.request_context'
URL-generaattori voidaan injektoida ohjain- ja renderointipalveluihin. Nyt meillä on perussovellus. Mikä tahansa muu palvelu voidaan määritellä samalla tavalla, kun määritystiedosto ruiskutetaan tiettyihin ohjaimiin tai tapahtumien lähettäjiin. Esimerkiksi tässä on joitain Twig- ja Doctrine-kokoonpanoja.
Twig on Symfony2-kehyksen oletusmallimoottori. Monet Symfony2-komponentit voivat käyttää sitä ilman sovittimia. Joten se on ilmeinen valinta sovelluksellemme.
$ 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' ]
Oppi on ORM, jota käytetään Symfony2-kehyksessä. Voimme käyttää mitä tahansa muuta ORM: ää, mutta Symfony2-komponentit voivat jo käyttää monia Docrine-ominaisuuksia.
$ 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: DoctrineORMConfiguration factory: [ DoctrineORMToolsSetup, createAnnotationMetadataConfiguration ] arguments: paths: '%doctrine.paths%' isDevMode: '%doctrine.is_dev%' doctrine.entity_manager: class: DoctrineORMEntityManager factory: [ DoctrineORMEntityManager, 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' }
Voimme käyttää myös YML- ja XML-kartoitusasetustiedostoja merkintöjen sijaan. Meidän on vain käytettävä 'createYAMLMetadataConfiguration' - ja 'createXMLMetadataConfiguration' -menetelmiä ja asetettava polku kansioon, jossa on nämä asetustiedostot.
Voi olla nopeasti ärsyttävää pistää kaikki tarvittavat palvelut kaikkiin ohjaimiin erikseen. DI-konttiosuudella on abstraktit palvelut ja palveluperintö, jotta siitä tulisi hieman parempi. Joten voimme määritellä joitain abstrakteja ohjaimia:
# in config/controllers.yml services: controller.base_web: lazy: true abstract: true class: AppControllerBaseWebController arguments: request: '@request' templating: '@templating.twig' entityManager: '@doctrine.entity_manager' urlGenerator: '@router.generator' controller.default: class: AppControllerDefaultController parent: controller.base_web controller.page: class: AppControllerPageController parent: controller.base_web
// in src/App/Controller/Base/WebController.php namespace AppControllerBase; use SymfonyComponentHttpFoundationRequest; use Twig_Environment; use DoctrineORMEntityManager; use SymfonyComponentRoutingGeneratorUrlGenerator; 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-komponentteja, kuten lomake, komento ja omaisuus, on monia muita. Ne kehitettiin itsenäisinä komponenteina, joten niiden integrointi DI-konttien avulla ei saisi olla ongelma.
DI-kontissa on myös tunnistejärjestelmä. Tunnisteet voidaan käsitellä Compiler Pass -luokilla. Event Dispatcher -komponentilla on oma Compiler Pass -toiminto tapahtumakuuntelijan tilauksen yksinkertaistamiseksi, mutta se käyttää ContainerAwareEventDispatcher-luokkaa EventDispatcher-luokan sijaan. Joten emme voi käyttää sitä. Mutta voimme toteuttaa omat kääntäjäliput tapahtumia, reittejä, tietoturvaa ja muita tarkoituksia varten.
Otetaan esimerkiksi käyttöön reititysjärjestelmän tunnisteet. Nyt reitin määrittelemiseksi meidän on määritettävä reittipalvelu reitin määritystiedostossa config / reitti-kansiossa ja lisättävä se sitten reitin keräyspalveluun config / routing.yml-tiedostossa. Se näyttää epäjohdonmukaiselta, koska määritämme reitittimen parametrit yhteen paikkaan ja reitittimen nimen toiseen paikkaan.
Tunnistejärjestelmällä voimme vain määrittää reitin nimen tagissa ja lisätä tämän reittipalvelun reittikokoelmaan tagin nimen avulla.
DI-kontti-komponentti käyttää kääntäjän passiluokkia muuttaakseen kontin kokoonpanoa ennen varsinaista alustusta. Joten toteutetaan kääntäjän passiluokka reitittimen tunnistejärjestelmälle.
// in src/CompilerPass/RouterTagCompilerPass.php namespace CompilerPass; use SymfonyComponentDependencyInjectionCompilerCompilerPassInterface; use SymfonyComponentDependencyInjectionContainerBuilder; use SymfonyComponentDependencyInjectionDefinition; use SymfonyComponentDependencyInjectionReference; 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 CompilerPassRouterTagCompilerPass; // ... $container = new self(); $container->addCompilerPass(new RouterTagCompilerPass()); // ...
Nyt voimme muokata kokoonpanoa:
# in config/routing.yml # … route.collection: class: SymfonyComponentRoutingRouteCollection tags: - { name: route_collection } # ...
# in config/routes/default.yml services: route.home: class: SymfonyComponentRoutingRoute arguments: path: / defaults: _controller: ['@controller.default', 'defaultAction'] tags: - { name: route, route_name: 'route_home' } route.page: class: SymfonyComponentRoutingRoute arguments: path: /page/{id} defaults: _controller: ['@controller.page', 'defaultAction'] tags: - { name: route, route_name: 'route_page' }
Kuten näette, reittikokoelmat saamme tunnisteen nimellä palvelun nimen sijasta, joten reittimerkkijärjestelmämme ei riipu todellisesta kokoonpanosta. Lisäksi reittejä voidaan lisätä mihin tahansa keräyspalveluun 'add' -menetelmällä. Kääntäjän ohikulkijat voivat yksinkertaistaa huomattavasti riippuvuuksien määrityksiä. Mutta ne voivat lisätä odottamattoman käyttäytymisen DI-säilöön, joten on parempi olla muuttamatta olemassa olevaa logiikkaa, kuten argumenttien, metodikutsujen tai luokkien nimien muuttamista. Lisää vain uusi, joka oli olemassa, kuten teimme käyttämällä tunnisteita.
Meillä on nyt sovellus, joka käyttää vain DI-säiliökuviota, ja se on rakennettu vain DI-säiliön määritystiedostoista. Kuten näette, maassa ei ole vakavia haasteita Symfony-sovelluksen rakentaminen tällä tavalla. Ja voit yksinkertaisesti visualisoida kaikki sovelluksesi riippuvuudet. Ainoa syy, miksi ihmiset käyttävät DI-kontteja palvelun paikantimena, on se, että palvelupaikan käsite on helpommin ymmärrettävissä. Ja huikea koodipohja DI-kontilla, jota käytetään palvelun paikantimena, on luultavasti seurausta tästä syystä.
Löydät tämän sovelluksen lähdekoodin GitHubissa .