nette / latte

☕ Latte: the safest & truly intuitive templates for PHP. Engine for those who want the most secure PHP sites.
https://latte.nette.org
Other
1.14k stars 109 forks source link

Please add an early exit feature for latte templates #287

Closed BernhardBaumrock closed 1 year ago

BernhardBaumrock commented 2 years ago

As explained in this forum question I'd need an early exit feature in latte templates: https://forum.nette.org/en/35172-early-exit-in-latte-template-file

This follows the guard clause instead of if/else-nesting, see https://www.youtube.com/watch?v=EumXak7TyQ0 why this can be good for reducing code complexity and making it easier to read and maintain.

I've built a page builder for the ProcessWire CMS that looks like this:

img

These content blocks consist of a controller file and a view file. The controller file is PHP (eg Gallery.php), the view file is LATTE (eg Gallery.latte).

A simple view file could look like this:

<h1>{$block->headline}</h1>
<div n:inner-foreach="$block->images() as $img">
   ...
</div>

Now what I'd like to do is to only render the view file if certain conditions are met. In this example it would be great to only render the gallery view file if the block has at least one uploaded image.

{returnif !$block->images()->count()}
<h1>{$block->headline}</h1>
<div n:inner-foreach="$block->images() as $img">
   ...
</div>

Without this feature I need to wrap my view markup in an if condition:

{if $block->images()->count()}
  <h1>{$block->headline}</h1>
  <div n:inner-foreach="$block->images() as $img">
     ...
  </div>
{/if}

Thx for considering :)

MartinMystikJonas commented 2 years ago

Why not simply use plain if? Like this:

{if $block->images()->count()}
  <h1>{$block->headline}</h1>
  <div n:inner-foreach="$block->images() as $img">
     ...
  </div>
{/if}
dg commented 2 years ago

I would probably use skipIf rather than returnIf

BernhardBaumrock commented 2 years ago

@MartinMystikJonas good point, didn't think of that. IMHO it's ugly, so it's partly just a personal preference :) But there are often cases where early exists are a lot cleaner than if/else/and/or...

{returnIf !$block->images()->count()}
{returnIf $block->settings->foo}
{returnIf $block->settings->bar AND $block->title == 'bar'}
<h1>{$block->headline}</h1>
<div n:inner-foreach="$block->images() as $img">
   ...
</div>

Also it does not add unnecessary indentation etc. It's the same as having n:if where you could also wrap the tag in {if}...{/if} but n:if or n:tag-if producing much cleaner code!

dakur commented 2 years ago

@dg I would expect skipIf to be a pair tag that bounds what should be skipped. Maybe stopIf would be an option? Or maybe breakIf, even though it's not a loop. returnIf on the other hand best fits to the nature of a template – that it's a function.

BernhardBaumrock commented 2 years ago

@dakur that's exactly what skipIf should NOT be. As you describe it {skipIf} would just be the opposite of {if} and that's not what I want. I'm talking of a new concept that skips rendering of the whole latte file if a condition is met. So {skipIf} would not have any closing tag by design.

dakur commented 2 years ago

@BernhardBaumrock I know, I understood your proposal. I'm discussing the word to be used and its sense – how it's read and understood in the code.

Word "skip" in my opinion implies that it will be further defined (inside of the tag) what should be skipped, because skipping means that you will continue somewhere further (below), whereas "return"/"break"/"stop" clearly says that the code execution stops and doesn't continue for the rest of the file.

Example – if I see this code:

{returnIf !$block->images()->count()}
<h1>{$block->headline}</h1>
<div n:inner-foreach="$block->images() as $img">
   ...
</div>

I clearly understand that if condition is not met, whole template ends rendering. While if I see

{skipIf !$block->images()->count()}
<h1>{$block->headline}</h1>
<div n:inner-foreach="$block->images() as $img">
   ...
</div>

a question arises: "wait, but what should be skipped? where will it continue?"

At least that's my perception of it.

BernhardBaumrock commented 2 years ago

Ah, thx @dakur good point. I think that will also fit better into the principle of latte being very close to plain PHP and not a different language. +1 for returnIf :)

dg commented 2 years ago

For me it was about reusing existing tag in a new context https://latte.nette.org/en/tags#toc-continueif-skipif-breakif

dakur commented 2 years ago

In that context, skipIf seems like a solution for a particular problem – being able to continueIf while preserving the counter. But still, the loop gives a sense of the context – what is being skipped. When the context is whole template file, I think it's not so obvious. I can see more similarity to breakIf than to skipIf.

But it's just wording, the point is the functionality of course, words can be changed any later. :-)

milo commented 2 years ago

@dg What about somehow extend the ifContent macro? To be able hide parents/siblings?

dg commented 2 years ago

