Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Lazy injections for typed properties #2701

Draft
wants to merge 21 commits into
base: 8.2
Choose a base branch
from

Conversation

robertlemke
Copy link
Member

@robertlemke robertlemke commented Feb 21, 2022

This change adjusts the proxy building and dependency injection process to properly handle PHP 7.4 class property type declarations.

It resolves #2114
Also resolve #2601

How to verify it

What did not work before this change was using property type declarations for lazily injected properties.

Consider the following code: injecting $testSingleton is what this fix makes possible.

<?php
namespace Acme\LazyTest\Command;

use Acme\LazyTest\TestSingleton;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Cache\CacheManager;
use Neos\Flow\Cli\CommandController;
use Psr\Log\LoggerInterface;

/**
 * @Flow\Scope("singleton")
 */
class TestCommandController extends CommandController
{
    /**
     * LoggerInterface doesn't have to be nullable, because the proxy class
     * builder will not even try to use lazy injection, since it is an interface
     *
     * @Flow\Inject
     **/
    protected LoggerInterface $logger;

    /**
     * CacheManager must be nullable, because it can be injected lazily
     *
     * @Flow\Inject
     */
    protected ?CacheManager $cacheManager;

    /**
     * @Flow\Inject
     */
    protected ?TestSingleton $testSingleton;

    /**
     * Test command
     *
     * @return void
     */
    public function testCommand(): void
    {
        # Will be the actual logger, because an interface is injected,
        # which results in an eager dependency:

        var_dump(get_class($this->logger)); # Neos\Flow\Log\Psr\Logger

        # Will be the actual Cache Manager, because as soon as the
        # Cache Manager is used (instantiated) elsewhere, it is also
        # injected eagerly into this controller:

        var_dump(get_class($this->cacheManager));                      # Neos\Flow\Cache\CacheManager

        # The TestSingleton is not injected into any other class and
        # therefore is a real Lazy Proxy before it is used for the
        # first time by calling hello():

        var_dump(get_class($this->testSingleton));                     # Acme\LazyTest\TestSingleton_LazyProxy
        var_dump($this->testSingleton instanceof TestSingleton); # bool(true)
        $this->testSingleton->hello('world');
        var_dump(get_class($this->testSingleton));                     # Acme\LazyTest\TestSingleton
    }
}

In order to verify this change, you can use the test package (see zip file) and play a bit with the injected properties. Things to try out (at least):

  • verify the output in Development context after initial run of ./flow test:test with an empty cache
  • verify the output on a second rund
  • verify that it works when you make a little change, so that proxy classes are built
  • check that all this works in Production

Acme.LazyTest.zip

@robertlemke
Copy link
Member Author

The code works now and should solve the problem at hand.

What's still missing are the respective test adjustments.

@robertlemke robertlemke linked an issue Feb 22, 2022 that may be closed by this pull request
@robertlemke robertlemke changed the title BUGFIX: PHP 7.4 property type declarations (WIP) BUGFIX: PHP 7.4 property type declarations Feb 22, 2022
@robertlemke robertlemke changed the title BUGFIX: PHP 7.4 property type declarations FEATURE: Fully support PHP 7.4 property type declarations Feb 22, 2022
@robertlemke robertlemke marked this pull request as ready for review February 23, 2022 15:51
@robertlemke
Copy link
Member Author

This is now ready for reviews.

@robertlemke
Copy link
Member Author

Hmm, that's weird – all functional and unit tests succeeded in Phpstorm using PHP 7.4.

image

@robertlemke robertlemke linked an issue Feb 23, 2022 that may be closed by this pull request
@robertlemke
Copy link
Member Author

robertlemke commented Feb 24, 2022

Sigh, PHP 7.3 fails. So I either need to scrub this more, so that it doesn't fail with PHP 7.3, or don't release this in Flow 7.3 but only 8.0.

Let's discuss this here: https://discuss.neos.io/t/rfc-drop-php-7-3-support-in-all-future-flow-versions/5844

Copy link
Member

@albe albe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look at the 7.3 failures this evening. Looks as if the generated code for the proxies is just slightly off, though not sure why only on 7.3? Maybe a different laminas version is installed there with slightly different behaviour?

@robertlemke robertlemke changed the base branch from 7.3 to master March 15, 2022 13:22
@robertlemke robertlemke changed the base branch from master to 7.3 March 15, 2022 13:28
This change adjusts the proxy building and dependency injection process
to properly handle PHP 7.4 class property type declarations.
neos#2114
This change makes sure that a reflected class property type does not
contain a "?" prefix when the property is nullable.
@robertlemke robertlemke changed the base branch from 7.3 to master March 15, 2022 13:45
@robertlemke
Copy link
Member Author

This change is now targeted and rebased for master.

@robertlemke
Copy link
Member Author

