DEV Community

Alexander Schnitzler
Alexander Schnitzler

Posted on

Extbase entities and lazy loaded typed properties

Hi folks,

today I want to write about property types for lazy loaded entity properties in extbase:

use TYPO3\CMS\Extbase\Annotation as Extbase;

class Foo
{
    /**
     * @Extbase\ORM\Lazy
     * @var Bar
     */
    private $bar;
}
Enter fullscreen mode Exit fullscreen mode

That's how things looked until PHP 7.3, but how do we deal with it with it with PHP 7.4+ (property types) and PHP 8.0+ (union types)?

Well, let's start with 7.4 and also take \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage into account.

Something we cannot do with PHP 7.4 is to use union types. The following example look neat but is impossible to implement.

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Foo
{
    /**
     * @Extbase\ORM\Lazy
     */
    private Bar|LazyLoadingProxy|null $bar = null;
}
Enter fullscreen mode Exit fullscreen mode

But missing union types in PHP 7.4 aren't the only limitation we are facing here. There is a limitation/bug in extbase's reflection framework which lets extbase only detect a single type even in phpdoc blocks. So annotating @var Bar|LazyLoadingProxy already leads to an error as extbase does skip the whole property processing.

So the last example isn't working due to a missing language feature (in PHP 7.4), but the following example isn't working because of a limitation/bug in extbase:

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Foo
{
    /**
     * @Extbase\ORM\Lazy
     * @var Bar|LazyLoadingProxy
     */
    private $bar;
}
Enter fullscreen mode Exit fullscreen mode

Given the circumstances, what's the best approach here to be as strict and meaningful as possible? Here's my current best practice:

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Foo
{
    /**
     * @Extbase\ORM\Lazy
     * @var Bar|null
     * @phpstan-var Bar|LazyLoadingProxy|null
     */
    private ?object $bar = null;
}
Enter fullscreen mode Exit fullscreen mode

As phpdoc take precedence in extbase, the ?object property type is not relevant for extbase, but it already narrows down the allowed types. Depending on your IDE and it's support for custom annotations for static code analysis tools like phpstan, it will or will not know that the property can be a LazyLoadingProxy, but it will at least know the type is null or Bar. PHPStan, in this case, will know the whole truth and to me that's more important than full IDE support.

Speaking if LazyLoadingProxy...
\TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy::_loadRealInstance() is our public api to resolve the actual value of the proxy but unfortunately, the return type annotation of that method is just wrong. It says object which is neither specific nor correct. The actual return type is hard to tell because it is actually dynamic, in theory at least. It's very unlikely that the internal logic may fail but from a static code analysis standpoint, it's possible. If the actual value of the proxy cannot be resolved, _loadRealInstance() returns the actual value of the property, which in most cases is null. This means, the following code can break:

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Foo
{
    /**
     * @Extbase\ORM\Lazy
     * @var Bar|null
     * @phpstan-var Bar|LazyLoadingProxy|null
     */
    private ?object $bar = null;

    public function getBar(): ?Bar
    {
        return $this->bar instanceof LazyLoadingProxy
            ? $this->bar->_loadRealInstance()
            : $this->bar;
    }
}
Enter fullscreen mode Exit fullscreen mode

It's really just a theoretical case but my best practice for those getters looks like this:

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Foo
{
    /**
     * @Extbase\ORM\Lazy
     * @var Bar|null
     * @phpstan-var Bar|LazyLoadingProxy|null
     */
    private ?object $bar = null;

    public function getBar(): ?Bar
    {
        if ($this->bar instanceof LazyLoadingProxy) {
            /** @var Bar|null $resolvedValue */
            $resolvedValue = $this->bar->_loadRealInstance();
            return $this->bar = $resolvedValue instanceof Bar
                ? $resolvedValue
                : null;
        }

        return $this->bar;
    }
}
Enter fullscreen mode Exit fullscreen mode

What about LazyObjectStorage?

Right, there is LazyObjectStorage as well. How to deal with that? Well, this is a lot easier because LazyObjectStorage is a sub class of ObjectStorage which enables us to write this code:

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Foo
{
    /**
     * @Extbase\ORM\Lazy
     */
    private ObjectStorage $bar;

    public function __construct() {
        $this->initializeObject();
    }

    public function initializeObject() {
        $this->bar = new ObjectStorage();
    }

    public function getBar(): ObjectStorage
    {
        return $this->bar;
    }
}
Enter fullscreen mode Exit fullscreen mode

We completely omit the LazyObjectStorage type here because we really don't need it. There is no benefit for PHPStan and the like to know, neither for your IDE or you personally because the api of LazyObjectStorage is the same as for ObjectStorage and the underlying code can work the same both. Easy, right?

That's it for today.
Have a nice one!

Top comments (0)