RFC: Entry points to the TS/Fusion Runtime rendering stack

Motivation

In projects of the more ambitious kind, the need for rather complex Fusion objects arises. Think of a faceted search with parameters for query, filters, pagination, hits per page, reference coordinates for distance calculation etc. All those data, both input and result, have to be relayed to nested Fusion objects and finally the view, without sending the same request multiple times.

Status quo

To make all those parameters available where they are needed, you can either use * @context to add variables to nested objects * Eel helpers to add variables where needed, supported by a singleton service that performs the search request

Why I consider this un-Neosy

Adding data with the given complexity via @context is not viable. The complex search query alone gives the term "Eel expression" a completely new (and fitting) meaning. Also you cannot access @context variables within other @context variables which prevents you from effectively dividing the declaration in multiple parts.

Eel helpers are a much better approach as you can hand over most of the logic from Fusion to PHP.
But I am not completely happy with this approach because as I see them, Eel helpers should be general purpose, contextless helpers to be used in any place that supports eel expressions. Also you have to configure them explicitly to be be usable in Fusion. In this case, the helper would both need a context (e.g. which node to search in) and is single purpose to a single Fusion prototype and that prototype alone.

Proposition: Supporting write access to the current rendering context for Fusion implementation classes

In my opinion, a more clean approach would be to use a custom implementation class with the goal to server as a kind of presentation model. It derives its data from the same service the Eel helper would use, but is tightly bound to the Fusion prototype it is configured in (by MyPackage:Search.@class=MyPackage\\Presentation\\Model\\Search). It also has direct access to the context and can e.g. fetch the current site directly from there.

What’s currently also possible (though explictly disencouraged) is to add additional context variables by using something like

    $context = $this->tsRuntime->popContext();

    /** @var NodeInterface $site */
    $site = $context['site'];

    $context = array_merge($context, $this->contextVariableService->getFinderVariables($site));
    $this->tsRuntime->pushContextArray($context);

in your fusion implementation class. That’s basically the same what @context does in the first place.

My suggestion is (given that there are no serious drawbacks I overlooked) to add an addToContextArray() method to the Fusion runtime that can be used by @context and fusion implementation classes alike and serves as an official entry point.

Open questions

Why is the pushContextArray declared internal in the first place?

This seriously impacts caching for example, also it might break the side effect free promise. You basically have to evaluate every TS object along the path to be sure the context is correct.

Hey Bernhard,

really nice discussion - thanks for your proposal :slight_smile:

For me, it is extremely important that we have a side-effect free rendering, meaning the following would not be possible:

root = Array
root.object1 = AddToContext {
   variable = "Foo"
}
root.object2 = Value {
    value = ${variable}
}

In this case, the rendering of “object2” would not be side-effect free anymore, because “object1” would need to be rendered beforehand (and is not on the TS path). This would break caching and out of band rendering, as @christianm has written.

However, we could do something like the following:

root = ArrayWhichAddsAlsoSomeStuffToContext
root.object2 = Value {
    value = ${variable}
}

Every Fusion object must ensure to clean up the Context stack accordingly after it was rendered - so this needs to be guaranteed.

Could you elaborate some more which case you would actually like to implement?

All the best,
Sebastian

1 Like

Hi Sebastian,

thanks for the comment!

It’s clear to me why the first example would not work. And even if it did, it would not be a solution for my problem because there would still be lots and lots of Fusion code required that would be unnecessary in PHP.

Your second example is exactly what I was referring to and what I already successfully implemented in projects. I’ll try to demonstrate it with a simplified example (@bwaidelich should be familiar with it :slight_smile:)

The main prototype has an implementation class and several nested objects:

prototype(Vendor.Project:Finder) < prototype(TS:Template) {
    @class = 'Vendor\\Project\\Presentation\\Model\\Finder'

    searchForm = Vendor.Project:SearchForm {
        query = ${query}
    }

    searchResult = TS:Collection {
        collection = ${searchResult}
        itemName = 'item'
        itemRenderer = Vendor.Project:SearchResultItem
    }
}

The respective presentation model looks like this:

class Finder extends TemplateImplementation
{

    /**
     * @Flow\Inject
     * @var FinderService
     */
    protected $contextVariableService;


    /**
     * @return string
     */
    public function evaluate()
    {
        $context = $this->tsRuntime->popContext();

        /** @var NodeInterface $site */
        $site = $context['site'];

        $context = array_merge($context, $this->contextVariableService->getFinderVariables($site));
        $this->tsRuntime->pushContextArray($context);

        return parent::evaluate();
    }
}

But to do this, I have to use two methods (popContext and pushContextArray) that are not public API (actually quite to the contrary). It would be much nicer if the AbstractTypoScriptObject / AbstractFusionObject would have an

addToContext($array)