Thanks, @kdambekalns!

At this point all pending questions are answered and code suggestions were committed.

@mhsdesign
Copy link
Member

@robertlemke i really want to approve this since the syntax is much much much much fancier ... and cooler ^^

but i found out in my tests that this really increases the time to get flow ready from 0 - 100.
eg. its common practice to do a rm -rf Data/Temporary or ./flow flow:cache:flush every once and then if something isnt working that great and since caches often dont cover every scenario when they should be invalidated.

so i directly noticed a performance increase while doing functional Fusion tests (i was trying out your new functionality there)
from a flushed cache it took flow@master ~11.5 seconds to start the bootstrap and run one test.
with flow@robert it took ~ 18 seconds...

to make this more comparable i flushed the caches and manually timed how long ./flow flow:core:compile took (made a few iterations, result is pie mal daumen):

flow@master flow@robert
./flow flow:core:compile 2.6s 5s

Copy link
Member

@mhsdesign mhsdesign left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imagine i have this code: (DslFactory is a singelton)

    /**
     * @Flow\Inject
     * @var DslFactory
     */
    protected $dslFactory;

i now want to use typed properties, so i naively do so first:

    /**
     * @Flow\Inject
     */
    protected DslFactory $dslFactory;

and get some cryptic exception about: code line:

Cannot access uninitialized non-nullable property Neos\Fusion\Core\Parser_Original::$dslFactory by reference
DependencyInjection/PropertyInjectionTrait.php:32

then i go back to your nice PR body and ahh i need to use ? so i do so.

    /**
     * @Flow\Inject
     */
    protected ?DslFactory $dslFactory;

i think we should have a better exception message than the of php. -> thats easy.

but the problem is my code doesnt work now directly, i needed to flush the caches - what takes some time longer and then it works.


in short:

do this:

    /**
     * @Flow\Inject
     * @var DslFactory
     */
    protected $dslFactory;

run flow ...

    /**
     * @var DslFactory
     */
    protected $dslFactory;

run flow ... (doesnt work)

    /**
     * @Flow\Inject
     * @var DslFactory
     */
    protected $dslFactory;

run flow ... flow@master works here, but flow@robert doesnt... in my case the DslFactory had no working @Flow\InjectConfiguration and the $objectManger wasnt injected too (so no working DI) after a subtle change in the DslFactory File the injection was working again too (or after cache flush).

@mhsdesign
Copy link
Member

mhsdesign commented Mar 24, 2022

so at the moment this feels sometimes buggy - just go around fx. in the neos dev collection and try to change to typed properties here and there -> you will often find yourself in need to make a cache flush so the proxybuilding is properly working again.

but @robertlemke i would like to help tommorow to make this working!

@robertlemke
Copy link
Member Author

@mhsdesign very good catch!

Here are my timings for comparison:

flow@master flow@robert
neos/flow unit tests 780ms 782ms
neos/flow functional tests 46s 46s
neos/flow functional tests (only object management) 115ms 112ms
rm -rf Data/Temporary && time ./flow 1.39s 2.4s ⚠️

I did a quick first profiling and apparently, the Reflection Service is the culprit. From a total wall time of 15.2 seconds (it takes longer because of the profiling engine), my branch is more than 6 seconds slower while reflecting classes.

image

I'll take a closer look and see where code can be optimized.

@robertlemke
Copy link
Member Author

As I feared … the slow part is the new Laminas Class Building code, which doesn't use our optimized Reflection Service. Let's see if I can work around this a bit.

image

@robertlemke
Copy link
Member Author

If someone is reviewing this right now – please postpone your review a little. I need to rework some of the internals due to the performance issues.

@mhsdesign
Copy link
Member

do i see this right that using Laminas is not crucial and just bewares us from manually string concatenating everything together?

@robertlemke
Copy link
Member Author

do i see this right that using Laminas is not crucial and just bewares us from manually string concatenating everything together?

Mostly, yes. But it's also a handy API which allows you to pass an existing class reflection and then modify code through certain setters. The idea was to transition to a library (Laminas) instead of doing all that on our own (like we did before). But unfortunately, it doesn't work (fast enough) out of the box.

@robertlemke
Copy link
Member Author

PS: There are more problems than that, because the Laminas Code Builder tries to use our doc comment type hints in certain cases, which may be invalid, like in this example:

image

I'll work around that, but you see, it's a diligent but boring piece of work …

