1
votes

Symfony 4. Comment accéder au service depuis le contrôleur sans injection de dépendances?

J'ai plusieurs services: DieselCaseService, CarloanCaseService LvCaseService.

Le contrôleur décide quels services obtenir.

class DieselCaseService implements CaseInterface 
{
.
.
.
}

Les alias de service sont déclarés comme ceci:

case_service.diesel:
    alias: App\Service\Cases\DieselCaseService
    public: true
$type = $quickCheck["type"];
/**
 * @var $caseService \App\Service\Cases\CaseInterface
*/
$type = 'diesel; // for test purposes

$caseService = $this->get('case_service.' . $type);

Si j'essaye de obtenir le DieselCaseService, j'obtiens une erreur

Service "case_service.diesel" introuvable: même s'il existe dans le
conteneur de l'application, le conteneur à l'intérieur
"App \ Controller \ Api \ AccountController" est un localisateur de services plus petit qui ne connaît que la "doctrine", "form.factory",
"fos_rest.view_handler", "http_kernel", "parameter_bag",
"request_stack", "router", "security.authorization_checker",
"security.csrf.token_manager", "security.token_storage", "serializer",
Services "session", "templating" et "twig". Essayez d'utiliser la dépendance
injection à la place.

Que puis-je faire? Je ne veux pas injecter tous les services au contrôleur


4 commentaires

C'est un travail pour les abonnés au service. AbstractController est déjà abonné. Vous pouvez également ajouter vos propres services. Faites défiler jusqu'à ce que vous voyiez l'exemple de contrôleur ici . Il existe plusieurs autres approches à ce problème, alors lisez la documentation. Il est possible de créer un CaseServiceLocator qui contiendra automatiquement tous services qui implémentent CaseInterface .


Au lieu de la programmation de configuration (très mauvaise pratique comme vous pouvez le voir), vous pouvez ajouter la méthode getName () à CaseInterface . De cette façon, vous pouvez facilement accéder à n'importe quel service sans le limiter à une valeur de chaîne que vous tapez dans config.


Et ensuite injecter uniquement CaseInterface dans l'action? Ou ai-je mal compris? Avez-vous un exemple?


@ olek07 Dans le constructeur plutôt. J'ai ajouté un exemple plus détaillé à la réponse. Si quelque chose n'est pas clair, n'hésitez pas à demander dans les commentaires en dessous.


3 Réponses :


3
votes

Pour toute situation «plusieurs instances du même type par clé», vous pouvez utiliser un tableau câblé automatiquement.

1. Services de découverte automatique avec espace de noms App \

use Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutowireArrayParameterCompilerPass;

final class AppKernel extends Kernel
{
    protected function build(ContainerBuilder $containerBuilder): void
    {
        $containerBuilder->addCompilerPass(new AutowireArrayParameterCompilerPass);
    }
}

2. Nécessite un tableau autowired dans le constructeur

<?php

namespace App\Controller\Api;

use App\Service\Cases\DieselCaseService

final class AccountController
{
    /**
     * @var CaseInterface[]
     */
    private $cases;

    /**
     * @param CaseInterface[] $cases
     */
    public function __construct(array $cases)
    {
        foreach ($cases as $case) {
            $this->cases[$case->getName()] = $cases;
        }
    }

    public function someAction(): void
    {
        $dieselCase = $this->cases['diesel']; // @todo maybe add validation for exisiting key
        $dieselCase->anyMethod();
    }
}

3. Enregistrer le passage du compilateur dans le noyau

La fonctionnalité tableau autowired n'est pas dans le noyau de Symfony. C'est possible grâce aux passes du compilateur. Vous pouvez écrire le vôtre ou utiliser celui-ci:

services:
    _defaults:
        autowire: true

    App\:
        resource: ../src

C'est tout! :)

Je l'utilise sur tous mes projets et cela fonctionne comme un charme.


En savoir plus sur post sur les tableaux autowired a > J'ai écrit.


8 commentaires

D'où Symfony sait-il que le paramètre $ cases dans le constructeur signifie injecter tous les cas (DieselCaseService, CarloanCaseService LvCaseService)? Et où est déclaré 'diesel' pour pouvoir être utilisé ici $ this-> cases ['diesel']; ?


De la passe du compilateur. Le nom est défini ici $ this-> cases [$ case-> getName ()] = $ cases; Vous devez étendre votre interface avec la méthode getName () , qui fournirait la clé que vous utiliserez dans le contrôleur pour correspondre.


Dois-je vraiment rendre tous les services publics?


J'ai lu que rendre tous les services publics est une mauvaise pratique. De plus, j'ai une solution propre. Regarde ma réponse


Le public n'est pas pertinent dans ma réponse, je l'ai supprimé. Eh bien, utiliser le conteneur dans votre réponse est la pire pratique. Cela tue l'injection de constructeur et vous invitez le localisateur de services dans votre code.


Pourquoi l'utilisation du conteneur est-elle la pire pratique? J'ai lu à ce sujet, mais je ne comprends pas pourquoi. Votre solution fonctionne-t-elle sans public: true ?


Il est caché statique dans votre code - très facile à utiliser, mais presque impossible à éliminer ces derniers temps. J'ai écrit sur les mauvaises conséquences ici . "Votre solution fonctionne-t-elle sans public: vrai?" Oui, je l'ai ajouté par accident, ce n'est pas pertinent pour le code.


Laissez-nous continuer cette discussion dans le chat .



-1
votes

J'ai créé un service wrapper

