0

I've got a project written in Symfony 4 (can update to the latest version if needed). In it I have a situation similar to this:

There is a controller which sends requests to an external system. It goes through records in the DB and sends a request for every row. To do that there is an MagicApiConnector class which connects to the external system, and for every request there is a XxxRequest class (like FooRequest, BarRequest, etc).

So, something like this general:

foreach ( $allRows as $row ) {
    $request = new FooRequest($row['a'], $row['b']);
    $connector->send($request);
}

Now in order to do all the parameter filling magic, the requests need to access a service which is defined in Symfony's DI. The controller itself neither knows nor cares about this service, but the requests need it.

How can my request classes access this service? I don't want to set it as a dependency of the controller - I could, but it kinda seems awkward, as the controller really doesn't care about it and would only pass it through. It's an implementation detail of the request, and I feel like it shouldn't burden the users of the request with this boilerplate requirement.

Then again, sometimes you need to make a sacrifice in the name of the greater good, so perhaps this is one of those cases? It feels like I'm "going against the grain" and haven't grasped some ideological concept.


Added: OK, the full gory details, no simplification.

This all is happening in the context of two homebrew systems. Let's call them OldApp and NewApp. Both are APIs and NewApp is calling into the OldApp. The APIs are simple REST/JSON style. OldApp is not built on Symfony (mostly even doesn't use a framework), the NewApp is. My question is about NewApp.

The authentication for OldApp APIs comes in three different flavors and might get more in the future if needed (it's not yet dead!) Different API calls use different authentication methods; sometimes even the same API call can be used with different methods (depending on who is calling it). All these authentication methods are also homebrew. One uses POST fields, another uses custom HTTP headers, don't remember about the third.

Now, NewApp is being called by an Android app which is distributed to many users. Android app actually uses both NewApp and OldApp. When it calls NewApp it passes along extra HTTP headers with authentication data for OldApp (method 1). Thus NewApp can impersonate the Android app user for OldApp. In addition, NewApp also needs to use a special command of OldApp that users themselves cannot call (a question of privilege). Therefore it uses a different authentication mechanism (method 2) for that command. The parameters for that command are stored in local configuration (environment variables).

Before me, a colleague had created the scheme of a APIConnector and APICommand where you get the connector as a dependency and create command instances as needed. The connector actually performs the HTTP request; the commands tell it what POST fields and what headers to send. I wish to keep this scheme.

But now how do the different authentication mechanisms fit into this? Each command should be able to pass what it needs to the connector; and the mechanisms should be reusable for multiple commands. But one needs access to the incoming request, the other needs access to configuration parameters. And neither is instantiated through DI. How to do this elegantly?

Vilx-
  • 104,512
  • 87
  • 279
  • 422

1 Answers1

3

This sounds like a job for factories.

function action(MyRequestFactory $requestFactory)
{
    foreach ( $allRows as $row ) {
        $request = $requestFactory->createFoo($row['a'], $row['b']);
        $connector->send($request);
    }

The factory itself as a service and injected into the controller as part of the normal Symfony design. Whatever additional services that are needed will be injected into the factory. The factory in turn can provide whatever services the individual requests might happen to need as it creates the request.

Cerad
  • 48,157
  • 8
  • 90
  • 92
  • Hmm, but there are potentially dozens of different requests... does that mean making a separate factory for each one of them? Or one Master Factory with dozens of methods? Perhaps the DI can act as a factory itself? Can I ask it to create a new instance of a service? That would turn it more into a service locator, actually. – Vilx- May 22 '19 at 19:38
  • You certainly don't want dozens of factories. But a [service locator](https://symfony.com/doc/current/service_container/service_subscribers_locators.html) might indeed be the way to go. It depends in part on what this rather mysterious parameter filling service needed by your requests does. This [question/answer](https://stackoverflow.com/questions/54946647/symfony-get-service-via-class-name-from-iterable-injected-tagged-services/54949631#54949631) may or may not be relevant. – Cerad May 22 '19 at 19:50
  • And you can [configure your services to be non-shared](https://symfony.com/doc/current/service_container/shared.html). Which means you get a different instance each time the service is pulled from the container or a locator. – Cerad May 22 '19 at 19:55
  • I was trying to simplify it... The truth is, the requests use different methods to authenticate to the API. Some will use data in the current request headers. Others will use some environment variables. Others might do something else yet. I wish to hide this detail from the caller. I... guess I could make the `MagicApiConnector` (which is a proper dependency itself) to do extra magic... But it kinda feels like it violates the single responsibility principle. Now there are two classes that have to be aware of the underlying authentication mechanism... – Vilx- May 22 '19 at 19:57
  • For some reason I always end up regretting trying to answer foo/bar type questions. I tried to address the title of your question as well as the code in your question. But thanks to your latest comments, I have no idea what you are actually trying to accomplish. Something about authentication? Consider starting a question with an actual example of what you want to accomplish. – Cerad May 22 '19 at 20:07
  • I really wish to reduce it to the minimum example, because the actual code is pretty complicated and filled with tangentially relevant details... Ugh... OK, I'll append the full situation in a moment. – Vilx- May 22 '19 at 20:11
  • OK, I added almost all of the additional details. There's one more wrinkle that I'll add later (I need to run now), but most of it is there. If you're not asleep yet while reading it, I'd greatly appreciate an answer. – Vilx- May 22 '19 at 20:44
  • Ahh... nevermind. I figured out a different way. In essence, I'll let the responsibility for auth rest on the shoulders of the `Connector`. The command will merely report the type rather than try to add the necessary data. – Vilx- May 22 '19 at 21:10
  • Injection (construtor) is the way to go, just beware of [action anti-pattern](https://www.tomasvotruba.cz/blog/2018/04/23/how-to-slowly-turn-your-symfony-project-to-legacy-with-action-injection/) – Tomas Votruba May 22 '19 at 22:17