Flow generates faulty proxy classes

Hi everybody,

I’m having a strange problem here. A rather long time ago we developed a small in-house application based on Flow 2.3 for a customer. Since the customer bought a new server he wanted to update Flow to the most recent version. The update process was a bit “bumpy”, but I got it to work.
Along the way (I believe it started with version 5.0) Flow started generating faulty proxy classes. First I thought it was maybe a problem of the early versions (I always upgraded to .0 versions), but the problem persists even with Flow 7.1.2

The problem occurs in 2 classes (at first, it was only one) and is the same for both: the constructor in the generated proxy class does not contain the necessary parameter. The classes in question are TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider and Neos\FluidAdaptor\Core\ViewHelper\TemplateVariableContainer

For both classes, the proxies are generated without the $variables parameter (constructor is empty). There are two funny things though:

  1. The annotations of the auto-generated Constructors actually contain the parameter
  2. While TemplateVariableContainer inherits it’s constructor from StandardVariableProvider (so the “original” class in the cache file has no constructor), StandardVariableProvider (and thus the “original” class in the cache file) defines it’s own constructor which makes this behaviour all the more awkward for me.

Any ideas why this might be happening? As I did not find any clues on this so far I’m guessing this is not a common problem.
BTW: I also did a fresh installation of Flow 7.1.2 and copied my packages there, so the problem does not seem to be caused by the upgrade process.

Any help is greatly appreciated.

Best,
Christian

Hi @ifx_cw

The proxy classes are created in such way

  1. Rename the original class name to ClassName_Original
  2. Create a new class wit the original ClassName and extend the ClassName_Original class - and by that keep the constructor argument.

Is that not the case in your issue? Can you post the code here of the proxy class?
And does Flow throw any exception, that brought this to your attention?

Hi and thanks for the quick reply.

Obviously something goes wrong here in my setup. When I flush the cache and have it warmed up again, the constructor arguments are gone in the extended classes. For testing I got by by just adding them in the generated proxies, but I would like to solve the problem (even more: understand the cause) before trying to update the customer’s installation.

I noticed this behavior because the application simply won’t work without fixing this (PHP naturally complains about incompatible constructor declarations).

This is the code just generated for StandardVariableProvider - here you can see that the original contains the parameter, yet the proxy does not (even though it keeps the annotation):

<?php 
namespace TYPO3Fluid\Fluid\Core\Variables;

/*
 * This file belongs to the package "TYPO3 Fluid".
 * See LICENSE.txt that was shipped with this package.
 */

/**
 * Class StandardVariableProvider
 */
class StandardVariableProvider_Original implements VariableProviderInterface
{
    const ACCESSOR_ARRAY = 'array';
    const ACCESSOR_GETTER = 'getter';
    const ACCESSOR_ASSERTER = 'asserter';
    const ACCESSOR_PUBLICPROPERTY = 'public';

    /**
     * Variables stored in context
     *
     * @var mixed
     */
    protected $variables = [];

    /**
     * Variables, if any, with which to initialize this
     * VariableProvider.
     *
     * @param array $variables
     */
    public function __construct(array $variables = [])
    {
        $this->variables = $variables;
    }

    /**
     * @param array|\ArrayAccess $variables
     * @return VariableProviderInterface
     */
    public function getScopeCopy($variables)
    {
        if (!array_key_exists('settings', $variables) && array_key_exists('settings', $this->variables)) {
            $variables['settings'] = $this->variables['settings'];
        }
        $className = get_class($this);
        return new $className($variables);
    }

    /**
     * Set the source data used by this VariableProvider. The
     * source can be any type, but the type must of course be
     * supported by the VariableProvider itself.
     *
     * @param mixed $source
     * @return void
     */
    public function setSource($source)
    {
        $this->variables = $source;
    }

    /**
     * @return mixed
     */
    public function getSource()
    {
        return $this->variables;
    }

