ezsystems/ezpublish-kernel
eZ Publish Kernel is the core of the eZ Publish/eZ Platform CMS, providing the content repository, field types, search integration, and services for building and extending PHP-based content applications with a modular, API-driven architecture.
As PHP API in eZ Platform is often used to populate templates via generic controllers, there is often a need to provide possibility to easily get additional info relevant directly to the domain objects involved.
Prior to this feature this is only possible by:
While relevant for all domain objects, the most pressing need is for the eZ content model to get for instance:
However when doing so we need to take some technical consideration into account to avoid creating more problems than we solve.
* see technical consideration #3
In eZ Platform the objects involved here are DDD Value objects, which in itself contain as little logic as possible.
So one technical constraint here is that we should not end up with loading logic spread across all value objects, and instead make sure to keep such logic in internal repository services which can be unit tested more cleanly and more easily refactored later.
Avoid common performance pitfalls found in other PHP applications:
Such functionality existed in eZ Publish "legacy", however it was often the culprit of performance issues, as they were loading data O^n, leading to a massive amount of SQL calls to load data repeatedly.
Example:
{foreach $nodes as $child_node}
{$child_node.name} {* One raw sql call to get name(s) *}
{$child_node.className} {* One fetch for object and one for content class *}
{$child_node.object.owner.name} {* One fetch for owner (object feteched above) *}
{/foreach}
Inside the loop we have at least 4 sql calls going on, which will be done on each iteration, and while it could be
reduced to 3 per iteration by changing first line to {$child_node.object.name}, many of the sql lookups could rather
have been done in bulk while still being lazy in such cases.
Doctrine provides a range of ways to let you specify how to load reference(s), from eager, to lazy and extra lazy.
However when iterating entities and accessing a reference(s) property, or when accessing properties not pre loaded, there will be additional sql calls being made per entity just like in eZ Publish example above, with same outcome.
Given the dynamic nature of lazy properties it's beneficial to take care when designing them to avoid ability to traverse whole repository (or the whole content structure) using them. As this is not a feature they are meant to solve.
Anticipating bulk loading of lazy properties across collections as mentioned in #2 can only help so much against the performance problems mentioned. To further avoid the situation lazy properties should also not allow traversing the object graph beyond the root aggregate.
E.g.
Based on the context above, our requirements can be be defined as such:
* Introducing API cache would require us to refactor Core/Repository quite a bit.
Lower hanging fruit would probably be to re-introduce a v2 SPI Persistence in-memory cache for meta data which don't
frequently change (types, sections, states, ..). This can for instance be done in Core/Persistence/Cache in
similar ttl based way as CachedPermissionService now does for permission lookups.
As researched in PR 2094 which was focusing on lazy loading collections, using lazy collections and especially using PHP's generators directly would lead to BC breaks in current API.
To overcome these issues further attempts showed an opportunity to combine the following concepts:
Generator->send($id) for both bulk and singular useExample:
trait GeneratorProxyTrait
{
// (properties ...)
public function __construct(Generator $generator, mixed $id)
{
$this->generator = $generator;
$this->id = $id;
}
public function __get($name)
{
if ($name === 'id') {
return $this->id;
}
if ($this->object === null) {
$this->loadObject();
}
return $this->object->$name;
}
// (...)
protected function loadObject()
{
$this->object = $this->generator->send($this->id);
$this->generator->next();
unset($this->generator);
}
}
class ContentTypeGroupProxy extends APIContentTypeGroup
{
use GeneratorProxyTrait;
/** [@var](https://github.com/var) \eZ\Publish\API\Repository\Values\ContentType\ContentTypeGroup|null */
protected $object;
public function getNames()
{
if ($this->object === null) {
$this->loadObject();
}
return $this->object->getNames();
}
// (rest of methods ...)
}
class ContentTypeDomainMapper
{
// (...)
public function buildContentTypeGroupProxyList(array $ids, array $prioritizedLanguages = []) : array
{
$groups = [];
$generator = $this->generatorForContentTypeGroupList($ids, $prioritizedLanguages);
foreach ($ids as $id) {
$groups[] = new ContentTypeGroupProxy($generator, $id);
}
return $groups;// to be used in for instance ContentType->contentTypeGroups
}
private function generatorForContentTypeGroupList(array $ids, array $prioritizedLanguages = []) : \Generator
{
$groups = $this->contentTypeHandler->loadGroups($ids);
while (!empty($groups)) {
$id = yield;
yield $this->buildContentTypeGroupDomainObject(
$groups[$id],
$prioritizedLanguages
);
unset($groups[$id]);
}
}
// (...)
}
This allows us to:
Downsides:
How can I help you explore Laravel packages today?