Invoke and render content of a certain Controller from an EventListenerInterface instance in CakePHP 5

Hi guys,
I am facing a situation where I need help. I did not find any appropriate solution, neither in the internet, nor via ChatGPT.

My goal is to provide a functionality like in WordPress, where somewhere in a content field of an Entity I can place like a ShortCode, later to be replaced with some dynamic content from the target controller, whereas I could prevent implementing custom functions to generate the dynamic content and could just use the existing actions, like view, index, etc.:

this is some content before the dynamic content...
[dc plugin="PageManager" controller="Pages" action="view" pass="contact"]
this is some content after the dynamic content...

What I did:

  • created a DynamicContentPlugin
  • plugin provides an DynamicContentConsumerInterface, which Entities can implement
  • Entities return a list with fields that can have a certain ShortCode/DynamicContent. For example: a Page Entity will return the body field name, since the Page is displayed by reading it’s content from the database, like Articles (in the CookBook) do.
  • created an EventListenerInterface that has implementEvents: Controller.beforeRender
  • here I listen for the according event and check if the viewVar objects of the controllers ViewBuilder are an instance of DynamicContentConsumerInterface. If so, then I call the object interface-method to get the fields where a dynamic content could be
  • after finding the “ShortCode”-definition in the content, like [dc plugin=“PageManager” controller=“Pages” action=“view” pass=“contact”], I try to replace it by the rendered content of the target Controller.action

My problem now was to invoke the controllers action. More precise:

  1. create an appropriate ServerRequest object
  2. to only render the main content without the whole layout.

I think, the second obstacle could be accomplished by setting another layout for the render process. But the first problem with generating a ServerRequest was, that the ServerRequest did not have the params set with the appropriate controller and action names. The properties of the params always stayed null. For that I made a dirty workaround, see the comment in the code.

class DynamicContentHandlerEventListener implements EventListenerInterface {
  private $_containerInterface = null; // Cake\Core\ContainerInterface

  function __construct(ContainerInterface $containerInterface) {
    $this->_containerInterface = $containerInterface;
  }

  /**
   * Define Events which we want to react to.
   * */
  public function implementedEvents(): array { ... }

  /**
   * Event: onBeforeRender
   * */
  public function onBeforeRender ($event) { ... }


  /**
   * Parse the the fields of the entity.
   * */
  protected function parseDynamicContent (DynamicContentConsumerInterface $entity) { ... }


  /**
   * Process the content of the entity.
   * */
  protected function processDynamicContent (string $content): string { ... }


  /**
   * Return the dynamic content from desired controller
   * */
  protected function getDynamicContentFromController($params): string {
    $plugin     = $params['plugin'] ?? null;
    $controller = $params['controller'] ?? null;
    $action     = $params['action'] ?? null;
    $pass       = $params['pass'] ?? '';
    $query      = $params['params'] ?? [];

    if (!$controller || !$action) {
      return '';
    }

    try {
      $url = \Cake\Routing\Router::url([
        'plugin' => $plugin,
        'controller' => $controller,
        'action' => $action,
        $pass,
        '?' => $query
      ]);

      $request = \Cake\Http\ServerRequestFactory::fromGlobals(
        [
          'REQUEST_URI' => $url,
          'REQUEST_METHOD' => 'GET'
        ],
        [],
        null,
        [],
        []
      );

      // UGLY WORKAROUND TO GET AN APPRORIATE SERVERREQUEST OBJECT
      $request = $this->_containerInterface->get(\Cake\Http\ServerRequest::class);
      $request = $request->withParam('plugin', $plugin)
        ->withParam('controller', $controller)
        ->withParam('action', $action)
        ->withParam('pass', $pass)
        ->withParam('_matchedRoute', null);

      // $this->_containerInterface was passed in the constructor on this class in the plugins bootstrap() method
      $controllerFactory = new \Cake\Http\ControllerFactory($this->_containerInterface);
      $controllerInstance = $controllerFactory->create($request);

      ob_start();
      //$controllerInstance->{$action}($pass);
      echo $controllerFactory->invoke($controllerInstance);
      $output = ob_get_clean();

      return $output;

    } catch (\Exception $e) {
      return '';
    }
  }

}

So, my questions are:

  1. How to create a correct ServerRequest by having plugin, controller, action and pass variables.
  2. How to only render the main content? I thing by giving the request an extra query param to tell the target controller to use another layout?

Any help and hints would be nice :slight_smile: Thank you in advance!

I’d go with Cells to implement a similar functionality like wordpress shortcodes.

Also you should not re-create the whole request object and manually invoke a controller, you will get in a whole lot of other issues if you keep that structure.


If you don’t need any further data loading inside your shortcodes and instead just want to render a more complicated HTML with a simpler version you can either just use Elements or (if you prefer something like JSX in the JS ecoworld) you can try my custom html elements package

If you want to test my package I can give you further guidance on how to get it to work with CakePHP.

Grüß dich Kevin, du auch mit einem “f” :slight_smile:

Thank you very much for the quick reply. Ok, the thing with the ServerRequest is a critical issue, so this approach is not going to work.

And thank you for the provided links, these are good ideas :slight_smile:
Maybe another time …

I do need specific data loading and rendering, and also without repeating the code and just use the render functionality of a controllers action. I think, the moment in the code when a ShortCode was recognized in the content of an Entity could then just be replaced by a loader animation and inserting an AJAX call function to let fetching the desired actions content via JS, without the whole layout. But with this solution I am not so really satisfied. Maybe …

The Cell approach sound good. But the Cells are called in the code somewhere in the View or template, as far I understood. And my desire is to place a placeholder somewhere in the HTML content attribute of the page Entity that is loaded from the database. Otherwise the page content is just a simple HTML edited with the TinyMCE editor, not having the possibility to render something dynamic of other controllers. And the other entities in the app are just like static, just displaying what the template provides. I don’t know, whether my idea was clearly written.
My idea is just to be able to decide on the content side what to display without touching the templates.

Ok, then maybe I should try another approach via Cells. So do I understand it correctly as it is documented:

If you need controller logic to decide which cells to load in a request, you can use the CellTrait in your controller to enable the cell() method there

If I move my code to the AppController and use the CellTrait, then I could react on the ShortCode in an entities content and then replace it with a certain Cell rendered content? And for that I’ll probably need as many CellViews as I have ShortCodes defined in my application, like:

  • ShortCode to display the username and the articles count use one Cell
  • ShortCode to display the a small list of published articles use another Cell

  • And it could be a combination with Elements, whereas the regular action views could use them and also a Cell.

Seems to me like you are creating your own kind of framework based upon Cake… this is far beyond typical MVC.

Yes, Cells are created, loaded and rendered directly in the view by default, so each cell gets rendered individually. You’d have to create your own kind of caching system to not re-load the same data in the same request if you render the same shortcode multiple times.

BUT as the docs say if you add the CellTrait to your controller all you need to do is call

$rendered = $this->cell('ProjectStatistic', [$dataForCell])->render();

instead of doing

<?= $this->cell('ProjectStatistic') ?>

cells can be cached as well, but only for the given request, not across requests

If its dynamic data which can be rendered on any page I’d personally go for an AJAX/Client side based logic, but I of course don’t know all about your usecase - besides the fact that you already tried that.

Thank you @KevinPfeifer!

No, I do not try to create another framework within cake, just wanted to enhance the static template content generation in some cases.

Then ok, the Cell approach is the better solution. And no, the caching is not important since a dynamic content would be inserted just one time on the whole page.

Thank you and have a good time!