Service Container в Masonite
Service Container — чрезвычайно мощная функция Masonite и её следует использовать максимально широко. Важно понимать концепции Service Container. Это простые концепции, которые, тем не менее, могут показаться волшебными, если вы не понимаете, что происходит под капотом.
Начало
Service Container — это просто словарь, в который записаны классы в виде пар ключ-значение. Затем они могут быть извлечены либо по ключу, либо по значению через определяемые объекты.
Думайте об "определяемых объектах" в соответствии с утверждением Masonite: "Что нужно вашему объекту? Ок, они есть в этом словаре, позвольте мне достать их для вас".
Контейнер содержит все классы и возможности фреймворка, так что добавление функционала в Masonite только добавляет классы в контейнер для использования разработчиком позже. Обычно это означает "регистрацию" этих классов в контейнере (подробнее об этом позже). Это позволяет Masonite быть модульным.
Некоторые объекты определены в контейнере по умолчанию. К ним относятся ваши методы контроллера (которые встречаются наиболее часто и вы, вероятно, использовали их ранее), конструкторы драйверов и middleware. И любые другие классы которые указаны в документации.
Есть 3 метода, которые важны во взаимодействии с контейнером: bind
, make
и resolve
.
Bind
Для связывания классов внутри контейнера следует использовать метод bind
в нашем приложении.
В service provider это будет выглядеть следующим образом:
from masonite.provider import ServiceProvider
from app.User import User
class UserModelProvider(ServiceProvider):
def register(self):
self.application.bind('User', User)
def boot(self):
pass
Это загрузит пары ключ - значение в словарь providers
в контейнере. После этого вызова словарь будет
выглядеть так:
Service Container доступен в объекте Request
и может быть получен так:
Простая привязка
Иногда не важно, какой ключ у объекта, который вы связываете. Например, вы можете привязать класс
Markdown
к контейнеру, но на самом деле все равно, как называется связанный ключ.
Это отличная причина использовать простую привязку, которая установит ключ как объект класса:
from masonite.provider import ServiceProvider
from app.User import User
class UserModelProvider(ServiceProvider):
def register(self):
self.application.simple(User)
def boot(self):
pass
Make
Для того чтобы получить класс из service container, мы можем просто использовать метод make()
.
Singleton
Вы можете привязывать singleton к контейнеру. Это определит объект во время привязки. Что позволит использовать один и тот же объект на протяжении всего срока службы контейнера.
from masonite.provider import ServiceProvider
from app.helpers import SomeClass
class UserModelProvider(ServiceProvider):
def register(self):
self.application.singleton('SomeClass', SomeClass)
def boot(self):
pass
Has
Вы можете также проверить, существует ли ключ в контейнере, используя метод .has()
:
in
:
Collecting (Группировка)
Вам может понадобиться собрать специфический набор объектов из контейнера по какому-то ключу.
Например, могут понадобиться все объекты, которые начинаются с "Exception"
и заканчиваются "Hook"
.
Или все ключи, которые заканчиваются на "ExceptionHook"
, если мы создаём обработчик исключений.
Группировка по ключу
Мы можем легко собрать все объекты по ключу:
Будет возвращен словарь всех объектов, связанных с контейнером, которые начинаются с чего угодно и заканчиваются на"ExceptionHook"
, такие, как "SentryExceptionHook"
или "AwesomeExceptionHook"
.
Мы также можем сделать противоположное и собрать все объекты, которые начинаются со специфического ключа.
Будут собраны все ключи, которые начинаются с"Sentry"
, такие, как "SentryWebhook"
или
"SentryExceptionHandler"
.
Наконец, мы можем собрать объекты, которые начинаются с "Sentry"
заканчиваются на "Hook"
.
"SentryExceptionHook"
и "SentryHandlerHook"
.
Группировка по объекту
Вы можете также собрать все подклассы объекта или если хотите собрать все сущности специфического класса из контейнера.
from cleo import Command
...
app.collect(Command)
# Вернёт {'FirstCommand': <class ...>, 'AnotherCommand': ...}
Resolve (Определение)
Это наиболее мощная часть контейнера. Можно получить объекты из контейнера просто передав их в список параметров любого объекта. Некоторые области Masonite определены, такие, как, методы контроллера, middleware и drivers.
Например, мы хотим получить класс Request
и поместить его в наш контроллер. Все методы контроллера
определены контейнером.
Request
.
Masonite будет знать, что вы пытаетесь получить класс Request
и фактически извлечёт этот класс из
контейнера. Masonite найдет класс Request
(несмотря на то, какой ключ в контейнере), вернёт его и
передаст в метод контроллера. Эффективное создание IOC контейнера с dependency injection.
Думайте об этом как о get by value или get by key в примерах ранее.
Очень мощная штука, да?
Masonite также будет определять ваш пользовательский, специфичный для приложения класс, включая те,
которые вы явно не связываете с помощью app.bind()
Продолжая пример выше, следующее будет работать из коробки (при условии, что соответствующие классы существуют), без необходимости связывать пользовательские классы в контейнере:
# в другом месте...
class MyService:
def __init__(self, some_other_dependency: SomeOtherClass):
pass
def do_something(self):
pass
# в контроллере...
def show(self, request: Request, service: MyService):
request.user()
service.do_something()
Resolving Instances (Определение сущностей)
Следующая мощная особенность контейнера заключается в том, что он может фактически вернуть сущности
классов, которые вы упоминаете. Например, все драйверы Upload
наследуются от UploadContract
,
который работает как интерфейс для всех драйверов Upload
.
Множество парадигм программирования утверждают, что разработчики должны создавать интерфейс, а не реализацию, так что Masonite позволяет возвращать сущности классов для этого специфического использования.
Держите пример:
from masonite.contracts import UploadContract
def show(self, upload: UploadContract)
upload # <class masonite.drivers.UploadDiskDriver>
Обратите внимание, что мы передали контракт вместо класса Upload
.
Определение вашего собственного кода
Service Container может быть использован вне потока Masonite. Masonite принимает функцию или метод класса, и определяет их зависимости путём нахождения в service container и внедряя их для вас.
Благодаря этому, вы можете определить любой собственный класс или функцию.
from masonite.request import Request
from masonite.view import View
def randomFunction(view: View):
print(view)
def show(self, request: Request):
request.app().resolve(randomFunction) # Будет напечатан объект View
Note
Помните, что вам не следует вызывать функцию, а только указать имя. Service Container должен внедрять зависимости в объект, поэтому он требует ссылку, а не вызов.
Это позволит получить все параметры randomFunction
и извлечь их из service container. Возможно,
вам нечасто придётся регистрировать ваш собственный код, но такая возможность имеется.
Определение с дополнительными параметрами
Иногда может понадобиться определить ваш код в дополнение к передаче переменных в том же списке параметров. Например, если вам нужно 3 параметра наподобие этих:
from masonite.request import Request
from masonite import Mail
def send_email(request: Request, mail: Mail, email):
pass
Вы можете зарегистрировать и передать параметр одновременно, добавив их в метод resolve()
:
Использование контейнера за пределами потока Masonite
Если вам нужно использовать контейнер за пределами нормального потока Masonite, например внутри команды, вы можете импортировать контейнер напрямую.
Это будет выглядеть следующим образом:
from wsgi import container
from masonite import Queue
class SomeCommand:
def handle(self):
queue = container.make(Queue)
queue.push(..)
Container Swapping (Замена контейнера)
Иногда, когда вы регистрируете объект или класс, вы хотите, чтобы возвращалось другое значение.
Использование значения
Мы можем передать простое значение как второй параметр в методе swap
, который будет возвращён
вместо определяемого объекта. Например, для определения класса Mail
следующим образом:
swap
контейнера. Swap работает просто, он принимает объект в качестве первого параметра,
значение или callable в качестве второго.
Например, чтобы сымитировать вышеуказанную функциональность, выполним что-то вроде этого в методе
boot()
в Service Provider:
from masonite import Mail
def boot(self, mail: MailManager):
self.application.swap(Mail, manager.driver(self.application.make('MailConfig').DRIVER))
Mail
. В этом случае мы хотим указать драйвер по умолчанию, определённый в
конфигурации проекта.
Использование callable
Вместо прямой передачи значения как второго параметра мы можем вместо этого передать callable. Callable ДОЛЖЕН получить 2 параметра. Первым параметром будет аннотация, которую мы пытаемся определить, а вторым параметром будет сам контейнер.
Вот пример, как вышеизложенное будет работать с callable:
from masonite import Mail
from somewhere import NewObject
...
def mail_callback(obj, container):
return NewObject
...
def boot(self):
self.application.swap(Mail, mail_callback)
Mail
.
Запомните
Запомните: если второй параметр это callable, он будет вызван. Если это значение, оно будет просто возращено вместо определяемого объекта.
Container Hooks (Связи контейнера)
Иногда нам хочется запустить код, когда что-то происходит внутри нашего контейнера. Например, мы хотим
запустить некоторую произвольную функцию для определения объекта Request
из контейнера. Или хотим
привязать некоторые значения к классу View
всякий раз, когда мы связываем Response
с контейнером.
Это отлично подходит для тестирования, если мы хотим привязать пользовательский объект к запросу
каждый раз, когда он определяется.
У нас есть три варианта: on_bind
, on_make
, on_resolve
. Все, что нам нужно для первого варианта -
это ключ или объект, к которому мы хотим привязать хук, а вторым вариантом будет функция,
принимающая два аргумента. Первый аргумент - это рассматриваемый объект, а второй аргумент -
это весь контейнер.
Код может выглядеть следующим образом:
on_make
, функция запускается.
Мы также можем привязываться к конкретным объектам вместо ключей:
Request
сам создаётся из контейнера.
Заметьте, что все идентично, кроме строки 6, где мы используем объект вместо строки.
Мы можем сделать тоже с другими вариантами: