alexeyrybak/blitz

Scope context inside included templates not work?

Opened this issue · 16 comments

Hello.

We used our template preprocessor before, which injected all the include recursively into the parent templates and everything worked. But, having decided to use pure Blitz, we ran into the following problem. Context scope not working inside included templates.

Tested in:
PHP 7.4.33, Blitz 0.10.5, blitz.scope_lookup_limit=8, blitz.enable_include=1
PHP 8.2.7, Blitz 0.10.5, blitz.scope_lookup_limit=8, blitz.enable_include=1

index.php

<?php

error_reporting(E_ERROR);
header('Content-Type: text/plain');

$template_plain     = 'films.tpl';
$template_include   = 'films_with_include.tpl';

$end_data   = [];
$end_data['films']  = [
    ['film' => 'Film One'],
    ['film' => 'Film Two'],
];
$end_data['country']   = 'Wonderland';
$end_data['years']     = ['year' => '2023'];
$end_data['cast']      = [
    ['name' => 'Jeff Bridges'],
    ['name' => 'John Goodman'],
    ['name' => 'Julianne Moore'],
    ['name' => 'Steve Buscemi'],
];

$T1 = new Blitz($template_plain);
echo $T1->parse($end_data);

$T2 = new Blitz($template_include);
echo $T2->parse($end_data);

Want country, years and cast inside each film in films, set blitz.scope_lookup_limit=8
With this plain template all works

$template_plain = 'films.tpl';

{{ BEGIN films }}
Film Name: {{ $film }}
Country: {{ $country }}
Years: {{ BEGIN years }}{{ $year }}{{ END }}
Cast: {{ BEGIN cast }}{{ UNLESS $_first }}, {{ END }}{{ $name }}{{ END }}

{{ END }}
-------------------------------------------------------------

and all work ok

Film Name: Film One
Country: Wonderland
Years: 2023
Cast: Jeff Bridges, John Goodman, Julianne Moore, Steve Buscemi

Film Name: Film Two
Country: Wonderland
Years: 2023
Cast: Jeff Bridges, John Goodman, Julianne Moore, Steve Buscemi

-------------------------------------------------------------

But if we want move some template part in include, scope in include looks as broken

$template_include = 'films_with_include.tpl';

{{ BEGIN films }}
Film Name: {{ $film }}
{{ include("film_include.tpl") }}

{{ END }}
-------------------------------------------------------------

and film_include.tpl

Country: {{ $country }}
Years: {{ BEGIN years }}{{ $year }}{{ END }}
Cast: {{ BEGIN cast }}{{ UNLESS $_first }}, {{ END }}{{ $name }}{{ END }}

Failed result

Film Name: Film One
Country: 
Years: 
Cast: 

Film Name: Film Two
Country: 
Years: 
Cast: 

-------------------------------------------------------------

Most likely, this could be solved with the block method (actualy NOT, tested after this issue), but I would not want extra data loops inside the code. Perhaps this can be forcibly solved by adding a new ini parameter that would change this behavior or force include templates into the parent template (in fact, it works for me with my template preprocessor, but I would like it to work natively in pure Blitz)

First, I think that if you form the set a bit differently then there will be even no scope-lookups at all and everything should work. "Films" should be an array and "cast" should be an array inside this array, this is a "natural" approach. Your current structure is different.

Second, even with structures like yours (which I still think is a bit incorrect) - upper lookups should work too, but probably it's again your structure, where "cast" for some reason sits on the same level as "films". Could you please have a look at tests/include_multi.phpt and tests/include_scope.phpt, this explains how scope feature works.

Alexey. Here the data is for example, in real work their meaning, of course, is different. A typical example. We have a list of forum topics, after the third we want to insert something external to this list, for example, links to articles on the topic of a section of this forum (but the article list is not part of the topic list, L-Logic, khe-khe)

Example data

$data['topics'] = [
    ['topic_id'=>1,'topic_title'=>'Some Title 1'],
    ['topic_id'=>2,'topic_title'=>'Some Title 2'],
    ['topic_id'=>3,'topic_title'=>'Some Title 3'],
    ['topic_id'=>4,'topic_title'=>'Some Title 4'],
];
$data['articles'] = [
    ['article_url'=>'/some_url_1','article_title'=>'Article 1'],
    ['article_url'=>'/some_url_2','article_title'=>'Article  2'],
    ['article_url'=>'/some_url_3','article_title'=>'Article  3'],
];

topics_list.tpl

<div class="topic-list">
{{ BEGIN topics }}
    <div class="topic"><a href="/topic/{{ $topic_id }}">{{ $topic_title }}</a></div>
    {{ IF $_num ==3 }}
        <div class="article-list">
        {{ BEGIN articles }}
            <div class="article"><a href="{{ $article_url }}">{{ $article_title }}</a></div>
        {{ END }}
        </div>
    {{ END if-_num }}
{{ END }}
</div>

For the purpose of reusing templates, I would like to replace the article list block with include, because such a list of articles can be inserted in several different places
We would put the following code in a separate template (article_list.tpl) for later use elsewhere via include

<div class="article-list">
    {{ BEGIN articles }}
        <div class="article"><a href="{{ $article_url }}">{{ $article_title }}</a></div>
    {{ END }}
</div>

In fact, a list of topics is obtained from one source, a list of links for this inclusion is obtained from another source. This list of links is meant to be inserted anywhere in the template or any subtemplate, so it is "outside" the list of topics.
Therefore, it seems to us that if the include of a subtemplate should work the same way if the contents of the subtemplate were inserted into the template itself as is - that's how it works (and work with scope-lookup > 0)

$T2->setGlobals(['country'=>$end_data['country']]);
$T2->setGlobals(['years'=>$end_data['years']]);
$T2->setGlobals(['cast'=>$end_data['cast']]);

OR

$T2->setGlobals([
    'country'   => $end_data['country'],
    'years'     => $end_data['years'],
    'cast'      => $end_data['cast'],
]);

Result

Film Name: Film One
Country: Wonderland
Years: 2023
Cast: Jeff Bridges, John Goodman, Julianne Moore, Steve Buscemi

Film Name: Film Two
Country: Wonderland
Years: 2023
Cast: Jeff Bridges, John Goodman, Julianne Moore, Steve Buscemi

-------------------------------------------------------------

Using setGlobal for the code from the first post, you can make everything work, but this is "extra unnecessary" code :-)
At the time of sending the data to the class that will render the templates, the Blitz instance may not be available to call setGlobals, and in general, global scope seems superfluous to us.

