Laravel — Разрешение зависимости с зависимостью конструктора от контейнера

avatar
Pieter Steyn
8 августа 2021 в 19:58
521
2
1

Я хочу разрешить экземпляр класса с помощью string, конструктор которого зависит от контейнера. Я хочу передать зависимость конструктора при разрешении класса.

Простой пример, который я тестировал: Foo — класс, который я хочу разрешить, а $id — аргумент конструктора:

class Foo
{  
  public function __construct(public $id)
  {}
}

Обвязка в служебном контейнере:

$this->app->bind('foo', function($app, $id) {
  return new Foo($id);
});

И разрешение из контейнера:

$foo = App::makeWith('foo', ['id' => 1]);

$foo затем разрешается как

Foo 
{
   +id: ["id" => 1],
}

Что показывает, что общедоступное свойство $id задано как массив.

Однако, что, если я хочу иметь $id не массив, а целое число?

Я должен сделать привязку следующим образом:

App::bind('foo', function($app, $arg) {
  return new Foo($arg['id']);
});

Это кажется крайне хакерским. Является ли это злоупотреблением сервисным контейнером?

Некоторые могут возразить, что не стоит передавать аргумент конструктора при разрешении из сервисного контейнера, лучше сделать его без состояния или использовать установщик. Но если многие методы в Foo будут использовать, скажем, $id, наиболее удобно и лучше всего передать $id в качестве конструктора, верно?

Источник
Mohsen Nazari
8 августа 2021 в 21:42
1

Если вы уверены в том, где вам нужны эти конкретные $id=1, вы можете использовать контекстную привязку с when()->needs()->give().

Ответы (2)

avatar
matiaslauriti
8 августа 2021 в 22:09
3

Поскольку пользователь mrhn уже ответил с решением, я просто объясню, ПОЧЕМУ в вашем случае вы получаете array вместо int.

Прежде всего, ВСЕГДА вводите подсказку, которую хотите:

class Foo
{  
  public function __construct(public int $id) {}
}

Посмотрите, что у меня есть тип int для $id.

Во-вторых, вы получаете массив, потому что у вас есть собственный "инстанциатор" (closure), ваша ошибка в том, что $id не $id, а $parameters.

См. исходный код, он выполняет: $concrete($this, $this->getLastParameterOverride()); поэтому в коде (как показано ниже) вы передаете первый параметр ($app) как $this, а второй ($id) как $this->getLastParameterOverride(), и этот последний метод вернет массив, следовательно, ['id' => 1] вместо 1.

$this->app->bind('foo', function($app, $id) {
  return new Foo($id);
});

Итак, как сказал другой использованный, ВСЕГДА ссылка на конкретные классы, следовательно, app(Foo::class, ['id' => 1]); решит вашу проблему (мой способ менее громоздкий, но делает то же самое).

avatar
mrhn
8 августа 2021 в 21:56
2

Вы делаете пользовательские привязки и, конечно же, вам придется самостоятельно анализировать аргументы. Вместо этого, почему бы просто не позволить прямо из коробки справиться с этим? например удалите текущую привязку и используйте привязки класса.

app()->makeWith(Foo::class, ['id' => 1]);

Не нужно ничего, кроме следующего, протестированного и запущенного в проекте Laravel 8.

Во-вторых, конструкция контейнера и его параметры всегда были ненадежными. Начав с Java/C# в академических кругах и перейдя на Laravel в своей профессиональной карьере. Я часто сталкивался с той же проблемой, что и вы здесь.

На мой взгляд, решение состоит в том, чтобы избегать этого подхода и вместо этого часто решать его с помощью гибкого дизайна службы.

class Foo {
    private $id;

    public function setId($id) {
        $this->id = $id;
    }
}

Вместо этого установите идентификатор с помощью установщика, теперь вам будет легко создавать экземпляры контейнера, вместо этого вам придется выполнять некоторые действия, вызывая класс.

resolve(Foo::class)->setId($model->id);

Это может показаться излишним, но я думаю, что этот код выглядит намного лучше, чем этот. Этот подход также устраняет некоторые проблемы, с которыми вы можете столкнуться, если конструкторы слишком много работают, поскольку Laravel поскольку инфраструктуре необходимо создавать экземпляры классов для выполнения определенных действий (на ум приходит маршрут, который должен создавать экземпляры контроллеров).

$foo = App::makeWith('foo', ['id' => 1]);
matiaslauriti
8 августа 2021 в 22:27
1

Небольшое примечание для тех, кто читает это: вы также можете использовать app(WhateverClass::class, ['param1' => 123, 'param2' => 321]) или resolve(WhateverClass::class, ['param1' => 123, 'param2' => 321]) (resolve вызывает app с теми же параметрами в том же порядке, так что это просто псевдоним). Нет необходимости писать так много вещей, чтобы заставить это работать, но это просто другой способ написать это.

Mohsen Nazari
8 августа 2021 в 23:44
0

в Laravel 8, если вы используете App::makeWith('foo', ['id' => 1]), значением параметра $id внутри конструктора будет массив ['id' => 1], а не 1.

matiaslauriti
9 августа 2021 в 00:19
1

@MohsenNazari, это неверно, прочитайте мой ответ (см. исходный код). App::makeWith - это своего рода псевдоним, он будет идти к resolve, и я объяснил в своем ответе, что resolve псевдоним к app, так что вы не правы.

Mohsen Nazari
9 августа 2021 в 00:25
0

@matiaslauriti Вы правы, небольшая разница, но большая разница :)

mrhn
9 августа 2021 в 07:42
0

кстати. это не большая разница, если вы используете App, app(), resolve(), это просто предпочтения. В целом я предпочитаю разрешение, так как избегаю статического контента.