    /**
     * Get every variable provisioned by the VariableProvider
     * implementing the interface. Must return an array or
     * ArrayAccess instance!
     *
     * @return array|\ArrayAccess
     */
    public function getAll()
    {
        return $this->variables;
    }

    /**
     * Add a variable to the context
     *
     * @param string $identifier Identifier of the variable to add
     * @param mixed $value The variable's value
     * @return void
     * @api
     */
    public function add($identifier, $value)
    {
        $this->variables[$identifier] = $value;
    }

    /**
     * Get a variable from the context. Throws exception if variable is not found in context.
     *
     * If "_all" is given as identifier, all variables are returned in an array,
     * if one of the other reserved variables are given, their appropriate value
     * they're representing is returned.
     *
     * @param string $identifier
     * @return mixed The variable value identified by $identifier
     * @api
     */
    public function get($identifier)
    {
        return $this->getByPath($identifier);
    }

    /**
     * Get a variable by dotted path expression, retrieving the
     * variable from nested arrays/objects one segment at a time.
     * If the second variable is passed, it is expected to contain
     * extraction method names (constants from VariableExtractor)
     * which indicate how each value is extracted.
     *
     * @param string $path
     * @param array $accessors Optional list of accessors (see class constants)
     * @return mixed
     */
    public function getByPath($path, array $accessors = [])
    {
        $subject = $this->variables;
        foreach (explode('.', $this->resolveSubVariableReferences($path)) as $index => $pathSegment) {
            $accessor = isset($accessors[$index]) ? $accessors[$index] : null;
            $subject = $this->extractSingleValue($subject, $pathSegment, $accessor);
            if ($subject === null) {
                break;
            }
        }
        return $subject;
    }

    /**
     * Remove a variable from context. Throws exception if variable is not found in context.
     *
     * @param string $identifier The identifier to remove
     * @return void
     * @api
     */
    public function remove($identifier)
    {
        if (array_key_exists($identifier, $this->variables)) {
            unset($this->variables[$identifier]);
        }
    }

    /**
     * Returns an array of all identifiers available in the context.
     *
     * @return array Array of identifier strings
     */
    public function getAllIdentifiers()
    {
        return array_keys($this->variables);
    }

    /**
     * Checks if this property exists in the VariableContainer.
     *
     * @param string $identifier
     * @return boolean TRUE if $identifier exists, FALSE otherwise
     * @api
     */
    public function exists($identifier)
    {
        return array_key_exists($identifier, $this->variables);
    }

    /**
     * Clean up for serializing.
     *
     * @return string[]
     */
    public function __sleep()
    {
        return ['variables'];
    }

    /**
     * Adds a variable to the context.
     *
     * @param string $identifier Identifier of the variable to add
     * @param mixed $value The variable's value
     * @return void
     */
    public function offsetSet($identifier, $value)
    {
        $this->add($identifier, $value);
    }

    /**
     * Remove a variable from context. Throws exception if variable is not found in context.
     *
     * @param string $identifier The identifier to remove
     * @return void
     */
    public function offsetUnset($identifier)
    {
        $this->remove($identifier);
    }

    /**
     * Checks if this property exists in the VariableContainer.
     *
     * @param string $identifier
     * @return boolean TRUE if $identifier exists, FALSE otherwise
     */
    public function offsetExists($identifier)
    {
        return $this->exists($identifier);
    }

    /**
     * Get a variable from the context. Throws exception if variable is not found in context.
     *
     * @param string $identifier
     * @return mixed The variable identified by $identifier
     */
    public function offsetGet($identifier)
    {
        return $this->get($identifier);
    }

