12.15. सर्विसेज़ और डिपेंडेंसी इंजेक्शन।
जब हम Drupal का उपयोग करते हैं और हमें किसी contrib मॉड्यूल या core मॉड्यूल का कोड अपने custom मॉड्यूल में इस्तेमाल करना होता है, तो हम hook'स और services (सर्विसेज़) का उपयोग करते हैं। हम पहले ही इस लेख में hook'स का उपयोग कर चुके हैं:
12.11.3. Entity के साथ काम करने के लिए хуки।
अब आइए services को समझते हैं। Service एक PHP ऑब्जेक्ट होता है। इसलिए जब आप अपने custom मॉड्यूल में नया PHP क्लास बनाते हैं, तो उसे service के रूप में तैयार करना बेहतर है, ताकि बाद में आपका कोड किसी दूसरे मॉड्यूल में standard तरीके से उपयोग किया जा सके।
Drupal सभी services को PHP ऑब्जेक्ट Service Container में इकट्ठा करता है, इसलिए Drupal सभी उपलब्ध और उपयोग किए जा रहे सर्विसेज़ की जानकारी एक ही जगह रखता है। आप इस ऑब्जेक्ट को कॉल कर सकते हैं और देख सकते हैं कि आपके पास कौन-कौन सी सर्विसेज़ उपयोग में हैं:
<?php
$container = \Drupal::getContainer();
?>
https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A%3AgetContainer/9.2.x
आप इस ऑब्जेक्ट के साथ has/get/set मेथड्स से काम कर सकते हैं, लेकिन आमतौर पर हम अपनी मॉड्यूल्स के *.services.yml फाइल के ज़रिए सर्विसेज़ को कंटेनर में जोड़ते हैं।
आइए getContainer() मेथड की इम्प्लीमेंटेशन देखें:
<?php
public static function getContainer() {
if (static::$container === NULL) {
throw new ContainerNotInitializedException('\\Drupal::$container अभी तक इनिशियलाइज़ नहीं हुआ है। \\Drupal::setContainer() को असली कंटेनर के साथ कॉल करना ज़रूरी है।');
}
return static::$container;
}
?>
Service कंटेनर वेरिएबल को static के रूप में परिभाषित किया गया है, इसका मतलब यह है कि index.php कॉल होने के बाद और कॉल की पूरी प्रोसेसिंग तक हम इस वेरिएबल का मान किसी भी फाइल में प्राप्त कर सकते हैं: यह किसी भी क्लास, मॉड्यूल में hook, या यहां तक कि थीम की .theme फाइल भी हो सकती है।
Drupal में सर्विसेज़ का उपयोग कैसे करें?
अब आइए देखें कि Drupal में Service Container का उपयोग कैसे करें। $container ऑब्जेक्ट में सर्विसेज़ के ऑब्जेक्ट्स स्टोर होते हैं, जो कंस्ट्रक्टर में ऑब्जेक्ट बनाने की सारी ज़रूरी लॉजिक को पूरा करते हैं और हमें तैयार ऑब्जेक्ट हमारे custom मॉड्यूल में देते हैं। उदाहरण के लिए, अगर हमें डेटाबेस पर SQL क्वेरी लिखनी है, तो हम बस डेटाबेस से काम करने वाले ऑब्जेक्ट को Service Container से कॉल करेंगे और यह ऑब्जेक्ट पहले ही हमारे settings.php फाइल से credentials लेकर MySQL से कनेक्शन बना लेगा:
$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();
अगर आप database() मेथड की इम्प्लीमेंटेशन देखेंगे, तो पाएंगे कि हम Service Container से database सर्विस का ऑब्जेक्ट इस्तेमाल कर रहे हैं:
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');
}
?>
इस तरह हम अपने कोड के लिए केवल वही क्लासेज़ जोड़ते हैं, जिनकी हमें अभी ज़रूरत है। इसी के लिए हम Service Container के एकीकृत स्टोरेज का उपयोग करते हैं।
Service container में सर्विस कैसे जोड़ें?
जब हम *.services.yml फाइल बनाते हैं, तो Drupal इन फाइलों से सर्विसेज़ को लोड करता है और उनके ऑब्जेक्ट्स को Service Container में स्टोर करता है।
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 }
$container वेरिएबल में set() मेथड के ज़रिए सर्विस जोड़ सकते हैं, लेकिन आमतौर पर इसका उपयोग टेस्टिंग में dependencies को mock करने के लिए किया जाता है:
Dependency Injection क्या है?
अगर आप Code Sniffer चलाते हैं, तो यह आपको बताएगा कि Drupal::database() को ठीक करके database को उस क्लास के कंस्ट्रक्टर में कॉल करना चाहिए जिसमें हम Service Container से ऑब्जेक्ट का उपयोग कर रहे हैं। जब आप किसी क्लास के कंस्ट्रक्टर में Service Container से ऑब्जेक्ट कॉल करते हैं, तो इसे Dependency Injection (DI) कहा जाता है, उदाहरण के लिए:
<?php
namespace Drupal\wisenet_connect\Form;
use Drupal\Core\Database\Connection;
/**
* Implements the WisenetConfigurationForm form controller.
*/
class WisenetGetCourseForm extends FormBase {
/**
* Active database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
public function __construct(Connection $database) {
$this->database = $database;
}
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
);
}
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();
...
}
इस उदाहरण में डेटाबेस क्वेरी की ज़रूरत form में है, इसलिए हमने create() मेथड जोड़ा, जो क्लास का इंस्टेंस बनाने के लिए इस्तेमाल होता है। create() मेथड हमेशा $container वेरिएबल को ContainerInterface टाइप के रूप में पैरामीटर लेता है। अगर create() मेथड में Service Container से कोई ऑब्जेक्ट कॉल होता है, तो वह कंस्ट्रक्टर __construct() में आर्ग्युमेंट के रूप में पास हो जाता है।
आगे हम देखेंगे कि Controller, Block, Form (BaseForm), ConfigForm और custom क्लास/सर्विस के कंस्ट्रक्टर में Service Container से ऑब्जेक्ट्स को सही तरीके से कैसे कॉल और उपयोग करें।
उसके बाद हम देखेंगे कि अपने खुद के सर्विसेज़ कैसे बनाएं और कैसे किसी contrib मॉड्यूल के क्लास को अपने custom मॉड्यूल के क्लास से ओवरराइड करें।
Service container और Dependency Injection की ज़रूरत क्यों है?
हम namespaces का उपयोग करके मॉड्यूल से सीधे कोड कनेक्ट कर सकते हैं और ज़रूरत वाली जगह पर ऑब्जेक्ट बना सकते हैं, लेकिन इससे कोड अपडेट करने में समस्या होती है। उदाहरण के लिए, अगर हमें ईमेल भेजने के क्लास को बदलना हो और वह 200 जगहों पर कॉल हो रहा हो, तो हमें 200 जगहों पर बदलाव करना पड़ेगा। इसके बजाय हम एक सर्विस बनाते हैं, अब अगर हमें PHP mail() की बजाय smtp से ईमेल भेजना हो, तो बस सर्विस का क्लास बदलना होगा, बाकी जगह कुछ बदलने की ज़रूरत नहीं होगी।
Dependency Injection एक ही क्लास में सर्विस को बार-बार कॉल करने की समस्या को हल करता है। हमें Service Container को बार-बार कॉल करने की ज़रूरत नहीं पड़ती, बल्कि हम एक बार सर्विस ऑब्जेक्ट को क्लास की property में रखते हैं और फिर $this->serviceName से उपयोग करते हैं।
बेशक, हम Service Container और Dependency Injection के बिना भी काम कर सकते हैं, लेकिन ये पैटर्न हमारे कोड को यूनिफाई करते हैं और अपडेट करना आसान बनाते हैं।
मैं सर्विस का नाम कहाँ देख सकता हूँ?
हमारे उदाहरण में हमारे पास "database" सर्विस है:
public static function create(ContainerInterface $container) {
return new static(
// यहाँ हम सर्विस का नाम जोड़ रहे हैं।
$container->get('database'),
);
}
लेकिन अगर आप किसी contrib/custom मॉड्यूल से सर्विस जोड़ते हैं, तो उसका नाम कुछ इस तरह हो सकता है:
module_name.service_name
आप *.services.yml फाइल में सर्विस का नाम देख सकते हैं। सर्विस का नाम हमेशा module_name.* से शुरू होना ज़रूरी नहीं है, लेकिन आमतौर पर ऐसा ही होता है। उदाहरण के लिए:
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')
);
}