API method which does the popContext and pushContextArray internally and could be used like this:

    /**
     * @return string
     */
    public function evaluate()
    {
        $context = $this->tsRuntime->getCurrentContext();

        /** @var NodeInterface $site */
        $site = $context['site'];

        $this->addToContext($this->contextVariableService->getFinderVariables($site));

        return parent::evaluate();
    }
}

which results in even nicer code and no usage of internal methods. And as I see it, the object only manipulates its own section of the rendering stack (just as @context (I hope no one ever uses that as a username) does) and thus should mess with neither side-effect-freeness nor caching.

Or did I overlook something?

Hey Bernhard,

actually your implementation class is wrongly implemented; as it in fact replaces the current context.

This way, the “new” context is still available on next rendering.

What it, however, should do, is the following:

$context = $this->tsRuntime->getCurrentContext();
// add things to $context;
$this->tsRuntime->pushContextArray($context);
$result = $this->tsRuntime->evaluate(...);

// !!! IMPORTANT:
$this->tsRuntime->popContext();
return $result;

Does this make it more clear now? :slight_smile:

All the best,
Sebastian

Mmh. I think I don’t get all of it.
Isn’t the context there for the Fusion object on the current path (and its nested objects) alone?
What’s wrong with replacing it? What do you mean by “next rendering”? Is there anything done with this context after the runtime is finished with the object on the current path?

Hey Bernhard,

the context contains all variables available. It can be accessed using two ways:

  • in Eel, so by using ${variableName}
  • in Fusion, using the internal API described above.

Internally, the context is a stack, meaning you can “push” a new value-set to it, and then again “pop” from it to make the old value-set visible.

However, currently this push and pop is NOT done in the Fusion Runtime (for most parts), but done by the few Fusion objects which modify the context. It is their responsibility to do the following:

pushContextArray(newContext)
.... render nested stuff ...
popContext()
return $rendered;

The only place where the Runtime modifies the context internally is when using @context.

Proposal

While writing this down, I had an idea how we can make the context handling more flexible. Currently, it is only possible to set variables one-by-one by using @context.variable1, @context.variable2 etc.

However, we could make it possible to write @context = TypoScriptObjectWhichReturnsAnArray - so that we can make multi-assignment easily possible.

I’d like this cross checked by e.g. @christopher or @christianm - to think about whether this could work out :slight_smile:

All the best,
Sebastian

@sebastian You could already write something like

@context.myArray = FusionObjectThatReturnsAndArray

So I think rather than compromising the extensibility aspect there’s not a real advantage when directly assigning a value to @context.

One thing that @Nezaniel mentioned and which bugged me too is that one context variable expression cannot access another one because there is no deterministic order of execution (and thus context variables might not yet be evaluated when used). With the introduction of a positional sorting on context variables (with before or after this matches pretty good for the use case at hand) we could allow this scenario to make Eel expressions more modular (which is what I understood one point that needs a better solution).

1 Like

IMHO that just complicates things, lets not introduce that. I know the respective part and I would rather keep it undeterministic and forbid access to other context variables set in the same “run”. Basically you could use the RawArray already as you pointed out and in there the order is deterministic. So if you need order and multiple variables I think that’s the way to go.

Just to clarify that is almost the only place the context is modified besides a few fusion objects like Collections, but they also clean up internally.

The context (including @context) of any TS object is set pretty early, before we check for a cache entry and is always cleaned up afterwards. Whatever we do, these two things needs to be ensured otherwise it will break caching.

2 Likes

Thanks all of you, that clears up a lot.
Somehow I was under the wrong impression that the Runtime does all that stack management by adding the current context to the stack for the object on the current path and cleaning up afterwards.

Knowing that now, I am thinking of a way to circumvent the whole issue.
One way would be an abstract getAddtionalContextVariables() (name open for discussion) method in the AbstractTypoScriptObject class that may provide arbitrary variables for the Runtime.

The Runtime itself would have to be only minimally adapted. The @context is applied in

\TYPO3\TypoScript\Core\Runtime::prepareContextForTypoScriptObject

Most conveniently, the AbstractTypoScriptObject is present there, so

$typoScriptConfiguration[‘__meta’][‘context’]

could be easily merged with

AbstractTypoScriptObject::getAddtionalContextVariables()

before being applied. This way, the manually configured $typoScriptConfiguration[‘__meta’][‘context’] can also overwrite automatically set variables in getAddtionalContextVariables.

Objections?

@sebastian @christopher
What I don’t like about the

@context.myArray = FusionObjectThatReturnsAndArray

approach is that it introduces a second Fusion prototype taking over some of the presentation logic that actually belongs to the one it supplements (and thus making the latter dependent on it). Would be nicer if only one was necessary.

IMHO the TypoScriptObject should not have any say in it’s own context, that should always come from outside. I think changing that will bite us in the long run as it heavily chains the context to the TS object AND makes it necessary to instanciate the TS object before knowing the actual context (something I have plans for to not do if possible)