@mhsdesign mhsdesign disabled auto-merge March 25, 2022 14:31
# lazy loading is implemented. If any of the check fails, we fall back to eager loading:
if (
$propertyConfiguration->isLazyLoading() &&
$this->objectConfigurations[$propertyObjectName]->getScope() !== Configuration::SCOPE_PROTOTYPE &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We dont allow prototypes lazly injected - but on my pc there are tousands of lazy proxies for prototypes generated ... isnt this a waste of performance?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely a thing to optimize before merging.

$method->setBody(
<<< CODE
\$arguments = func_get_args();
{$returnKeyword}\$this->_activateDependency()->{$methodName}(...\$arguments);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesnt work for static methods ... (Using '$this' when not in object context)

noticed when looking at some random proxy files:

    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, \Bar $bar)
    {
        $arguments = func_get_args();
            return $this->_activateDependency()->renderStatic(...$arguments);
    }

what should we do in that case?
this?

    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, \Bar $bar)
    {
        $arguments = func_get_args();
            return self::renderStatic(...$arguments);
    }

{
$injection_reference = &$this->$propertyName;
$this->$propertyName = \Neos\Flow\Core\Bootstrap::$staticObjectManager->getInstance($propertyObjectName);
if ($this->$propertyName === null) {
$this->$propertyName = \Neos\Flow\Core\Bootstrap::$staticObjectManager->getLazyDependencyByHash($setterArgumentHash, $injection_reference);
if ($this->$propertyName === null) {
$this->$propertyName = \Neos\Flow\Core\Bootstrap::$staticObjectManager->createLazyDependency($setterArgumentHash, $injection_reference, $propertyClassName, $lazyInjectionResolver);
if ($this->$propertyName === null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we allow with this change less lazy dependency injection that before?
related: interfaces fx. cant be injected lazily anymore? #2701 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it just make sure that if a dependency can not be injected lazily (createLazyDependency returns null), it falls back to eager injection

@@ -27,14 +27,18 @@ trait PropertyInjectionTrait
* @param callable $lazyInjectionResolver
* @return void
*/
private function Flow_Proxy_LazyPropertyInjection($propertyObjectName, $propertyClassName, $propertyName, $setterArgumentHash, callable $lazyInjectionResolver)
private function Flow_Proxy_LazyPropertyInjection(string $propertyObjectName, string $propertyClassName, string $propertyName, string $setterArgumentHash, callable $lazyInjectionResolver)
{
$injection_reference = &$this->$propertyName;
$this->$propertyName = \Neos\Flow\Core\Bootstrap::$staticObjectManager->getInstance($propertyObjectName);
if ($this->$propertyName === null) {
$this->$propertyName = \Neos\Flow\Core\Bootstrap::$staticObjectManager->getLazyDependencyByHash($setterArgumentHash, $injection_reference);
if ($this->$propertyName === null) {
$this->$propertyName = \Neos\Flow\Core\Bootstrap::$staticObjectManager->createLazyDependency($setterArgumentHash, $injection_reference, $propertyClassName, $lazyInjectionResolver);
Copy link
Member

@mhsdesign mhsdesign Mar 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i finally kindof found the reson for this buggy behavior that on the fly di changes dont work correctly:
#2701 (review)

it seems that this method createLazyDependency is returning null and the next call: $staticObjectManager->get($propertyObjectName);

doesnt return an object of the proxied class but rather an object based of the class in my working dir...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so #2701 (review) even shorter:
go to a lazy dependency injection (in the old doc comment style - so you can compare this exact behaviour with flow@master)

  • remove the @Flow\Inject and run a flow
  • add the @Flow\Inject again and run flow again -> now the property should be injected.

but at flow@robert as i seems this code:

// If proxy classes were partially flushed in Development context, it might happen that

is triggered but flow@master is working in this scenario

@jonnitto
Copy link
Member

I think I can't help much, but I want to thank you for all your effort you put into this ❤️

@bwaidelich bwaidelich removed their request for review May 19, 2022 08:35
@mhsdesign mhsdesign changed the title FEATURE: Fully support PHP 7.4 property type declarations FEATURE: Lazy injections for typed properties Feb 15, 2023
@mhsdesign
Copy link
Member

@fcool thought me that it works already like this:

#[Flow\Inject(name: BlaInterface::class, lazy: true)]
protected BlaInterface|DependencyProxy|null $bla;

it would be great of course if i could leave out the (redundant) name: BlaInterface::class
(and idk if lazy: true is needed i dont remember ^^)

@mficzel
Copy link
Member

mficzel commented Mar 3, 2023

I kindoff like this style. Makes the lazyness very explicit and opt in. For me it would be fine to document this.

@mhsdesign
Copy link
Member

I kindoff like this style. Makes the lazyness very explicit and opt in. For me it would be fine to document this.

me too (especially cases, where youd need to pass the dependency it becomes clear why youd need to activate it first), but i would like to get rid of the extra arguments so we can write:

#[Flow\Inject()]
protected BlaInterface|DependencyProxy|null $bla;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Full PHP8 typesafe injectable properties Full support for PHP 7.4 property type declarations
7 participants