Multibindings

Overview of Multibinder, MapBinder

Multibinder is intended for plugin-type architectures.

Multibinding

Using Multibinder to host plugins.

Multibinder

Multibindings make it easy to support plugins in your application. Made popular by IDEs and browsers, this pattern exposes APIs for extending the behaviour of an application.

Neither the plugin consumer nor the plugin author need write much setup code for extensible applications with Ray.Di. Simply define an interface, bind implementations, and inject sets of implementations! Any module can create a new Multibinder to contribute bindings to a set of implementations. To illustrate, we’ll use plugins to summarize ugly URIs like http://bit.ly/1mzgW1 into something readable on Twitter.

First, we define an interface that plugin authors can implement. This is usually an interface that lends itself to several implementations. For this example, we would write a different implementation for each website that we could summarize.

interface UriSummarizerInterface
{
    /**
     * Returns a short summary of the URI, or null if this summarizer doesn't
     * know how to summarize the URI.
     */
    public function summarize(Uri $uri): string;
}

Next, we’ll get our plugin authors to implement the interface. Here’s an implementation that shortens Flickr photo URLs:

class FlickrPhotoSummarizer implements UriSummarizer
{
    public function __construct(
        private readonly PhotoPaternMatcherInterface $matcher
    ) {}

    public function summarize(Uri $uri): ?string
    {
        $match = $this->matcher->match($uri);
        if (! $match) {
            return null;
        }
        $id = $this->matcher->group(1);
        $photo = Photo::loockup($id);

        return $photo->getTitle();
    }
  }
}

The plugin author registers their implementation using a multibinder. Some plugins may bind multiple implementations, or implementations of several extension-point interfaces.

class FlickrPluginModule extends AbstractModule
{
    public function configure(): void 
    {
        $uriBinder = Multibinder::newInstance($this, UriSummarizerInterface::class);
        $uriBinder->addBinding()->to(FlickrPhotoSummarizer::class);

        // ...bind plugin dependencies, such as our Flickr API key
   }
}

Now we can consume the services exposed by our plugins. In this case, we’re summarizing tweets:

class TweetPrettifier
{
    /**
     * @param Map<UriSummarizerInterface> $summarizers
     */
    public function __construct(
        #[Set(UriSummarizer::class)] private readonyl Map $summarizers;
        private readonly EmoticonImagifier $emoticonImagifier;
    ) {}
    
    public function prettifyTweet(String tweetMessage): Html
    {
        // split out the URIs and call prettifyUri() for each
    }

    public function prettifyUri(Uri $uri): string
    {
        // loop through the implementations, looking for one that supports this URI
        foreach ($this->summarizer as summarizer) {
            $summary = $summarizer->summarize($uri);
            if ($summary != null) {
                return $summary;
            }
       }

        // no summarizer found, just return the URI itself
        return $uri->toString();
    }
}

Note: The method Multibinder::newInstance($module, $type) can be confusing. This operation creates a new binder, but doesn’t override any existing bindings. A binder created this way contributes to the existing Set of implementations for that type. It would create a new set only if one is not already bound.

Finally we must register the plugins themselves. The simplest mechanism to do so is to list them programatically:

class PrettyTweets
{
    public function __invoke(): void
    {
        $injector = new Injector(
            new GoogleMapsPluginModule(),
            new BitlyPluginModule(),
            new FlickrPluginModule()
            // ...      
        );

        $injector->getInstance(Frontend::class)->start();
  }
}
(new PrettyTweets)();

MapBinder

You can name the classes you add in the multibinder.

class FlickrPluginModule extends AbstractModule
{
    public function configure(): void 
    {
        $uriBinder = Multibinder::newInstance($this, UriSummarizerInterface::class);
        $uriBinder->addBinding('flickr')->to(FlickrPhotoSummarizer::class);

        // ...bind plugin dependencies, such as our Flickr API key
   }
}

In the application, you can retrieve a Map injected by specifying attributes such as #[Set(UriSummarizer::class)] with the name as it was when specified by the binding.


class TweetPrettifier
{
    /**
     * @param Map<UriSummarizerInterface> $summarizers
     */
    public function __construct(
        #[Set(UriSummarizer::class)] private readonly Map $summarizers;
    ) {}

    public doSomething(): void
    {
        $filickerSummarizer = $this->summarizers['flicker'];
        assert($filickerSummarizer instanceof FlickrPhotoSummarizer);
    }    
}

Set binding

The setBinding() method overrides any previous binding.

$UriBinder = Multibinder::newInstance($this, UriSummarizerInterface::class);
$UriBinder->setBinding('flickr')->(FlickrPhotoSummarizer::class);

Map

Map objects are treated as generics in static analysis. If the injected interface is T, it is written as Map<T>.

/** @param Map<UriSummarizerInterface> $summarizers **/

Annotation

Since it is not possible to annotate the argument, annotate the property to be assigned with the same name and annotate the property with @Set.

class TweetPrettifier
{
    /** @Set(UriSummarizer::class) */
    private $summarizers;
    
    /**
     * @param Map<UriSummarizerInterface> $summarizers
     */
    public function __construct(Map $summarizers) {
        $this->summarizers = $summarizers;
    }
}