Я хотел бы обрабатывать данные в таблице с помощью службы, при этом в записи указывается, какая служба будет использоваться. Итак, предположим, у меня есть таблица со следующими данными
| 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 спасибо - это именно то, что я искал
Так же, как @Cerad, упомянутый выше, вы можете использовать локатор сервисов. Однако есть более приятная версия, которая помечена как услуги. См. Подробный ответ ниже.




У вас есть два очевидных кандидата:
Я лично (всегда) использую тегированные сервисы (шаблон стратегии) для такого сценария, но все же даю вам примеры для каждого, так что вам решать.
Примечание: у вас будут дублирования и немного некрасивого кода в вашем сервисе, если вы использовали локатор сервисов, как показано ниже.
ПОИСК ОБСЛУЖИВАНИЯ
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.
Но большая разница в том, что стратегический подход фактически создает экземпляры всех доступных служб, в то время как шаблон локатора создает экземпляры только тогда, когда вы действительно обращаетесь к данной службе. Есть ли в этом реальная разница? Может быть, а может и нет, но, как правило, не создавать объекты, если они вам не нужны. Вы также можете прочитать обсуждение, которое я провел в моем связанном ответе.
Я согласен, пример локатора сервисов - не полноценный, а скорее начальный пример обмена знаниями. Однако версия !tagged не предназначена для поиска сервисов. Это чисто для примера помеченных сервисов.
Я предпочитаю максимально отделять свой код от используемого фреймворка. Я также делаю все возможное, чтобы избегать вещей, препятствующих шаблону, и придерживаться правильных шаблонов, таких как стратегический шаблон. Паттерн стратегии - отличный пример принципа открытости / закрытости. Я не чувствую, что должен пренебрегать принципами ООП, как и передовыми практиками, только потому, что опция локатора сервисов мгновенно создает объекты только тогда, когда это необходимо. Если бы мне было все равно, то да, локатор услуг был бы идеальным. Я склоняюсь к лучшим практикам ООП, чем к удобству. PS - unnecessarliy complicated - вот как это делает Symfony doc, а не я :)
Поиск услуг - это, по сути, контейнер с ограниченным количеством служб, реализующих данный интерфейс.