Перейти к содержанию

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 в контейнере. После этого вызова словарь будет выглядеть так:

>>> app.providers

{'User': <class app.User.User>}

Service Container доступен в объекте Request и может быть получен так:

def show(self, request: Request):
    request.app() # вернет service container

Простая привязка

Иногда не важно, какой ключ у объекта, который вы связываете. Например, вы можете привязать класс 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().

>>> from app.User import User
>>> app.bind('User', User)
>>> app.make('User')

<class app.User.User>
Это полезно в качестве IOC (инверсия управления) контейнера, который вы можете загрузить отдельным классом в контейнер и потом использовать его повсюду в вашем проекте.

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():

app.has('request')
Также можно проверить, существует ли ключ в контейнере, используя ключевое слово in:
'request' in app

Collecting (Группировка)

Вам может понадобиться собрать специфический набор объектов из контейнера по какому-то ключу. Например, могут понадобиться все объекты, которые начинаются с "Exception" и заканчиваются "Hook". Или все ключи, которые заканчиваются на "ExceptionHook", если мы создаём обработчик исключений.

Группировка по ключу

Мы можем легко собрать все объекты по ключу:

app.collect('*ExceptionHook')
Будет возвращен словарь всех объектов, связанных с контейнером, которые начинаются с чего угодно и заканчиваются на "ExceptionHook", такие, как "SentryExceptionHook" или "AwesomeExceptionHook".

Мы также можем сделать противоположное и собрать все объекты, которые начинаются со специфического ключа.

app.collect('Sentry*')
Будут собраны все ключи, которые начинаются с "Sentry", такие, как "SentryWebhook" или "SentryExceptionHandler".

Наконец, мы можем собрать объекты, которые начинаются с "Sentry" заканчиваются на "Hook".

app.collect('Sentry*Hook')
Мы получим такие ключи как "SentryExceptionHook" и "SentryHandlerHook".

Группировка по объекту

Вы можете также собрать все подклассы объекта или если хотите собрать все сущности специфического класса из контейнера.

from cleo import Command

...

app.collect(Command)

# Вернёт {'FirstCommand': <class ...>, 'AnotherCommand': ...}

Resolve (Определение)

Это наиболее мощная часть контейнера. Можно получить объекты из контейнера просто передав их в список параметров любого объекта. Некоторые области Masonite определены, такие, как, методы контроллера, middleware и drivers.

Например, мы хотим получить класс Request и поместить его в наш контроллер. Все методы контроллера определены контейнером.

def show(self, request: Request):
    request.user()
В этом примере, перед тем как показать что метод вызван, Masonite посмотрит на параметры и посмотрит внутрь контейнера в поисках объекта 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():

app.resolve(send_email, 'user@email.com')
Masonite просматривает каждый список параметров и определяет их, а если он не находит параметр, он получит его из других указанных параметров. Эти параметры могут идти в любом порядке.

Использование контейнера за пределами потока 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 следующим образом:

from masonite import Mail


def show(self, mail: Mail):
    mail #== <masonite.drivers.MailSmtpDriver>
Но определение класса Mail здесь выглядит так:
class Mail:

    pass
Откуда он знает, что вместо этого нужно зарегистрировать smpt драйвер? Это происходит, потому что использован 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)
Обратите внимание, что вторым параметром является callable. Это значит, что он будет вызван каждый раз, когда мы пытаемся определить класс Mail.

Запомните

Запомните: если второй параметр это callable, он будет вызван. Если это значение, оно будет просто возращено вместо определяемого объекта.

Container Hooks (Связи контейнера)

Иногда нам хочется запустить код, когда что-то происходит внутри нашего контейнера. Например, мы хотим запустить некоторую произвольную функцию для определения объекта Request из контейнера. Или хотим привязать некоторые значения к классу View всякий раз, когда мы связываем Response с контейнером. Это отлично подходит для тестирования, если мы хотим привязать пользовательский объект к запросу каждый раз, когда он определяется.

У нас есть три варианта: on_bind, on_make, on_resolve. Все, что нам нужно для первого варианта - это ключ или объект, к которому мы хотим привязать хук, а вторым вариантом будет функция, принимающая два аргумента. Первый аргумент - это рассматриваемый объект, а второй аргумент - это весь контейнер.

Код может выглядеть следующим образом:

from masonite.request import Request


def attribute_on_make(request_obj, container):
    request_obj.attribute = 'some value'

...

container = App()

# устанавливает hook
container.on_make('request', attribute_on_make)
container.bind('request', Request)

# запускает функцию attribute_on_make
request = container.make('request')
request.attribute # 'some value'
Заметим, что мы создаём функцию, которая принимает 2 значения, объект с которым мы работаем и контейнер. Каждый раз, когда мы запускаем on_make, функция запускается.

Мы также можем привязываться к конкретным объектам вместо ключей:

from masonite.request import Request

# ...

# устанавливает hook
container.on_make(Request, attribute_on_make)
container.bind('request', Request)

# запускает функцию attribute_on_make
request = container.make('request')
request.attribute # 'some value'
После этого он вызывает тот же атрибут, но каждый раз объект Request сам создаётся из контейнера. Заметьте, что все идентично, кроме строки 6, где мы используем объект вместо строки.

Мы можем сделать тоже с другими вариантами:

container.on_bind(Request, attribute_on_make)
container.on_make(Request, attribute_on_make)
container.on_resolve(Request, attribute_on_make)