    /**
     * @param string $propertyPath
     * @return array
     */
    public function getAccessorsForPath($propertyPath)
    {
        $subject = $this->variables;
        $accessors = [];
        $propertyPathSegments = explode('.', $propertyPath);
        foreach ($propertyPathSegments as $index => $pathSegment) {
            $accessor = $this->detectAccessor($subject, $pathSegment);
            if ($accessor === null) {
                // Note: this may include cases of sub-variable references. When such
                // a reference is encountered the accessor chain is stopped and new
                // accessors will be detected for the sub-variable and all following
                // path segments since the variable is now fully dynamic.
                break;
            }
            $accessors[] = $accessor;
            $subject = $this->extractSingleValue($subject, $pathSegment);
        }
        return $accessors;
    }

    /**
     * @param string $propertyPath
     * @return string
     */
    protected function resolveSubVariableReferences($propertyPath)
    {
        if (strpos($propertyPath, '{') !== false) {
            preg_match_all('/(\{.*\})/', $propertyPath, $matches);
            foreach ($matches[1] as $match) {
                $subPropertyPath = substr($match, 1, -1);
                $propertyPath = str_replace($match, $this->getByPath($subPropertyPath), $propertyPath);
            }
        }
        return $propertyPath;
    }

    /**
     * Extracts a single value from an array or object.
     *
     * @param mixed $subject
     * @param string $propertyName
     * @param string|null $accessor
     * @return mixed
     */
    protected function extractSingleValue($subject, $propertyName, $accessor = null)
    {
        if (!$accessor || !$this->canExtractWithAccessor($subject, $propertyName, $accessor)) {
            $accessor = $this->detectAccessor($subject, $propertyName);
        }
        return $this->extractWithAccessor($subject, $propertyName, $accessor);
    }

    /**
     * Returns TRUE if the data type of $subject is potentially compatible
     * with the $accessor.
     *
     * @param mixed $subject
     * @param string $propertyName
     * @param string $accessor
     * @return boolean
     */
    protected function canExtractWithAccessor($subject, $propertyName, $accessor)
    {
        $class = is_object($subject) ? get_class($subject) : false;
        if ($accessor === self::ACCESSOR_ARRAY) {
            return (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)));
        } elseif ($accessor === self::ACCESSOR_GETTER) {
            return ($class !== false && method_exists($subject, 'get' . ucfirst($propertyName)));
        } elseif ($accessor === self::ACCESSOR_ASSERTER) {
            return ($class !== false && $this->isExtractableThroughAsserter($subject, $propertyName));
        } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY) {
            return ($class !== false && property_exists($subject, $propertyName));
        }
        return false;
    }

    /**
     * @param mixed $subject
     * @param string $propertyName
     * @param string $accessor
     * @return mixed
     */
    protected function extractWithAccessor($subject, $propertyName, $accessor)
    {
        if ($accessor === self::ACCESSOR_ARRAY && is_array($subject) && array_key_exists($propertyName, $subject)
            || $subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)
        ) {
            return $subject[$propertyName];
        } elseif (is_object($subject)) {
            if ($accessor === self::ACCESSOR_GETTER) {
                return call_user_func_array([$subject, 'get' . ucfirst($propertyName)], []);
            } elseif ($accessor === self::ACCESSOR_ASSERTER) {
                return $this->extractThroughAsserter($subject, $propertyName);
            } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY && property_exists($subject, $propertyName)) {
                return $subject->$propertyName;
            }
        }
        return null;
    }

    /**
     * Detect which type of accessor to use when extracting
     * $propertyName from $subject.
     *
     * @param mixed $subject
     * @param string $propertyName
     * @return string|NULL
     */
    protected function detectAccessor($subject, $propertyName)
    {
        if (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName))) {
            return self::ACCESSOR_ARRAY;
        }
        if (is_object($subject)) {
            $upperCasePropertyName = ucfirst($propertyName);
            $getter = 'get' . $upperCasePropertyName;
            if (method_exists($subject, $getter)) {
                return self::ACCESSOR_GETTER;
            }
            if ($this->isExtractableThroughAsserter($subject, $propertyName)) {
                return self::ACCESSOR_ASSERTER;
            }
            if (property_exists($subject, $propertyName)) {
                return self::ACCESSOR_PUBLICPROPERTY;
            }
        }

        return null;
    }

    /**
     * Tests whether a property can be extracted through `is*` or `has*` methods.
     *
     * @param mixed $subject
     * @param string $propertyName
     * @return bool
     */
    protected function isExtractableThroughAsserter($subject, $propertyName)
    {
        return method_exists($subject, 'is' . ucfirst($propertyName))
            || method_exists($subject, 'has' . ucfirst($propertyName));
    }

    /**
     * Extracts a property through `is*` or `has*` methods.
     *
     * @param object $subject
     * @param string $propertyName
     * @return mixed
     */
    protected function extractThroughAsserter($subject, $propertyName)
    {
        if (method_exists($subject, 'is' . ucfirst($propertyName))) {
            return call_user_func_array([$subject, 'is' . ucfirst($propertyName)], []);
        }

        return call_user_func_array([$subject, 'has' . ucfirst($propertyName)], []);
    }
}