In the Wiki (https://github.com/alexeyrybak/blitz/wiki/Basics#variable-scope-and-contexts) the paragraph is slightly misleading about this. Especially the last line of the list

To have variable inside block you have three options:

  • pass it via params manually for every context iteration
  • pass it as global once
  • force Blitz to make "upper" lookups when parameter was not found

Nick, I finally understand the issue: scope doesn't work with include.
It was long time ago, few quick steps could be trying these two things:
(1) try setting blitz.scope_lookup_limit to non-zero
(2) if this doesn't work please see tests/include_scope.phpt you will find a hack to pass data literally into include

Alex, yes, scope doesn't work with include.
(1) blitz.scope_lookup_limit not affect for this
(2) I don't want to wrap the data output with php code to control the template.
Now works with setGlobals.
But is it possible in the future to do something like a configuration parameter that would include all possible include on their content before the template is rendered?
Now we have solved this in a php-wrapper that parses the template and recursively injects the contents of all files included in it

(2) I don't want to wrap the data output with php code to control the template.

you just pass additional params into the include call, that's all, you don't wrap the data output

(2) I don't want to wrap the data output with php code to control the template.

you just pass additional params into the include call, that's all, you don't wrap the data output

No one knows at the level of the parent template what data will be transferred there or not, there may not be data, their set may be different. This will be checked already inside the included template

quite common example

profile.tpl

{{ BEGIN profile }}
<div class="profile">
  {{ $nick }}, {{ $age }}
  {{ include("avatar.tpl") }}
</div>
{{ END }}

avatar.tpl

{{ IF $avatar }}
  {{ BEGIN avatar }}
   <div class="avatar"><img src="{{ $avatar_url }}"></div>
  {{ END }}
{{ END if-avatar }}

some code

$data1['profile'] = ['nick'=>'Nick','age'=>48];
$data2['profile'] = ['nick'=>'Alex','age'=>50,'avatar'=>['avatar_url'=>'https://some.host/avatar.jpg']];

$T = new Blitz();
$T->load('profile.tpl');
echo $T->parse(data1);
echo $T->parse(data2);

It would be great if after load the template would represent the final version of the template code including all inclusions
Example end version profile.tpl

{{ BEGIN profile }}
<div class="profile">
  {{ $nick }}, {{ $age }}
  <!-- included from avatar.tpl start -->
    {{ IF $avatar }}
      {{ BEGIN avatar }}
        <div class="avatar"><img src="{{ $avatar_url }}"></div>
      {{ END }}
    {{ END if-avatar }}
  <!-- included from avatar.tpl end -->
</div>
{{ END }}

No one knows at the level of the parent template what data will be transferred there or not

I'm afraid you didn't understand my previous comment. Please just use scope => $_ and you will get what you need. See my example.

I'm afraid you didn't understand my previous comment. Please just use scope => $_ and you will get what you need. See my example.

That is, if I fix the specified template as follows, will it work?

profile.tpl

{{ BEGIN profile }}
<div class="profile">
  {{ $nick }}, {{ $age }}
  {{ include("avatar.tpl",  scope   =   \$_) }}
</div>
{{ END }}

So?
Make some test tomorrow, but i will try to explain the problem, it is local to our application

For example, I generally have an external layout designer (not always able to programming or able to work with template engines), he does not know about Blitz or any other templating engine at all. He produces templates or such small inclusions that can be reused in different places. The maximum that it can do is issue pseudo includes via SSI so that you can see how it looks on a web server like Apache or Nginx. Advanced designer know Laravel Blade syntax. Forcing them to set parameters in the include template does not look like a convenient way.
From the backend side, we have a mixture of Blitz and Blade here, so we don’t want extra code for the Blitz template controller either, we want uniformity. Like now it's on Blitz, tomorrow someone will refactor under Laravel/Blade. In this case, I can simply convert the Blitz templates to Blade with a script and that's it. Moreover, we already have such a "script".
And we also have a reverse way - using Blitz in Laravel (https://github.com/NickyX3/laravel-blitz-view) to possibly temporarily use Blitz templates directly in Laravel through the Laravel Facade

Tests from fist message

{{ BEGIN films }}
Film Name: {{ $film }}
{{ include("film_include.tpl") }}

{{ END }}
-------------------------------------------------------------

With

$T2->setGlobals([
    'country'   => $end_data['country'],
    'years'     => $end_data['years'],
    'cast'      => $end_data['cast'],
]);

Result is normal

Film Name: Film One
Country: Wonderland
Years: 2023
Cast: Jeff Bridges, John Goodman, Julianne Moore, Steve Buscemi

Film Name: Film Two
Country: Wonderland
Years: 2023
Cast: Jeff Bridges, John Goodman, Julianne Moore, Steve Buscemi

-------------------------------------------------------------

Without setGlobals include template in included, but variables not passed

Film Name: Film One
Country: 
Years: 
Cast: 

Film Name: Film Two
Country: 
Years: 
Cast: 

-------------------------------------------------------------

Change include call for scope hack

{{ BEGIN films }}
Film Name: {{ $film }}
{{ include("film_include.tpl",scope=\$_) }}

{{ END }}
-------------------------------------------------------------

Result, include template is NOT included in the code at all

Film Name: Film One


Film Name: Film Two


-------------------------------------------------------------

ЧЯДНТ?
PHP Version 8.2.8
Blitz version 0.10.7, PHP8 branch
blitz.scope_lookup_limit = 8
blitz.enable_include = 1
blitz.warn_context_duplicates = 0

scope=$_ not scope=\$_
In the example I use two-quoted strings so I escape $

scope=$_ not scope=$_ In the example I use two-quoted strings so I escape $

Ok, now

{{ BEGIN films }}
Film Name: {{ $film }}
{{ include("film_include.tpl",scope=$_) }}

{{ END }}
-------------------------------------------------------------

Include template is included, but variables not passed

Film Name: Film One
Country: 
Years: 
Cast: 

Film Name: Film Two
Country: 
Years: 
Cast: 

-------------------------------------------------------------

Nick, let's get two steps back. You don't need to pass any scope for include, I misunderstood the issue. Let's go back your code. This is what you should do and what's working. Please take this example and use it carefully so we don't have endless thread. The main problem is that you don't prepare the data in a right way.

fisher@fisher bugs % /local/php-8.1.2/bin/php issue79.php
Film Name: Film #1
Country: Country#1
Years: 2023
Cast: Cast#1.1, Cast#1.2

Film Name: Film #2
Country: Country#2
Years: 2024
Cast: Cast#2.1, Cast#2.2

-------------------------------------------------------------
fisher@fisher bugs %
fisher@fisher bugs %
fisher@fisher bugs % cat issue79.php
<?

error_reporting(E_ALL);
header('Content-Type: text/plain');

$template_plain     = 'issue79.tpl';
$template_include   = 'issue79-item.tpl';

$data = [
	'films' => [
		['name' => 'Film #1', 'country' => 'Country#1', 'year' => 2023, 'cast' => [['name' =>'Cast#1.1'], ['name' =>'Cast#1.2']]],
		['name' => 'Film #2', 'country' => 'Country#2', 'year' => 2024, 'cast' => [['name' =>'Cast#2.1'], ['name' =>'Cast#2.2']]],
	]
];

$T1 = new Blitz($template_plain);
$T1->display($data);

?>
fisher@fisher bugs % cat issue79.tpl
{{ BEGIN films }}
Film Name: {{ $name }}
{{ include("issue79-item.tpl", scope = $_) }}
{{ END }}
-------------------------------------------------------------
fisher@fisher bugs % cat issue79-item.tpl
Country: {{ $country }}
Years: {{ $year }}
Cast: {{ BEGIN cast }}{{ UNLESS $_first }}, {{ END }}{{ $name }}{{ END }}


Алексей, я по-русски объясню понятнее
В данном примере с фильмами я не хочу в каждый фильм готовить данные, я хочу получить их из более общих для всех фильмов данных, для чего и служит как я понимаю scope_lookup_limit > 0 (не нашли данных у фильма, посмотрели выше, если они есть выше, подставили сюда).
Оно работает если шаблон один и scope_lookup_limit > 0.
Но если взять кусочек шаблона и вынести его в include (для целей повторного использования, как в примере с avatar), то недостающие в нем переменные не ищутся выше. Я про это.
Таких применений на самом деле множестве везде, где есть отношения один к многим (тема и сообщения, место и события в нем) и где внутри множества элементов нужно иметь возможность отображать свойства родителя, не затаскивая эти свойства в каждый элемент

In this example with movies, I don’t want to prepare data for each movie, I want to get them from data that is more common for all movies, which, as I understand it, scope_lookup_limit > 0 serves (they didn’t find data for the movie, looked above if they are above , inserted here).
It works if there is only one template and scope_lookup_limit > 0.
But if you take a piece of the template and put it in include (for reuse purposes), then the missing variables in it are not looked for above. I'm talking about it.
In fact, there are many such applications wherever there is a one-to-many relationship (topic and messages, place and events in it) and where inside the set of elements you need to be able to display the properties of the parent without dragging these properties into each element

OK. There are some limitations for include statement, scope lookup didn't work as expected with arbitrary data structures. So new feature was introduced, if you literally say {{ include('issue79-item.tpl', vars = vars) }} then vars will be taken from the scope and passed into the included array as "vars":

fisher@fisher bugs % cat issue79.tpl
{{ BEGIN films }}
Film Name: {{ $name }}
{{ include('issue79-item.tpl', vars = vars) }}
{{ END }}
-------------------------------------------------------------
fisher@fisher bugs % cat issue79-item.tpl
Country: {{ vars.country }}
Year: {{ vars.year }}
Cast: {{ BEGIN cast }}{{ UNLESS $_first }}, {{ END }}{{ $name }}{{ END }}
fisher@fisher bugs % cat issue79.php
<?

error_reporting(E_ALL);
header('Content-Type: text/plain');
ini_set('blitz.scope_lookup_limit', 8);

$template_plain     = 'issue79.tpl';
$template_include   = 'issue79-item.tpl';

$data = [
	'vars' => ['country' => 'TheCountry', 'year' => 1999],
	'films' => [
		['name' => 'Film #1', 'country' => 'Country#1', 'year' => 2023, 'cast' => [['name' =>'Cast#1.1'], ['name' =>'Cast#1.2']]],
		['name' => 'Film #2', 'country' => 'Country#2', 'cast' => [['name' =>'Cast#2.1'], ['name' =>'Cast#2.2']]],
	]
];

$T1 = new Blitz($template_plain);
$T1->display($data);

?>
fisher@fisher bugs % /local/php-8.1.2/bin/php issue79.php
Film Name: Film #1
Country: TheCountry
Year: 1999
Cast: Cast#1.1, Cast#1.2

Film Name: Film #2
Country: TheCountry
Year: 1999
Cast: Cast#2.1, Cast#2.2

-------------------------------------------------------------

if you don't pass this into include then only the current scope will be visible. If yo ask me why - I don't remember, and I would agree that this is wierd, but there are no plans to fix this.

Алексей, передача внутрь include через scope какие-то задачи конечно решит, но в целом понятно. Не совсем логично работает, но я понял.
Я бы предложил если будет возможность сделать рекурсивный "включатеть" подшаблонов в зависимости от парамерта в ini. К примеру он может работать только если включение без указания области видимости и включенном параметре.
Если ввести параметр blitz.include_sub_recursive = 1 и в теле шаблона только чистый {{ include("sub.tpl") }} без scope, то рекурсивно включить все подшаблоны в основной и рендерить уже его.
Такой вот feature request

Aleksey, passing inside include through scope will certainly solve some problems, but in general it’s understandable. Doesn't make sense, but I get it.
I would suggest if it's possible to recursively "includer" sub-templates depending on the ini parameter.
For example, it can only work if the inclusion without specifying the scope and the parameter set is on.
If you add the parameter blitz.include_sub_recursive = 1 and only pure {{ include("sub.tpl") }} without scopes in the template body, then recursively include all sub-templates in the main one and render it already.
Such is the feature request