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:
- create an appropriate ServerRequest object
- 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:
- How to create a correct ServerRequest by having plugin, controller, action and pass variables.
- 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 Thank you in advance!