Dependency injection as a way to handle inter-app comunication

Continuing the discussion from Dependency Injection custom interface Class not found:

I do not want to break the other tread, so I created a linked one, @z4k.

Do I get your idea right, that you want to have - lets’s say - 2 apps A and B. These should communicate. All right. To do so, there is an interface defined (call it IInterfaceOfB). Inside app B there is a corresponding class InterfaceOfBImpl that implements the interface and provides an appropriate implementation.
In B’s Application class, you can register a service on the interface and use the service on IInterfaceOfB by resolving to InterfaceOfBImpl by means of

$context->registerService(IInterfaceOfB::class, function (ContainerInterface $c): AuthorMapper {
    return $c->get(InterfaceOfBImpl::class);
});

Then, you can use dependency injection on A to get access to the interface (which is backed by the actual class).

Was this the idea?


There are multiple points that this might fail, one I consider really nasty.

  1. The interface IInterfaceOfB must be part of either app (or the server). If the corresponding app is not installed, the use OCA\B\Interfaces\IInterfaceOfB; will succeed as well as access to IInterfaceOfB::class as it only is some static namespace calculation in PHP. Once, you rename the interface or put it in a different namespace, the static code is no longer valid.
  2. If B was not installed and A needs a class of B in its constructor, the container will fail to create the class. I see no way to circumvent this, except for manually handling the dependency injection for this part: You could just depend your class in \Psr\Container\ContainerInterface and query it for the class/interface in question. You could add a local fallback in case the query fails (app B is not installed).

As already mentioned, it is possible to handle this with some dirty hacks (read plumbing commands on the container etc). The idea with the wrapper as suggested in How to work the OAuth2 with Nextcloud? was to abstract that away on a single class that needs to be carved carefully and be tightly looked over. But the underlying (technical) methods are in fact the same.


If your idea is something completely different, feel free to sketch it. Maybe (hopefully!) we could create a general pattern to follow in order to integrate different apps.

Yes, it’s the idea !

As developers I would like to use a class from another app.
Let’s imagine, in the Talk app you have TalkManagerInterface with CRUD (for example).

From my App, for example : AppNote, I would like to use the TalkManager final class in my constructor from AppNoteManager :

<?php

namespace OCA\AppNote;

use OCA\Talk\ITalkManager;

class AppNoteManager
{
  public function __construct(
     private ITalkManager $talkManager // I can use create, show, edit, delete functions
   )
  {
  }
}

But, one of the problems of this class is that it uses other interfaces :

<?php

namespace OCA\Spreed;

class TalkManager implements ITalkManager {
  public function __construct(private IUserManager, private IGroupManager)
  {
  }

  // CRUD functions and some code...

}

And the Injection Dependency could resolve this problem from the Spreed app to inject all dependencies !

With a text schema :

AppNote -> ITalkManager -> OCA\Spreed\Application (Inject all Dependencies for IUserManager and IGroupManager) -> ITalkManager -> TalkManager (the interface points on this class)

I don’t know if my explanations are clear ?

I think it’s not a problem if we use the IAppManager::isInstalled interface when our app is installing with Repair Steps.
But, we have to create a best practice in the developer documentation.

Yes, I will use this method later in my app.


Edit 1st December, 07:05pm

I don’t know if I’m getting confused the Repair Steps with the Bootstraping system :thinking:

I guess so, as the repair steps are more about database and storage fixes during updates etc. The bootstrapping is done on each invocation of the Nextcloud server using a request from a browser or Cron.

I mentally replace the repair step by the bootstrapping system.

I see a significant problem here. The app B must provide a register method that will register the interface IInterfaceOfB. The registration process is described in the documentation: the register method is not doing anything. Instead, it provides the core with a callback function on how to create certain services. The server decides which classes are needed, e.g. the correct controller class. Having drive the name, the class needs to be built. Looking at the parameters of the conductor, more classes will be needed. This schema is followed recursively until a class can be constructed without unknown classes. The server builds the class using the operator new and stores the instance (pointer) in the container of the currently running app. This way, the complete tree of classes is built automatically in a backstapping manner.

The problematic point is that the register method is called first and it is considered to be called without all dependences already allocated. Here the implicit dependencies for interfaces are defined. Then the core does the actual instantiation completely before the developer has again the control in the boot method.

So, if the constructor of any class of app A depends on a class or interface of app B, the server will fall to resolve the dependency during bootstrap of B was not installed. There is no way to catch this or make it conditional as it is done by the server.

All you could do as app A: inside the register method somehow check if the app B is present (without usage of any other classes of the server like IAppManager as these are not yet built!) and conditionally register a fallback class just to avoid breaking the server dependency injection algorithm. Not nice, as the registered interface lies in the namespace of another app (B).

Does this make sense?

Yes, I was a little tired and I didn’t think a lot about it ^^’

Hmmm… I don’t get it :thinking:
Where is the documentation which describes “the register method is not doing anything” ?

I don’t see on the dev documentation or the namespace documentation

I think I understand. But what I understand above that I cannot use the Bootstraping to check if an app is installed on the instance with the IAppManager interface.

And it’s not possible to use the registerService to use an external App from my App :thinking:

Excepted with this code : How to work the OAuth2 with Nextcloud? - #9 by z4k

This means we could improve the registerService function or create a new type register to expose an API for an Application in the OCP\AppFramework\App class type.

What do you think?

All right :wink:

It is in the dev documentation:

In each […] enabled app that […] that also implements IBootstrap, the register method will be called. This method […] can prime the dependency injection container and register other services lazily […]. The emphasis is on laziness. At this very early stage of the process lifetime, no other apps nor all of the server components are ready. Therefore the app must not try to use anything except the API provided by the context. That shall ensure that all apps can safely run their registration logic before any services are queried (instantiated) from the DI container or related code is run.

Exactly, the IAppManager is (during the bootstrap) not guaranteed to be present and working. Most probably it is not even prepared.

Not directly to my understanding. In fact, its main usage is to make names known to the container. So, it has nothing to do with the inter-app communication in the first glance.

Well, this is neither dependency injection nor using the registration of services. You are tightly coupling the apps by requesting it to be existing during construction from the (server) container.

I would say, this is a structural problem. IMHO, we would need a weak registration (Weak symbol - Wikipedia) in order to allow simpler inter-app communications. That way, the original provider would provide a strong service while the using app would provide a weak one. Then, DI would still work as normal and our problem would be smaller.

However, I have not heard of any such concept in the case of DI yet. Moreover, I am unsure how much of the concepts are standardized or coming from underlying libraries (symfony). So, I am not sure if this is easily manageable, to be honest.

Yet, still, there could be version clashes of the interface in question, so this is no optimal solution.

How would you see this working? Can you explain more in depth?