I see it exactly the opposite :-) The term return doesn't mean anything to me in the context of templates. What to return? On the contrary, it is common to use skip to … ehm … skipping https://phpunit.readthedocs.io/en/9.5/incomplete-and-skipped-tests.html#skipping-tests https://maven.apache.org/plugins-archives/maven-surefire-plugin-2.12.4/examples/skipping-test.html https://github.com/cypress-io/cypress-skip-test etc

@milo it is not about content, but about your conditions

dakur commented 2 years ago

In tests, you skip whole test, you don't say "skip" from the middle of the test. But it's true that Bernhard posted a use-case with skip at the beginning, not in the middle, so I give up. 🙂

dg commented 2 years ago

I understood it from the beginning as early exit, i.e. ending in header.

Maybe we could also use endIf.

BernhardBaumrock commented 2 years ago

For me none of skipIf, breakIf or endIf feels right. skipIf and breakIf do belong to loops. And endIf belongs to a preceeding {if}.

If you don't like returnIf, what about exitIf?

{exitIf !$block->images()->count()}
{exitIf $block->settings->foo}
{exitIf $block->settings->bar AND $block->title == 'bar'}
<h1>{$block->headline}</h1>
<div n:inner-foreach="$block->images() as $img">
   ...
</div>
dg commented 2 years ago

I'm gonna try to do a poll on this. https://twitter.com/nettefw/status/1517165228898848769

MartinMystikJonas commented 2 years ago

exitIf - similar to exit() skipIf - already used in different context endIf - can be confused with {/if} returnIf - IMHO best but not exactly right either

What about terminateIf?

pavelmlejnek commented 2 years ago

Few quick thoughts:

abortIf breakIf cancelIf discontinueIf haltIf endIf

Dne čt 21. dub 2022 18:19 uživatel Martin Mystik Jonáš < @.***> napsal:

exitIf - similar to exit() skipIf - already used in different context endIf - can be confused with {/if} returnIf - IMHO best but not exactly right either

What about terminateIf?

— Reply to this email directly, view it on GitHub https://github.com/nette/latte/issues/287#issuecomment-1105434831, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABROI46VIQKIK5VDFY77F3VGF5YTANCNFSM5T4DGF7Q . You are receiving this because you are subscribed to this thread.Message ID: @.***>

vojtech-dobes commented 2 years ago

finishIf

dg commented 2 years ago

The goal was to reach a consensus :-)

milo commented 2 years ago

What about the opposite meaning?

{renderIf $block->images()->count()}

<h1>{$block->headline}</h1>
<div n:inner-foreach="$block->images() as $img">
   ...
</div>
loilo commented 2 years ago

Just to throw in more wrenches, I'll suggest stopIf. 😁

But seriously, I don't really care about the name. I just wanted to chime in suggesting to avoid tags containing return or exit since both are loaded with a different meaning in PHP.

dg commented 2 years ago

The quitIf and leaveIf hasn't come up yet :)

BernhardBaumrock commented 2 years ago

Ok I didn't know back in April, but now I know that my suggested priciple does actually have a name and is called "guard clause". A guard clause is here to reduce complexity and make code more readable, which is exactly why I proposed the feature. Read here: https://deviq.com/design-patterns/guard-clause or see here: https://www.youtube.com/watch?v=EumXak7TyQ0

badpenguin commented 2 years ago

Well i've always used just php return since 2.4 but now with 2.* it force me to use "return []"

BernhardBaumrock commented 1 year ago

any news on exitIf @dg ? With some time passed by I think exit is really the best wording :)

BernhardBaumrock commented 1 year ago

@badpenguin what do you mean? I don't understand. On my end there seems to be no easy way out of a rendered latte file once rendering has started...

badpenguin commented 1 year ago

I just do <?php return; ?>

BernhardBaumrock commented 1 year ago

@badpenguin we are talking about LATTE files, not PHP files ;) That's exactly what I'm requesting. Make a feature that is extremely easy to do in PHP also available to LATTE.

BernhardBaumrock commented 1 year ago

@dg related to this discussion there's also another request/question:

In the PW forum someone came up with a hacky "includeIF" solution:

{include $foo == 'bar' ? "test.latte" : "blank.latte"}

The problem is that you need to define a second template otherwise latte throws an error.

It would be nice to have something like this:

{include $foo == 'bar' ? 'somefile.latte'}
dg commented 1 year ago

@BernhardBaumrock I find this more understandable:

{if $foo == 'bar'}{include 'somefile.latte'}{/if}
BernhardBaumrock commented 1 year ago

Hey @dg unfortunately I'm not able to use this new feature :( Any idea what I could be doing wrong?

image

I tested with {exitIf $page->template != 'releasepage'} initially but same error.

dg commented 1 year ago

fixed

BernhardBaumrock commented 1 year ago

Great, thx @dg !