#
# Start of Flow generated Proxy code
#
namespace TYPO3Fluid\Fluid\Core\Variables;

use Doctrine\ORM\Mapping as ORM;
use Neos\Flow\Annotations as Flow;

/**
 * Class StandardVariableProvider
 * @codeCoverageIgnore
 */
class StandardVariableProvider extends StandardVariableProvider_Original implements \Neos\Flow\ObjectManagement\Proxy\ProxyInterface {


    /**
     * Autogenerated Proxy Method
     * @param array $variables
     */
    public function __construct()
    {
        $arguments = func_get_args();
        if (method_exists(get_parent_class(), 'Flow_Aop_Proxy_buildMethodsAndAdvicesArray') && is_callable('parent::Flow_Aop_Proxy_buildMethodsAndAdvicesArray')) parent::Flow_Aop_Proxy_buildMethodsAndAdvicesArray();
        parent::__construct(...$arguments);
    }

    /**
     * Autogenerated Proxy Method
     */
    public function __wakeup()
    {
        if (method_exists(get_parent_class(), 'Flow_Aop_Proxy_buildMethodsAndAdvicesArray') && is_callable('parent::Flow_Aop_Proxy_buildMethodsAndAdvicesArray')) parent::Flow_Aop_Proxy_buildMethodsAndAdvicesArray();
    }
}

This part, takes arguments given to the constructor and pass it to the original constructor.

Does Flow throw a exception?

@sorenmalling Flow does not throw any exceptions as the whole application simply does not work with the auto-generated classes. The only (frontend) output is a PHP error:

Fatal error: Declaration of TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider::__construct() must be compatible with TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface::__construct(array $variables = Array)

Or am I misunderstanding your intention?

What version of Flow and PHP are you running?

Current Flow Version is 7.1.2, but as described the problem started occurring when I updated to 5.0 I believe - I just kept updating (but the problem also persists with a “fresh” installation of Flow 7.1.2).
The current PHP Version is 7.3.19 but the problem also occured with PHP 7.2 along the way of the update (don’t remember the minor version right now).

It appears to me that Flow produces this error when a constructor with arguments is defined within an interface. I was just able to reproduce the error in another older Flow project (running on Flow 5.3 and PHP 7.2) - I created an interface like this:

<?php

namespace Infoworxx\Test;

interface Testinterface {

    public function __construct(array $test = []);

}

and a controller like this:

<?php
namespace Infoworxx\Test\Controller;
    
use Neos\Flow\Annotations as Flow;

class TestController extends \Neos\Flow\Mvc\Controller\ActionController implements \Infoworxx\Test\Testinterface {

    public function __construct(array $test = []) {
    }
}

The controller actually contains some methods, but that’s not the point here - as soon as I do this I get the same error in the generated code and thus a similar error message: Declaration of Infoworxx\Test\Controller\TestController::__construct() must be compatible with Infoworxx\Test\Testinterface::__construct(array $test = Array)

