12.15. Servisi i Dependency Injection.
Kada koristimo Drupal i treba nam kod iz contrib modula ili modula jezgra u našem prilagođenom modulu, koristimo hook-ove i servise (services). Već smo koristili hook-ove u ovom članku:
12.11.3. Hook-ovi za rad sa Entity.
Sada da se pozabavimo servisima. Servis je PHP objekat. Zato kada pravite novu PHP klasu u svom prilagođenom modulu, bolje je odmah je dizajnirati kao servis, kako bi vaš kod kasnije mogao da se koristi u drugom modulu na standardan način.
Drupal skuplja sve servise u PHP objektu zvanom Service Container, tako da Drupal čuva informacije o svim dostupnim i korišćenim servisima na jednom mestu. Možete pozvati ovaj objekat i videti koji se servisi koriste:
<?php
$container = \Drupal::getContainer();
?>
https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A%3AgetContainer/9.2.x
Možete raditi sa ovim objektom koristeći metode has/get/set, ali obično ćemo dodavati servise u kontejner pomoću *.services.yml fajlova u našim modulima.
Hajde da pogledamo implementaciju metode getContainer():
<?php
public static function getContainer() {
if (static::$container === NULL) {
throw new ContainerNotInitializedException('\\Drupal::$container is not initialized yet. \\Drupal::setContainer() must be called with a real container.');
}
return static::$container;
}
?>
Promenljiva Service Container je definisana kao statička, što znači da posle poziva index.php i do kraja obrade bilo kog zahteva možemo u bilo kom fajlu tokom izvršavanja pristupiti vrednosti ove promenljive: to može biti bilo koja klasa, hook u modulu ili čak .theme fajl teme.
Kako koristiti servise u Drupalu?
Sada da pređemo na to kako koristiti Service Container u Drupalu. U objektu $container nalaze se objekti servisa, što omogućava da se sva neophodna logika za kreiranje objekta izvrši u konstruktoru i da dobijemo spreman za korišćenje objekat u našem prilagođenom modulu. Na primer, ako treba da napišemo SQL upit bazi, samo pozovemo objekat za rad sa bazom iz Service Container-a koji već koristi kredencijale iz našeg settings.php i uspostavlja konekciju sa MySQL prilikom izvršenja upita:
$query = \Drupal::database()->select('node_field_data', 'n');
$query->addField('n', 'nid');
$query->condition('n.title', 'About Us');
$query->range(0, 1);
$nid = $query->execute()->fetchField();
Ako pogledate implementaciju metode database(), videćete da koristimo servis database iz Service Container-a:
https://api.drupal.org/api/drupal/core%21lib%21Drupal.php/function/Drupal%3A%3Adatabase/9.2.x
<?php
public static function database() {
return static::getContainer()
->get('database');
}
?>
Na ovaj način učitavamo samo one klase koje nam u datom trenutku trebaju. Zato koristimo jedinstveno skladište objekata - Service Container.
Kako dodati servis u Service Container?
Kada kreiramo fajl *.services.yml, Drupal učitava servise iz tih fajlova i čuva njihove objekte u Service Container-u.
https://api.drupal.org/api/drupal/core%21modules%21syslog%21syslog.services.yml/9.2.x
core/modules/syslog/syslog.services.yml:
services:
logger.syslog:
class: Drupal\syslog\Logger\SysLog
arguments: ['@config.factory', '@logger.log_message_parser']
tags:
- { name: logger }
U promenljivu $container možete dodati servis metodom set(), ali to se obično koristi za pravljenje mokova (mocking) u testovima:
Šta je Dependency Injection?
Ako pokrenete Code Sniffer, dobićete grešku da treba izmeniti Drupal::database() tako da se database poziva u konstruktoru klase u kojoj koristimo objekat iz Service Container-a. Kada pozivate objekat iz Service Container-a u konstruktoru klase, to se zove Dependency Injection (DI), na primer:
<?php
namespace Drupal\wisenet_connect\Form;
use Drupal\Core\Database\Connection;
/**
* Implementira WisenetConfigurationForm form kontroler.
*
* Ovaj primer pokazuje jednostavan formular sa jednim tekstualnim unosom.
* Proširujemo FormBase, što je najjednostavnija osnovna klasa za formu u Drupalu.
*
* @see \Drupal\Core\Form\FormBase
*/
class WisenetGetCourseForm extends FormBase {
/**
* Aktivna konekcija ka bazi.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Konstruktor WisenetGetCourseForm objekta.
*
* @param \Drupal\Core\Database\Connection $database
* Konekcija ka bazi koja se koristi.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
);
}
...
/**
* Implementira handler za čuvanje kursa.
*
* Funkcija za čuvanje podataka kursa u content type kursa.
*/
public function saveCourse($courses) {
...
$query = $this->database->select('node__field_course_wisenet_id', 'nc');
$query->addField('n', 'nid');
$query->join('node_field_data', 'n', 'nc.entity_id = n.nid');
$query->condition('nc.field_course_wisenet_id_value', $course['CourseOfferId']);
$query->range(0, 1);
$nid = $query->execute()->fetchField();
...
}
U ovom primeru, upit ka bazi je potreban u formi, pa smo dodali dodatni metod create(), koji se koristi za kreiranje instance klase. Metod create() može biti u različitim klasama i interfejsima, ali uvek ima parametar $container tipa ContainerInterface. Ako se u metodu create() pozove objekat iz Service Container-a $container->get('myservice.name'), vraćeni objekat se prosleđuje u konstruktor __construct() kao argument (u našem primeru $container->get('database') i argument Connection $database).
O tome kako treba pravilno pozivati objekte iz Service Container-a u konstruktoru kontrolera (Controller), bloka (Block), forme (BaseForm), konfiguracione forme (ConfigForm) prilagođene klase/servisa, obradićemo u narednim člancima.
Nakon što prođemo kako pravilno povezati i koristiti objekte iz Service Container-a, razmotrićemo i kako praviti svoje servise.
Takođe ćemo pokazati kako preklapati klase za servise, da bismo umesto klase iz contrib modula koristili klasu iz prilagođenog modula.
Zašto su potrebni Service Container i Dependency Injection?
Možemo koristiti namespaces i direktno pozvati kod iz modula, praviti instance za spoljne klase gde nam treba bez korišćenja Service Container-a. Ali to izaziva probleme kod održavanja koda. Na primer, treba da zamenimo klasu koja šalje email poruke, a ta klasa se poziva na 200 različitih mesta. Radi lakšeg održavanja koda pravimo servis, a ne direktno uključivanje fajlova. Kada želimo da šaljemo email preko SMTP umesto PHP mail(), samo menjamo klasu servisa, a ne putanju do nove klase na 200 mesta.
Dependency Injection rešava problem dvostrukog poziva servisa u jednoj klasi. Ne moramo dva puta pristupati Service Container-u ako koristimo servis u različitim metodama iste klase. Jednostavno zapišemo servis objekat u svojstvo naše klase i koristimo ga iz tog svojstva preko $this->serviceName.
Naravno, možemo funkcionisati i bez Service Container-a i Dependency Injection-a, ali ovi obrasci unifikuju naš kod i čine ga jednostavnijim za održavanje i ažuriranje.
Gde mogu videti ime servisa?
U našem primeru imamo servis "database":
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
// Ovde dodajemo ime servisa.
$container->get('database'),
);
}
Ali ako dodate servis iz contrib ili custom modula, on može imati ime:
ime_modula.ime_servisa
Možete proveriti ime servisa u fajlu *.services.yml. Ime servisa ne mora uvek da počinje sa module_name.*, ali obično je tako. Na primer:
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('commerce_cart.cart_provider'),
$container->get('entity_type.manager')
);
}