Dependency injection in event listener

I’m working on a CakePHP 4.x app and experimenting with the dependency injection feature, but I’m running into an issue with accessing the DI container. I’m aware dependencies are just injected in controllers and console commands for now, but I’m trying to catch an update in beforeSave() and use a service method (through an interface).

My app has a SubscriptionServiceInterface and one concrete implementation is our StripeSubscriptionService. Ideally, I would be able to catch an update to a value (e.x. quantity) on our Client model and call a method like updateQuanitity() to have the Stripe subscription updated. I have no idea if Stripe will stay as the subscription provider, so I’m very happy with being able to inject a subscription interface.

My plan was to use beforeSave() and check if the quantity was updated, then fire an updatedQuantity() event, and in that event listener use our StripeSubscriptionService (as a SubscriptionServiceInterface) to deal with the Stripe API. Unfortunately, the DI container is only accessible from Controllers and Commands.

My fallback would be creating a specific controller action for updating quantities, but I was curious if this approach was completely wrong or if the core team would consider adding DI to some other places in the framework.

If anyone has any other suggestions for dealing with this, I’d greatly appreciate it. Thanks!

Check out CakeDC | Dependency Injection with CakePHP | The minds behind CakePHP
You can add a Middleware which injects the DI Container into the request object.

With that everywhere where you can access the request object you can access the DI container.

So basically

// src/Middleware/ContainerInjectorMiddleware.php

<?php
declare( strict_types = 1 );

namespace App\Middleware;

use Cake\Core\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * Container Injector Middleware
 * https://www.cakedc.com/yevgeny_tomenko/2021/08/09/dependency-injection
 */
class ContainerInjectorMiddleware implements MiddlewareInterface {

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

  /**
   * Constructor
   *
   * @param ContainerInterface $container The container to build controllers with.
   */
  public function __construct( ContainerInterface $container ) {
    $this->container = $container;
  }

  /**
   * Serve assets if the path matches one.
   *
   * @param ServerRequestInterface $request The request.
   * @param RequestHandlerInterface $handler The request handler.
   * @return ResponseInterface A response.
   */
  public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ): ResponseInterface {
    return $handler->handle( $request->withAttribute( 'container', $this->container ) );
  }

}

// src/Application.php 

public function middleware( MiddlewareQueue $middlewareQueue ): MiddlewareQueue {
  ...
 
      // Add DI Container to Request object, so it can more easily be used
      // https://www.cakedc.com/yevgeny_tomenko/2021/08/09/dependency-injection
      ->add( new ContainerInjectorMiddleware( $this->getContainer() ) );
  ...
}

// somewhere with a request object

/** @var ContainerInterface $di_container */
$di_container           = $this->request->getAttribute( 'container' );
$this->bitbucketService = $di_container->get( BitbucketServiceInterface::class );
1 Like

Thank you so much for linking that blog post. I had no idea about injecting the DI container into the request object. That would work great for my use case.

I hope some information like this is added to the docs page or added as a core middleware in the future.

Thanks again!

Well to be fair - the whole DI container is in an experimental state

https://book.cakephp.org/4/en/development/dependency-injection.html

so there may be some changes which would make our life easier (like a global static get function maybe?)

Therefore adding this to the doc is not really appropriate because we misuse the middleware to write something in the request object which doesnt have to be in there.

But this “trick” is one way to at least bridge the gap till the DI container may be a bit more approachable outside of controllers and commands.

Understandable that the DI container is experimental. A global static function would make working with the container so much easier.