So obviously there is actually an error in the way Flow generates it’s proxy classes - at least with specific PHP Versions, though I doubt that PHP is the reason here.
The only thing I don’t really understand right now is why one Flow installation generates a proxy class for \Neos\FluidAdaptor\Core\ViewHelper\TemplateVariableContainer while the other one doesn’t (even though TemplateVariableContainer is also used)

Edit: I just made a fresh install of Flow 7.1.2, kickstarted a package and just added an Interface to the generated StandardController as described above - the problem also occurs here so at least it’s a problem that’s easy to reproduce.

@sorenmalling I had a look at the proxy class generation code and found that constructors are always created without parameters, thus making Flow incapable of creating proxy classes for classes that implement interfaces which define constructors with parameters.

Do you happen to know any way to work around this? Or do you maybe have any idea why my current Flow installation would create Proxies for the aforementioned classes but my other Flow 5.3 installation does not (despite the fact that is uses those classes)?

Edit FYI I created an issue for this on GitHub (Flow creates faulty proxy classes when certain interfaces are used · Issue #3418 · neos/neos-development-collection · GitHub)

Constructors in interfaces are discouraged, so that issue can be solved by having a static create method instead, if you will require a create-ish method in your interface

Note:
Although they are supported, including constructors in interfaces is strongly discouraged. Doing so significantly reduces the flexibility of the object implementing the interface. Additionally, constructors are not enforced by inheritance rules, which can cause inconsistent and unexpected behavior.

https://www.php.net/manual/en/language.oop5.interfaces.php

Regarding the exception here

Fatal error: Declaration of TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider::__construct() must be compatible with TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface::__construct(array $variables = Array)

It seems as if Fluid did not live up to it’s own interface at that point in time/version you are running with - could it be a issue, that was solved at a later version, so you must pin that specific version?

I’m personally running PHP 7.4 in both CLI and FPM, with 7.0.7 and do not have similar issues. So trying to narrow it in to a specific version issue.
I don’t even have a StandardVariableProvider proxy class that is mentioned in your original exception
image

@sorenmalling I think you misunderstood - the Fatal error also occurs in the most recent version of Flow. The problem is also not Fluid not sticking to it’s own interface definition but Flow generating a faulty proxy class (and thus: Flow creating erroneous code for classes that are part of the base distribution). This is not a problem that I could solve in any way other than changing base classes of the most recent version of the core distribution (namely removing the constructor definition from the interface).
The thing that bothers me is that Flow even created that proxy even though my other Flow installation does not (yet, as you can see, this is all “Core standard stuff”, not anything I did in my code - I do not even use Neos\FluidAdaptor\Core\ViewHelper\TemplateVariableContainer or TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider at any point explicitly).

The examples with the fresh Flow installation were just create a means to be able to reproduce the core problem (which should - as mentioned in the GitHub issue - be possible in basically any Flow version).

Alright, I finally figured out the cause - I had a method pointcut in my policy configuration that was too broad - which lead to the mentioned classes being proxied which then caused the error.

Still, I feel this is a bug in the Framework - as long constructors in interfaces are possible Flow should also support generating working proxy classes for those cases IMHO. Especially if code in the base distribution contains such an interface…

Great that you found the issue - I could not pinpoint you issue in a fresh setup :+1:

@ifx_cw Feel free to submit your take on how the ProxyClassBuilder could solve this.

It’s interesting to see your take on it, and I’m sure we will love to support with review and feedback :star_struck:

Thanks a lot for all the effort you put into this! I agree with your conclusion that’s a bug in the Framework. Proxy constructors should have the same signature as the original class.

FYI: See class(.*\\SpecialController) leads to ProxyBuilding of TYPO3Fluid\\Fluid\\Core\\Variables\\StandardVariableProvider · Issue #2553 · neos/flow-development-collection · GitHub for more details on the bug