Динамическое использование службы

Я хотел бы обрабатывать данные в таблице с помощью службы, при этом в записи указывается, какая служба будет использоваться. Итак, предположим, у меня есть таблица со следующими данными

| id | value1  | value2  | service        | result |
|----|---------|---------|----------------|--------|
| 1  | string1 | string2 | string_version |        |
| 2  | int1    | int2    | int_version    |        |
| 3  | string3 | string3 | string_version |        |

Я хочу обработать каждую строку в моей родительской службе calculator, она зацикливает каждую запись и получает службу на основе значения service, служба вычисляет результат, используя value1 и value2, а затем сохраняет результат в result.

На данный момент я создал службу calculator с одним из параметров, являющимся контейнером службы, тогда я могу использовать get для получения фактической службы, которая мне нужна:

class Calculator {
  private $container;

  public function __construct(ContainerInterface $container) {
     $this->container = $container;
  }

  public function calcResult($record) {
    $service = $this->container->get($record->getService());
    $result = $service->process($record->getValue1(),$result->getValue2());
    $record->setResult($result);
  }

}

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

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

Как я могу изменить этот код, чтобы я мог получить услугу на основе динамического значения в базе данных и позволить третьим сторонам добавлять услуги «обработки» - конечно, они будут реализовывать интерфейс - поэтому убедитесь, что присутствуют правильные методы

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

Cerad 07.01.2019 17:34

@Cerad спасибо - это именно то, что я искал

Manse 07.01.2019 18:12

Так же, как @Cerad, упомянутый выше, вы можете использовать локатор сервисов. Однако есть более приятная версия, которая помечена как услуги. См. Подробный ответ ниже.

BentCoder 07.01.2019 23:04
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Symfony Station Communiqué - 17 февраля 2023 г
Symfony Station Communiqué - 17 февраля 2023 г
Это коммюнике первоначально появилось на Symfony Station , вашем источнике передовых новостей Symfony, PHP и кибербезопасности.
Управление ответами api для исключений на Symfony с помощью KernelEvents
Управление ответами api для исключений на Symfony с помощью KernelEvents
Много раз при создании api нам нужно возвращать клиентам разные ответы в зависимости от возникшего исключения.
1
3
86
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

У вас есть два очевидных кандидата:

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

Примечание: у вас будут дублирования и немного некрасивого кода в вашем сервисе, если вы использовали локатор сервисов, как показано ниже.

ПОИСК ОБСЛУЖИВАНИЯ

interface ServiceLocatorInterface
{
    public function locate(string $id);
}

-

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;

class ServiceLocator implements ServiceLocatorInterface, ServiceSubscriberInterface
{
    private $locator;

    public function __construct(ContainerInterface $locator)
    {
        $this->locator = $locator;
    }

    public static function getSubscribedServices()
    {
        return [
            'string_version' => StringCalculator::class,
            'int_version' => IntCalculator::class,
        ];
    }

    public function locate(string $id)
    {
        if (!$this->locator->has($id)) {
            throw new ServiceLocatorException('Service was not found.');
        }

        try {
            return $this->locator->get($id);
        } catch (ContainerExceptionInterface $e) {
            throw new ServiceLocatorException('Failed to fetch service.');
        }
    }
}

-

class StringCalculator
{
    public function calculate($value1, $value2)
    {
        return $value1.' - '.$value2;
    }
}

-

class IntCalculator
{
    public function calculate($value1, $value2)
    {
        return $value1 + $value2;
    }
}

Использование:

class YourService
{
    private $serviceLocator;

    public function __construct(\App\ServiceLocatorInterface $serviceLocator)
    { 
        $this->serviceLocator = $serviceLocator;
    }

    public function yourMethod()
    {
        /** @var StringCalculator $calculator */
        $calculator = $this->serviceLocator->locate('string_version');
        $result = $calculator->calculate('1', '2'); // result: 1 - 2

        /** @var IntCalculator $calculator */
        $calculator = $this->serviceLocator->locate('int_version');
        $result = $calculator->calculate(1, 2); // result: 3
    }
}