public function saveRegisterData(Request $request, AccountService $accountService, ServiceFactory $serviceFactory) 
{
.
.
.

    if (!$serviceFactory->hasService('case_service.'.$type)) {
        throw new \LogicException("no valid case_service found");
    }

    /**
    * @var $caseService \App\Service\Cases\CaseInterface
    */
    $caseService = $serviceFactory->getService('case_service.' . $type);

.
.
.
}

puis j'injecte ce service dans le contrôleur

<?php
namespace App;

use Symfony\Component\DependencyInjection\ContainerInterface;

class ServiceFactory
{

    /** @var ContainerInterface */
    private $container;


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

    public function getService($alias)
    {
        return $this->container->get($alias);
    }

    /**
     * @param $alias
     * @return bool
     */
    public function hasService($alias)
    {
        return $this->container->has($alias);
    }
}


2 commentaires

L'injection du conteneur complet est quelque chose à éviter car cela donne un accès global à tous vos services et peut rendre les tests un peu plus difficiles. Jetez un œil à mon commentaire dans votre question initiale. Celui avec trois votes positifs. Fondamentalement, vous voulez commencer avec CaseServiceFactory étend ServiceLocater et continuez à partir de là.


Si je définis le localisateur de service comme ceci: class DoingLocator étend ServiceLocator {protected $ services = ['bar' => 'app.service.bar', 'baz' => 'app.service.baz']; public function Locate ($ string) {return $ this-> get ($ this-> services [$ string]); }} J'obtiens l'erreur: Définition invalide pour le service "App \ DoingLocator": un tableau de références est attendu comme premier argument lorsque la balise "container.service_locator" est définie.



0
votes

Un code de dimanche après-midi pluvieux. Cette question est fondamentalement un doublon de plusieurs autres questions mais il y a suffisamment de pièces mobiles pour que je suppose qu'il soit utile de fournir des solutions spécifiques.

Commencez par définir les cas juste pour vous assurer que nous sommes tous sur la même page: p>

class CaseController extends AbstractController
{
    public function action2(CaseLocator $caseLocator)
    {
        $case = $caseLocator->get(CarloanCaseService::class);

        return new Response(get_class($case));
    }

Le moyen le plus simple de répondre à la question est d'ajouter les services de cas directement au conteneur du contrôleur:

# src/Kernel.php
// Make the kernel a compiler pass by implementing the pass interface
class Kernel extends BaseKernel implements CompilerPassInterface 
{
    protected function build(ContainerBuilder $container)
    {
        // Tag all case interface classes for later use
        $container->registerForAutoconfiguration(CaseInterface::class)->addTag('case');
    }
    // and this is the actual compiler pass code
    public function process(ContainerBuilder $container)
    {
        // Add all the cases to the case locator
        $caseIds = [];
        foreach ($container->findTaggedServiceIds('case') as $id => $tags) {
            $caseIds[$id] = new Reference($id);
        }
        $caseLocator = $container->getDefinition(CaseLocator::class);
        $caseLocator->setArguments([$caseIds]);
    }

La seule chose J'ai changé car votre question utilise les noms de classe pour les identifiants de service au lieu de quelque chose comme «case_service.diesel». Vous pouvez bien sûr modifier le code pour utiliser vos identifiants, mais les noms de classe sont plus standard. Notez qu'aucune entrée dans services.yaml n'est nécessaire pour que cela fonctionne.

Il y a quelques problèmes avec le code ci-dessus qui peuvent ou non être un problème. La première est que seul le seul contrôleur aura accès aux services de cas. Ce qui peut être tout ce dont vous avez besoin. Le deuxième problème potentiel est que vous devez répertorier explicitement tous vos services de cas. Ce qui peut être correct, mais il peut être intéressant de sélectionner automatiquement les services qui implémentent l'interface de cas.

Ce n'est pas difficile à faire mais cela nécessite de suivre toutes les étapes.

Démarrer définir un localisateur de cas:

use Symfony\Component\DependencyInjection\ServiceLocator;

class CaseLocator extends ServiceLocator
{

}

Nous avons maintenant besoin de code pour trouver tous les services de cas et les injecter dans le localisateur. Il existe plusieurs approches, mais la plus simple est peut-être d'utiliser la classe Kernel.

class CaseController extends AbstractController
{
    public function action1()
    {
        $case = $this->get(DieselCaseService::class);

        return new Response(get_class($case));
    }
    // https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
    public static function getSubscribedServices()
    {
        return array_merge(parent::getSubscribedServices(), [
            // ...
            DieselCaseService::class => DieselCaseService::class,
            CarloanCaseService::class => CarloanCaseService::class,
        ]);
    }
}

Si vous suivez toutes les étapes ci-dessus, vous pouvez injecter votre localisateur de cas dans n'importe quel contrôleur (ou autre service) qui en a besoin:

interface CaseInterface { }
class DieselCaseService implements CaseInterface {}
class CarloanCaseService implements CaseInterface{}

Et encore une fois, il n'est pas nécessaire d'apporter des modifications à votre services.yaml pour que tout cela fonctionne.


2 commentaires

Merci beaucoup! Ça marche!!! Il me reste une petite question. Je veux obtenir le service par leurs alias, comme ceci $ caseService = $ caseLocator-> get ('case_service.'. $ Type) En fait, cela ne fonctionne qu'avec le nom de classe $ case = $ caseLocator-> get (CarloanCaseService :: class);


Pour être parfaitement honnête, à mon avis, il est préférable de laisser à l'étudiant le soin de modifier le code pour utiliser des alias.