ПОМЕЩЕННЫЕ УСЛУГИ

service:
    App\Strategy\Calculator:
        arguments: [!tagged calculator]

    App\Strategy\StringCalculatorStrategy:
        tags:
            - { name: calculator }

    App\Strategy\IntCalculatorStrategy:
        tags:
            - { name: calculator }

-

use Traversable;

class Calculator
{
    private $calculators;

    public function __construct(Traversable $calculators)
    {
        $this->calculators = $calculators;
    }

    public function calculate(string $serviceName, $value1, $value2)
    {
        /** @var CalculatorStrategyInterface $calculator */
        foreach ($this->calculators as $calculator) {
            if ($calculator->canProcess($serviceName)) {
                return $calculator->process($value1, $value2);
            }
        }
    }
}

-

interface CalculatorStrategyInterface
{
    public function canProcess(string $serviceName): bool;

    public function process($value1, $value2);
}

-

class StringCalculatorStrategy implements CalculatorStrategyInterface
{
    public function canProcess(string $serviceName): bool
    {
        return $serviceName === 'string_version';
    }

    public function process($value1, $value2)
    {
        return $value1.' '.$value2;
    }
}

-

class IntCalculatorStrategy implements CalculatorStrategyInterface
{
    public function canProcess(string $serviceName): bool
    {
        return $serviceName === 'int_version';
    }

    public function process($value1, $value2)
    {
        return $value1 + $value2;
    }
}

Использование:

class YourService
{
    private $calculator;

    public function __construct(\App\Strategy\Calculator $calculator)
    { 
        $this->calculator = $calculator;
    }

    public function yourMethod()
    {
        // result: 1 - 2
        $result = $this->calculator->calculate('string_version', 1, 2);
        // result: 3
        $result = $this->calculator->calculate('int_version', 1, 2);
    }
}

Вы правы в том, что существует несколько подходов к этой общей проблеме, однако ваш пример локатора служб излишне сложен. Воспользуйтесь подписчиком, если заранее знаете, какие именно услуги вам нужны. Немного затрудняет добавление дополнительных услуг с изменением кода подписки. Расширьте класс Symfony ServiceLocator, когда вам нужно что-то более динамичное. И, к сожалению, обозначение! Tagged в настоящее время не работает с Service Locator.

Cerad 07.01.2019 23:15

Но большая разница в том, что стратегический подход фактически создает экземпляры всех доступных служб, в то время как шаблон локатора создает экземпляры только тогда, когда вы действительно обращаетесь к данной службе. Есть ли в этом реальная разница? Может быть, а может и нет, но, как правило, не создавать объекты, если они вам не нужны. Вы также можете прочитать обсуждение, которое я провел в моем связанном ответе.

Cerad 07.01.2019 23:19

Я согласен, пример локатора сервисов - не полноценный, а скорее начальный пример обмена знаниями. Однако версия !tagged не предназначена для поиска сервисов. Это чисто для примера помеченных сервисов.

BentCoder 07.01.2019 23:19

Я предпочитаю максимально отделять свой код от используемого фреймворка. Я также делаю все возможное, чтобы избегать вещей, препятствующих шаблону, и придерживаться правильных шаблонов, таких как стратегический шаблон. Паттерн стратегии - отличный пример принципа открытости / закрытости. Я не чувствую, что должен пренебрегать принципами ООП, как и передовыми практиками, только потому, что опция локатора сервисов мгновенно создает объекты только тогда, когда это необходимо. Если бы мне было все равно, то да, локатор услуг был бы идеальным. Я склоняюсь к лучшим практикам ООП, чем к удобству. PS - unnecessarliy complicated - вот как это делает Symfony doc, а не я :)

BentCoder 08.01.2019 01:43

Другие вопросы по теме