<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
	<title>phpFashion</title>
	<link>https://phpfashion.com/en/</link>
	<language>cs</language>
	<category>blog</category>
	<docs>http://backend.userland.com/rss</docs>
	<image>
		<url>/assets/img/logo.gif?v=1539821622</url>
		<title>phpFashion</title>
		<link>https://phpfashion.com/en/</link>
	</image>
	<atom:link rel="self" href="https://phpfashion.com/en/feed/rss" type="application/rss+xml" />

	<item>
		<title>Duck Typing Is Dead, Dave!</title>
		<link>https://phpfashion.com/en/duck-typing-is-dead</link>
		<description>There are moments when a truth in technology reveals itself so
clearly that it&apos;s impossible to ignore. When the whole world independently
arrives at the same conclusion. When the numbers say out loud what you&apos;ve felt
in your code for years.

Duck typing is dead. Types have won. In all languages, in all communities, on
all fronts. And I&apos;m thrilled about it, because this path – the path of strong
types – was always the right one.



The Prophecy

Monkey patching, duck typing, rejecting dependency injection – all of it
grows from the same root. From the conviction that explicit structure is
unnecessary. That code doesn&apos;t need to say what it expects and what it returns.
That freedom matters more than predictability. And that whoever wants types, DI,
and clear contracts simply doesn&apos;t understand the beauty of dynamic
languages.

In 2007, I wrote an article Ruby on Rails? No,
Thanks, where I criticized monkey patching – the Ruby habit of
arbitrarily redefining existing classes at runtime. The community explained to
me that I was rigid and didn&apos;t understand modern programming.

In 2012 came the article Will
Rails Discover Dependency Injection?, where I described how Rails rejected
dependency injection with the argument that “Ruby is such a good language that
it doesn&apos;t need DI at all.” The Post model served as both entity
and repository, dependencies hid behind static calls, and the whole system held
together only through conventions and hope. I predicted that Rails would one
day discover DI and it would be a big deal.

It&apos;s 2026.

They still haven&apos;t ☹

JavaScript Is Dead

Look at the results of the State of JavaScript
2025 survey. 40 percent of developers write exclusively in TypeScript. And
how many write exclusively in plain JavaScript, without any types
whatsoever?

Six percent.

That&apos;s fewer than people who believe the Earth is flat. In 2025, TypeScript
became the most-used
language on GitHub, overtaking Python and JavaScript itself. In a single
year, it gained a million new contributors – a 66% increase.

What&apos;s more: TC39 itself, the committee responsible for
JavaScript&apos;s evolution, is working on a proposal to add
type annotations directly to the language. You know why? Because “static
typing” was the most-requested missing feature in the State of JS survey five
years running. Every year. 2020, 2021, 2022, 2023, 2024. Developers wanted
types so desperately that it imprinted itself into the very specification of the
language.

JavaScript without types is a dead language. It just doesn&apos;t know
it yet.

The Duck Showed Its True
Colors

And Python? Python, the sacred land of duck typing? The homeland where
generations of programmers learned that “if it walks like a duck and quacks
like a duck, then it&apos;s a duck and you don&apos;t need to know anything more”?

86%
of Python developers now regularly use type annotations. Guido van Rossum,
Python&apos;s own papa – the very person who for decades put no types in the
language – declared type annotations a crucial benefit for large project
productivity. And then went to work on mypy, a tool for static type
checking.

FastAPI, the most popular Python web framework, is literally built on
types – it won&apos;t even start without type annotations. Pydantic, the data
validation library that half the AI ecosystem runs on, is existentially
dependent on types.

The duck got type annotations fitted.

And Ruby? Ruby&apos;s type ecosystem is split into two incompatible systems –
Sorbet from Stripe and the official RBS. Neither has achieved mass adoption.
DHH, the creator of Ruby on Rails, stands on the barricade with a banner reading
“No to Types!”, rejecting them, rejecting DI, rejecting essentially
everything that software engineering has considered best practice for the last
twenty years. At RubyKaigi 2025, both camps finally tried to find common ground
and bridge Sorbet with RBS. Whether it leads anywhere remains to be seen.

All dynamic languages headed the same direction. Toward types. Some faster,
some slower. But all of them.

We Were Doing This from the
Start

PHP has had runtime types since 2004, when version 5 was released. But the
road to full types wasn&apos;t straightforward. In 2015, there was a vote on scalar
types for PHP 7, and they nearly didn&apos;t pass. The vote ended 108 to 48 –
just barely above the required two-thirds majority. Four votes the other way and
PHP wouldn&apos;t have scalar types. It was called “The Great Scalar Type War”
and it was one of the most dramatic votes in PHP&apos;s history. Fortunately, reason
prevailed.

In Nette, we blazed this trail long before it was trendy. Nette Framework had
full dependency injection since version 2.0 in 2012. Not as an optional
add-on, but as the foundation of the entire architecture. At a time when DHH was
declaring DI an unnecessary Java pattern, we built a complete ecosystem around
DI and wrote
documentation that to this day remains one of the clearest explanations of
dependency injection in any language. And we educated the entire Czech PHP
community on it.

Nette 3.0, released in April 2019, became the first full-stack PHP framework
in the world with a complete type system. Every file
declare(strict_types=1). Every parameter typed. Every return value.
Every property.

Symfony 5.0 added full types seven months later. Laravel?

Laravel doesn&apos;t have strict types even in 2026. And often lacks even
native types.

And then there&apos;s PHPStan by Ondřej Mirtes – a Czech tool with
344 million downloads today that makes type checking in PHP stricter than in
many a statically typed language. In the latest version, PHPStan 2.0, level
10 was added, which detects even implicitly typed mixed values.

Why AI Killed It

You know what definitively buried duck typing? Not programmers. Not
frameworks. Artificial intelligence.

A 2025 study
found that 94% of compilation errors in code generated by language models are
type-checking errors. Ninety-four percent. The type system is a safety net for
code you didn&apos;t write yourself.

When I sit down with Claude Code today and work on PHP code with full types
and PHPStan, the AI agent understands my code. It knows what functions accept.
It knows what they return. It knows what properties objects have. It knows
what&apos;s allowed and what isn&apos;t. In duck-typed code, it would be
flying blind.

Duck typing was always a dumpster fire. But at least a dumpster fire that
somehow worked at small scale. In an era where AI agents generate hundreds of
lines of code daily, it&apos;s a dumpster fire that doesn&apos;t work at all.



So let me sum up: TypeScript buried vanilla JavaScript. Python added types
and 86% of developers use them. Ruby is floundering. And PHP? PHP has full
runtime types, PHPStan with 344 million downloads, and Nette, which was doing
this before it was cool.

Normally I&apos;d end with a self-deprecating paragraph about how nobody likes the
guy who says “I told you so.” But you know what?

I told you so.
</description>
		<content:encoded><![CDATA[
		<p class="perex">There are moments when a truth in technology reveals itself so
clearly that it's impossible to ignore. When the whole world independently
arrives at the same conclusion. When the numbers say out loud what you've felt
in your code for years.</p>

<p>Duck typing is dead. Types have won. In all languages, in all communities, on
all fronts. And I'm thrilled about it, because this path – the path of strong
types – was always the right one.</p>

<figure><img loading="lazy" src="/media/1b209a2782.webp" alt="" width="1400"
height="933"></figure>

<h2 id="toc-the-prophecy">The Prophecy</h2>

<p>Monkey patching, duck typing, rejecting dependency injection – all of it
grows from the same root. From the conviction that explicit structure is
unnecessary. That code doesn't need to say what it expects and what it returns.
That freedom matters more than predictability. And that whoever wants types, DI,
and clear contracts simply doesn't understand the beauty of dynamic
languages.</p>

<p>In 2007, I wrote an article <a
href="https://phpfashion.com/en/ruby-on-rails-no-thanks">Ruby on Rails? No,
Thanks</a>, where I criticized monkey patching – the Ruby habit of
arbitrarily redefining existing classes at runtime. The community explained to
me that I was rigid and didn't understand modern programming.</p>

<p>In 2012 came the article <a
href="https://phpfashion.com/en/will-rails-discover-dependency-injection">Will
Rails Discover Dependency Injection?</a>, where I described how Rails rejected
dependency injection with the argument that “Ruby is such a good language that
it doesn't need DI at all.” The <code>Post</code> model served as both entity
and repository, dependencies hid behind static calls, and the whole system held
together only through conventions and hope. I predicted that Rails would one
day discover DI and it would be a big deal.</p>

<p>It's 2026.</p>

<p>They still haven't ☹</p>

<h2 id="toc-javascript-is-dead">JavaScript Is Dead</h2>

<p>Look at the results of the <a
href="https://2025.stateofjs.com/en-US/usage/">State of JavaScript
2025</a> survey. 40 percent of developers write exclusively in TypeScript. And
how many write exclusively in plain JavaScript, without any types
whatsoever?</p>

<p>Six percent.</p>

<p>That's fewer than people who believe the Earth is flat. In 2025, TypeScript
became the <a
href="https://github.blog/ai-and-ml/llms/why-ai-is-pushing-developers-toward-typed-languages/">most-used
language on GitHub</a>, overtaking Python and JavaScript itself. In a single
year, it gained a million new contributors – a 66% increase.</p>

<p>What's more: TC39 itself, the committee responsible for
JavaScript's evolution, is working on a <a
href="https://github.com/tc39/proposal-type-annotations">proposal</a> to add
type annotations directly to the language. You know why? Because “static
typing” was the most-requested missing feature in the State of JS survey five
years running. Every year. 2020, 2021, 2022, 2023, 2024. Developers wanted
types so desperately that it imprinted itself into the very specification of the
language.</p>

<p>JavaScript without types is a dead language. It just doesn't know
it yet.</p>

<h2 id="toc-the-duck-showed-its-true-colors">The Duck Showed Its True
Colors</h2>

<p>And Python? Python, the sacred land of duck typing? The homeland where
generations of programmers learned that “if it walks like a duck and quacks
like a duck, then it's a duck and you don't need to know anything more”?</p>

<p><a
href="https://engineering.fb.com/2025/12/22/developer-tools/python-typing-survey-2025-code-quality-flexibility-typing-adoption/">86%
of Python developers</a> now regularly use type annotations. Guido van Rossum,
Python's own papa – the very person who for decades put no types in the
language – declared type annotations a crucial benefit for large project
productivity. And then went to work on mypy, a tool for static type
checking.</p>

<p>FastAPI, the most popular Python web framework, is literally <i>built</i> on
types – it won't even start without type annotations. Pydantic, the data
validation library that half the AI ecosystem runs on, is existentially
dependent on types.</p>

<p>The duck got type annotations fitted.</p>

<p>And Ruby? Ruby's type ecosystem is split into two incompatible systems –
Sorbet from Stripe and the official RBS. Neither has achieved mass adoption.
DHH, the creator of Ruby on Rails, stands on the barricade with a banner reading
“No to Types!”, rejecting them, rejecting DI, rejecting essentially
everything that software engineering has considered best practice for the last
twenty years. At RubyKaigi 2025, both camps finally tried to find common ground
and bridge Sorbet with RBS. Whether it leads anywhere remains to be seen.</p>

<p>All dynamic languages headed the same direction. Toward types. Some faster,
some slower. But all of them.</p>

<h2 id="toc-we-were-doing-this-from-the-start">We Were Doing This from the
Start</h2>

<p>PHP has had runtime types since 2004, when version 5 was released. But the
road to full types wasn't straightforward. In 2015, there was a vote on scalar
types for PHP 7, and they nearly didn't pass. The vote ended 108 to 48 –
just barely above the required two-thirds majority. Four votes the other way and
PHP wouldn't have scalar types. It was called “The Great Scalar Type War”
and it was one of the most dramatic votes in PHP's history. Fortunately, reason
prevailed.</p>

<p>In Nette, we blazed this trail long before it was trendy. Nette Framework had
full dependency injection since version 2.0 in 2012. Not as an optional
add-on, but as the foundation of the entire architecture. At a time when DHH was
declaring DI an unnecessary Java pattern, we built a complete ecosystem around
DI and <a
href="https://doc.nette.org/en/dependency-injection/introduction">wrote
documentation</a> that to this day remains one of the clearest explanations of
dependency injection in any language. And we educated the entire Czech PHP
community on it.</p>

<p>Nette 3.0, released in April 2019, became the first full-stack PHP framework
in the world with a complete type system. Every file
<code>declare(strict_types=1)</code>. Every parameter typed. Every return value.
Every property.</p>

<p>Symfony 5.0 added full types seven months later. Laravel?</p>

<p>Laravel doesn't have strict types even in 2026. And often lacks even
native types.</p>

<p>And then there's PHPStan by Ondřej Mirtes – a Czech tool with
344 million downloads today that makes type checking in PHP stricter than in
many a statically typed language. In the latest version, PHPStan 2.0, level
10 was added, which detects even implicitly typed mixed values.</p>

<h2 id="toc-why-ai-killed-it">Why AI Killed It</h2>

<p>You know what definitively buried duck typing? Not programmers. Not
frameworks. Artificial intelligence.</p>

<p>A <a
href="https://github.blog/ai-and-ml/llms/why-ai-is-pushing-developers-toward-typed-languages/">2025 study</a>
found that 94% of compilation errors in code generated by language models are
type-checking errors. Ninety-four percent. The type system is a safety net for
code you didn't write yourself.</p>

<p>When I sit down with Claude Code today and work on PHP code with full types
and PHPStan, the AI agent understands my code. It knows what functions accept.
It knows what they return. It knows what properties objects have. It knows
what's allowed and what isn't. In duck-typed code, it would be
flying blind.</p>

<p>Duck typing was always a dumpster fire. But at least a dumpster fire that
somehow worked at small scale. In an era where AI agents generate hundreds of
lines of code daily, it's a dumpster fire that doesn't work at all.</p>

<hr>

<p>So let me sum up: TypeScript buried vanilla JavaScript. Python added types
and 86% of developers use them. Ruby is floundering. And PHP? PHP has full
runtime types, PHPStan with 344 million downloads, and Nette, which was doing
this before it was cool.</p>

<p>Normally I'd end with a self-deprecating paragraph about how nobody likes the
guy who says “I told you so.” But you know what?</p>

<p>I told you so.</p>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/duck-typing-is-dead#comments</comments>
		<pubDate>Mon, 06 Apr 2026 23:58:00 +0200</pubDate>
		<guid isPermaLink="false">item2492@http://phpfashion.com</guid>
		<enclosure url="https://phpfashion.com/media/1b209a2782.webp" length="83832" type="image/webp" />
	</item>
	<item>
		<title>How to Make Your Code Worse by Fixing PHPStan Errors</title>
		<link>https://phpfashion.com/en/phpstan-fix-making-things-worse</link>
		<description>PHPStan reports an error, you fix it. But paradoxically, that
fix actually makes your code worse. How is that possible?



I remember going through PHPStan output once, systematically fixing one
warning after another. I felt productive. Code is cleaner, types are correct,
green everywhere. A month later I was scratching my head over why a function
was returning an empty string when it shouldn&apos;t. An hour-long detective story,
yet the culprit was obvious – me and my “fix.”

One thing first, though: PHPStan does
exactly what it should. It warns you that a function may return
null or false, and forces you to think about it.
That&apos;s great. The problem is your reaction.

An Innocent Example

Say we have a function that removes extra whitespace from text:

function normalizeSpaces(string $s): string
{
	return preg_replace(&apos;#\s+#&apos;, &apos; &apos;, $s);
}


PHPStan reports: Function preg_replace returns string|null but function
should return string. Sure enough, preg_replace can return
null if a regex error occurs. So let&apos;s fix it, right?

function normalizeSpaces(string $s): string
{
	return (string) preg_replace(&apos;#\s+#&apos;, &apos; &apos;, $s);
}


PHPStan is happy. Commit, push, done.

Except.

What You Actually Did

The original code was actually better. If preg_replace
ever returned null – say, because of… (no, nothing comes
to mind) – PHP would throw a TypeError. A fatal error. Tracy would
light up, it would show up in the log, you&apos;d simply know about it. (Oh
right – that&apos;s why it can return null!)

After your “fix”, null silently gets cast to an empty
string. The function returns &quot;&quot;, the application keeps running, and
you have no idea something went wrong. Data gets corrupted without any
warning.

Congratulations, you just made your code worse 🙂

And preg_replace isn&apos;t an isolated case. Plenty of PHP functions
return false or null for situations that practically
never occur in normal usage – json_encode,
ob_get_contents, getcwd, gzcompress, a
whole range of Intl functions. Every time you reach for a type cast, stop and
ask yourself: am I throwing away information about an (unlikely) error?

The Right Approach

If you want to satisfy PHPStan while preserving the original behavior, use a
throw expression:

function normalizeSpaces(string $s): string
{
	return preg_replace(&apos;#\s+#&apos;, &apos; &apos;, $s)
		?? throw new \LogicException(&apos;preg_replace failed&apos;);
}


This says: “I know an error can theoretically occur. If it does, I want
to know.” You&apos;ve essentially written out explicitly what was there implicitly
before – a fatal on failure. PHPStan happy, code unharmed.

But hold on. There&apos;s an even simpler way. You can just ignore these trivial
errors in PHPStan – add them to ignoreErrors in
phpstan.neon or use the @phpstan-ignore annotation.
And that&apos;s perfectly legitimate. After all, the original behavior where PHP
throws a TypeError is basically what you want. Why change the code
for that?

Even better is not having the problem at all. That&apos;s why wrappers exist –
for instance, Nette\Utils\Strings::replace() wraps
preg_replace and throws an exception on error. Similarly
Nette\Utils\Json::encode() instead of json_encode. Use
one function and the problem disappears – no null, no
false, nothing to deal with.

Another option is to solve it at the PHPStan level with an extension that
removes false or null from return types of selected
functions. For example, nette/phpstan-rules does this
for dozens
of PHP functions. For regex functions, it even checks whether the pattern is
a constant string – if so, it strips null from the type, since a
regex error can&apos;t occur.

This is an opinionated approach, of course. And that&apos;s exactly what I like
about PHPStan – it&apos;s strict, and I can customize it with extensions to suit
my needs.



A silent error is worse than a loud one. And (string) is the
quietest way to produce it.
</description>
		<content:encoded><![CDATA[
		<p class="perex">PHPStan reports an error, you fix it. But paradoxically, that
fix actually makes your code worse. How is that possible?</p>

<figure><img loading="lazy" src="/media/f782a4e49e.webp" alt="" width="1400"
height="933"></figure>

<p>I remember going through PHPStan output once, systematically fixing one
warning after another. I felt productive. Code is cleaner, types are correct,
green everywhere. A month later I was scratching my head over why a function
was returning an empty string when it shouldn't. An hour-long detective story,
yet the culprit was obvious – me and my “fix.”</p>

<p>One thing first, though: <a href="https://phpstan.org">PHPStan</a> does
exactly what it should. It warns you that a function may return
<code>null</code> or <code>false</code>, and forces you to think about it.
That's great. The problem is your reaction.</p>

<h2 id="toc-an-innocent-example">An Innocent Example</h2>

<p>Say we have a function that removes extra whitespace from text:</p>

<pre
class="language-php"><code>function normalizeSpaces(string $s): string
{
	return preg_replace(&#039;#\s+#&#039;, &#039; &#039;, $s);
}
</code></pre>

<p>PHPStan reports: <em>Function preg_replace returns string|null but function
should return string.</em> Sure enough, <code>preg_replace</code> can return
<code>null</code> if a regex error occurs. So let's fix it, right?</p>

<pre
class="language-php"><code>function normalizeSpaces(string $s): string
{
	return (string) preg_replace(&#039;#\s+#&#039;, &#039; &#039;, $s);
}
</code></pre>

<p>PHPStan is happy. Commit, push, done.</p>

<p>Except.</p>

<h2 id="toc-what-you-actually-did">What You Actually Did</h2>

<p>The original code was <b>actually better</b>. If <code>preg_replace</code>
ever returned <code>null</code> – say, because of… <em>(no, nothing comes
to mind)</em> – PHP would throw a TypeError. A fatal error. Tracy would
light up, it would show up in the log, you'd simply know about it. <em>(Oh
right – <b>that's</b> why it can return null!)</em></p>

<p>After your “fix”, <code>null</code> silently gets cast to an empty
string. The function returns <code>""</code>, the application keeps running, and
you have no idea something went wrong. Data gets corrupted without any
warning.</p>

<p>Congratulations, you just made your code worse 🙂</p>

<p>And <code>preg_replace</code> isn't an isolated case. Plenty of PHP functions
return <code>false</code> or <code>null</code> for situations that practically
never occur in normal usage – <code>json_encode</code>,
<code>ob_get_contents</code>, <code>getcwd</code>, <code>gzcompress</code>, a
whole range of Intl functions. Every time you reach for a type cast, stop and
ask yourself: am I throwing away information about an (unlikely) error?</p>

<h2 id="toc-the-right-approach">The Right Approach</h2>

<p>If you want to satisfy PHPStan while preserving the original behavior, use a
<code>throw</code> expression:</p>

<pre
class="language-php"><code>function normalizeSpaces(string $s): string
{
	return preg_replace(&#039;#\s+#&#039;, &#039; &#039;, $s)
		?? throw new \LogicException(&#039;preg_replace failed&#039;);
}
</code></pre>

<p>This says: “I know an error can theoretically occur. If it does, I want
to know.” You've essentially written out explicitly what was there implicitly
before – a fatal on failure. PHPStan happy, code unharmed.</p>

<p>But hold on. There's an even simpler way. You can just ignore these trivial
errors in PHPStan – add them to <code>ignoreErrors</code> in
<code>phpstan.neon</code> or use the <code>@phpstan-ignore</code> annotation.
And that's perfectly legitimate. After all, the original behavior where PHP
throws a TypeError is basically what you want. Why change the code
for that?</p>

<p>Even better is not having the problem at all. That's why wrappers exist –
for instance, <code>Nette\Utils\Strings::replace()</code> wraps
<code>preg_replace</code> and throws an exception on error. Similarly
<code>Nette\Utils\Json::encode()</code> instead of <code>json_encode</code>. Use
one function and the problem disappears – no <code>null</code>, no
<code>false</code>, nothing to deal with.</p>

<p>Another option is to solve it at the PHPStan level with an extension that
removes <code>false</code> or <code>null</code> from return types of selected
functions. For example, <a
href="https://github.com/nette/phpstan-rules">nette/phpstan-rules</a> does this
for <a
href="https://github.com/nette/phpstan-rules/blob/master/extension-php.neon">dozens
of PHP functions</a>. For regex functions, it even checks whether the pattern is
a constant string – if so, it strips <code>null</code> from the type, since a
regex error <i>can't</i> occur.</p>

<p>This is an opinionated approach, of course. And that's exactly what I like
about PHPStan – it's strict, and I can customize it with extensions to suit
my needs.</p>

<hr>

<p>A silent error is worse than a loud one. And <code>(string)</code> is the
quietest way to produce it.</p>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/phpstan-fix-making-things-worse#comments</comments>
		<pubDate>Sat, 21 Feb 2026 15:25:00 +0100</pubDate>
		<guid isPermaLink="false">item2445@http://phpfashion.com</guid>
		<enclosure url="https://phpfashion.com/media/b5572c439e.webp" length="38352" type="image/webp" />
	</item>
	<item>
		<title>What Claude Code Can Do</title>
		<link>https://phpfashion.com/en/what-claude-code-can-do</link>
		<description>
	The other day I told Claude: “Turn off wifi.” And it did – because it
	knows how to do that from the command line. Then I said: “Turn on wifi.”
	And it did nothing. Because it had no internet.


This experience nicely illustrates what Claude Code actually is. It&apos;s not
just a chatbot you talk to. It&apos;s a tool that has access to your computer and
can perform actions on it. It can read and edit files, run programs, install
software, work with data. Basically anything you&apos;d normally do manually.

In the previous
article we showed how to install Claude Code. Now let&apos;s look at what you
can use it for. And trust me – there&apos;s a lot more than you&apos;d expect.



Programming

Let&apos;s start with the most expected use case. Claude Code is primarily a tool
for programmers, and it truly excels in this area.

Writing new code. Describe what function you need, and Claude will
write it. You don&apos;t need to know the syntax, you don&apos;t need to google – just
say what the code should do, and it creates it. A team of data engineers at
Anthropic had Claude write a complete React application – 5,000 lines of
TypeScript – even though they didn&apos;t know React themselves.

Bug fixes. Throw an error message at it and let it find the cause and
fix it. Claude will go through your code, understand the context, and suggest a
fix. You don&apos;t have to spend hours figuring out where the problem is.

Refactoring. Got old messy code? Claude will clean it up, split long
functions into smaller ones, rename variables to meaningful names. One developer
described how he refactored a 210-line Python function down to less than
30 lines.

Tests. Claude will write unit tests for your code. It can even work
with test-driven development – first writing tests, then code that
passes them.

Documentation. It documents functions, creates README files, describes
APIs. Everything programmers hate doing manually.

Code review. It goes through code and suggests improvements –
better names, more efficient algorithms, potential bugs.

Working with files

This is where Claude Code becomes interesting even for non-programmers. It
has access to your files and can work with them.

Organization. Got a chaotic Downloads folder full of randomly named
files? Claude will sort it out. It&apos;ll create subfolders by type, date,
project – whatever you need.

Bulk renaming. Need to rename hundreds of files according to a certain
pattern? For example, renaming invoices to “2024–01–15 Vendor –
Invoice.pdf”? Claude handles it in seconds.

Format conversion. Converts data between formats – CSV to JSON, XML
to a table, whatever you need.

Writing and working with text

Although it&apos;s called “Code,” Claude Code is surprisingly good at helping
with text.

Articles and blogs. Helps structure thoughts, write first drafts, edit
and improve. One user called it the best writing support system they&apos;d
ever tried.

Organizing notes. Got chaotic notes scattered across different files?
Claude will go through them and organize them into a coherent whole. One user
uploaded voice notes from walks with a stroller – Claude transcribed them,
organized them into research topics, and eventually wrote an article in
her style.

Translations. Translates text while preserving formatting. Can even
batch translate all text files in a project.

Proofreading. Fixes spelling, grammar, style. Suggests better
phrasing.

Data analysis

Unlike web Claude, which has file size limits, Claude Code can process huge
datasets. And most importantly – it can write its own code for analysis and
run it immediately.

Reports on demand. You tell it: “Look at this sales data and create
a report with charts and trend analysis.” And it does it. Loads the data,
analyzes it, creates visualizations, and writes a summary.

Finding patterns. Claude finds anomalies and trends that you&apos;d spend
hours looking for manually. Which product has declining sales? Where are the
seasonal fluctuations?

Data cleaning. Got a list of addresses full of typos and duplicates?
Claude will clean it up, standardize the format, and remove errors.

One user described how they simply uploaded a CSV file with customer data and
said: “Imagine you&apos;re a data consultant. Perform an analysis and suggest
recommendations.” Claude went through the data and delivered a detailed report
with findings.

Automation

This is where Claude Code truly shines. It can create automated processes
that save you hours of work.

Repetitive tasks. Got something you do the same way every day or week?
Claude will automate it. Downloading data, generating reports, sending
notifications.

Connecting services. Claude can connect different tools together.
Anthropic&apos;s marketing team used this approach to create a system that
automatically generates hundreds of ad text variations from a CSV file with ad
performance data.

Monitoring and notifications. You can have Claude monitor logs or data
and alert you when something breaks or an interesting pattern appears.

System administration

Claude Code can function as your personal IT support.

Diagnostics. “Why is my computer slow?” – and Claude checks CPU
usage, memory, disk, running processes, Docker containers, everything possible.
And suggests solutions.

Configuration. Need to set something up in your system but don&apos;t know
how? Describe what you want, and Claude will configure it.

Installation. Claude installs software, configures the environment,
sets up dependencies.

One user described how Claude connected via SSH to a remote server, modified
the configuration of a monitoring tool, and adapted when some commands
failed.

For more advanced users, Claude Code also helps with infrastructure
management. It sets up automatic builds and deploys, configures server
monitoring, helps with Docker containers. Even a non-technical person can set up
a basic CI/CD pipeline with Claude&apos;s help.

But beware – it&apos;s still AI

Before you get carried away with enthusiasm, there&apos;s one important thing to
say: Claude Code makes mistakes. Like any AI. Sometimes small ones, sometimes…
well, sometimes monumental ones.

One programmer told me the following story. He gave Claude the task of
modernizing an outdated project. Claude ran for an hour. Rewrote everything.
Legacy code from 2015? Gone. Outdated patterns? Replaced with elegant modern
solutions. Confusing architecture? Now crystal clear. The result looked like it
was from a textbook – TypeScript, tests, documentation, everything.

It was absolutely top-notch code.

Which didn&apos;t work at all.

That&apos;s why this holds true: the better programmer you are and the more you
understand your field, the better results you&apos;ll achieve with Claude Code. Not
because you&apos;d have to write the code yourself – but because you&apos;ll recognize
when Claude makes a mistake. And it will. The question isn&apos;t if, but when.

Think of Claude as a junior with unlimited energy and encyclopedic knowledge
who still needs supervision. Let it work, but check the results. Test. Verify it
actually works before you push it to production.

What does this mean?

Claude Code isn&apos;t just a tool for programmers. It&apos;s an “everything
agent” – a universal assistant that has access to your computer and can
perform almost any task.

The key to success is simple: describe what you want in plain language. You
don&apos;t need to know commands, you don&apos;t need to understand technicalities. Just
say what you want to achieve, and Claude will find a way to do it.

Users report 40–80% time savings on routine tasks. Some startups have built
entire products with practically no traditional programming. All because you
have an intelligent helper at your disposal who can control your computer.

Just watch out for that wifi.
</description>
		<content:encoded><![CDATA[
		<blockquote>
	<p>The other day I told Claude: “Turn off wifi.” And it did – because it
	knows how to do that from the command line. Then I said: “Turn on wifi.”
	And it did nothing. Because it had no internet.</p>
</blockquote>

<p>This experience nicely illustrates what Claude Code actually is. It's not
just a chatbot you talk to. It's a tool that has access to your computer and
can perform actions on it. It can read and edit files, run programs, install
software, work with data. Basically anything you'd normally do manually.</p>

<p>In the <a href="https://phpfashion.com/en/how-to-install-claude-code-beginners-guide">previous
article</a> we showed how to install Claude Code. Now let's look at what you
can use it for. And trust me – there's a lot more than you'd expect.</p>

<figure><img loading="lazy" src="/media/f08eecd752.webp" alt="" width="1400"
height="933"></figure>

<h2 id="toc-programming">Programming</h2>

<p>Let's start with the most expected use case. Claude Code is primarily a tool
for programmers, and it truly excels in this area.</p>

<p><b>Writing new code.</b> Describe what function you need, and Claude will
write it. You don't need to know the syntax, you don't need to google – just
say what the code should do, and it creates it. A team of data engineers at
Anthropic had Claude write a complete React application – 5,000 lines of
TypeScript – even though they didn't know React themselves.</p>

<p><b>Bug fixes.</b> Throw an error message at it and let it find the cause and
fix it. Claude will go through your code, understand the context, and suggest a
fix. You don't have to spend hours figuring out where the problem is.</p>

<p><b>Refactoring.</b> Got old messy code? Claude will clean it up, split long
functions into smaller ones, rename variables to meaningful names. One developer
described how he refactored a 210-line Python function down to less than
30 lines.</p>

<p><b>Tests.</b> Claude will write unit tests for your code. It can even work
with test-driven development – first writing tests, then code that
passes them.</p>

<p><b>Documentation.</b> It documents functions, creates README files, describes
APIs. Everything programmers hate doing manually.</p>

<p><b>Code review.</b> It goes through code and suggests improvements –
better names, more efficient algorithms, potential bugs.</p>

<h2 id="toc-working-with-files">Working with files</h2>

<p>This is where Claude Code becomes interesting even for non-programmers. It
has access to your files and can work with them.</p>

<p><b>Organization.</b> Got a chaotic Downloads folder full of randomly named
files? Claude will sort it out. It'll create subfolders by type, date,
project – whatever you need.</p>

<p><b>Bulk renaming.</b> Need to rename hundreds of files according to a certain
pattern? For example, renaming invoices to “2024–01–15 Vendor –
Invoice.pdf”? Claude handles it in seconds.</p>

<p><b>Format conversion.</b> Converts data between formats – CSV to JSON, XML
to a table, whatever you need.</p>

<h2 id="toc-writing-and-working-with-text">Writing and working with text</h2>

<p>Although it's called “Code,” Claude Code is surprisingly good at helping
with text.</p>

<p><b>Articles and blogs.</b> Helps structure thoughts, write first drafts, edit
and improve. One user called it the best writing support system they'd
ever tried.</p>

<p><b>Organizing notes.</b> Got chaotic notes scattered across different files?
Claude will go through them and organize them into a coherent whole. One user
uploaded voice notes from walks with a stroller – Claude transcribed them,
organized them into research topics, and eventually wrote an article in
her style.</p>

<p><b>Translations.</b> Translates text while preserving formatting. Can even
batch translate all text files in a project.</p>

<p><b>Proofreading.</b> Fixes spelling, grammar, style. Suggests better
phrasing.</p>

<h2 id="toc-data-analysis">Data analysis</h2>

<p>Unlike web Claude, which has file size limits, Claude Code can process huge
datasets. And most importantly – it can write its own code for analysis and
run it immediately.</p>

<p><b>Reports on demand.</b> You tell it: “Look at this sales data and create
a report with charts and trend analysis.” And it does it. Loads the data,
analyzes it, creates visualizations, and writes a summary.</p>

<p><b>Finding patterns.</b> Claude finds anomalies and trends that you'd spend
hours looking for manually. Which product has declining sales? Where are the
seasonal fluctuations?</p>

<p><b>Data cleaning.</b> Got a list of addresses full of typos and duplicates?
Claude will clean it up, standardize the format, and remove errors.</p>

<p>One user described how they simply uploaded a CSV file with customer data and
said: “Imagine you're a data consultant. Perform an analysis and suggest
recommendations.” Claude went through the data and delivered a detailed report
with findings.</p>

<h2 id="toc-automation">Automation</h2>

<p>This is where Claude Code truly shines. It can create automated processes
that save you hours of work.</p>

<p><b>Repetitive tasks.</b> Got something you do the same way every day or week?
Claude will automate it. Downloading data, generating reports, sending
notifications.</p>

<p><b>Connecting services.</b> Claude can connect different tools together.
Anthropic's marketing team used this approach to create a system that
automatically generates hundreds of ad text variations from a CSV file with ad
performance data.</p>

<p><b>Monitoring and notifications.</b> You can have Claude monitor logs or data
and alert you when something breaks or an interesting pattern appears.</p>

<h2 id="toc-system-administration">System administration</h2>

<p>Claude Code can function as your personal IT support.</p>

<p><b>Diagnostics.</b> “Why is my computer slow?” – and Claude checks CPU
usage, memory, disk, running processes, Docker containers, everything possible.
And suggests solutions.</p>

<p><b>Configuration.</b> Need to set something up in your system but don't know
how? Describe what you want, and Claude will configure it.</p>

<p><b>Installation.</b> Claude installs software, configures the environment,
sets up dependencies.</p>

<p>One user described how Claude connected via SSH to a remote server, modified
the configuration of a monitoring tool, and adapted when some commands
failed.</p>

<p>For more advanced users, Claude Code also helps with infrastructure
management. It sets up automatic builds and deploys, configures server
monitoring, helps with Docker containers. Even a non-technical person can set up
a basic CI/CD pipeline with Claude's help.</p>

<h2 id="toc-but-beware-it-s-still-ai">But beware – it's still AI</h2>

<p>Before you get carried away with enthusiasm, there's one important thing to
say: Claude Code makes mistakes. Like any AI. Sometimes small ones, sometimes…
well, sometimes monumental ones.</p>

<p>One programmer told me the following story. He gave Claude the task of
modernizing an outdated project. Claude ran for an hour. Rewrote everything.
Legacy code from 2015? Gone. Outdated patterns? Replaced with elegant modern
solutions. Confusing architecture? Now crystal clear. The result looked like it
was from a textbook – TypeScript, tests, documentation, everything.</p>

<p>It was absolutely top-notch code.</p>

<p>Which didn't work at all.</p>

<p>That's why this holds true: the better programmer you are and the more you
understand your field, the better results you'll achieve with Claude Code. Not
because you'd have to write the code yourself – but because you'll recognize
when Claude makes a mistake. And it will. The question isn't if, but when.</p>

<p>Think of Claude as a junior with unlimited energy and encyclopedic knowledge
who still needs supervision. Let it work, but check the results. Test. Verify it
actually works before you push it to production.</p>

<h2 id="toc-what-does-this-mean">What does this mean?</h2>

<p>Claude Code isn't just a tool for programmers. It's an “everything
agent” – a universal assistant that has access to your computer and can
perform almost any task.</p>

<p>The key to success is simple: describe what you want in plain language. You
don't need to know commands, you don't need to understand technicalities. Just
say what you want to achieve, and Claude will find a way to do it.</p>

<p>Users report 40–80% time savings on routine tasks. Some startups have built
entire products with practically no traditional programming. All because you
have an intelligent helper at your disposal who can control your computer.</p>

<p>Just watch out for that wifi.</p>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/what-claude-code-can-do#comments</comments>
		<pubDate>Fri, 09 Jan 2026 16:21:00 +0100</pubDate>
		<guid isPermaLink="false">item2441@http://phpfashion.com</guid>
		<enclosure url="https://phpfashion.com/media/f892734c66.webp" length="35606" type="image/webp" />
	</item>
	<item>
		<title>The Ultimate PHP Comparison Reference is Here!</title>
		<link>https://phpfashion.com/en/ultimate-php-comparison-reference</link>
		<description>No more writing test scripts when you&apos;re not entirely sure. No
more tedious documentation diving. The PHP truth table has finally arrived. I&apos;ve
prepared the definitive PHP Comparison Cheat Sheet for you. It&apos;s your
map for the territory where === doesn&apos;t rule.

Since PHP 8 rewrote the comparison rules, there are two tables:

👉 Table for PHP
8.x (The present you need to master)
👉 Table
for PHP 7.x (For legacy warriors and code archaeologists)



We&apos;ve all learned to use === for a good night&apos;s sleep.
It&apos;s our safety net. But what happens when you don&apos;t need to know if values are
identical, but which one is greater or smaller? That&apos;s where certainty
evaporates. The operators &lt;, &gt;,
&lt;=, and &gt;= have no “strict” version. PHP
grabs the wheel and fires up type juggling. Do you know for certain how
comparing a number to a string behaves? Or null to
false?

Just check the table and instantly see how types interact when PHP forces
them together. It starts with a comprehensive overview of all operators,
including the spaceship (&lt;=&gt;).

Catch Silent Bugs Before They
Strike

Here are two examples that can cost you hours of debugging. Different PHP
functions use different comparison strategies. The sort() function,
for instance, defaults to SORT_REGULAR.

How does it handle strings that look like numbers, such as &quot;042&quot;
and &quot; 42&quot;? How will it sort them?


	I find the SORT_REGULAR
	section in the table

	I look up the intersection of these values

	I see the symbol =

	What it means: PHP treats them as identical in this mode. The resulting
	order of these elements after sorting will be undefined. If order matters, we&apos;ve
	got a problem.


What about array_unique()? Will it silently “cannibalize” my
data when &quot;042&quot; and &quot; 42&quot; meet in the array? No need
to test.


	The array_unique() function defaults to SORT_STRING

	I check the intersection of these values

	I see the symbol &lt;

	What it means: We&apos;re in the clear – the values differ (because everything
	converts to string)


Thanks to the table, no more guessing. You see instantly when you need to
switch flags to make your application behave exactly as intended.

(And yes, there&apos;s no flag for strict comparison without type juggling in
PHP 😤)

The Fun Stuff: DateTime and
Closures

Or what you probably don&apos;t know about comparing objects in PHP.

Take DateTime. Many developers believe objects can&apos;t be compared, so
they desperately convert dates to timestamps or formatted strings like
&apos;Y-m-d H:i:s&apos; just to figure out what came first. Completely
unnecessary! The DateTime and DateTimeImmutable
classes have built-in logic for standard comparison operators. You can check for
greater/smaller as naturally as with numbers. No helpers, no formatting, just
clean syntax. That&apos;s why it deserves its own DateTime
section in the table.

The real fun starts with equality. While === is ruthless with
objects and only cares if you&apos;re holding the exact same instance, the
== operator takes a more pragmatic approach with dates – it
compares the time value. This means you can compare two different objects, and
if they represent the same moment in time, PHP says “yes, these are equal”.
Better yet – it works cross-class between DateTime and
DateTimeImmutable!

And the cherry on top? Closures. Even anonymous functions are objects. When
are two closures equal? Check the table!
</description>
		<content:encoded><![CDATA[
		<p class="perex">No more writing test scripts when you're not entirely sure. No
more tedious documentation diving. The PHP truth table has finally arrived. I've
prepared the definitive <b>PHP Comparison Cheat Sheet</b> for you. It's your
map for the territory where <code>===</code> doesn't rule.</p>

<p>Since PHP 8 rewrote the comparison rules, there are two tables:</p>

<p>👉 <b><a
href="https://phpfashion.com/attachments/php-comparison/8.x/">Table for PHP
8.x</a></b> (The present you need to master)<br>
👉 <b><a href="https://phpfashion.com/attachments/php-comparison/7.x/">Table
for PHP 7.x</a></b> (For legacy warriors and code archaeologists)</p>

<figure><img loading="lazy" src="/media/fb22c7f238.webp" alt="" width="1400"
height="933"></figure>

<p>We've all learned to use <code>===</code> for a good night's sleep.
It's our safety net. But what happens when you don't need to know if values are
identical, but which one is greater or smaller? That's where certainty
evaporates. The operators <code>&lt;</code>, <code>&gt;</code>,
<code>&lt;=</code>, and <code>&gt;=</code> have no “strict” version. PHP
grabs the wheel and fires up type juggling. Do you know for certain how
comparing a number to a string behaves? Or <code>null</code> to
<code>false</code>?</p>

<p>Just check the table and instantly see how types interact when PHP forces
them together. It starts with a comprehensive overview of all operators,
including the spaceship (<code>&lt;=&gt;</code>).</p>

<h2 id="toc-catch-silent-bugs-before-they-strike">Catch Silent Bugs Before They
Strike</h2>

<p>Here are two examples that can cost you hours of debugging. Different PHP
functions use different comparison strategies. The <code>sort()</code> function,
for instance, defaults to <code>SORT_REGULAR</code>.</p>

<p>How does it handle strings that look like numbers, such as <code>"042"</code>
and <code>" 42"</code>? How will it sort them?</p>

<ul>
	<li>I find the <a
	href="https://phpfashion.com/attachments/php-comparison/8.x/#SORT_REGULAR">SORT_REGULAR</a>
	section in the table</li>

	<li>I look up the intersection of these values</li>

	<li>I see the symbol <b><code>=</code></b></li>

	<li>What it means: PHP treats them as identical in this mode. The resulting
	order of these elements after sorting will be undefined. If order matters, we've
	got a problem.</li>
</ul>

<p>What about <code>array_unique()</code>? Will it silently “cannibalize” my
data when <code>"042"</code> and <code>" 42"</code> meet in the array? No need
to test.</p>

<ul>
	<li>The <code>array_unique()</code> function defaults to <a
	href="https://phpfashion.com/attachments/php-comparison/8.x/#SORT_STRING">SORT_STRING</a></li>

	<li>I check the intersection of these values</li>

	<li>I see the symbol <b><code>&lt;</code></b></li>

	<li>What it means: We're in the clear – the values differ (because everything
	converts to string)</li>
</ul>

<p>Thanks to the table, no more guessing. You see instantly when you need to
switch flags to make your application behave exactly as intended.</p>

<p>(And yes, there's no flag for strict comparison without type juggling in
PHP 😤)</p>

<h2 id="toc-the-fun-stuff-datetime-and-closures">The Fun Stuff: DateTime and
Closures</h2>

<p>Or what you probably don't know about comparing objects in PHP.</p>

<p>Take <b>DateTime</b>. Many developers believe objects can't be compared, so
they desperately convert dates to timestamps or formatted strings like
<code>'Y-m-d H:i:s'</code> just to figure out what came first. Completely
unnecessary! The <code>DateTime</code> and <code>DateTimeImmutable</code>
classes have built-in logic for standard comparison operators. You can check for
greater/smaller as naturally as with numbers. No helpers, no formatting, just
clean syntax. That's why it deserves its own <a
href="https://phpfashion.com/attachments/php-comparison/8.x/#datetime">DateTime</a>
section in the table.</p>

<p>The real fun starts with equality. While <code>===</code> is ruthless with
objects and only cares if you're holding the exact same instance, the
<code>==</code> operator takes a more pragmatic approach with dates – it
compares the time value. This means you can compare two different objects, and
if they represent the same moment in time, PHP says “yes, these are equal”.
Better yet – it works cross-class between <code>DateTime</code> and
<code>DateTimeImmutable</code>!</p>

<p>And the cherry on top? Closures. Even anonymous functions are objects. When
are two closures equal? Check the <a
href="https://phpfashion.com/attachments/php-comparison/8.x/#closures">table</a>!</p>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/ultimate-php-comparison-reference#comments</comments>
		<pubDate>Fri, 26 Dec 2025 20:37:00 +0100</pubDate>
		<guid isPermaLink="false">item2439@http://phpfashion.com</guid>
		<enclosure url="https://phpfashion.com/media/fb22c7f238.webp" length="84458" type="image/webp" />
	</item>
	<item>
		<title>How to Install Claude Code: A Beginner&apos;s Guide</title>
		<link>https://phpfashion.com/en/how-to-install-claude-code-beginners-guide</link>
		<description>(Updated January 2026) Claude Code is a tool that brings AI
capabilities in the form of a virtual programmer. Think of it as a colleague
sitting beside you, helping with your projects. Unlike regular browser-based
chatting where you copy code back and forth, Claude Code works directly with
your files on your computer.

What You Can Do With It

Here are just a few examples of what you can accomplish:


	💡 Describe the function you need, and Claude writes it for you

	🐛 Pass it an error message and let it hunt down and fix the problem

	🔧 Refactor existing code, add tests, write documentation

	🗺️ When you need to understand an unfamiliar project, just ask how it
	all works


And here&apos;s what you might not know – Claude Code isn&apos;t just for
programming. Since it has access to your terminal, you can ask it to handle all
sorts of tasks:


	📋 Organize your downloads folder

	⚙️ Install packages and configure your system

	📁 Batch rename hundreds of files or search through data


Basically anything you&apos;d normally do on the command line – just instead of
googling commands, you describe what you want in plain English and Claude does
it for you. We&apos;ll explore more in the article What Claude Code Can Do.

Who This Guide Is For

You don&apos;t need to be an AI expert or a seasoned programmer – Claude Code
is designed to be accessible to everyone. That said, the better you understand
programming and your domain, the better results you&apos;ll get.

This first article is aimed at complete beginners and walks you through
installing Claude Code. In later parts, we&apos;ll dive into practical usage, but
first we need to tackle that crucial first step – getting it up and running.
By the end of this guide, you&apos;ll have everything set up and ready to start
exploring Claude Code.

How to Use Claude Code?

You can use Claude Code in three ways:

Desktop Application (easiest) 🖥️

A traditional application with windows and buttons – no geeky terminal
required. You work with local files on your computer just like in any editor.
You watch code being written in real-time and simply approve each step along the
way. You won&apos;t have access to the full arsenal of advanced commands, but that
won&apos;t bother a beginner at all.

Command Line / Terminal (for advanced users) 💻

You launch Claude Code from the terminal using text commands. If you&apos;re not
familiar with terminals, think of them like those text adventure games from the
80s – you type a command, hit Enter, and the computer responds. Except
instead of “go north,” you type “build me a website.” You get a
full-featured tool with all advanced functions, but you need to be comfortable
working with the command line.

Web (for quick testing) 🌐

Claude Code can also be used directly on the
web, but without access to local files. However, if you have your data on
Google Drive, this might actually be an advantage – you don&apos;t need to install
anything and can start immediately.

For beginners, I recommend starting with the desktop application.

Installation: Desktop
Application

Head to claude.com/download and
download the application. Open the installer and complete the installation
process. Then launch the application from Applications (Mac) or the Start menu
(Windows).



You&apos;ll also need to open gitforwindows.org, click Download,
and install Git, which Claude Code requires to function.

After launching the app for the first time:


	Find the Code tab in the application&apos;s sidebar and click it

	Install runtime dependencies – the app will prompt you to install
	required components, click “Install dependencies” and wait for the
	installation to complete

	Sign in with your Claude account – if you don&apos;t have one yet or
	need a subscription, check out the section How Much Does Claude Code
	Cost? below

	Select a project folder using the “Select folder” button

	Start working! Describe what you want to create, and Claude will make
	it happen


The app will ask you to confirm “Trust workspace” when you first access a
folder – this is a security feature. Simply approve and continue.

Desktop users can skip the rest of this article – the following
sections are only for terminal installation.

Installation: Terminal on
macOS and Linux


	For macOS: Open Terminal (find it in Applications &gt; Utilities, or
	press Cmd+Space and type “Terminal”)

	For Linux: Open your preferred terminal (usually Ctrl+Alt+T)


curl -fsSL https://claude.ai/install.sh | bash


This command downloads and runs the installation script. Once it completes,
you&apos;re all set and can skip ahead to Launching Claude Code from
Terminal.

On macOS, you can alternatively use Homebrew:

brew install --cask claude-code


Installation: Terminal on
Windows

On Windows, you have two options for running Claude Code in the terminal:

Option 1: PowerShell (quickest start 🚀)

This is the fastest way to get going. Open PowerShell (press Win+X and select
“Windows PowerShell” or “Terminal”) and run:

irm https://claude.ai/install.ps1 | iex


You&apos;ll also need to open gitforwindows.org, click Download,
and install Git, which Claude Code requires to function.

That&apos;s it! Claude Code is now installed and you can skip to Launching Claude Code from
Terminal.

Option 2: WSL (for power users 🔧)

Choose this route if you&apos;re already actively using WSL (Linux inside
Windows), or if you run into issues or need a tool that requires a Linux
environment. (You can always start with PowerShell and switch to WSL later if
needed.)

Open your terminal (press Win+X and select “Windows PowerShell” or
“Terminal”) and enable WSL with:

wsl --install


This command enables WSL and installs Ubuntu as your default Linux
distribution. Once it completes, restart your computer. Then run
ubuntu to access the Linux command line.

ubuntu


It will first ask you to create a username and password for Linux. You&apos;ll
need this password when installing programs (the sudo command), so
make sure to remember it.

Now you can install Claude Code in the Ubuntu terminal:

curl -fsSL https://claude.ai/install.sh | bash


One of the best features of WSL is that Windows and Linux share the same
files. You don&apos;t need to copy anything – you&apos;re simply working with the same
folders from both systems. Your Windows C: drive appears in Linux at
/mnt/c/, your D: drive at /mnt/d/, and so on. So for
instance, the folder C:\Users\John\Projects is accessible in Linux
as /mnt/c/Users/John/Projects. Conversely, in File Explorer you can
type \\wsl$ in the address bar to see all your Linux distributions.
Your Ubuntu home folder will typically be at
\\wsl$\Ubuntu\home\your-name.

Launching Claude Code from
Terminal

Great, you&apos;ve successfully installed Claude Code – let&apos;s fire it up!

The key is to launch it in the folder where you want to create a new project
or work on an existing one. First, you need to navigate to that folder using the
cd (change directory) command.

Example for Windows (PowerShell):

cd C:\Users\John\Projects\my-website


Example for Windows (WSL):

cd /mnt/c/Users/John/Projects/my-website


Example for macOS/Linux:

cd ~/projects/my-website


Then launch Claude Code with:

claude



	Claude Code after launch


As you can see from the screenshot, Claude Code immediately prompts you to
sign in.

How Much Does Claude Code Cost?

There&apos;s something important to clarify here, since Claude isn&apos;t as
well-known in the Czech Republic yet. Claude is
a web-based chatbot from Anthropic – similar to OpenAI&apos;s ChatGPT. You can
chat with it in your browser, ask it anything, get help with writing, and so on.
That&apos;s the basic Claude.

Claude Code is something different. It&apos;s a standalone tool that runs
in your terminal or desktop app and works directly with your files. While
web-based Claude involves chatting and copying text back and forth, Claude Code
actually edits your files, executes commands, and tests whether
everything works.

And here&apos;s the good news: Claude Code is included with your
subscription to web Claude. So when you subscribe to Claude as a chatbot (as
an alternative to ChatGPT), you get Claude Code thrown in.

You start with Claude Pro at $20/month, which is perfectly adequate
for regular use. Personally, I fell in love with Claude and use it so
intensively that I upgraded to Claude Max (starting at $100/month),
which offers 5–20× more usage. But Pro is definitely sufficient to get
started.

The desktop app will take you to the sign-in page on first launch, just like
the terminal version. Sign in or create an account there, choose a plan that
suits you, and return to the application.


	Authentication


Tip for Nette Developers

If you work with Nette, I have great news. There&apos;s a special plugin for Claude Code that gives
Claude deep knowledge of the Nette framework.

Instead of generic PHP suggestions, you get recommendations that follow Nette
conventions – from presenters and forms to Latte templates and database
queries. The plugin includes 10 automated skills covering the main areas of
development, plus automatic validation for Latte and NEON files, so you catch
errors right away.

Want to master Claude Code? I&apos;m running a course that
takes you from the basics to advanced techniques. You&apos;ll learn how to get the
most out of Claude and avoid common pitfalls. More information and
registration

What&apos;s Next?

You now have Claude Code ready to go. In upcoming articles, we&apos;ll cover:


	Basic commands and controls in Claude Code

	Working with existing projects

	Creating new projects from scratch


Claude Code represents a revolution in how we program. With this tool, you
can bring your ideas to life faster than ever before – you just need to know
how to use it effectively.
</description>
		<content:encoded><![CDATA[
		<p><em>(Updated January 2026)</em> Claude Code is a tool that brings AI
capabilities in the form of a virtual programmer. Think of it as a colleague
sitting beside you, helping with your projects. Unlike regular browser-based
chatting where you copy code back and forth, Claude Code works directly with
your files on your computer.</p>

<h2 id="toc-what-you-can-do-with-it">What You Can Do With It</h2>

<p>Here are just a few examples of what you can accomplish:</p>

<ul>
	<li>💡 Describe the function you need, and Claude writes it for you</li>

	<li>🐛 Pass it an error message and let it hunt down and fix the problem</li>

	<li>🔧 Refactor existing code, add tests, write documentation</li>

	<li>🗺️ When you need to understand an unfamiliar project, just ask how it
	all works</li>
</ul>

<p>And here's what you might not know – Claude Code isn't just for
programming. Since it has access to your terminal, you can ask it to handle all
sorts of tasks:</p>

<ul>
	<li>📋 Organize your <em>downloads folder</em></li>

	<li>⚙️ Install packages and configure your system</li>

	<li>📁 Batch rename hundreds of files or search through data</li>
</ul>

<p>Basically anything you'd normally do on the command line – just instead of
googling commands, you describe what you want in plain English and Claude does
it for you. We'll explore more in the article <a
href="https://phpfashion.com/what-claude-code-can-do">What Claude Code Can Do</a>.</p>

<h2 id="toc-who-this-guide-is-for">Who This Guide Is For</h2>

<p>You don't need to be an AI expert or a seasoned programmer – Claude Code
is designed to be accessible to everyone. That said, the better you understand
programming and your domain, the better results you'll get.</p>

<p>This first article is aimed at complete beginners and walks you through
installing Claude Code. In later parts, we'll dive into practical usage, but
first we need to tackle that crucial first step – getting it up and running.
By the end of this guide, you'll have everything set up and ready to start
exploring Claude Code.</p>
<!--more-->
<h2 id="toc-how-to-use-claude-code">How to Use Claude Code?</h2>

<p>You can use Claude Code in three ways:</p>

<p><b>Desktop Application (easiest) 🖥️</b></p>

<p>A traditional application with windows and buttons – no geeky terminal
required. You work with local files on your computer just like in any editor.
You watch code being written in real-time and simply approve each step along the
way. You won't have access to the full arsenal of advanced commands, but that
won't bother a beginner at all.</p>

<p><b>Command Line / Terminal (for advanced users) 💻</b></p>

<p>You launch Claude Code from the terminal using text commands. If you're not
familiar with terminals, think of them like those text adventure games from the
80s – you type a command, hit Enter, and the computer responds. Except
instead of “go north,” you type “build me a website.” You get a
full-featured tool with all advanced functions, but you need to be comfortable
working with the command line.</p>

<p><b>Web (for quick testing) 🌐</b></p>

<p>Claude Code can also be used <a href="https://claude.ai/code">directly on the
web</a>, but without access to local files. However, if you have your data on
Google Drive, this might actually be an advantage – you don't need to install
anything and can start immediately.</p>

<p>For beginners, I recommend starting with the <b>desktop application</b>.</p>

<h2 id="toc-installation-desktop-application">Installation: Desktop
Application</h2>

<p>Head to <a href="https://claude.com/download">claude.com/download</a> and
download the application. Open the installer and complete the installation
process. Then launch the application from Applications (Mac) or the Start menu
(Windows).</p>

<figure><img loading="lazy" src="/media/adff9e134d.webp" alt="" width="1400"
height="760"></figure>

<p>You'll also need to open <a
href="https://gitforwindows.org">gitforwindows.org</a>, click <i>Download</i>,
and install Git, which Claude Code requires to function.</p>

<p>After launching the app for the first time:</p>

<ol>
	<li><b>Find the Code tab</b> in the application's sidebar and click it</li>

	<li><b>Install runtime dependencies</b> – the app will prompt you to install
	required components, click “Install dependencies” and wait for the
	installation to complete</li>

	<li><b>Sign in</b> with your Claude account – if you don't have one yet or
	need a subscription, check out the section <a
	href="#toc-how-much-does-claude-code-cost">How Much Does Claude Code
	Cost?</a> below</li>

	<li><b>Select a project folder</b> using the “Select folder” button</li>

	<li><b>Start working!</b> Describe what you want to create, and Claude will make
	it happen</li>
</ol>

<p>The app will ask you to confirm “Trust workspace” when you first access a
folder – this is a security feature. Simply approve and continue.</p>

<p><b>Desktop users can skip the rest of this article</b> – the following
sections are only for terminal installation.</p>

<h2 id="toc-installation-terminal-on-macos-and-linux">Installation: Terminal on
macOS and Linux</h2>

<ul>
	<li><b>For macOS:</b> Open Terminal (find it in Applications &gt; Utilities, or
	press Cmd+Space and type “Terminal”)</li>

	<li><b>For Linux:</b> Open your preferred terminal (usually Ctrl+Alt+T)</li>
</ul>

<pre
class="language-shell"><code>curl -fsSL https://claude.ai/install.sh | bash
</code></pre>

<p>This command downloads and runs the installation script. Once it completes,
you're all set and can skip ahead to <a
href="#toc-launching-claude-code-from-terminal">Launching Claude Code from
Terminal</a>.</p>

<p>On macOS, you can alternatively use Homebrew:</p>

<pre class="language-shell"><code>brew install --cask claude-code
</code></pre>

<h2 id="toc-installation-terminal-on-windows">Installation: Terminal on
Windows</h2>

<p>On Windows, you have two options for running Claude Code in the terminal:</p>

<p><b>Option 1: PowerShell (quickest start 🚀)</b></p>

<p>This is the fastest way to get going. Open PowerShell (press Win+X and select
“Windows PowerShell” or “Terminal”) and run:</p>

<pre
class="language-shell"><code>irm https://claude.ai/install.ps1 | iex
</code></pre>

<p>You'll also need to open <a
href="https://gitforwindows.org">gitforwindows.org</a>, click <i>Download</i>,
and install Git, which Claude Code requires to function.</p>

<p>That's it! Claude Code is now installed and you can skip to <a
href="#toc-launching-claude-code-from-terminal">Launching Claude Code from
Terminal</a>.</p>

<p><b>Option 2: WSL (for power users 🔧)</b></p>

<p>Choose this route if you're already actively using WSL (Linux inside
Windows), or if you run into issues or need a tool that requires a Linux
environment. (You can always start with PowerShell and switch to WSL later if
needed.)</p>

<p>Open your terminal (press Win+X and select “Windows PowerShell” or
“Terminal”) and enable WSL with:</p>

<pre class="language-shell"><code>wsl --install
</code></pre>

<p>This command enables WSL and installs Ubuntu as your default Linux
distribution. Once it completes, <b>restart your computer</b>. Then run
<code>ubuntu</code> to access the Linux command line.</p>

<pre class="language-shell"><code>ubuntu
</code></pre>

<p>It will first ask you to create a username and password for Linux. You'll
need this password when installing programs (the <code>sudo</code> command), so
make sure to remember it.</p>

<p>Now you can install Claude Code in the Ubuntu terminal:</p>

<pre
class="language-shell"><code>curl -fsSL https://claude.ai/install.sh | bash
</code></pre>

<p>One of the best features of WSL is that Windows and Linux share the same
files. You don't need to copy anything – you're simply working with the same
folders from both systems. Your Windows C: drive appears in Linux at
<code>/mnt/c/</code>, your D: drive at <code>/mnt/d/</code>, and so on. So for
instance, the folder <code>C:\Users\John\Projects</code> is accessible in Linux
as <code>/mnt/c/Users/John/Projects</code>. Conversely, in File Explorer you can
type <code>\\wsl$</code> in the address bar to see all your Linux distributions.
Your Ubuntu home folder will typically be at
<code>\\wsl$\Ubuntu\home\your-name</code>.</p>

<h2 id="toc-launching-claude-code-from-terminal">Launching Claude Code from
Terminal</h2>

<p>Great, you've successfully installed Claude Code – let's fire it up!</p>

<p>The key is to launch it in the folder where you want to create a new project
or work on an existing one. First, you need to navigate to that folder using the
<code>cd</code> (change directory) command.</p>

<p>Example for Windows (PowerShell):</p>

<pre
class="language-shell"><code>cd C:\Users\John\Projects\my-website
</code></pre>

<p>Example for Windows (WSL):</p>

<pre
class="language-shell"><code>cd /mnt/c/Users/John/Projects/my-website
</code></pre>

<p>Example for macOS/Linux:</p>

<pre class="language-shell"><code>cd ~/projects/my-website
</code></pre>

<p>Then launch Claude Code with:</p>

<pre class="language-shell"><code>claude
</code></pre>

<figure><img loading="lazy" src="/media/407e1d547c.webp" alt="" width="1400"
height="985">
	<figcaption>Claude Code after launch</figcaption>
</figure>

<p>As you can see from the screenshot, Claude Code immediately prompts you to
sign in.</p>

<h2 id="toc-how-much-does-claude-code-cost">How Much Does Claude Code Cost?</h2>

<p>There's something important to clarify here, since Claude isn't as
well-known in the Czech Republic yet. <a href="https://claude.ai">Claude</a> is
a web-based chatbot from Anthropic – similar to OpenAI's ChatGPT. You can
chat with it in your browser, ask it anything, get help with writing, and so on.
That's the basic Claude.</p>

<p><b>Claude Code is something different.</b> It's a standalone tool that runs
in your terminal or desktop app and works directly with your files. While
web-based Claude involves chatting and copying text back and forth, Claude Code
actually edits your files, executes commands, and tests whether
everything works.</p>

<p>And here's the good news: Claude Code is <b>included with your
subscription</b> to web Claude. So when you subscribe to Claude as a chatbot (as
an alternative to ChatGPT), you get Claude Code thrown in.</p>

<p>You start with <b>Claude Pro at $20/month</b>, which is perfectly adequate
for regular use. Personally, I fell in love with Claude and use it so
intensively that I upgraded to <b>Claude Max</b> (starting at $100/month),
which offers 5–20× more usage. But Pro is definitely sufficient to get
started.</p>

<p>The desktop app will take you to the sign-in page on first launch, just like
the terminal version. Sign in or create an account there, choose a plan that
suits you, and return to the application.</p>

<figure><img loading="lazy" src="/media/c8d1933462.webp" alt="" width="1345"
height="804">
	<figcaption>Authentication</figcaption>
</figure>

<h2 id="toc-tip-for-nette-developers">Tip for Nette Developers</h2>

<p>If you work with Nette, I have great news. There's a special <a
href="https://ai.nette.org/en/claude-code">plugin for Claude Code</a> that gives
Claude deep knowledge of the Nette framework.</p>

<p>Instead of generic PHP suggestions, you get recommendations that follow Nette
conventions – from presenters and forms to Latte templates and database
queries. The plugin includes 10 automated skills covering the main areas of
development, plus automatic validation for Latte and NEON files, so you catch
errors right away.</p>

<p class="callout"><b>Want to master Claude Code?</b> I'm running a course that
takes you from the basics to advanced techniques. You'll learn how to get the
most out of Claude and avoid common pitfalls. <a
href="https://www.umeligence.cz/kurz-vibecoding">More information and
registration</a></p>

<h2 id="toc-what-s-next">What's Next?</h2>

<p>You now have Claude Code ready to go. In upcoming articles, we'll cover:</p>

<ul>
	<li>Basic commands and controls in Claude Code</li>

	<li>Working with existing projects</li>

	<li>Creating new projects from scratch</li>
</ul>

<p>Claude Code represents a revolution in how we program. With this tool, you
can bring your ideas to life faster than ever before – you just need to know
how to use it effectively.</p>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/how-to-install-claude-code-beginners-guide#comments</comments>
		<pubDate>Mon, 19 May 2025 15:44:00 +0200</pubDate>
		<guid isPermaLink="false">item2395@http://phpfashion.com</guid>
		<enclosure url="https://phpfashion.com/media/407e1d547c.webp" length="44434" type="image/webp" />
	</item>
	<item>
		<title>100 minutes is less than 50? PHP paradoxes during time changes</title>
		<link>https://phpfashion.com/en/100-minutes-is-less-than-50-php-paradoxes-during-time-changes</link>
		<description>“When shall we meet?” – “Tomorrow at three.” “When is that
meeting?” – “Next month.” For everyday life, such time specifications
are perfectly adequate. But try the same in programming and you&apos;ll quickly
discover you&apos;ve entered a labyrinth full of traps and unexpected surprises.

Time in programming is like a beast that appears tame until you step on its
tail. And one of this beast&apos;s most powerful tricks is daylight saving time with
its insidious transitions. A system supposedly created to save candles now
causes programmers sleepless nights (probably around 2:30 AM when they suddenly
realize their servers are doing weird things).

Let&apos;s explore the dark corners of daylight saving time transitions, how PHP
(mis)handles them, and how I attempted to fix this madness in Nette Utils. Prepare for moments when
1 + 1 ≠ 2 and when adding a longer time paradoxically returns an earlier
hour. Not even Einstein could have conceived of this.



First, let&apos;s run through
some terminology

Before diving into the problem, let&apos;s explain a few key concepts:


	UTC (Coordinated Universal Time) – the fundamental time standard
	from which all other time zones are derived. It&apos;s essentially the “zero
	point” for measuring time across the world.

	Time offset – how many hours need to be added to or subtracted
	from UTC to obtain local time. It&apos;s denoted as UTC+X or UTC-X.

	CET (Central European Time) – the Central European Time we use in
	winter. It has an offset of UTC+1, which means that when it&apos;s noon in UTC,
	it&apos;s 1:00 PM here.

	CEST (Central European Summer Time) – the Central European Summer
	Time we use in summer. It has an offset of UTC+2, so when it&apos;s noon in UTC,
	it&apos;s 2:00 PM here.

	Daylight Saving Time – a system where during a certain part of the
	year (usually summer) we move the clocks forward by one hour to better utilize
	daylight.


That moment lasted a whole
light-year

Let&apos;s break down, second by second, how the transition to daylight saving
time and back works. As an example, let&apos;s take the recent time change in the
Czech Republic on Sunday, March 30, 2025:


	At 01:59:58 Central European Time (CET), everything is normal.

	At 01:59:59 CET, everything is still normal.

	In the next second, 02:00:00 CET does NOT occur. Instead, the clocks
	magically jump forward.

	This is followed by 03:00:00 Central European Summer Time
	(CEST).


The entire hour between 02:00:00 and 02:59:59 locally “doesn&apos;t exist” on
this day. If you were supposed to have an important phone call at 2:30 AM,
you&apos;re out of luck.

Similarly, during the transition back to standard time (sometimes called
“winter time”) in autumn (e.g., October 26, 2025), the opposite situation
occurs:


	At 02:59:59 summer time (CEST), everything is normal.

	In the next second, 03:00:00 CEST does NOT occur. The clocks
	go back.

	This is followed by 02:00:00 standard Central European
	Time (CET).


In this case, the hour between 02:00:00 and 02:59:59 occurs twice.
First in summer time (CEST) and then in standard time (CET). How do we
distinguish which 2:30 we mean? By using the time zone designation (CET/CEST),
the UTC offset (+01:00 / +02:00), or simply by referring to “summer” and
“winter” time.

Time zones: What
does Europe/Prague actually denote?

When we use a time zone identifier like Europe/Prague in PHP (or
elsewhere), it&apos;s not just information about the current offset from UTC.
It&apos;s a reference to a record in the IANA Time Zone Database (abbreviated
as tz database), which contains comprehensive historical and future rules for a
given geographical area:


	Standard UTC offset (how many hours are added to or subtracted
	from UTC).

	Daylight saving time rules (when it starts, when it ends).

	Historical changes in offsets or daylight saving time rules (which can
	change by government decisions).


There are hundreds of such zones (America/New_York,
Asia/Tokyo, Australia/Sydney). Some areas don&apos;t use
daylight saving time at all (e.g., most of Africa and Asia, or regions near the
equator) and have the same UTC offset year-round (e.g., Etc/UTC or
Africa/Nairobi).

Absolute time: UTC and
Timestamp

To avoid confusion with local times and daylight saving time, there are
absolute time references:


	UTC: As we&apos;ve already mentioned, it&apos;s the basic time standard that
	doesn&apos;t use daylight saving time. All local times are defined as offsets from
	UTC. UTC is essentially “pure” time, to which each time zone then adds its
	offset.

	Unix Timestamp: The number of seconds that have elapsed since the
	beginning of the Unix epoch (January 1, 1970, 00:00:00 UTC), not counting leap
	seconds. The timestamp is also absolute and independent of time zone or daylight
	saving time.


It&apos;s precisely the conversion between absolute time (UTC/timestamp) and
local time in a specific zone where daylight saving time rules come
into play.

PHP DateTime: When the clocks
turn

When you work with a DateTime or DateTimeImmutable
object in PHP, it always has an assigned time zone. If you don&apos;t
explicitly specify one, the default zone set in PHP is used (via configuration
or using date_default_timezone_set()).

What happens when you try to create a time that doesn&apos;t exist due to daylight
saving time, or a time that exists twice?

Non-existent time (spring jump):

// Attempt to create a time in the &quot;gap&quot; on March 30, 2025
$dt = new DateTime(&apos;2025-03-30 02:30:00&apos;, new DateTimeZone(&apos;Europe/Prague&apos;));
echo $dt-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Output: 2025-03-30 03:30:00 CEST (+02:00)


PHP typically “normalizes” this invalid time by moving it forward
by one hour to the first valid time after the jump. So 02:30 becomes 03:30.

Ambiguous time (fall back):

// Attempt to create a time in the &quot;overlap&quot; on October 26, 2025
$dt = new DateTime(&apos;2025-10-26 02:30:00&apos;, new DateTimeZone(&apos;Europe/Prague&apos;));
echo $dt-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Output: 2025-10-26 02:30:00 CET (+01:00)


Here, PHP by default chooses the second occurrence of that time. Why
the second and not the first? Because PHP considers standard time (CET) to be
the default, basic state and daylight saving time (CEST) only as a temporary
adjustment. From the system&apos;s perspective, daylight saving time is just a
temporary deviation from the “normal” state, and therefore when ambiguous,
it prefers standard time.

Relative time
expressions and their subtleties

Now we come to the really tricky part. PHP allows working with relative time
expressions – strings like +30 minutes, -1 hour, or
1 day 2 hours. We can use these expressions in two ways:


	Directly in the constructor new DateTime(&apos;+50 minutes&apos;)

	In the $date-&gt;modify(&apos;+50 minutes&apos;) method


By the way, Nette has always promoted these relative time expressions because
they are intuitive and clear. You definitely know them from the “expiration”
configuration for sessions or in other parts of the framework.

Intuitively, we would expect that when we add a longer time period, the
resulting time would be later. But with relative time expressions during the
spring transition, this might not hold true! And this problem manifests itself
both when used in the DateTime constructor and in the
modify() method.

Imagine it&apos;s half past one in the morning, just before the spring jump. At
that moment, something very bizarre might be happening in your application,
which most of us won&apos;t notice because we&apos;re either peacefully sleeping in bed or
even more contentedly sharing wisdom at the pub. But in server rooms around the
world, the code quietly continues to run…

// for all subsequent examples we&apos;ll set the default time zone
date_default_timezone_set(&apos;Europe/Prague&apos;);

// It&apos;s now 2025-03-30 01:30:00 and we create a DateTime with a relative time of +50 minutes
$dt50 = new DateTime(&apos;+50 minutes&apos;);
echo $dt50-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Output: 2025-03-30 03:20:00 CEST (+02:00)


“Wait a minute, 1:30 plus 50 minutes is 2:20. Why does it show 3:20?” As
we&apos;ve already discussed, the hour between 2:00 and 3:00 doesn&apos;t exist. So 2:20
is an invalid time, which PHP corrects by moving it forward by an hour. Thus to
3:20 in summer time.

And what if we add a longer interval to the same starting time – say
100 minutes?

$dt100 = new DateTime(&apos;+100 minutes&apos;);
echo $dt100-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Output: 2025-03-30 03:10:00 CEST (+02:00)


See that! Yes, you&apos;re reading correctly. After adding 100 minutes, we got a
time (03:10) that is earlier than the time after adding
50 minutes (03:20).


	+50 minutes: 01:30 + 50 min = 02:20. This
	time doesn&apos;t exist because it&apos;s in that “lost hour”. PHP normalizes it by
	moving it forward by an hour to 03:20 CEST.

	+100 minutes:
	01:30 + 100 min (1h 40m) = 03:10. This time already exists. So PHP
	uses it as is.


The modify() method and constructor with relative strings in PHP
tend to perform arithmetic first at the level of “clock time” and only
then deal with invalid times caused by the daylight saving time jump. The
result is completely unintuitive behavior that most time libraries in other
languages don&apos;t exhibit. They typically interpret +X minutes as
adding an exact duration (X * 60 seconds) to the absolute time
moment.

DateInterval: Another
layer of complications

The story gets even more complicated with the DateInterval
class. It was created specifically for working with time intervals and could
offer a solution to our problem. But alas…

To create a DateInterval instance, you must use the ISO
8601 format. Honestly, would you understand at first glance what
PT100M means? No? Me neither. It&apos;s “Period of Time,
100 Minutes”. Standardized, but definitely not intuitive at first sight.

Nevertheless, if we swallow this strange notation, suddenly everything works
correctly!

$dt = new DateTime(&apos;2025-03-30 01:30:00&apos;);
$dt-&gt;add(new DateInterval(&apos;PT100M&apos;)); // 100 Minutes - that wonderful ISO 8601 format
echo $dt-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Output: 2025-03-30 04:10:00 CEST (+02:00) - hurray, works correctly!


Great! Here it really behaves as we would expect – it adds exactly
100 minutes to the absolute time. This could be our solution… but what about
that strange format?

PHP developers were aware that PT100M isn&apos;t exactly
user-friendly, so they added the
DateInterval::createFromDateString() method, which understands
those nice text expressions like 100 minutes:

$dt = new DateTime(&apos;2025-03-30 01:30:00&apos;);
$dt-&gt;add(DateInterval::createFromDateString(&apos;100 minutes&apos;));
echo $dt-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Output: 2025-03-30 03:10:00 CEST (+02:00) - ouch, wrong again!


And we&apos;re back where we started! The same problem as with
modify(). What&apos;s happening?

In reality, we&apos;re dealing with a kind of “dual nature” of the
DateInterval class. It depends on how we create it:


	When we use the constructor with ISO 8601 format
	new DateInterval(&apos;PT100M&apos;), it creates a real duration,
	which is added to the absolute time.

	When we use createFromDateString(&apos;100 minutes&apos;), it creates
	more of a calendar interval, which behaves similarly to
	modify() – it first performs “clock” arithmetic and then
	deals with problems with invalid times.


So not all DateIntervals are created equal. It&apos;s a completely
different face of the same named object depending on how we create it.

One solution: Escape to UTC

One way to avoid these problems is to perform all time arithmetic in UTC,
where no daylight saving time exists, and only convert the final result to the
desired local zone:

$dt = new DateTime(&apos;2025-03-30 01:30:00&apos;);
$dt-&gt;setTimezone(new DateTimeZone(&apos;UTC&apos;)); // Convert to UTC
$dt-&gt;modify(&apos;+100 minutes&apos;);               // Perform operation in UTC
$dt-&gt;setTimezone(new DateTimeZone(&apos;Europe/Prague&apos;)); // Convert back
echo $dt-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Correct output: 2025-03-30 04:10:00 CEST (+02:00)


Hurray! Or not? This trick can be counterproductive when adding whole days or
other calendar units. First, let&apos;s verify that when we add 1 day to a time
before the spring jump, we get the expected result:

$dt = new DateTime(&apos;2025-03-30 01:30:00&apos;); // Before the jump (CET +01:00)
$dt-&gt;modify(&apos;+1 day&apos;);
echo $dt-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Output: 2025-03-31 01:30:00 CEST (+02:00)


We see that when adding one day, the same “clock” value (01:30) remains,
but the time zone changes from CET to CEST.

But what happens when we use our UTC trick?

$dt = new DateTime(&apos;2025-03-30 01:30:00&apos;); // Before the jump (CET +01:00)
$dt-&gt;setTimezone(new DateTimeZone(&apos;UTC&apos;)); // Converts to UTC
$dt-&gt;modify(&apos;+1 day&apos;);
$dt-&gt;setTimezone(new DateTimeZone(&apos;Europe/Prague&apos;)); // Converts back to local zone
echo $dt-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Output: 2025-03-31 02:30:00 CEST (+02:00) - one hour more!


Oops! The hour shifted from 1:30 to 2:30. Why?


	We converted the original time (01:30 CET) to UTC (00:30 UTC)

	We added a day in UTC (00:30 UTC the following day)

	But the following day, Prague is already on summer time (CEST), which has an
	offset of +2 hours from UTC

	So when we convert 00:30 UTC back, we get 02:30 CEST


This “escape to UTC” can therefore cause calendar operations to not
behave intuitively from a local time perspective. So what is the correct
behavior? It depends on your needs – sometimes you want to preserve the
absolute time interval (like 24 hours), other times you want to preserve the
calendar meaning (like “same time next day”).

Solution in Nette Utils

Because working with time, time zones, and daylight saving time is
notoriously complex, I decided to add a fix for PHP&apos;s problematic behavior to
Nette Utils. Specifically to the
Nette\Utils\DateTime class, fixing both the constructor and the
modify() method. I&apos;m just hesitant about whether it&apos;s a BC
break – I&apos;ll come back to that in the conclusion.

$dt = new Nette\Utils\DateTime(&apos;2025-03-30 01:30:00&apos;);
$dt-&gt;modify(&apos;+100 minutes&apos;);
echo $dt-&gt;format(&apos;Y-m-d H:i:s T (P)&apos;);
// Output: 2025-03-30 04:10:00 CEST (+02:00) - CORRECT!


With Nette\Utils\DateTime, the result for
+100 minutes is always later than for +50 minutes,
even at half past one in the morning!

When is 1 + 1 ≠ 2? When
working with time!

The implementation in Nette Utils also solves more complex cases where we
combine adding days and hours. Here we come to a really interesting problem:
there are two possible interpretations of a relative expression like “+1 day
+1 hour”. And these two interpretations give different results during
the transition to daylight saving time! Let&apos;s demonstrate with an example:

First interpretation:

$dt = new DateTime(&apos;2025-03-30 01:30:00&apos;); // CET

$dt1 = clone $dt;
$dt1-&gt;modify(&apos;+1 day&apos;); // First add a day: 2025-03-31 01:30:00 CEST
$dt1-&gt;modify(&apos;+1 hour&apos;); // Then add an hour: 2025-03-31 02:30:00 CEST


Second interpretation:

$dt2 = clone $dt;
$dt2-&gt;modify(&apos;+1 hour&apos;); // First add an hour: 2025-03-30 03:30:00 CEST
$dt2-&gt;modify(&apos;+1 day&apos;);         // Then add a day: 2025-03-31 03:30:00 CEST


The difference is a whole hour! As you can see, the order of operations plays
a crucial role here.

In Nette\Utils\DateTime, I chose the first interpretation as
the default behavior because it&apos;s more intuitive. When we want to add “1 day
and 1 hour”, we usually mean “same time next day plus an hour”. And
what&apos;s best? It doesn&apos;t matter in which order you write the units. Whether you
use +1 day +1 hour or +1 hour +1 day, the result will
always be the same.

This consistency makes working with time expressions much more predictable
and safe.

Time is hard, don&apos;t struggle
alone

Working with time in PHP can be treacherous, especially around daylight
saving time transitions. Relative time expressions and even some ways of using
DateInterval can lead to unintuitive results.

If you need reliable time manipulation:


	Use Nette\Utils\DateTime, which fixes the problematic
	behavior.

	Or perform time arithmetic in the UTC zone and then convert back to the
	local zone.

	Always test the behavior of your code during daylight saving time
	transitions.


Now I&apos;m just wondering if fixing the DateTime behavior in Nette Utils will be
a BC break. Honestly, I don&apos;t think anyone consciously relies on the
treacherous current behavior during daylight saving time transitions. So
I would probably include it in Nette Utils 4.1.

Time is a difficult topic in all programming languages, not just PHP. Cache
invalidation doesn&apos;t hold a candle to it.
</description>
		<content:encoded><![CDATA[
		<p>“When shall we meet?” – “Tomorrow at three.” “When is that
meeting?” – “Next month.” For everyday life, such time specifications
are perfectly adequate. But try the same in programming and you'll quickly
discover you've entered a labyrinth full of traps and unexpected surprises.</p>

<p>Time in programming is like a beast that appears tame until you step on its
tail. And one of this beast's most powerful tricks is daylight saving time with
its insidious transitions. A system supposedly created to save candles now
causes programmers sleepless nights (probably around 2:30 AM when they suddenly
realize their servers are doing weird things).</p>

<p>Let's explore the dark corners of daylight saving time transitions, how PHP
(mis)handles them, and how I attempted to fix this madness in <a
href="https://doc.nette.org/en/utils">Nette Utils</a>. Prepare for moments when
1 + 1 ≠ 2 and when adding a longer time paradoxically returns an earlier
hour. Not even Einstein could have conceived of this.</p>

<figure><img loading="lazy" src="/media/7bf0994f8f.webp" alt="" width="1400"
height="933"></figure>

<h2 id="toc-first-let-s-run-through-some-terminology">First, let's run through
some terminology</h2>

<p>Before diving into the problem, let's explain a few key concepts:</p>

<ul>
	<li><b>UTC</b> (Coordinated Universal Time) – the fundamental time standard
	from which all other time zones are derived. It's essentially the “zero
	point” for measuring time across the world.</li>

	<li><b>Time offset</b> – how many hours need to be added to or subtracted
	from UTC to obtain local time. It's denoted as UTC+X or UTC-X.</li>

	<li><b>CET</b> (Central European Time) – the Central European Time we use in
	winter. It has an offset of UTC+1, which means that when it's noon in UTC,
	it's 1:00 PM here.</li>

	<li><b>CEST</b> (Central European Summer Time) – the Central European Summer
	Time we use in summer. It has an offset of UTC+2, so when it's noon in UTC,
	it's 2:00 PM here.</li>

	<li><b>Daylight Saving Time</b> – a system where during a certain part of the
	year (usually summer) we move the clocks forward by one hour to better utilize
	daylight.</li>
</ul>

<h2 id="toc-that-moment-lasted-a-whole-light-year">That moment lasted a whole
light-year</h2>

<p>Let's break down, second by second, how the transition to daylight saving
time and back works. As an example, let's take the recent time change in the
Czech Republic on Sunday, March 30, 2025:</p>
<!--more-->
<ul>
	<li>At 01:59:58 Central European Time (CET), everything is normal.</li>

	<li>At 01:59:59 CET, everything is still normal.</li>

	<li>In the next second, <b>02:00:00 CET does NOT occur.</b> Instead, the clocks
	magically jump forward.</li>

	<li><b>This is followed by 03:00:00 Central European Summer Time</b>
	(CEST).</li>
</ul>

<p>The entire hour between 02:00:00 and 02:59:59 locally “doesn't exist” on
this day. If you were supposed to have an important phone call at 2:30 AM,
you're out of luck.</p>

<p>Similarly, during the transition back to standard time (sometimes called
“winter time”) in autumn (e.g., October 26, 2025), the opposite situation
occurs:</p>

<ul>
	<li>At 02:59:59 summer time (CEST), everything is normal.</li>

	<li>In the next second, <b>03:00:00 CEST does NOT occur</b>. The clocks
	go back.</li>

	<li><b>This is followed by 02:00:00 standard Central European
	Time</b> (CET).</li>
</ul>

<p>In this case, the hour between 02:00:00 and 02:59:59 occurs <b>twice</b>.
First in summer time (CEST) and then in standard time (CET). How do we
distinguish which 2:30 we mean? By using the time zone designation (CET/CEST),
the UTC offset (+01:00 / +02:00), or simply by referring to “summer” and
“winter” time.</p>

<h2 id="toc-time-zones-what-does-europe-prague-actually-denote">Time zones: What
does Europe/Prague actually denote?</h2>

<p>When we use a time zone identifier like <code>Europe/Prague</code> in PHP (or
elsewhere), it's not just information about the current offset from UTC.
It's a reference to a record in the <b>IANA Time Zone Database</b> (abbreviated
as tz database), which contains comprehensive historical and future rules for a
given geographical area:</p>

<ul>
	<li>Standard UTC offset (how many hours are added to or subtracted
	from UTC).</li>

	<li>Daylight saving time rules (when it starts, when it ends).</li>

	<li>Historical changes in offsets or daylight saving time rules (which can
	change by government decisions).</li>
</ul>

<p>There are hundreds of such zones (<code>America/New_York</code>,
<code>Asia/Tokyo</code>, <code>Australia/Sydney</code>). Some areas don't use
daylight saving time at all (e.g., most of Africa and Asia, or regions near the
equator) and have the same UTC offset year-round (e.g., <code>Etc/UTC</code> or
<code>Africa/Nairobi</code>).</p>

<h2 id="toc-absolute-time-utc-and-timestamp">Absolute time: UTC and
Timestamp</h2>

<p>To avoid confusion with local times and daylight saving time, there are
absolute time references:</p>

<ul>
	<li><b>UTC:</b> As we've already mentioned, it's the basic time standard that
	doesn't use daylight saving time. All local times are defined as offsets from
	UTC. UTC is essentially “pure” time, to which each time zone then adds its
	offset.</li>

	<li><b>Unix Timestamp:</b> The number of seconds that have elapsed since the
	beginning of the Unix epoch (January 1, 1970, 00:00:00 UTC), not counting leap
	seconds. The timestamp is also absolute and independent of time zone or daylight
	saving time.</li>
</ul>

<p>It's precisely the conversion between absolute time (UTC/timestamp) and
local time in a specific zone where daylight saving time rules come
into play.</p>

<h2 id="toc-php-datetime-when-the-clocks-turn">PHP DateTime: When the clocks
turn</h2>

<p>When you work with a <code>DateTime</code> or <code>DateTimeImmutable</code>
object in PHP, it <b>always</b> has an assigned time zone. If you don't
explicitly specify one, the default zone set in PHP is used (via configuration
or using <code>date_default_timezone_set()</code>).</p>

<p>What happens when you try to create a time that doesn't exist due to daylight
saving time, or a time that exists twice?</p>

<p><b>Non-existent time (spring jump):</b></p>

<pre
class="language-php"><code>// Attempt to create a time in the &quot;gap&quot; on March 30, 2025
$dt = new DateTime(&#039;2025-03-30 02:30:00&#039;, new DateTimeZone(&#039;Europe/Prague&#039;));
echo $dt-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Output: 2025-03-30 03:30:00 CEST (+02:00)
</code></pre>

<p>PHP typically “normalizes” this invalid time by <b>moving it forward</b>
by one hour to the first valid time after the jump. So 02:30 becomes 03:30.</p>

<p><b>Ambiguous time (fall back):</b></p>

<pre
class="language-php"><code>// Attempt to create a time in the &quot;overlap&quot; on October 26, 2025
$dt = new DateTime(&#039;2025-10-26 02:30:00&#039;, new DateTimeZone(&#039;Europe/Prague&#039;));
echo $dt-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Output: 2025-10-26 02:30:00 CET (+01:00)
</code></pre>

<p>Here, PHP by default chooses the <b>second occurrence</b> of that time. Why
the second and not the first? Because PHP considers standard time (CET) to be
the default, basic state and daylight saving time (CEST) only as a temporary
adjustment. From the system's perspective, daylight saving time is just a
temporary deviation from the “normal” state, and therefore when ambiguous,
it prefers standard time.</p>

<h2 id="toc-relative-time-expressions-and-their-subtleties">Relative time
expressions and their subtleties</h2>

<p>Now we come to the really tricky part. PHP allows working with relative time
expressions – strings like <code>+30 minutes</code>, <code>-1 hour</code>, or
<code>1 day 2 hours</code>. We can use these expressions in two ways:</p>

<ol>
	<li>Directly in the constructor <code>new DateTime('+50 minutes')</code></li>

	<li>In the <code>$date-&gt;modify('+50 minutes')</code> method</li>
</ol>

<p>By the way, Nette has always promoted these relative time expressions because
they are intuitive and clear. You definitely know them from the “expiration”
configuration for sessions or in other parts of the framework.</p>

<p>Intuitively, we would expect that when we add a longer time period, the
resulting time would be later. But with relative time expressions during the
spring transition, this might not hold true! And this problem manifests itself
both when used in the <code>DateTime</code> constructor and in the
<code>modify()</code> method.</p>

<p>Imagine it's half past one in the morning, just before the spring jump. At
that moment, something very bizarre might be happening in your application,
which most of us won't notice because we're either peacefully sleeping in bed or
even more contentedly sharing wisdom at the pub. But in server rooms around the
world, the code quietly continues to run…</p>

<pre
class="language-php"><code>// for all subsequent examples we&#039;ll set the default time zone
date_default_timezone_set(&#039;Europe/Prague&#039;);

// It&#039;s now 2025-03-30 01:30:00 and we create a DateTime with a relative time of +50 minutes
$dt50 = new DateTime(&#039;+50 minutes&#039;);
echo $dt50-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Output: 2025-03-30 03:20:00 CEST (+02:00)
</code></pre>

<p>“Wait a minute, 1:30 plus 50 minutes is 2:20. Why does it show 3:20?” As
we've already discussed, the hour between 2:00 and 3:00 doesn't exist. So 2:20
is an invalid time, which PHP corrects by moving it forward by an hour. Thus to
3:20 in summer time.</p>

<p>And what if we add a longer interval to the same starting time – say
100 minutes?</p>

<pre
class="language-php"><code>$dt100 = new DateTime(&#039;+100 minutes&#039;);
echo $dt100-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Output: 2025-03-30 03:10:00 CEST (+02:00)
</code></pre>

<p>See that! Yes, you're reading correctly. After adding 100 minutes, we got a
time (<code>03:10</code>) that is <i>earlier</i> than the time after adding
50 minutes (<code>03:20</code>).</p>

<ul>
	<li><b><code>+50 minutes</code></b>: <code>01:30 + 50 min = 02:20</code>. This
	time doesn't exist because it's in that “lost hour”. PHP normalizes it by
	moving it forward by an hour to <code>03:20 CEST</code>.</li>

	<li><b><code>+100 minutes</code></b>:
	<code>01:30 + 100 min (1h 40m) = 03:10</code>. This time already exists. So PHP
	uses it as is.</li>
</ul>

<p>The <code>modify()</code> method and constructor with relative strings in PHP
tend to perform arithmetic first at the level of “clock time” and only
<i>then</i> deal with invalid times caused by the daylight saving time jump. The
result is completely unintuitive behavior that most time libraries in other
languages don't exhibit. They typically interpret <code>+X minutes</code> as
adding an <em>exact duration</em> (X * 60 seconds) to the absolute time
moment.</p>

<h2 id="toc-dateinterval-another-layer-of-complications">DateInterval: Another
layer of complications</h2>

<p>The story gets even more complicated with the <code>DateInterval</code>
class. It was created specifically for working with time intervals and could
offer a solution to our problem. But alas…</p>

<p>To create a <code>DateInterval</code> instance, you must use the ISO
8601 format. Honestly, would you understand at first glance what
<code>PT100M</code> means? No? Me neither. It's “Period of Time,
100 Minutes”. Standardized, but definitely not intuitive at first sight.</p>

<p>Nevertheless, if we swallow this strange notation, suddenly everything works
correctly!</p>

<pre
class="language-php"><code>$dt = new DateTime(&#039;2025-03-30 01:30:00&#039;);
$dt-&gt;add(new DateInterval(&#039;PT100M&#039;)); // 100 Minutes - that wonderful ISO 8601 format
echo $dt-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Output: 2025-03-30 04:10:00 CEST (+02:00) - hurray, works correctly!
</code></pre>

<p>Great! Here it really behaves as we would expect – it adds exactly
100 minutes to the absolute time. This could be our solution… but what about
that strange format?</p>

<p>PHP developers were aware that <code>PT100M</code> isn't exactly
user-friendly, so they added the
<code>DateInterval::createFromDateString()</code> method, which understands
those nice text expressions like <code>100 minutes</code>:</p>

<pre
class="language-php"><code>$dt = new DateTime(&#039;2025-03-30 01:30:00&#039;);
$dt-&gt;add(DateInterval::createFromDateString(&#039;100 minutes&#039;));
echo $dt-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Output: 2025-03-30 03:10:00 CEST (+02:00) - ouch, wrong again!
</code></pre>

<p>And we're back where we started! The same problem as with
<code>modify()</code>. What's happening?</p>

<p>In reality, we're dealing with a kind of “dual nature” of the
<code>DateInterval</code> class. It depends on how we create it:</p>

<ol>
	<li>When we use the constructor with ISO 8601 format
	<code>new DateInterval('PT100M')</code>, it creates a real <i>duration</i>,
	which is added to the absolute time.</li>

	<li>When we use <code>createFromDateString('100 minutes')</code>, it creates
	more of a <em>calendar interval</em>, which behaves similarly to
	<code>modify()</code> – it first performs “clock” arithmetic and then
	deals with problems with invalid times.</li>
</ol>

<p>So not all <code>DateInterval</code>s are created equal. It's a completely
different face of the same named object depending on how we create it.</p>

<h2 id="toc-one-solution-escape-to-utc">One solution: Escape to UTC</h2>

<p>One way to avoid these problems is to perform all time arithmetic in UTC,
where no daylight saving time exists, and only convert the final result to the
desired local zone:</p>

<pre
class="language-php"><code>$dt = new DateTime(&#039;2025-03-30 01:30:00&#039;);
$dt-&gt;setTimezone(new DateTimeZone(&#039;UTC&#039;)); // Convert to UTC
$dt-&gt;modify(&#039;+100 minutes&#039;);               // Perform operation in UTC
$dt-&gt;setTimezone(new DateTimeZone(&#039;Europe/Prague&#039;)); // Convert back
echo $dt-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Correct output: 2025-03-30 04:10:00 CEST (+02:00)
</code></pre>

<p>Hurray! Or not? This trick can be counterproductive when adding whole days or
other calendar units. First, let's verify that when we add 1 day to a time
before the spring jump, we get the expected result:</p>

<pre
class="language-php"><code>$dt = new DateTime(&#039;2025-03-30 01:30:00&#039;); // Before the jump (CET +01:00)
$dt-&gt;modify(&#039;+1 day&#039;);
echo $dt-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Output: 2025-03-31 01:30:00 CEST (+02:00)
</code></pre>

<p>We see that when adding one day, the same “clock” value (01:30) remains,
but the time zone changes from CET to CEST.</p>

<p>But what happens when we use our UTC trick?</p>

<pre
class="language-php"><code>$dt = new DateTime(&#039;2025-03-30 01:30:00&#039;); // Before the jump (CET +01:00)
$dt-&gt;setTimezone(new DateTimeZone(&#039;UTC&#039;)); // Converts to UTC
$dt-&gt;modify(&#039;+1 day&#039;);
$dt-&gt;setTimezone(new DateTimeZone(&#039;Europe/Prague&#039;)); // Converts back to local zone
echo $dt-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Output: 2025-03-31 02:30:00 CEST (+02:00) - one hour more!
</code></pre>

<p>Oops! The hour shifted from 1:30 to 2:30. Why?</p>

<ol>
	<li>We converted the original time (01:30 CET) to UTC (00:30 UTC)</li>

	<li>We added a day in UTC (00:30 UTC the following day)</li>

	<li>But the following day, Prague is already on summer time (CEST), which has an
	offset of +2 hours from UTC</li>

	<li>So when we convert 00:30 UTC back, we get 02:30 CEST</li>
</ol>

<p>This “escape to UTC” can therefore cause calendar operations to not
behave intuitively from a local time perspective. So what is the correct
behavior? It depends on your needs – sometimes you want to preserve the
absolute time interval (like 24 hours), other times you want to preserve the
calendar meaning (like “same time next day”).</p>

<h2 id="toc-solution-in-nette-utils">Solution in Nette Utils</h2>

<p>Because working with time, time zones, and daylight saving time is
notoriously complex, I decided to add a fix for PHP's problematic behavior to
<a href="https://doc.nette.org/en/utils">Nette Utils</a>. Specifically to the
<code>Nette\Utils\DateTime</code> class, fixing both the constructor and the
<code>modify()</code> method. I'm just hesitant about whether it's a BC
break – I'll come back to that in the conclusion.</p>

<pre
class="language-php"><code>$dt = new Nette\Utils\DateTime(&#039;2025-03-30 01:30:00&#039;);
$dt-&gt;modify(&#039;+100 minutes&#039;);
echo $dt-&gt;format(&#039;Y-m-d H:i:s T (P)&#039;);
// Output: 2025-03-30 04:10:00 CEST (+02:00) - CORRECT!
</code></pre>

<p>With <code>Nette\Utils\DateTime</code>, the result for
<code>+100 minutes</code> is always later than for <code>+50 minutes</code>,
even at half past one in the morning!</p>

<h2 id="toc-when-is-1-1-2-when-working-with-time">When is 1 + 1 ≠ 2? When
working with time!</h2>

<p>The implementation in Nette Utils also solves more complex cases where we
combine adding days and hours. Here we come to a really interesting problem:
there are two possible interpretations of a relative expression like “+1 day
+1 hour”. And these two interpretations give <b>different results</b> during
the transition to daylight saving time! Let's demonstrate with an example:</p>

<p>First interpretation:</p>

<pre
class="language-php"><code>$dt = new DateTime(&#039;2025-03-30 01:30:00&#039;); // CET

$dt1 = clone $dt;
$dt1-&gt;modify(&#039;+1 day&#039;); // First add a day: 2025-03-31 01:30:00 CEST
$dt1-&gt;modify(&#039;+1 hour&#039;); // Then add an hour: 2025-03-31 02:30:00 CEST
</code></pre>

<p>Second interpretation:</p>

<pre
class="language-php"><code>$dt2 = clone $dt;
$dt2-&gt;modify(&#039;+1 hour&#039;); // First add an hour: 2025-03-30 03:30:00 CEST
$dt2-&gt;modify(&#039;+1 day&#039;);         // Then add a day: 2025-03-31 03:30:00 CEST
</code></pre>

<p>The difference is a whole hour! As you can see, the order of operations plays
a crucial role here.</p>

<p>In <code>Nette\Utils\DateTime</code>, I chose the first interpretation as
the default behavior because it's more intuitive. When we want to add “1 day
and 1 hour”, we usually mean “same time next day plus an hour”. And
what's best? It doesn't matter in which order you write the units. Whether you
use <code>+1 day +1 hour</code> or <code>+1 hour +1 day</code>, the result will
always be the same.</p>

<p>This consistency makes working with time expressions much more predictable
and safe.</p>

<h2 id="toc-time-is-hard-don-t-struggle-alone">Time is hard, don't struggle
alone</h2>

<p>Working with time in PHP can be treacherous, especially around daylight
saving time transitions. Relative time expressions and even some ways of using
<code>DateInterval</code> can lead to unintuitive results.</p>

<p>If you need reliable time manipulation:</p>

<ol>
	<li>Use <code>Nette\Utils\DateTime</code>, which fixes the problematic
	behavior.</li>

	<li>Or perform time arithmetic in the UTC zone and then convert back to the
	local zone.</li>

	<li>Always test the behavior of your code during daylight saving time
	transitions.</li>
</ol>

<p>Now I'm just wondering if fixing the DateTime behavior in Nette Utils will be
a BC break. Honestly, I don't think anyone consciously relies on the
treacherous current behavior during daylight saving time transitions. So
I would probably include it in Nette Utils 4.1.</p>

<p>Time is a difficult topic in all programming languages, not just PHP. Cache
invalidation doesn't hold a candle to it.</p>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/100-minutes-is-less-than-50-php-paradoxes-during-time-changes#comments</comments>
		<pubDate>Fri, 04 Apr 2025 11:01:00 +0200</pubDate>
		<guid isPermaLink="false">item2391@http://phpfashion.com</guid>
		<enclosure url="https://phpfashion.com/media/7bf0994f8f.webp" length="103172" type="image/webp" />
	</item>
	<item>
		<title>Var, Let, Const: Stop Complicating Your Life in JavaScript</title>
		<link>https://phpfashion.com/en/var-let-const-in-javascript</link>
		<description>JavaScript offers three ways to declare variables: var,
let, and const. Many programmers aren&apos;t entirely clear
on when to use which one, and most tutorials and linters force you to use them
incorrectly. Let&apos;s see how to write cleaner and more understandable code
without unnecessary rules that don&apos;t actually help us.

Let&apos;s Start with the Most
Dangerous Part

JavaScript has one treacherous quirk: by simply omitting a variable
declaration, you can unknowingly use a global variable. All it takes is
forgetting var, let, or const:

function calculatePrice(amount) {
    price = amount * 100;    // Omission! Missing &apos;let&apos;
    return price;            // We&apos;re using a global variable &apos;price&apos;
}

function processOrder() {
    price = 0;               // We&apos;re using the same global variable!
    // ... some code calling calculatePrice()
    return price;            // We&apos;re returning a completely different value than expected
}


This is every developer&apos;s nightmare – the code appears to work correctly
until something mysteriously starts failing elsewhere in the application.
Debugging such errors can take hours because a global variable can be
overwritten anywhere in the application.

That&apos;s why it&apos;s absolutely crucial to always declare variables using
let or const.

Forget About var

The var keyword has been in JavaScript since its inception in
1995 and carries some problematic properties that were considered features at
the time of the language&apos;s creation but proved to be a source of many bugs over
time. After twenty years of language development, JavaScript&apos;s authors decided
to address these problems – not by fixing var (to maintain
backward compatibility) but by introducing the new let keyword in
ES2015.

You can find plenty of articles on the internet dissecting the problems with
var in the finest detail. But you know what? There&apos;s no need to
get bogged down in the details. Let&apos;s just treat var as a relic of
the past and focus on modern JavaScript.

When to Use let

let is the modern way to declare variables in JavaScript.

The nice thing is that the variable only exists within the code block
(between curly braces) where it was defined. This makes the code more
predictable and safer.

if (someCondition) {
    let temp = calculateSomething();
    // temp is only available here
}
// temp no longer exists here


In loops, the declaration is technically placed before the curly braces, but
don&apos;t let that confuse you – the variable only exists within the loop:

for (let counter = 0; counter &lt; 10; counter++) {
    // The counter variable only exists in the loop
}
// counter is no longer accessible here


When to Use const

const is used to declare constants. These are typically
important values at the module or application level that should never
change:

const PI = 3.14159;
const API_URL = &apos;https://api.example.com&apos;;
const MAX_RETRY_ATTEMPTS = 3;


However, it&apos;s important to understand one key detail: const only prevents
assigning a new value to the variable – it doesn&apos;t control what happens
with the value itself. This distinction is particularly evident with objects and
arrays (an array is also an object) – const doesn&apos;t make them
immutable objects, i.e., it doesn&apos;t prevent changes inside the object:

const CONFIG = {
    url: &apos;https://api.example.com&apos;,
    timeout: 5000
};

CONFIG.url = &apos;https://api2.example.com&apos;;  // This works!
CONFIG = { url: &apos;https://api2.example.com&apos; };  // This throws TypeError!


If you need a truly immutable object, you need to freeze
it first.

The let vs const
Dilemma

Now we come to a more interesting question. While the situation with
var vs let is clear, the use of const is
the subject of many community discussions. Most tutorials, style guides, and
linters promote the rule “use const wherever you can.” So we
commonly see const used in function or method bodies.

Let&apos;s explain why this popular “best practice” is actually an
anti-pattern that makes code less readable and unnecessarily restrictive.

The approach “if a variable&apos;s value isn&apos;t reassigned in the code, it
should be declared as const” seems logical at first glance. Why
else would const even exist? The more “constants,” the safer
and more predictable the code, right? And faster too, because the compiler can
better optimize it.

However, this entire approach fundamentally misunderstands the purpose of
constants. It&apos;s primarily about communicating intent – are we truly
trying to signal to other developers that this variable should never be
reassigned, or do we just happen not to reassign it in our current
implementation?

// Real constants - values that are constant by their nature
const PI = 3.14159;
const DAYS_IN_WEEK = 7;
const API_ENDPOINT = &apos;https://api.example.com&apos;;

// vs.

function processOrder(items) {
    // These AREN&apos;T constants, we just happen to not reassign them
    const total = items.reduce((sum, item) =&gt; sum + item.price, 0);
    const tax = total * 0.21;
    const shipping = calculateShipping(total);
    return { total, tax, shipping };
}


In the first case, we have values that are constants by their nature –
they express immutable properties of our system or important configuration data.
When we see PI or API_ENDPOINT somewhere in the code,
we immediately understand why these values are constants.

In the second case, we&apos;re using const just because we happen to
not reassign the values right now. But that&apos;s not their essential
characteristic – these are regular variables that we might want to change
in the next version of the function. And when we want to do that,
const will unnecessarily prevent us.

In the days when JavaScript was one big global code, it made sense to try to
secure variables against reassignment. But today we write code in modules and
classes. Today it&apos;s common and correct that the scope is a small function, and
within its scope, it makes no sense to worry about the difference between
let and const.

Because it creates completely unnecessary mental overhead:


	The programmer has to think while writing: “Will I change this value? No?
	Then I must use const…”

	It distracts readers! When they see const in the code, they
	wonder: “Why is this a constant? Is this some important value? Does it have
	any significance?”

	In a month we need to change the value and have to deal with: “Can
	I change const to let? Is someone relying on this?”


Simply use let and you don&apos;t have to deal with these
questions at all.

It&apos;s even worse when this decision is made automatically by a linter. That
is, when the linter “fixes” variables to const because it only sees one
assignment. The code reader then unnecessarily wonders: “Why must these
variables be constants here? Is it somehow important?” And yet it&apos;s not
important – it&apos;s just a coincidence. Don&apos;t use the prefer-const
rule in ESLint!

By the way, the optimization argument is a myth. Modern JavaScript engines
(like V8) can easily detect whether a variable is reassigned or not, regardless
of whether it was declared using let or const. So
using const provides no performance benefit.

Implicit Constants

In JavaScript, there are several constructs that implicitly create constants
without us having to use the const keyword:

// imported modules
import { React } from &apos;react&apos;;
React = something; // TypeError: Assignment to constant variable

// functions
function add(a, b) { return a + b; }
add = something; // TypeError: Assignment to constant variable

// classes
class User {}
User = something; // TypeError: Assignment to constant variable


This makes sense – these constructs define the basic building blocks of
our code, and overwriting them could cause chaos in the application. That&apos;s why
JavaScript automatically protects them against reassignment, just as if they
were declared using const.

Constants in Classes

Classes were added to JavaScript relatively recently (in ES2015), and their
functionality is still gradually maturing. For example, private members marked
with # didn&apos;t arrive until 2022. JavaScript is still waiting for
class constant support. For now, you can use static, but it&apos;s far
from the same thing – it marks a value shared between all class instances,
not an immutable one.

Conclusion


	Don&apos;t use var – it&apos;s outdated

	Use const for real constants at the module level

	In functions and methods, use let – it&apos;s more readable and
	clearer

	Don&apos;t let the linter automatically change let to
	const – it&apos;s not about the number of assignments, but
	about intent

</description>
		<content:encoded><![CDATA[
		<p>JavaScript offers three ways to declare variables: <code>var</code>,
<code>let</code>, and <code>const</code>. Many programmers aren't entirely clear
on when to use which one, and most tutorials and linters force you to use them
incorrectly. Let's see how to write cleaner and more understandable code
without unnecessary rules that don't actually help us.</p>

<h2 id="toc-let-s-start-with-the-most-dangerous-part">Let's Start with the Most
Dangerous Part</h2>

<p>JavaScript has one treacherous quirk: by simply omitting a variable
declaration, you can unknowingly use a global variable. All it takes is
forgetting <code>var</code>, <code>let</code>, or <code>const</code>:</p>

<pre
class="language-javascript"><code>function calculatePrice(amount) {
    price = amount * 100;    // Omission! Missing &#039;let&#039;
    return price;            // We&#039;re using a global variable &#039;price&#039;
}

function processOrder() {
    price = 0;               // We&#039;re using the same global variable!
    // ... some code calling calculatePrice()
    return price;            // We&#039;re returning a completely different value than expected
}
</code></pre>

<p>This is every developer's nightmare – the code appears to work correctly
until something mysteriously starts failing elsewhere in the application.
Debugging such errors can take hours because a global variable can be
overwritten anywhere in the application.</p>

<p>That's why it's absolutely crucial to <b>always</b> declare variables using
<code>let</code> or <code>const</code>.</p>

<h2 id="toc-forget-about-var">Forget About <code>var</code></h2>

<p>The <code>var</code> keyword has been in JavaScript since its inception in
1995 and carries some problematic properties that were considered features at
the time of the language's creation but proved to be a source of many bugs over
time. After twenty years of language development, JavaScript's authors decided
to address these problems – not by fixing <code>var</code> (to maintain
backward compatibility) but by introducing the new <code>let</code> keyword in
ES2015.</p>

<p>You can find plenty of articles on the internet dissecting the problems with
<code>var</code> in the finest detail. But you know what? There's no need to
get bogged down in the details. Let's just treat <code>var</code> as a relic of
the past and focus on modern JavaScript.</p>

<h2 id="toc-when-to-use-let">When to Use <code>let</code></h2>

<p><code>let</code> is the modern way to declare variables in JavaScript.</p>

<p>The nice thing is that the variable only exists within the code block
(between curly braces) where it was defined. This makes the code more
predictable and safer.</p>

<pre
class="language-javascript"><code>if (someCondition) {
    let temp = calculateSomething();
    // temp is only available here
}
// temp no longer exists here
</code></pre>

<p>In loops, the declaration is technically placed before the curly braces, but
don't let that confuse you – the variable only exists within the loop:</p>

<pre
class="language-js"><code>for (let counter = 0; counter &lt; 10; counter++) {
    // The counter variable only exists in the loop
}
// counter is no longer accessible here
</code></pre>

<h2 id="toc-when-to-use-const">When to Use <code>const</code></h2>

<p><code>const</code> is used to declare constants. These are typically
important values at the module or application level that should never
change:</p>

<pre
class="language-js"><code>const PI = 3.14159;
const API_URL = &#039;https://api.example.com&#039;;
const MAX_RETRY_ATTEMPTS = 3;
</code></pre>

<p>However, it's important to understand one key detail: <b>const only prevents
assigning a new value to the variable</b> – it doesn't control what happens
with the value itself. This distinction is particularly evident with objects and
arrays (an array is also an object) – const <b>doesn't</b> make them
immutable objects, i.e., it doesn't prevent changes inside the object:</p>

<pre
class="language-js"><code>const CONFIG = {
    url: &#039;https://api.example.com&#039;,
    timeout: 5000
};

CONFIG.url = &#039;https://api2.example.com&#039;;  // This works!
CONFIG = { url: &#039;https://api2.example.com&#039; };  // This throws TypeError!
</code></pre>

<p>If you need a truly immutable object, you need to <a
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze">freeze
it</a> first.</p>

<h2 id="toc-the-let-vs-const-dilemma">The <code>let</code> vs <code>const</code>
Dilemma</h2>

<p>Now we come to a more interesting question. While the situation with
<code>var</code> vs <code>let</code> is clear, the use of <code>const</code> is
the subject of many community discussions. Most tutorials, style guides, and
linters promote the rule “use <code>const</code> wherever you can.” So we
commonly see <code>const</code> used in function or method bodies.</p>

<p>Let's explain why this popular “best practice” is actually an
anti-pattern that makes code less readable and unnecessarily restrictive.</p>

<p>The approach “if a variable's value isn't reassigned in the code, it
should be declared as <code>const</code>” seems logical at first glance. Why
else would <code>const</code> even exist? The more “constants,” the safer
and more predictable the code, right? And faster too, because the compiler can
better optimize it.</p>

<p>However, this entire approach fundamentally misunderstands the purpose of
constants. It's primarily about <b>communicating intent</b> – are we truly
trying to signal to other developers that this variable should never be
reassigned, or do we just happen not to reassign it in our current
implementation?</p>

<pre
class="language-js"><code>// Real constants - values that are constant by their nature
const PI = 3.14159;
const DAYS_IN_WEEK = 7;
const API_ENDPOINT = &#039;https://api.example.com&#039;;

// vs.

function processOrder(items) {
    // These AREN&#039;T constants, we just happen to not reassign them
    const total = items.reduce((sum, item) =&gt; sum + item.price, 0);
    const tax = total * 0.21;
    const shipping = calculateShipping(total);
    return { total, tax, shipping };
}
</code></pre>

<p>In the first case, we have values that are constants by their nature –
they express immutable properties of our system or important configuration data.
When we see <code>PI</code> or <code>API_ENDPOINT</code> somewhere in the code,
we immediately understand why these values are constants.</p>

<p>In the second case, we're using <code>const</code> just because we happen to
not reassign the values right now. But that's <b>not their essential
characteristic</b> – these are regular variables that we might want to change
in the next version of the function. And when we want to do that,
<code>const</code> will unnecessarily prevent us.</p>

<p>In the days when JavaScript was one big global code, it made sense to try to
secure variables against reassignment. But today we write code in modules and
classes. Today it's common and correct that the scope is a small function, and
within its scope, it makes no sense to worry about the difference between
<code>let</code> and <code>const</code>.</p>

<p>Because it creates completely unnecessary mental overhead:</p>

<ol>
	<li>The programmer has to think while writing: “Will I change this value? No?
	Then I must use const…”</li>

	<li>It distracts readers! When they see <code>const</code> in the code, they
	wonder: “Why is this a constant? Is this some important value? Does it have
	any significance?”</li>

	<li>In a month we need to change the value and have to deal with: “Can
	I change const to let? Is someone relying on this?”</li>
</ol>

<p><b>Simply use <code>let</code> and you don't have to deal with these
questions at all.</b></p>

<p>It's even worse when this decision is made automatically by a linter. That
is, when the linter “fixes” variables to const because it only sees one
assignment. The code reader then unnecessarily wonders: “Why must these
variables be constants here? Is it somehow important?” And yet it's not
important – it's just a coincidence. Don't use the <a
href="https://eslint.org/docs/latest/rules/prefer-const"><code>prefer-const</code></a>
rule in ESLint!</p>

<p>By the way, the optimization argument is a myth. Modern JavaScript engines
(like V8) can easily detect whether a variable is reassigned or not, regardless
of whether it was declared using <code>let</code> or <code>const</code>. So
using <code>const</code> provides no performance benefit.</p>

<h2 id="toc-implicit-constants">Implicit Constants</h2>

<p>In JavaScript, there are several constructs that implicitly create constants
without us having to use the <code>const</code> keyword:</p>

<pre
class="language-js"><code>// imported modules
import { React } from &#039;react&#039;;
React = something; // TypeError: Assignment to constant variable

// functions
function add(a, b) { return a + b; }
add = something; // TypeError: Assignment to constant variable

// classes
class User {}
User = something; // TypeError: Assignment to constant variable
</code></pre>

<p>This makes sense – these constructs define the basic building blocks of
our code, and overwriting them could cause chaos in the application. That's why
JavaScript automatically protects them against reassignment, just as if they
were declared using <code>const</code>.</p>

<h2 id="toc-constants-in-classes">Constants in Classes</h2>

<p>Classes were added to JavaScript relatively recently (in ES2015), and their
functionality is still gradually maturing. For example, private members marked
with <code>#</code> didn't arrive until 2022. JavaScript is still waiting for
class constant support. For now, you can use <code>static</code>, but it's far
from the same thing – it marks a value shared between all class instances,
not an immutable one.</p>

<h2 id="toc-conclusion">Conclusion</h2>

<ol>
	<li>Don't use <code>var</code> – it's outdated</li>

	<li>Use <code>const</code> for real constants at the module level</li>

	<li>In functions and methods, use <code>let</code> – it's more readable and
	clearer</li>

	<li>Don't let the linter automatically change <code>let</code> to
	<code>const</code> – it's not about the number of assignments, but
	about intent</li>
</ol>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/var-let-const-in-javascript#comments</comments>
		<pubDate>Thu, 06 Feb 2025 23:54:51 +0100</pubDate>
		<guid isPermaLink="false">item2361@http://phpfashion.com</guid>
	</item>
	<item>
		<title>How to Deal with the Chaos of Empty Strings and NULL Values in MySQL?</title>
		<link>https://phpfashion.com/en/how-to-deal-with-the-chaos-of-empty-strings-and-null-values-in-mysql</link>
		<description>You know the situation – you create a query
WHERE street = &apos;&apos;, but the system doesn&apos;t return all the records
you&apos;d expect. Or your LEFT JOIN doesn&apos;t work as it should. The reason is a
common problem in databases: inconsistent use of empty strings and NULL values.
Let&apos;s see how to solve this chaos once and for all.

When to Use NULL
and When to Use an Empty String?

In theory, the difference is clear: NULL means “value is not set”, while
an empty string means “value is set and is empty”. Let&apos;s look at a real
example from an e-commerce site, where we have an orders table. Each order has a
required delivery address and an optional billing address for cases where the
customer wants to bill to a different location (typical checkbox “Bill to a
different address”):

CREATE TABLE orders (
    id INT PRIMARY KEY,
    delivery_street VARCHAR(255) NOT NULL,
    delivery_city VARCHAR(255) NOT NULL,
    billing_street VARCHAR(255) NULL,
    billing_city VARCHAR(255) NULL
);


The billing_city and billing_street fields are
nullable because the billing address is optional. But there&apos;s a difference
between them. While a street can be legitimately empty (villages without street
names) or unset (delivery address is used), the city must always be filled in if
a billing address is used. So either billing_city contains a city
name, or it&apos;s NULL – in which case the delivery address is used.

The Reality of Large Databases

In practice, both approaches often end up being mixed in the database. There
can be several reasons:


	Changes in application logic over time (e.g., switching from one ORM to
	another)

	Different teams or programmers using different conventions

	Buggy data migrations when merging databases

	Legacy code that behaves differently than new code

	Application bugs that occasionally let through an empty string instead of
	NULL or vice versa


This leads to situations where we have a mix of values in the database and
need to write complex conditions:

SELECT * FROM tbl
WHERE foo = &apos;&apos; OR foo IS NULL;


Even worse is that NULL behaves unintuitive when comparing:

SELECT * FROM tbl WHERE foo = &apos;&apos;; -- doesn&apos;t include NULL
SELECT * FROM tbl WHERE foo &lt;&gt; &apos;&apos;; -- also doesn&apos;t include NULL

-- we must use
SELECT * FROM tbl WHERE foo IS NULL;
SELECT * FROM tbl WHERE foo &lt;=&gt; NULL;


This inconsistency in comparison operators&apos; behavior is another reason why
it&apos;s better to use only one way of representing empty values in the
database.

Why Avoid the Dual Approach

A similar situation exists in JavaScript, where we have null
and undefined. After years of experience, many JavaScript
developers concluded that distinguishing between these two states brings more
problems than benefits and decided to use only the system-native
undefined.

In the database world, the situation is similar. Instead of constantly
dealing with whether something is an empty string or NULL, it&apos;s often simpler
to choose one approach and stick to it. For example, Oracle database essentially
equates empty strings and NULL values, thus elegantly avoiding this problem.
It&apos;s one of the places where Oracle deviates from the SQL standard, but it
simplifies working with empty/NULL values.

How can we achieve something similar in MySQL?

What Do We Actually Want to
Enforce?


	For required fields (NOT NULL), we want to enforce that they
	always contain meaningful values. That means preventing empty strings (or
	strings containing only spaces)

	For optional fields (NULL), we want to prevent storing empty
	strings. When a field is optional, NULL should be the only representation of an
	“unfilled value”. Mixing both approaches in one column leads to problems
	with querying and JOIN operations, as we showed above.


Solution in MySQL

Historically in MySQL, it made sense to use exclusively empty strings (&apos;&apos;)
instead of NULL values. It was the only approach that could be enforced using
the NOT NULL constraint. If we wanted an automatically consistent
database, this was the only way.

However, there&apos;s one important case where this approach fails – when we
need a unique index on the column. MySQL considers multiple empty strings as the
same value, while multiple NULL values are considered different.

However, since MySQL version 8.0.16, we can use CHECK constraints and have
more control over what values we allow. We can, for example, enforce that a
column will either be NULL or contain a non-empty string:

CREATE TABLE users (
    id INT PRIMARY KEY,

    -- Required field - must contain some non-empty text
    email VARCHAR(255) NOT NULL UNIQUE
        CONSTRAINT email_not_empty      -- rule name
        CHECK (email != &apos;&apos;),

    -- Optional field - either NULL or non-empty text
    nickname VARCHAR(255)
        CONSTRAINT nickname_not_empty
        CHECK (nickname IS NULL OR nickname != &apos;&apos;)
);


When creating a CHECK constraint, it&apos;s important to give it a meaningful
name using the CONSTRAINT keyword. This way, we get a meaningful error message
Check constraint ‘nickname_not_empty’ is violated instead of a
generic constraint violation notice. This significantly helps with debugging and
application maintenance.

The problem isn&apos;t just empty strings, but also strings containing only
spaces. We can improve the CHECK constraint solution using the TRIM
function:

CREATE TABLE users (
    id INT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE
        CONSTRAINT email_not_empty
        CHECK (TRIM(email) != &apos;&apos;),
   ...
);


Now these validation bypass attempts won&apos;t work either:

INSERT INTO users (email) VALUES (&apos;   &apos;);  -- all spaces


Practical Solution in Nette
Framework

A consistent approach to empty values needs to be handled at the application
level too. If you&apos;re using Nette Framework, you can use an elegant solution
using the setNullable() method:

$form = new Form;
$form-&gt;addText(&apos;billing_street&apos;)
    -&gt;setNullable(); // empty input transforms to NULL


Recommendations for Practice


	At the start of the project, decide on one approach:
		
			Either use only NULL for missing values

			Or use only empty strings for empty/missing values
		
	

	Document this decision in the project documentation

	Use CHECK constraints to enforce consistency

	For existing projects:
		
			Conduct an audit of the current state

			Prepare a migration script to unify the approach

			Don&apos;t forget to adjust application logic
		
	


With this approach, you&apos;ll avoid many problems with comparing, indexing, and
JOIN operations that arise from mixing NULL and empty strings. Your database
will be more consistent and queries simpler.
</description>
		<content:encoded><![CDATA[
		<p>You know the situation – you create a query
<code>WHERE street = ''</code>, but the system doesn't return all the records
you'd expect. Or your LEFT JOIN doesn't work as it should. The reason is a
common problem in databases: inconsistent use of empty strings and NULL values.
Let's see how to solve this chaos once and for all.</p>

<h2 id="toc-when-to-use-null-and-when-to-use-an-empty-string">When to Use NULL
and When to Use an Empty String?</h2>

<p>In theory, the difference is clear: NULL means “value is not set”, while
an empty string means “value is set and is empty”. Let's look at a real
example from an e-commerce site, where we have an orders table. Each order has a
required delivery address and an optional billing address for cases where the
customer wants to bill to a different location (typical checkbox “Bill to a
different address”):</p>

<pre
class="language-sql"><code>CREATE TABLE orders (
    id INT PRIMARY KEY,
    delivery_street VARCHAR(255) NOT NULL,
    delivery_city VARCHAR(255) NOT NULL,
    billing_street VARCHAR(255) NULL,
    billing_city VARCHAR(255) NULL
);
</code></pre>

<p>The <code>billing_city</code> and <code>billing_street</code> fields are
nullable because the billing address is optional. But there's a difference
between them. While a street can be legitimately empty (villages without street
names) or unset (delivery address is used), the city must always be filled in if
a billing address is used. So either <code>billing_city</code> contains a city
name, or it's NULL – in which case the delivery address is used.</p>

<h2 id="toc-the-reality-of-large-databases">The Reality of Large Databases</h2>

<p>In practice, both approaches often end up being mixed in the database. There
can be several reasons:</p>

<ul>
	<li>Changes in application logic over time (e.g., switching from one ORM to
	another)</li>

	<li>Different teams or programmers using different conventions</li>

	<li>Buggy data migrations when merging databases</li>

	<li>Legacy code that behaves differently than new code</li>

	<li>Application bugs that occasionally let through an empty string instead of
	NULL or vice versa</li>
</ul>

<p>This leads to situations where we have a mix of values in the database and
need to write complex conditions:</p>

<pre
class="language-sql"><code>SELECT * FROM tbl
WHERE foo = &#039;&#039; OR foo IS NULL;
</code></pre>

<p>Even worse is that NULL behaves unintuitive when comparing:</p>

<pre
class="language-sql"><code>SELECT * FROM tbl WHERE foo = &#039;&#039;; -- doesn&#039;t include NULL
SELECT * FROM tbl WHERE foo &lt;&gt; &#039;&#039;; -- also doesn&#039;t include NULL

-- we must use
SELECT * FROM tbl WHERE foo IS NULL;
SELECT * FROM tbl WHERE foo &lt;=&gt; NULL;
</code></pre>

<p>This inconsistency in comparison operators' behavior is another reason why
it's better to use only one way of representing empty values in the
database.</p>

<h2 id="toc-why-avoid-the-dual-approach">Why Avoid the Dual Approach</h2>

<p>A similar situation exists in JavaScript, where we have <code>null</code>
and <code>undefined</code>. After years of experience, many JavaScript
developers concluded that distinguishing between these two states brings more
problems than benefits and decided to use only the system-native
<code>undefined</code>.</p>

<p>In the database world, the situation is similar. Instead of constantly
dealing with whether something is an empty string or NULL, it's often simpler
to choose one approach and stick to it. For example, Oracle database essentially
equates empty strings and NULL values, thus elegantly avoiding this problem.
It's one of the places where Oracle deviates from the SQL standard, but it
simplifies working with empty/NULL values.</p>

<p>How can we achieve something similar in MySQL?</p>

<h2 id="toc-what-do-we-actually-want-to-enforce">What Do We Actually Want to
Enforce?</h2>

<ol>
	<li>For required fields (<code>NOT NULL</code>), we want to enforce that they
	always contain meaningful values. That means preventing empty strings (or
	strings containing only spaces)</li>

	<li>For optional fields (<code>NULL</code>), we want to prevent storing empty
	strings. When a field is optional, NULL should be the only representation of an
	“unfilled value”. Mixing both approaches in one column leads to problems
	with querying and JOIN operations, as we showed above.</li>
</ol>

<h2 id="toc-solution-in-mysql">Solution in MySQL</h2>

<p>Historically in MySQL, it made sense to use exclusively empty strings ('')
instead of NULL values. It was the only approach that could be enforced using
the <code>NOT NULL</code> constraint. If we wanted an automatically consistent
database, this was the only way.</p>

<p>However, there's one important case where this approach fails – when we
need a unique index on the column. MySQL considers multiple empty strings as the
same value, while multiple NULL values are considered different.</p>

<p>However, since MySQL version 8.0.16, we can use CHECK constraints and have
more control over what values we allow. We can, for example, enforce that a
column will either be NULL or contain a non-empty string:</p>

<pre
class="language-sql"><code>CREATE TABLE users (
    id INT PRIMARY KEY,

    -- Required field - must contain some non-empty text
    email VARCHAR(255) NOT NULL UNIQUE
        CONSTRAINT email_not_empty      -- rule name
        CHECK (email != &#039;&#039;),

    -- Optional field - either NULL or non-empty text
    nickname VARCHAR(255)
        CONSTRAINT nickname_not_empty
        CHECK (nickname IS NULL OR nickname != &#039;&#039;)
);
</code></pre>

<p>When creating a CHECK constraint, it's important to give it a meaningful
name using the CONSTRAINT keyword. This way, we get a meaningful error message
<b>Check constraint ‘nickname_not_empty’ is violated</b> instead of a
generic constraint violation notice. This significantly helps with debugging and
application maintenance.</p>

<p>The problem isn't just empty strings, but also strings containing only
spaces. We can improve the CHECK constraint solution using the <code>TRIM</code>
function:</p>

<pre
class="language-sql"><code>CREATE TABLE users (
    id INT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE
        CONSTRAINT email_not_empty
        CHECK (TRIM(email) != &#039;&#039;),
   ...
);
</code></pre>

<p>Now these validation bypass attempts won't work either:</p>

<pre
class="language-sql"><code>INSERT INTO users (email) VALUES (&#039;   &#039;);  -- all spaces
</code></pre>

<h2 id="toc-practical-solution-in-nette-framework">Practical Solution in Nette
Framework</h2>

<p>A consistent approach to empty values needs to be handled at the application
level too. If you're using Nette Framework, you can use an <a
href="https://doc.nette.org/en/forms/controls#toc-addtext">elegant solution</a>
using the <code>setNullable()</code> method:</p>

<pre
class="language-php"><code>$form = new Form;
$form-&gt;addText(&#039;billing_street&#039;)
    -&gt;setNullable(); // empty input transforms to NULL
</code></pre>

<h2 id="toc-recommendations-for-practice">Recommendations for Practice</h2>

<ol>
	<li>At the start of the project, decide on one approach:
		<ul>
			<li>Either use only NULL for missing values</li>

			<li>Or use only empty strings for empty/missing values</li>
		</ul>
	</li>

	<li>Document this decision in the project documentation</li>

	<li>Use CHECK constraints to enforce consistency</li>

	<li>For existing projects:
		<ul>
			<li>Conduct an audit of the current state</li>

			<li>Prepare a migration script to unify the approach</li>

			<li>Don't forget to adjust application logic</li>
		</ul>
	</li>
</ol>

<p>With this approach, you'll avoid many problems with comparing, indexing, and
JOIN operations that arise from mixing NULL and empty strings. Your database
will be more consistent and queries simpler.</p>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/how-to-deal-with-the-chaos-of-empty-strings-and-null-values-in-mysql#comments</comments>
		<pubDate>Thu, 06 Feb 2025 14:10:10 +0100</pubDate>
		<guid isPermaLink="false">item2359@http://phpfashion.com</guid>
	</item>
	<item>
		<title>Renaming ENUM Values Without Data Loss: A Safe Guide</title>
		<link>https://phpfashion.com/en/renaming-mysql-enum-without-data-loss</link>
		<description>Renaming values in a MySQL ENUM column can be tricky. Many
developers attempt a direct change, which often results in data loss or errors.
We&apos;ll show you the correct and safe way to do it.

Imagine a typical scenario: You have an orders table in your
database with a status column of type ENUM. It contains the values
waiting_payment, processing, shipped, and
cancelled. The requirement is to rename
waiting_payment to unpaid and shipped to
completed. How can this be done without risk?

What Doesn&apos;t Work

First, let&apos;s look at what does not work. Many developers try this
straightforward approach:

-- THIS DOES NOT WORK!
ALTER TABLE orders
MODIFY COLUMN status ENUM(
    &apos;unpaid&apos;,      -- previously &apos;waiting_payment&apos;
    &apos;processing&apos;,  -- unchanged
    &apos;completed&apos;,   -- previously &apos;shipped&apos;
    &apos;cancelled&apos;    -- unchanged
);


This approach is a recipe for disaster. MySQL will attempt to map existing
values to the new ENUM, and since the original values are no longer in the
definition, it will either replace them with an empty string or return the error
Data truncated for column &apos;status&apos; at row X. In a production
database, this would mean losing important data.

Backup First!

Before making any structural changes to your database, it is absolutely
crucial to create a data backup. Use MySQL-dump or another
trusted tool.

The Correct Approach

The correct approach consists of three steps:


	First, extend the ENUM with new values.

	Update the data.

	Finally, remove the old values.


Let&apos;s go through it step by step:

1. The first step is to add the new values to the ENUM while keeping the
original ones:

ALTER TABLE orders
MODIFY COLUMN status ENUM(
    &apos;waiting_payment&apos;,  -- original value
    &apos;processing&apos;,       -- unchanged
    &apos;shipped&apos;,         -- original value
    &apos;cancelled&apos;,       -- unchanged
    &apos;unpaid&apos;,          -- new value (replaces waiting_payment)
    &apos;completed&apos;        -- new value (replaces shipped)
);


2. Now we can safely update the existing data:

UPDATE orders SET status = &apos;unpaid&apos; WHERE status = &apos;waiting_payment&apos;;
UPDATE orders SET status = &apos;completed&apos; WHERE status = &apos;shipped&apos;;


3. Finally, once all data has been converted to the new values, we can
remove the old ones:

ALTER TABLE orders
MODIFY COLUMN status ENUM(
    &apos;unpaid&apos;,
    &apos;processing&apos;,
    &apos;completed&apos;,
    &apos;cancelled&apos;
);


Why Does This Work?

This works because of how MySQL handles ENUM values. When performing an
ALTER TABLE modification on an ENUM column, MySQL tries to map
existing values based on their textual representation. If the original value
does not exist in the new ENUM definition, MySQL will either throw an error (if
STRICT_ALL_TABLES is enabled in sql_mode) or replace
it with an empty string.

That&apos;s why it&apos;s crucial to have both old and new values present in the ENUM
simultaneously during the transition phase. In our case, this ensures that every
record in the database retains its exact textual equivalent. Only after
executing the UPDATE queries – when we are sure that all data is
using the new values – can we safely remove the old ones.
</description>
		<content:encoded><![CDATA[
		<p class="perex">Renaming values in a MySQL ENUM column can be tricky. Many
developers attempt a direct change, which often results in data loss or errors.
We'll show you the correct and safe way to do it.</p>

<p>Imagine a typical scenario: You have an <code>orders</code> table in your
database with a <code>status</code> column of type ENUM. It contains the values
<code>waiting_payment</code>, <code>processing</code>, <code>shipped</code>, and
<code>cancelled</code>. The requirement is to rename
<code>waiting_payment</code> to <code>unpaid</code> and <code>shipped</code> to
<code>completed</code>. How can this be done without risk?</p>

<h2 id="toc-what-doesn-t-work">What Doesn't Work</h2>

<p>First, let's look at what <b>does not work</b>. Many developers try this
straightforward approach:</p>

<pre
class="language-sql"><code>-- THIS DOES NOT WORK!
ALTER TABLE orders
MODIFY COLUMN status ENUM(
    &#039;unpaid&#039;,      -- previously &#039;waiting_payment&#039;
    &#039;processing&#039;,  -- unchanged
    &#039;completed&#039;,   -- previously &#039;shipped&#039;
    &#039;cancelled&#039;    -- unchanged
);
</code></pre>

<p>This approach is a recipe for disaster. MySQL will attempt to map existing
values to the new ENUM, and since the original values are no longer in the
definition, it will either replace them with an empty string or return the error
<code>Data truncated for column 'status' at row X</code>. In a production
database, this would mean losing important data.</p>

<h2 id="toc-backup-first">Backup First!</h2>

<p>Before making any structural changes to your database, it is absolutely
crucial to create a data backup. Use <a
href="https://github.com/dg/MySQL-dump">MySQL-dump</a> or another
trusted tool.</p>

<h2 id="toc-the-correct-approach">The Correct Approach</h2>

<p>The correct approach consists of three steps:</p>

<ol>
	<li>First, extend the ENUM with new values.</li>

	<li>Update the data.</li>

	<li>Finally, remove the old values.</li>
</ol>

<p>Let's go through it step by step:</p>

<p>1. The first step is to add the new values to the ENUM while keeping the
original ones:</p>

<pre
class="language-sql"><code>ALTER TABLE orders
MODIFY COLUMN status ENUM(
    &#039;waiting_payment&#039;,  -- original value
    &#039;processing&#039;,       -- unchanged
    &#039;shipped&#039;,         -- original value
    &#039;cancelled&#039;,       -- unchanged
    &#039;unpaid&#039;,          -- new value (replaces waiting_payment)
    &#039;completed&#039;        -- new value (replaces shipped)
);
</code></pre>

<p>2. Now we can safely update the existing data:</p>

<pre
class="language-sql"><code>UPDATE orders SET status = &#039;unpaid&#039; WHERE status = &#039;waiting_payment&#039;;
UPDATE orders SET status = &#039;completed&#039; WHERE status = &#039;shipped&#039;;
</code></pre>

<p>3. Finally, once all data has been converted to the new values, we can
remove the old ones:</p>

<pre
class="language-sql"><code>ALTER TABLE orders
MODIFY COLUMN status ENUM(
    &#039;unpaid&#039;,
    &#039;processing&#039;,
    &#039;completed&#039;,
    &#039;cancelled&#039;
);
</code></pre>

<h2 id="toc-why-does-this-work">Why Does This Work?</h2>

<p>This works because of how MySQL handles ENUM values. When performing an
<code>ALTER TABLE</code> modification on an ENUM column, MySQL tries to map
existing values based on their textual representation. If the original value
does not exist in the new ENUM definition, MySQL will either throw an error (if
<code>STRICT_ALL_TABLES</code> is enabled in <code>sql_mode</code>) or replace
it with an empty string.</p>

<p>That's why it's crucial to have both old and new values present in the ENUM
simultaneously during the transition phase. In our case, this ensures that every
record in the database retains its exact textual equivalent. Only after
executing the <code>UPDATE</code> queries – when we are sure that all data is
using the new values – can we safely remove the old ones.</p>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/renaming-mysql-enum-without-data-loss#comments</comments>
		<pubDate>Mon, 27 Jan 2025 02:16:00 +0100</pubDate>
		<guid isPermaLink="false">item2356@http://phpfashion.com</guid>
	</item>
	<item>
		<title>Property Hooks in PHP 8.4: Game Changer or Hidden Trap?</title>
		<link>https://phpfashion.com/en/property-hooks-in-php-8-4</link>
		<description>What if I told you your PHP objects could be cleaner, more elegant, and
easier to work with? Well, that dream is now a reality! PHP 8.4 introduces
revolutionary features called property hooks and asymmetric
visibility that completely transform object-oriented programming as we know
it. Say goodbye to clunky getters and setters – we now have a modern,
intuitive way to control object data access. Let&apos;s explore how these features
can revolutionize your code.

Property hooks provide a smart way to define what happens when you read from
or write to object properties – and they&apos;re much cleaner and more efficient
than the traditional magic methods __get/__set. Think of it as
getting all the power of magic methods without any of their usual drawbacks.

Let&apos;s look at a real-world example that shows why property hooks are so
valuable. Consider a common Person class with a public
age property:

class Person
{
	public int $age = 0;
}

$person = new Person;
$person-&gt;age = 25;  // OK
$person-&gt;age = -5;  // OK, but that makes no sense!


While PHP ensures the age will be an integer thanks to the int
type (available since PHP 7.4), what about that negative age? In the past, we&apos;d
need getters and setters, make the property private, and write a bunch of
boilerplate code. With hooks, there&apos;s a much more elegant solution:

class Person
{
	public int $age = 0 {
		set =&gt; $value &gt;= 0 ? $value : throw new InvalidArgumentException;
	}
}

$person-&gt;age = -5;  // Oops! InvalidArgumentException warns us about the invalid value


The beauty lies in its simplicity – from the outside, the property behaves
exactly like before. You can read and write directly through
$person-&gt;age, but now you have complete control over what
happens during the write operation. And that&apos;s just scratching the surface!

We can take it further and create hooks for reading too. Hooks can have
attributes, and they can contain complex logic beyond simple expressions. Check
out this example of working with names:

class Person
{
	public string $first;
	public string $last;
	public string $fullName {
		get {
			return &quot;$this-&gt;first $this-&gt;last&quot;;
		}
		set(string $value) {
			[$this-&gt;first, $this-&gt;last] = explode(&apos; &apos;, $value, 2);
		}
	}
}

$person = new Person;
$person-&gt;fullName = &apos;James Bond&apos;;
echo $person-&gt;first;  // outputs &apos;James&apos;
echo $person-&gt;last;   // outputs &apos;Bond&apos;


Here&apos;s something crucial to understand: hooks are always used whenever a
property is accessed (even within the Person class itself). The only exception
is when you directly access the actual variable inside the hook code.

A Blast from the
Past: Lessons from SmartObject

For those familiar with Nette Framework, here&apos;s an interesting historical
perspective. The framework offered similar functionality 17 years ago
through SmartObject,
which significantly enhanced object handling at a time when PHP was quite
limited in this area.

I remember the initial wave of overwhelming enthusiasm where developers used
properties everywhere, followed by a complete reversal where they avoided them
entirely. Why? There weren&apos;t clear guidelines about when to use methods versus
properties. But today&apos;s native solution is in a different league altogether.
Property hooks and asymmetric visibility are fully-fledged tools that provide
the same level of control as methods. This makes it much easier to determine
when a property is truly the right choice.

Backed or Virtual? The Key
Question!

Take a look at this code and try to answer quickly – it&apos;s a
little quiz:


	Can we write to $age?

	What about $adult – can we both read and write to it?


class Person
{
	public int $age = 0 {
		set =&gt; $value &gt;= 0 ? $value : throw new InvalidArgumentException;
	}

	public bool $adult {
		get =&gt; $this-&gt;age &gt;= 18;
	}
}


As we discussed earlier, $age is both readable and writable. But
surprise – $adult is read-only!

This brings us to the first tricky aspect of property hooks design. The
property signature doesn&apos;t tell us whether we can read from or write
to it!

The answer lies hidden in the hook implementation code. Properties come in
two flavors: backed (with actual memory storage) and virtual (which only
simulate a property&apos;s existence). What determines if a property is backed or
virtual? It&apos;s whether we reference it in the hook code.

A property is backed (has its own storage) when:


	we reference it in the hook body using
	$this-&gt;propertyName

	or it has a shortened set, which implicitly means writing
	to $this-&gt;propertyName


Looking at our example:


	Property $age is backed because it uses the shortened
	set (which implicitly writes to $this-&gt;age)

	Property $adult is virtual because none of its hooks
	uses $this-&gt;adult


While this is a clever solution, it&apos;s not ideal. Such crucial information
about whether a property is readable or writable should be immediately visible
in the API and signature, not buried in the implementation details.

References: Playing it Safe

References have been a part of PHP since its early days. Using the
&amp; symbol, you can link two variables so they point to the same
memory location. It&apos;s like having two remote controls for one TV – press
either one, and you&apos;re controlling the same screen.

But what if someone could get a reference to a property with a
set hook? They could modify its value directly, completely
bypassing all validation. Here&apos;s what that might look like:

class Person
{
	public int $age = 0 {
		set =&gt; $value &gt;= 0 ? $value : throw new InvalidArgumentException;
	}
}

$person = new Person;
$ref = &amp;$person-&gt;age;    // Fatal error: This isn&apos;t allowed!
$ref = -5;               // If it worked, it would make our validation useless


PHP (or more specifically, Ilija Tovilo and Larry Garfield, the hook
feature&apos;s authors) thought this through and came up with an elegant solution.
It&apos;s simply impossible to get a reference to such a property (specifically, to
a backed variable with a set hook). This makes perfect sense –
property hooks ensure that only valid values can be stored, and references would
provide a backdoor around this protection.

Arrays Meet
Property Hooks – An Interesting Challenge!

Working with arrays in PHP is usually straightforward and intuitive. We can
add elements to an array property in several ways:

class Person
{
	public array $phones = [];
}

$person = new Person;
$person-&gt;phones[] = &apos;777 123 456&apos;;          // adds number to the end of array
$person-&gt;phones[&apos;bob&apos;] = &apos;777 123 456&apos;;     // adds number with specific key


Here&apos;s where we run into an interesting challenge with property hooks. Say
we want to create a Person class that contains a list of phone numbers, and we
want to automatically trim whitespace from the beginning and end of each
number:

class Person
{
	public array $phones = [] {
		set =&gt; array_map(&apos;trim&apos;, $value);
	}
}

$person = new Person;
$person-&gt;phones[] = &apos;777 123 456&apos;;  // Surprise! Error: Indirect modification of Person::$phones is not allowed


So why doesn&apos;t this work? The operation $person-&gt;phones[] in
PHP actually happens in two steps:


	First, it gets a reference to the array through get

	Then it adds the new value to that array


This means the set hook never gets called. What&apos;s more, as we
learned earlier, we can&apos;t get a reference to a backed variable with a
set hook (that first step). That&apos;s why we get the error
message.

Even creating a method like addPhone() that calls
$this-&gt;phones[] = $phone won&apos;t help – remember, all property
access (even within the class) goes through hooks.

So what&apos;s the solution? Let&apos;s explore our options. Here&apos;s the first
approach that might come to mind:

$phones = $person-&gt;phones;    // read the array
$phones[] = &apos; 777 123 456 &apos;;  // add a number
$person-&gt;phones = $phones;    // save it back


Sure, it works, but… imagine having an array with thousands of numbers. Our
set hook would need to run trim() on every single
number again, even though we only added one. Not exactly efficient, right?

There&apos;s a better way – we need to realize that if an array needs special
handling of its elements (like trimming whitespace), that should be its
responsibility, not the job of the class that just happens to hold it. While
we can&apos;t teach new tricks to a basic array, we can “wrap” it in an object
that implements ArrayAccess:

class Phones implements ArrayAccess
{
	private array $data = [];

	public function __construct(array $data = [])
	{
		$this-&gt;data = array_map(&apos;trim&apos;, $data);
	}

	public function offsetSet(mixed $offset, mixed $value): void
	{
		$value = trim($value);
		if ($offset === null) {
			$this-&gt;data[] = $value;
		} else {
			$this-&gt;data[$offset] = $value;
		}
	}

	// implementation of other ArrayAccess methods...
}

class Person
{
	function __construct(
		public Phones $phones = new Phones,
	) {}
}

$person = new Person;
$person-&gt;phones[] = &apos; 777 123 456 &apos;;  // Perfect! The number is stored with whitespace trimmed


And here&apos;s the cherry on top – we can use a hook to allow writing a
regular array to $person-&gt;phones:

class Person
{
	function __construct(
		public Phones $phones = new Phones {
			set(array|Phones $value) =&gt; is_array($value) ? new Phones($value) : $value;
		},
	) {}
}

$person = new Person;
$person-&gt;phones = [&apos;  888 999 000  &apos;, &apos;777 888 999&apos;];  // Automatically converts to Phones and trims strings


As you can see, hooks work with promoted properties too.

Let&apos;s look at another approach. Remember that besides backed properties, we
also have virtual properties – those that don&apos;t use
$this-&gt;propertyName in their hook body. This gives us another
solution:

class Person
{
	private array $_phones = []; // actual storage for phone numbers

	public array $phones {  // virtual property for public access
		get =&gt; $this-&gt;_phones;
		set {
			$this-&gt;_phones = array_map(&apos;trim&apos;, $value);
		}
	}

	public function addPhone(string $phone): void
	{
		$this-&gt;_phones[] = trim($phone);
	}
}

$person = new Person;
$person-&gt;addPhone(&apos; 777 123 456 &apos;);  // Adds trimmed number
echo $person-&gt;phones[0];             // Shows &quot;777 123 456&quot;
$person-&gt;phones = [&apos;  888 999 000  &apos;]; // Sets new array with trimmed numbers


In this approach, we stick with a regular array but hide it behind a private
variable. To the outside world, we offer a virtual property for reading the
entire array and completely replacing it, plus a dedicated method for adding
individual numbers.

Hooks and Inheritance:
Passing the Torch

Children classes can not only add hooks to properties that didn&apos;t have them
before but also redefine existing ones. Here&apos;s an example:

class Person
{
	public string $email;

	public int $age {
		set =&gt; $value &gt;= 0
			? $value
			: throw new InvalidArgumentException(&apos;Age cannot be negative&apos;);
	}
}

class Employee extends Person
{
	// Adds hook to property that previously had none
	public string $email {
		set =&gt; strtolower($value);  // Always convert emails to lowercase
	}

	// Extends existing age validation
	public int $age {
		set {
			if ($value &lt;= 130) {  // First check the original condition
				throw new InvalidArgumentException(&apos;130 years? Not buying it!&apos;);
			}
			parent::$age::set($value);
		}
	}
}


Notice that interesting syntax parent::$age::set($value). While
it might look unusual at first, it makes perfect sense – we first reference
the property in the parent class, then its hook. It&apos;s like saying “hey, call
the set hook on my parent&apos;s age property”.

And there&apos;s more – we can mark hooks as final if we want to
prevent children from overriding them. We can even mark the entire property as
final – then children can&apos;t modify it in any way (neither add
hooks nor extend its visibility).

class Person
{
	// No one can override this hook
	public int $age {
		final set =&gt; $value &gt;= 0 ? $value : throw new InvalidArgumentException;
	}

	// And no one can modify this property at all
	final public string $id;
}


Properties in Interfaces:
A Game Changer

One of the most exciting new features is support for properties in interfaces
and abstract classes. Imagine you&apos;re creating an interface for entities that
contain a name string. Previously, we had to write something like this:

interface Named
{
	public function getName(): string;
	public function setName(string $name): void;
}


Pretty tedious, right? With property hooks, we can be much more elegant! We
can now declare properties directly in the interface, and even do it
asymmetrically – specifying separately what should be

interface Named
{
	// We&apos;re declaring: &quot;Any implementing class must provide a publicly readable name property&quot;
	public string $name { get; }
}


Now here&apos;s the interesting part – how do we implement such an interface?
We have several elegant options:

class Person implements Named
{
	public string $name;     // Simplest approach - a regular property
}

class Employee implements Named
{
	public string $name {    // More sophisticated - composite name
		get =&gt; $this-&gt;firstName . &apos; &apos; . $this-&gt;lastName;
	}
	private string $firstName;
	private string $lastName;
}


Notice something interesting – while the Named interface only
requires a read-only property, the Person class provides one
that&apos;s both readable and writable. This is perfectly fine – interfaces
define minimum requirements. It&apos;s like ordering a car that “must drive
forward” and getting one that can also reverse – it exceeds the minimum
requirements in a useful way.

A technical note for the detail-oriented: In interfaces, we must use the
public keyword with properties, even though it&apos;s redundant since
everything in an interface is inherently public. While using public
with methods would be redundant, it&apos;s required for properties to maintain
syntax consistency.

Here&apos;s another interesting detail – did you notice that unusual
{ get; set; } syntax? While in a class we can simply write
public string $name, interfaces require us to explicitly state
which operations the property supports. Though it requires more typing, this
makes sense – with interfaces, we want to be crystal clear about our
requirements.

Properties
in Abstract Classes: Getting the Best of Both Worlds

Abstract classes combine the best features of interfaces with their own
special capabilities. They can not only declare properties but also provide
default implementations for some hooks:

abstract class Person
{
	// Pure abstract property - child must implement
	abstract public string $name { get; }

	// Protected property supporting both operations
	abstract protected int $age { get; set; }

	// Here we provide ready-made email validation
	abstract public string $email {
		get; // this hook is abstract and must be implemented by child
		set =&gt; Nette\Utils\Validators::isEmail($value)
			? $value
			: throw new InvalidArgumentException(&apos;This doesn\&apos;t look like a valid email...&apos;);
	}
}


Covariance
and Contravariance: Not as Scary as They Sound!

These terms might sound intimidating, but the concept is actually quite
straightforward. Let&apos;s look at an example:

class Animal {}
class Dog extends Animal {}

interface PetShop
{
	// Read-only property can return a more specific type
	public Animal $pet { get; }
}

class DogShop implements PetShop
{
	// Returns a dog instead of an animal - perfectly valid!
	public Dog $pet { get; }
}


When a property has only a get hook, it can return a more
specific type in the child class (this is called covariance). Think of it this
way: “I promised you an animal, and a dog is definitely an animal,
right?”

On the flip side, a property with only a set hook can accept a
more general type in the child (contravariance). This makes sense – if you
can handle a specific type, you can handle its parent type too.

However, when a property has both get and set
hooks, the type must stay the same. Why? Because it could lead to
inconsistencies – we can&apos;t promise to return a dog when someone might try to
set a cat through the setter!

Asymmetric
Visibility: Fine-Tuned Access Control

Imagine you&apos;re building a Person class where you want everyone to be able to
read the date of birth, but only the class itself should be able to change it.
In the past, this meant getters and setters, but now we have an elegant
solution:

class Person
{
	public private(set) DateTimeImmutable $dateOfBirth;
}


This elegant syntax says: “Anyone can read it, but only the class itself
can write to it.” The first modifier public controls read access,
while private(set) controls write access. Since public reading is
the default, we can make it even simpler:

class Person
{
	private(set) DateTimeImmutable $dateOfBirth;
}


Naturally, there&apos;s a logical rule – write visibility can&apos;t be broader
than read visibility. You can&apos;t use something like
protected public(set) – that would be like saying “only
descendants can read it, but anyone can write to it.” Doesn&apos;t make much
sense, right?

What about inheritance? In PHP, a child class can either keep the same
visibility or expand it from protected to public. The same principle applies to
asymmetric visibility:

class Person
{
	public protected(set) string $name;  // Anyone can read, only descendants can write
}

class Employee extends Person
{
	public public(set) string $name;     // Child expands writing rights
}


An interesting case is private(set). Such a property is
automatically final – when we say only the class itself can
write to it, that logically means not even child classes can change this
behavior.

Best of all, we can combine asymmetric visibility with hooks:

class Person
{
	private(set) DateTimeImmutable $birthDate {
		set =&gt; $value &gt; new DateTimeImmutable
			? throw new InvalidArgumentException(&apos;Birth in the future? Nice sci-fi!&apos;)
			: $value;
	}
}


This property has it all: it&apos;s publicly readable, only the class itself can
write to it, and it validates that the date isn&apos;t in the future. Hooks handle
“what should happen,” while asymmetric visibility controls “who can do
it.” A perfect combination!

Asymmetric
Visibility and Arrays: A Smart Solution to an Old Problem

Remember our phone number challenge? Asymmetric visibility offers us another
elegant solution:

class Person
{
	private(set) array $phones = [];

	public function addPhone(string $phone): void
	{
		$this-&gt;phones[] = trim($phone);
	}
}

$person = new Person;
var_dump($person-&gt;phones);     // OK: we can read the array
$person-&gt;addPhone(&apos;...&apos;);      // OK: we can add a number
$person-&gt;phones = [];          // ERROR: we can&apos;t overwrite the entire array


The array is publicly readable, but no one from outside can overwrite it. We
provide a specialized method for adding new numbers. No complex array-simulating
objects, no virtual properties – just clean, clear access control.

It&apos;s worth noting that you can&apos;t get a reference from outside to a property
with restricted write access:

$ref = &amp;$person-&gt;phones;    // Fatal error: Not allowed!


References are only allowed from scopes where the property is writable. This
makes sense – a reference could bypass our write restrictions.

To summarize, we now have four solid approaches for working with arrays in
properties:


	Smart object simulating an array (offers more features but requires
	more code)

	Backed property with hook (prevents direct array modification)

	Virtual property with private storage (requires methods for
	modifications)

	Asymmetric visibility (moves logic to methods)


Which approach should you choose? As with many things in programming – it
depends on your specific needs. Try them out and see which API feels most
natural for your use case.

Readonly and
Asymmetric Visibility: Freedom at Last!

The readonly modifier
actually combines two features: it prevents multiple writes and restricts
writing to private scope. It&apos;s not really “readonly” – it&apos;s more like
“writeonce” combined with private(set).

The second part always felt unnecessarily restrictive. Why shouldn&apos;t a
readonly property be writable in child classes?

PHP 8.4 finally addresses this. Now readonly makes properties
protected(set) by default, meaning they&apos;re writable in child
classes too. And if we need different visibility? We can simply specify it:

class Person
{
	// Readonly accessible only inside the class (old behavior)
	public private(set) readonly string $name;

	// Readonly accessible in children (new default behavior)
	public readonly string $dateOfBirth;

	// Readonly writable anywhere (but only once!)
	public public(set) readonly string $id;

	public function rename(string $newName): void
	{
		$this-&gt;name = $newName;    // Inside class we can modify it
	}
}

class Employee extends Person
{
	public function setBirthDate(DateTimeImmutable $date): void
	{
		$this-&gt;dateOfBirth = $date;  // In child class we can modify it
	}
}

$person = new Person;
$person-&gt;id = &apos;abc123&apos;;     // This works
$person-&gt;id = &apos;xyz789&apos;;     // But this FAILS - it&apos;s readonly!


This gives us exactly the flexibility we need while maintaining safety.

When Terminology Clashes…

Let&apos;s examine an interesting inconsistency in PHP&apos;s terminology:


	We consistently talk about reading and writing properties

	We have the readonly modifier

	In phpDoc, we find @property-read and
	@property-write annotations

	Yet, in hooks and asymmetric visibility, we suddenly switch
	to get/set


Wouldn&apos;t using read and write terms make more
logical sense?

For hooks, the use of get/set is somewhat understandable –
they represent actions and align with the __get/__set magic
methods. But for asymmetric visibility? That&apos;s an entirely different
concept – it&apos;s not about “what should happen” like hooks, but rather
“who has permission to do it”. This is why using the term
write, as in private(write), would make much
more sense:

class Person
{
	private(set) string $name;     // current syntax
	private(write) string $name;   // more intuitive syntax
}


The second version would feel more natural. Moreover, it would better align
with the existing readonly modifier.

It appears that in PHP&apos;s pursuit of syntactic consistency between hooks and
asymmetric visibility, they inadvertently sacrificed semantic consistency with
the language&apos;s existing concepts.

A New Era in PHP:
The Object Design Revolution

For years, the PHP world adhered to a single “correct” approach to
object-oriented design: make all properties private and access them exclusively
through getters and setters. This wasn&apos;t just developers being fussy – public
properties came with genuine problems:


	They were completely exposed with no access control

	As part of the public API, any modification (like adding validation) would
	break backward compatibility

	In interfaces, they could only exist as comments


Anyone wanting to write robust code using interfaces and dependency
injection had no choice but to fall back on getters and setters. It was the
only path to maintaining full control over object behavior.

But PHP 8.4 ushers in a new era! Property hooks and asymmetric visibility
finally give us the same level of control over properties that we&apos;ve always had
with methods. Properties now become first-class citizens in the public API
because:


	We can introduce validation or value transformation whenever needed

	We have fine-grained control over access permissions

	We can properly declare them in interfaces


You can think of property hooks as an elegant, boilerplate-free replacement
for getters and setters. Or perhaps more accurately – getters and setters
were just a stopgap until PHP evolved to something better.

Speaking from extensive experience with Nette, where similar functionality
has existed for 17 years, I can attest to how game-changing this approach is.
Once you try it, there&apos;s no going back. Consider this comparison:

// Traditional approach
$this-&gt;getUser()-&gt;getIdentity()-&gt;getName()

// Modern approach
$this-&gt;user-&gt;identity-&gt;name


The second version isn&apos;t just more concise and readable – it feels more
natural. It&apos;s like the difference between formally asking “Would you be so
kind as to provide me with your name?” versus simply asking
“What&apos;s your name?”

Sure, some might argue that direct property access could tempt developers to
violate object-oriented principles, suggesting we should tell objects what to do
rather than ask for data (Tell-Don&apos;t-Ask). That&apos;s valid – but primarily for
objects with rich behavior implementing business logic. For data transfer
objects, value objects, or configuration classes, direct data access makes
perfect sense.

This does present an interesting challenge: what about existing projects? If
your library or framework consistently uses getters and setters, suddenly
introducing properties might create inconsistency. Users would need to guess
whether to use a method or a property in each case.

Over time, new conventions will emerge. Some projects may stick with getters
and setters, while others will embrace properties. The key is that we now have
options.

Naming Matters

How should we name our properties? With boolean values especially, it&apos;s not
as straightforward as you might think.

With methods, we commonly use prefixes like is or
has:

class Article {
	public function isPublished(): bool { ... }
	public function hasComments(): bool { ... }
}


But for properties, these prefixes feel awkward and redundant. A better
approach is to use adjectives or nouns:

class Article {
	public bool $published;     // better than $isPublished
	public bool $commented;     // better than $hasComments
	public bool $draft;         // better than $isDraft
}

if ($article-&gt;published) {      // reads naturally
	// ...
}


For quantities, plural forms work better:

class Article {
	public int $views;          // better than $viewCount
	public array $tags;         // clearly indicates a collection
}


The goal is to make your code read like natural language. When we write
if ($article-&gt;published), it flows much more naturally than
if ($article-&gt;isPublished). Properties should feel like
attributes, not methods missing their parentheses.

When to Choose Properties vs
Methods?

This is a crucial question! We can learn from languages like C# and Kotlin,
which have years of experience with properties. Properties excel for:

Value objects and DTOs:

class Money {
	public readonly float $amount;
	public readonly string $currency;
}


Simple entities:

class Article {
	public string $title;
	public string $content;
	public DateTimeImmutable $publishedAt;
	public bool $published {
		get =&gt; $this-&gt;publishedAt &lt;= new DateTimeImmutable;
	}
}


Computed values that depend on other properties:

class Rectangle {
	public float $width;
	public float $height;
	public float $area {
		get =&gt; $this-&gt;width * $this-&gt;height;
	}
}


Methods are better suited for:


	operations that work with multiple properties together

	operations with side effects (logging, notifications)

	actions that perform tasks (save, send, calculate…)

	complex validations or business logic

	operations that might fail for various reasons

	cases where you want a fluent
	interface


All these guidelines point to one fundamental principle: property access
shouldn&apos;t hide complex processes or side effects. The complexity of the
operation should match what we intuitively expect when reading or writing to a
variable.

Though… consider innerHTML in JavaScript. When you write
element.innerHTML = &apos;&lt;p&gt;Hello&lt;/p&gt;&apos;, it triggers a
complex chain of events – HTML parsing, DOM tree creation, page reflow… Yet
everyone finds this natural!

So perhaps what matters more than implementation complexity is whether the
operation _conceptually_ feels like a property. It&apos;s similar to a
car&apos;s start/stop button – it might trigger a complex sequence internally,
but to the driver, it&apos;s simply “on/off”.
</description>
		<content:encoded><![CDATA[
		<p>What if I told you your PHP objects could be cleaner, more elegant, and
easier to work with? Well, that dream is now a reality! PHP 8.4 introduces
revolutionary features called <b>property hooks</b> and <b>asymmetric
visibility</b> that completely transform object-oriented programming as we know
it. Say goodbye to clunky getters and setters – we now have a modern,
intuitive way to control object data access. Let's explore how these features
can revolutionize your code.</p>

<p>Property hooks provide a smart way to define what happens when you read from
or write to object properties – and they're much cleaner and more efficient
than the traditional magic methods <code>__get/__set</code>. Think of it as
getting all the power of magic methods without any of their usual drawbacks.</p>

<p>Let's look at a real-world example that shows why property hooks are so
valuable. Consider a common <code>Person</code> class with a public
<code>age</code> property:</p>

<pre
class="language-php"><code>class Person
{
	public int $age = 0;
}

$person = new Person;
$person-&gt;age = 25;  // OK
$person-&gt;age = -5;  // OK, but that makes no sense!
</code></pre>

<p>While PHP ensures the age will be an integer thanks to the <code>int</code>
type (available since PHP 7.4), what about that negative age? In the past, we'd
need getters and setters, make the property private, and write a bunch of
boilerplate code. With hooks, there's a much more elegant solution:</p>

<pre
class="language-php"><code>class Person
{
	public int $age = 0 {
		set =&gt; $value &gt;= 0 ? $value : throw new InvalidArgumentException;
	}
}

$person-&gt;age = -5;  // Oops! InvalidArgumentException warns us about the invalid value
</code></pre>

<p>The beauty lies in its simplicity – from the outside, the property behaves
exactly like before. You can read and write directly through
<code>$person-&gt;age</code>, but now you have complete control over what
happens during the write operation. And that's just scratching the surface!</p>

<p>We can take it further and create hooks for reading too. Hooks can have
attributes, and they can contain complex logic beyond simple expressions. Check
out this example of working with names:</p>

<pre
class="language-php"><code>class Person
{
	public string $first;
	public string $last;
	public string $fullName {
		get {
			return &quot;$this-&gt;first $this-&gt;last&quot;;
		}
		set(string $value) {
			[$this-&gt;first, $this-&gt;last] = explode(&#039; &#039;, $value, 2);
		}
	}
}

$person = new Person;
$person-&gt;fullName = &#039;James Bond&#039;;
echo $person-&gt;first;  // outputs &#039;James&#039;
echo $person-&gt;last;   // outputs &#039;Bond&#039;
</code></pre>

<p>Here's something crucial to understand: hooks are always used whenever a
property is accessed (even within the Person class itself). The only exception
is when you directly access the actual variable inside the hook code.</p>

<h2 id="toc-a-blast-from-the-past-lessons-from-smartobject">A Blast from the
Past: Lessons from SmartObject</h2>

<p>For those familiar with Nette Framework, here's an interesting historical
perspective. The framework offered similar functionality <b>17 years ago</b>
through <a href="https://doc.nette.org/en/utils/smartobject">SmartObject</a>,
which significantly enhanced object handling at a time when PHP was quite
limited in this area.</p>

<p>I remember the initial wave of overwhelming enthusiasm where developers used
properties everywhere, followed by a complete reversal where they avoided them
entirely. Why? There weren't clear guidelines about when to use methods versus
properties. But today's native solution is in a different league altogether.
Property hooks and asymmetric visibility are fully-fledged tools that provide
the same level of control as methods. This makes it much easier to determine
when a property is truly the right choice.</p>
<!--more-->
<h2 id="toc-backed-or-virtual-the-key-question">Backed or Virtual? The Key
Question!</h2>

<p>Take a look at this code and try to answer quickly – it's a
little quiz:</p>

<ul>
	<li>Can we write to <code>$age</code>?</li>

	<li>What about <code>$adult</code> – can we both read and write to it?</li>
</ul>

<pre
class="language-php"><code>class Person
{
	public int $age = 0 {
		set =&gt; $value &gt;= 0 ? $value : throw new InvalidArgumentException;
	}

	public bool $adult {
		get =&gt; $this-&gt;age &gt;= 18;
	}
}
</code></pre>

<p>As we discussed earlier, <code>$age</code> is both readable and writable. But
surprise – <code>$adult</code> is read-only!</p>

<p>This brings us to the first tricky aspect of property hooks design. <b>The
property signature doesn't tell us whether we can read from or write
to it!</b></p>

<p>The answer lies hidden in the hook implementation code. Properties come in
two flavors: backed (with actual memory storage) and virtual (which only
simulate a property's existence). What determines if a property is backed or
virtual? It's whether we reference it in the hook code.</p>

<p>A property is backed (has its own storage) when:</p>

<ul>
	<li>we reference it in the hook body using
	<code>$this-&gt;propertyName</code></li>

	<li>or it has a shortened <code>set</code>, which implicitly means writing
	to <code>$this-&gt;propertyName</code></li>
</ul>

<p>Looking at our example:</p>

<ul>
	<li>Property <code>$age</code> is <b>backed</b> because it uses the shortened
	<code>set</code> (which implicitly writes to <code>$this-&gt;age</code>)</li>

	<li>Property <code>$adult</code> is <b>virtual</b> because none of its hooks
	uses <code>$this-&gt;adult</code></li>
</ul>

<p>While this is a clever solution, it's not ideal. Such crucial information
about whether a property is readable or writable should be immediately visible
in the API and signature, not buried in the implementation details.</p>

<h2 id="toc-references-playing-it-safe">References: Playing it Safe</h2>

<p>References have been a part of PHP since its early days. Using the
<code>&amp;</code> symbol, you can link two variables so they point to the same
memory location. It's like having two remote controls for one TV – press
either one, and you're controlling the same screen.</p>

<p>But what if someone could get a reference to a property with a
<code>set</code> hook? They could modify its value directly, completely
bypassing all validation. Here's what that might look like:</p>

<pre
class="language-php"><code>class Person
{
	public int $age = 0 {
		set =&gt; $value &gt;= 0 ? $value : throw new InvalidArgumentException;
	}
}

$person = new Person;
$ref = &amp;$person-&gt;age;    // Fatal error: This isn&#039;t allowed!
$ref = -5;               // If it worked, it would make our validation useless
</code></pre>

<p>PHP (or more specifically, Ilija Tovilo and Larry Garfield, the hook
feature's authors) thought this through and came up with an elegant solution.
It's simply impossible to get a reference to such a property (specifically, to
a backed variable with a <code>set</code> hook). This makes perfect sense –
property hooks ensure that only valid values can be stored, and references would
provide a backdoor around this protection.</p>

<h2 id="toc-arrays-meet-property-hooks-an-interesting-challenge">Arrays Meet
Property Hooks – An Interesting Challenge!</h2>

<p>Working with arrays in PHP is usually straightforward and intuitive. We can
add elements to an array property in several ways:</p>

<pre
class="language-php"><code>class Person
{
	public array $phones = [];
}

$person = new Person;
$person-&gt;phones[] = &#039;777 123 456&#039;;          // adds number to the end of array
$person-&gt;phones[&#039;bob&#039;] = &#039;777 123 456&#039;;     // adds number with specific key
</code></pre>

<p>Here's where we run into an interesting challenge with property hooks. Say
we want to create a Person class that contains a list of phone numbers, and we
want to automatically trim whitespace from the beginning and end of each
number:</p>

<pre
class="language-php"><code>class Person
{
	public array $phones = [] {
		set =&gt; array_map(&#039;trim&#039;, $value);
	}
}

$person = new Person;
$person-&gt;phones[] = &#039;777 123 456&#039;;  // Surprise! Error: Indirect modification of Person::$phones is not allowed
</code></pre>

<p>So why doesn't this work? The operation <code>$person-&gt;phones[]</code> in
PHP actually happens in two steps:</p>

<ol>
	<li>First, it gets a reference to the array through <code>get</code></li>

	<li>Then it adds the new value to that array</li>
</ol>

<p>This means the <code>set</code> hook never gets called. What's more, as we
learned earlier, we can't get a reference to a backed variable with a
<code>set</code> hook (that first step). That's why we get the error
message.</p>

<p>Even creating a method like <code>addPhone()</code> that calls
<code>$this-&gt;phones[] = $phone</code> won't help – remember, all property
access (even within the class) goes through hooks.</p>

<p>So what's the solution? Let's explore our options. Here's the first
approach that might come to mind:</p>

<pre
class="language-php"><code>$phones = $person-&gt;phones;    // read the array
$phones[] = &#039; 777 123 456 &#039;;  // add a number
$person-&gt;phones = $phones;    // save it back
</code></pre>

<p>Sure, it works, but… imagine having an array with thousands of numbers. Our
<code>set</code> hook would need to run <code>trim()</code> on every single
number again, even though we only added one. Not exactly efficient, right?</p>

<p>There's a better way – we need to realize that if an array needs special
handling of its elements (like trimming whitespace), that should be <b>its
responsibility</b>, not the job of the class that just happens to hold it. While
we can't teach new tricks to a basic array, we can “wrap” it in an object
that implements ArrayAccess:</p>

<pre
class="language-php"><code>class Phones implements ArrayAccess
{
	private array $data = [];

	public function __construct(array $data = [])
	{
		$this-&gt;data = array_map(&#039;trim&#039;, $data);
	}

	public function offsetSet(mixed $offset, mixed $value): void
	{
		$value = trim($value);
		if ($offset === null) {
			$this-&gt;data[] = $value;
		} else {
			$this-&gt;data[$offset] = $value;
		}
	}

	// implementation of other ArrayAccess methods...
}

class Person
{
	function __construct(
		public Phones $phones = new Phones,
	) {}
}

$person = new Person;
$person-&gt;phones[] = &#039; 777 123 456 &#039;;  // Perfect! The number is stored with whitespace trimmed
</code></pre>

<p>And here's the cherry on top – we can use a hook to allow writing a
regular array to <code>$person-&gt;phones</code>:</p>

<pre
class="language-php"><code>class Person
{
	function __construct(
		public Phones $phones = new Phones {
			set(array|Phones $value) =&gt; is_array($value) ? new Phones($value) : $value;
		},
	) {}
}

$person = new Person;
$person-&gt;phones = [&#039;  888 999 000  &#039;, &#039;777 888 999&#039;];  // Automatically converts to Phones and trims strings
</code></pre>

<p>As you can see, hooks work with promoted properties too.</p>

<p>Let's look at another approach. Remember that besides backed properties, we
also have virtual properties – those that don't use
<code>$this-&gt;propertyName</code> in their hook body. This gives us another
solution:</p>

<pre
class="language-php"><code>class Person
{
	private array $_phones = []; // actual storage for phone numbers

	public array $phones {  // virtual property for public access
		get =&gt; $this-&gt;_phones;
		set {
			$this-&gt;_phones = array_map(&#039;trim&#039;, $value);
		}
	}

	public function addPhone(string $phone): void
	{
		$this-&gt;_phones[] = trim($phone);
	}
}

$person = new Person;
$person-&gt;addPhone(&#039; 777 123 456 &#039;);  // Adds trimmed number
echo $person-&gt;phones[0];             // Shows &quot;777 123 456&quot;
$person-&gt;phones = [&#039;  888 999 000  &#039;]; // Sets new array with trimmed numbers
</code></pre>

<p>In this approach, we stick with a regular array but hide it behind a private
variable. To the outside world, we offer a virtual property for reading the
entire array and completely replacing it, plus a dedicated method for adding
individual numbers.</p>

<h2 id="toc-hooks-and-inheritance-passing-the-torch">Hooks and Inheritance:
Passing the Torch</h2>

<p>Children classes can not only add hooks to properties that didn't have them
before but also redefine existing ones. Here's an example:</p>

<pre
class="language-php"><code>class Person
{
	public string $email;

	public int $age {
		set =&gt; $value &gt;= 0
			? $value
			: throw new InvalidArgumentException(&#039;Age cannot be negative&#039;);
	}
}

class Employee extends Person
{
	// Adds hook to property that previously had none
	public string $email {
		set =&gt; strtolower($value);  // Always convert emails to lowercase
	}

	// Extends existing age validation
	public int $age {
		set {
			if ($value &lt;= 130) {  // First check the original condition
				throw new InvalidArgumentException(&#039;130 years? Not buying it!&#039;);
			}
			parent::$age::set($value);
		}
	}
}
</code></pre>

<p>Notice that interesting syntax <code>parent::$age::set($value)</code>. While
it might look unusual at first, it makes perfect sense – we first reference
the property in the parent class, then its hook. It's like saying “hey, call
the set hook on my parent's age property”.</p>

<p>And there's more – we can mark hooks as <code>final</code> if we want to
prevent children from overriding them. We can even mark the entire property as
<code>final</code> – then children can't modify it in any way (neither add
hooks nor extend its visibility).</p>

<pre
class="language-php"><code>class Person
{
	// No one can override this hook
	public int $age {
		final set =&gt; $value &gt;= 0 ? $value : throw new InvalidArgumentException;
	}

	// And no one can modify this property at all
	final public string $id;
}
</code></pre>

<h2 id="toc-properties-in-interfaces-a-game-changer">Properties in Interfaces:
A Game Changer</h2>

<p>One of the most exciting new features is support for properties in interfaces
and abstract classes. Imagine you're creating an interface for entities that
contain a name string. Previously, we had to write something like this:</p>

<pre
class="language-php"><code>interface Named
{
	public function getName(): string;
	public function setName(string $name): void;
}
</code></pre>

<p>Pretty tedious, right? With property hooks, we can be much more elegant! We
can now declare properties directly in the interface, and even do it
asymmetrically – specifying separately what should be</p>

<pre
class="language-php"><code>interface Named
{
	// We&#039;re declaring: &quot;Any implementing class must provide a publicly readable name property&quot;
	public string $name { get; }
}
</code></pre>

<p>Now here's the interesting part – how do we implement such an interface?
We have several elegant options:</p>

<pre
class="language-php"><code>class Person implements Named
{
	public string $name;     // Simplest approach - a regular property
}

class Employee implements Named
{
	public string $name {    // More sophisticated - composite name
		get =&gt; $this-&gt;firstName . &#039; &#039; . $this-&gt;lastName;
	}
	private string $firstName;
	private string $lastName;
}
</code></pre>

<p>Notice something interesting – while the <code>Named</code> interface only
requires a read-only property, the <code>Person</code> class provides one
that's both readable and writable. This is perfectly fine – interfaces
define minimum requirements. It's like ordering a car that “must drive
forward” and getting one that can also reverse – it exceeds the minimum
requirements in a useful way.</p>

<p>A technical note for the detail-oriented: In interfaces, we must use the
<code>public</code> keyword with properties, even though it's redundant since
everything in an interface is inherently public. While using <code>public</code>
with methods would be redundant, it's required for properties to maintain
syntax consistency.</p>

<p>Here's another interesting detail – did you notice that unusual
<code>{ get; set; }</code> syntax? While in a class we can simply write
<code>public string $name</code>, interfaces require us to explicitly state
which operations the property supports. Though it requires more typing, this
makes sense – with interfaces, we want to be crystal clear about our
requirements.</p>

<h2
id="toc-properties-in-abstract-classes-getting-the-best-of-both-worlds">Properties
in Abstract Classes: Getting the Best of Both Worlds</h2>

<p>Abstract classes combine the best features of interfaces with their own
special capabilities. They can not only declare properties but also provide
default implementations for some hooks:</p>

<pre
class="language-php"><code>abstract class Person
{
	// Pure abstract property - child must implement
	abstract public string $name { get; }

	// Protected property supporting both operations
	abstract protected int $age { get; set; }

	// Here we provide ready-made email validation
	abstract public string $email {
		get; // this hook is abstract and must be implemented by child
		set =&gt; Nette\Utils\Validators::isEmail($value)
			? $value
			: throw new InvalidArgumentException(&#039;This doesn\&#039;t look like a valid email...&#039;);
	}
}
</code></pre>

<h2 id="toc-covariance-and-contravariance-not-as-scary-as-they-sound">Covariance
and Contravariance: Not as Scary as They Sound!</h2>

<p>These terms might sound intimidating, but the concept is actually quite
straightforward. Let's look at an example:</p>

<pre
class="language-php"><code>class Animal {}
class Dog extends Animal {}

interface PetShop
{
	// Read-only property can return a more specific type
	public Animal $pet { get; }
}

class DogShop implements PetShop
{
	// Returns a dog instead of an animal - perfectly valid!
	public Dog $pet { get; }
}
</code></pre>

<p>When a property has only a <code>get</code> hook, it can return a more
specific type in the child class (this is called covariance). Think of it this
way: “I promised you an animal, and a dog is definitely an animal,
right?”</p>

<p>On the flip side, a property with only a <code>set</code> hook can accept a
more general type in the child (contravariance). This makes sense – if you
can handle a specific type, you can handle its parent type too.</p>

<p>However, when a property has both <code>get</code> and <code>set</code>
hooks, the type must stay the same. Why? Because it could lead to
inconsistencies – we can't promise to return a dog when someone might try to
set a cat through the setter!</p>

<h2 id="toc-asymmetric-visibility-fine-tuned-access-control">Asymmetric
Visibility: Fine-Tuned Access Control</h2>

<p>Imagine you're building a Person class where you want everyone to be able to
read the date of birth, but only the class itself should be able to change it.
In the past, this meant getters and setters, but now we have an elegant
solution:</p>

<pre
class="language-php"><code>class Person
{
	public private(set) DateTimeImmutable $dateOfBirth;
}
</code></pre>

<p>This elegant syntax says: “Anyone can read it, but only the class itself
can write to it.” The first modifier <code>public</code> controls read access,
while <code>private(set)</code> controls write access. Since public reading is
the default, we can make it even simpler:</p>

<pre
class="language-php"><code>class Person
{
	private(set) DateTimeImmutable $dateOfBirth;
}
</code></pre>

<p>Naturally, there's a logical rule – write visibility can't be broader
than read visibility. You can't use something like
<code>protected public(set)</code> – that would be like saying “only
descendants can read it, but anyone can write to it.” Doesn't make much
sense, right?</p>

<p>What about inheritance? In PHP, a child class can either keep the same
visibility or expand it from protected to public. The same principle applies to
asymmetric visibility:</p>

<pre
class="language-php"><code>class Person
{
	public protected(set) string $name;  // Anyone can read, only descendants can write
}

class Employee extends Person
{
	public public(set) string $name;     // Child expands writing rights
}
</code></pre>

<p>An interesting case is <code>private(set)</code>. Such a property is
automatically <code>final</code> – when we say only the class itself can
write to it, that logically means not even child classes can change this
behavior.</p>

<p>Best of all, we can combine asymmetric visibility with hooks:</p>

<pre
class="language-php"><code>class Person
{
	private(set) DateTimeImmutable $birthDate {
		set =&gt; $value &gt; new DateTimeImmutable
			? throw new InvalidArgumentException(&#039;Birth in the future? Nice sci-fi!&#039;)
			: $value;
	}
}
</code></pre>

<p>This property has it all: it's publicly readable, only the class itself can
write to it, and it validates that the date isn't in the future. Hooks handle
“what should happen,” while asymmetric visibility controls “who can do
it.” A perfect combination!</p>

<h2
id="toc-asymmetric-visibility-and-arrays-a-smart-solution-to-an-old-problem">Asymmetric
Visibility and Arrays: A Smart Solution to an Old Problem</h2>

<p>Remember our phone number challenge? Asymmetric visibility offers us another
elegant solution:</p>

<pre
class="language-php"><code>class Person
{
	private(set) array $phones = [];

	public function addPhone(string $phone): void
	{
		$this-&gt;phones[] = trim($phone);
	}
}

$person = new Person;
var_dump($person-&gt;phones);     // OK: we can read the array
$person-&gt;addPhone(&#039;...&#039;);      // OK: we can add a number
$person-&gt;phones = [];          // ERROR: we can&#039;t overwrite the entire array
</code></pre>

<p>The array is publicly readable, but no one from outside can overwrite it. We
provide a specialized method for adding new numbers. No complex array-simulating
objects, no virtual properties – just clean, clear access control.</p>

<p>It's worth noting that you can't get a reference from outside to a property
with restricted write access:</p>

<pre
class="language-php"><code>$ref = &amp;$person-&gt;phones;    // Fatal error: Not allowed!
</code></pre>

<p>References are only allowed from scopes where the property is writable. This
makes sense – a reference could bypass our write restrictions.</p>

<p>To summarize, we now have four solid approaches for working with arrays in
properties:</p>

<ol>
	<li><b>Smart object</b> simulating an array (offers more features but requires
	more code)</li>

	<li><b>Backed property with hook</b> (prevents direct array modification)</li>

	<li><b>Virtual property</b> with private storage (requires methods for
	modifications)</li>

	<li><b>Asymmetric visibility</b> (moves logic to methods)</li>
</ol>

<p>Which approach should you choose? As with many things in programming – it
depends on your specific needs. Try them out and see which API feels most
natural for your use case.</p>

<h2 id="toc-readonly-and-asymmetric-visibility-freedom-at-last">Readonly and
Asymmetric Visibility: Freedom at Last!</h2>

<p><a href="https://phpfashion.com/en/php-readonly-properties">The <code>readonly</code> modifier</a>
actually combines two features: it prevents multiple writes and restricts
writing to private scope. It's not really “readonly” – it's more like
“writeonce” combined with <code>private(set)</code>.</p>

<p>The second part always felt unnecessarily restrictive. Why shouldn't a
readonly property be writable in child classes?</p>

<p>PHP 8.4 finally addresses this. Now <code>readonly</code> makes properties
<code>protected(set)</code> by default, meaning they're writable in child
classes too. And if we need different visibility? We can simply specify it:</p>

<pre
class="language-php"><code>class Person
{
	// Readonly accessible only inside the class (old behavior)
	public private(set) readonly string $name;

	// Readonly accessible in children (new default behavior)
	public readonly string $dateOfBirth;

	// Readonly writable anywhere (but only once!)
	public public(set) readonly string $id;

	public function rename(string $newName): void
	{
		$this-&gt;name = $newName;    // Inside class we can modify it
	}
}

class Employee extends Person
{
	public function setBirthDate(DateTimeImmutable $date): void
	{
		$this-&gt;dateOfBirth = $date;  // In child class we can modify it
	}
}

$person = new Person;
$person-&gt;id = &#039;abc123&#039;;     // This works
$person-&gt;id = &#039;xyz789&#039;;     // But this FAILS - it&#039;s readonly!
</code></pre>

<p>This gives us exactly the flexibility we need while maintaining safety.</p>

<h2 id="toc-when-terminology-clashes">When Terminology Clashes…</h2>

<p>Let's examine an interesting inconsistency in PHP's terminology:</p>

<ul>
	<li>We consistently talk about reading and writing properties</li>

	<li>We have the <code>readonly</code> modifier</li>

	<li>In phpDoc, we find <code>@property-read</code> and
	<code>@property-write</code> annotations</li>

	<li>Yet, in hooks and asymmetric visibility, we suddenly switch
	to <code>get/set</code></li>
</ul>

<p>Wouldn't using <code>read</code> and <code>write</code> terms make more
logical sense?</p>

<p>For hooks, the use of <code>get/set</code> is somewhat understandable –
they represent actions and align with the <code>__get/__set</code> magic
methods. But for asymmetric visibility? That's an entirely different
concept – it's not about “what should happen” like hooks, but rather
“who has permission to do it”. This is why using the term
<code>write</code>, as in <code>private(write)</code>, would make much
more sense:</p>

<pre
class="language-php"><code>class Person
{
	private(set) string $name;     // current syntax
	private(write) string $name;   // more intuitive syntax
}
</code></pre>

<p>The second version would feel more natural. Moreover, it would better align
with the existing <code>readonly</code> modifier.</p>

<p>It appears that in PHP's pursuit of syntactic consistency between hooks and
asymmetric visibility, they inadvertently sacrificed semantic consistency with
the language's existing concepts.</p>

<h2 id="toc-a-new-era-in-php-the-object-design-revolution">A New Era in PHP:
The Object Design Revolution</h2>

<p>For years, the PHP world adhered to a single “correct” approach to
object-oriented design: make all properties private and access them exclusively
through getters and setters. This wasn't just developers being fussy – public
properties came with genuine problems:</p>

<ul>
	<li>They were completely exposed with no access control</li>

	<li>As part of the public API, any modification (like adding validation) would
	break backward compatibility</li>

	<li>In interfaces, they could only exist as comments</li>
</ul>

<p>Anyone wanting to write robust code using interfaces and <a
href="https://doc.nette.org/en/dependency-injection/introduction">dependency
injection</a> had no choice but to fall back on getters and setters. It was the
only path to maintaining full control over object behavior.</p>

<p>But PHP 8.4 ushers in a new era! Property hooks and asymmetric visibility
finally give us the same level of control over properties that we've always had
with methods. Properties now become first-class citizens in the public API
because:</p>

<ul>
	<li>We can introduce validation or value transformation whenever needed</li>

	<li>We have fine-grained control over access permissions</li>

	<li>We can properly declare them in interfaces</li>
</ul>

<p>You can think of property hooks as an elegant, boilerplate-free replacement
for getters and setters. Or perhaps more accurately – getters and setters
were just a stopgap until PHP evolved to something better.</p>

<p>Speaking from extensive experience with Nette, where similar functionality
has existed for 17 years, I can attest to how game-changing this approach is.
Once you try it, there's no going back. Consider this comparison:</p>

<pre
class="language-php"><code>// Traditional approach
$this-&gt;getUser()-&gt;getIdentity()-&gt;getName()

// Modern approach
$this-&gt;user-&gt;identity-&gt;name
</code></pre>

<p>The second version isn't just more concise and readable – it feels more
natural. It's like the difference between formally asking “Would you be so
kind as to provide me with your name?” versus simply asking
“What's your name?”</p>

<p>Sure, some might argue that direct property access could tempt developers to
violate object-oriented principles, suggesting we should tell objects what to do
rather than ask for data (Tell-Don't-Ask). That's valid – but primarily for
objects with rich behavior implementing business logic. For data transfer
objects, value objects, or configuration classes, direct data access makes
perfect sense.</p>

<p>This does present an interesting challenge: what about existing projects? If
your library or framework consistently uses getters and setters, suddenly
introducing properties might create inconsistency. Users would need to guess
whether to use a method or a property in each case.</p>

<p>Over time, new conventions will emerge. Some projects may stick with getters
and setters, while others will embrace properties. The key is that we now have
options.</p>

<h2 id="toc-naming-matters">Naming Matters</h2>

<p>How should we name our properties? With boolean values especially, it's not
as straightforward as you might think.</p>

<p>With methods, we commonly use prefixes like <code>is</code> or
<code>has</code>:</p>

<pre
class="language-php"><code>class Article {
	public function isPublished(): bool { ... }
	public function hasComments(): bool { ... }
}
</code></pre>

<p>But for properties, these prefixes feel awkward and redundant. A better
approach is to use adjectives or nouns:</p>

<pre
class="language-php"><code>class Article {
	public bool $published;     // better than $isPublished
	public bool $commented;     // better than $hasComments
	public bool $draft;         // better than $isDraft
}

if ($article-&gt;published) {      // reads naturally
	// ...
}
</code></pre>

<p>For quantities, plural forms work better:</p>

<pre
class="language-php"><code>class Article {
	public int $views;          // better than $viewCount
	public array $tags;         // clearly indicates a collection
}
</code></pre>

<p>The goal is to make your code read like natural language. When we write
<code>if ($article-&gt;published)</code>, it flows much more naturally than
<code>if ($article-&gt;isPublished)</code>. Properties should feel like
attributes, not methods missing their parentheses.</p>

<h2 id="toc-when-to-choose-properties-vs-methods">When to Choose Properties vs
Methods?</h2>

<p>This is a crucial question! We can learn from languages like C# and Kotlin,
which have years of experience with properties. Properties excel for:</p>

<p>Value objects and DTOs:</p>

<pre
class="language-php"><code>class Money {
	public readonly float $amount;
	public readonly string $currency;
}
</code></pre>

<p>Simple entities:</p>

<pre
class="language-php"><code>class Article {
	public string $title;
	public string $content;
	public DateTimeImmutable $publishedAt;
	public bool $published {
		get =&gt; $this-&gt;publishedAt &lt;= new DateTimeImmutable;
	}
}
</code></pre>

<p>Computed values that depend on other properties:</p>

<pre
class="language-php"><code>class Rectangle {
	public float $width;
	public float $height;
	public float $area {
		get =&gt; $this-&gt;width * $this-&gt;height;
	}
}
</code></pre>

<p>Methods are better suited for:</p>

<ul>
	<li>operations that work with multiple properties together</li>

	<li>operations with side effects (logging, notifications)</li>

	<li>actions that perform tasks (save, send, calculate…)</li>

	<li>complex validations or business logic</li>

	<li>operations that might fail for various reasons</li>

	<li>cases where you want a <a
	href="https://doc.nette.org/en/introduction-to-object-oriented-programming#toc-fluent-interfaces">fluent
	interface</a></li>
</ul>

<p>All these guidelines point to one fundamental principle: property access
shouldn't hide complex processes or side effects. The complexity of the
operation should match what we intuitively expect when reading or writing to a
variable.</p>

<p>Though… consider <code>innerHTML</code> in JavaScript. When you write
<code>element.innerHTML = '&lt;p&gt;Hello&lt;/p&gt;'</code>, it triggers a
complex chain of events – HTML parsing, DOM tree creation, page reflow… Yet
everyone finds this natural!</p>

<p>So perhaps what matters more than implementation complexity is whether the
operation _conceptually_ feels like a property. It's similar to a
car's start/stop button – it might trigger a complex sequence internally,
but to the driver, it's simply “on/off”.</p>

		]]></content:encoded>
		<comments>https://phpfashion.com/en/property-hooks-in-php-8-4#comments</comments>
		<pubDate>Mon, 25 Nov 2024 03:23:00 +0100</pubDate>
		<guid isPermaLink="false">item2337@http://phpfashion.com</guid>
	</item>

		<item>
			<title>Five New Features in Latte 3.1 That Will Make Your Life Easier</title>
			<link>https://blog.nette.org/en/five-new-features-in-latte-3-1-that-will-make-your-life-easier</link>
			<description>Latte 3.1 brings five new features – new filters
|column, |commas and |limit, improved
|slice for iterators, and two new engine features. No revolution,
just small things you&apos;ll appreciate.

No More Variable Leaking from
{foreach}

This has bugged me for years. You write
{foreach $items as $item}, the loop ends, and $item
happily lives on with the value of the last element. Worse, it can overwrite a
variable you were counting on:

{var $name = &apos;Yay&apos;}

{foreach $users as $name}
    ...
{/foreach}

{$name} {* oops, no longer &apos;Yay&apos; *}


Latte has long warned you when foreach overwrote a variable passed to the
template via parameters. But variables created directly in the template, e.g.
via {var}, were overwritten silently. And that&apos;s the
sneaky case.

Now you can fix it with a single setting:

$latte-&gt;setFeature(Latte\Feature::ScopedLoopVariables);


Variables from {foreach} now exist only inside the loop. After
the loop ends, the original value is restored. And if the variable didn&apos;t exist
before? It disappears completely.

{var $name = &apos;Yay&apos;}

{foreach $users as $name}
    ...
{/foreach}

{$name} {* outputs &apos;Yay&apos;, yay! *}


And the old warning about overwritten parameters? It gets disabled with this
feature, because it no longer makes sense – nothing gets overwritten
anymore.

It works with destructuring {foreach $data as [$a, $b]} and of
course with keys {foreach $arr as $k =&gt; $v} – everything is
cleaned up after the loop. Nested loops have independent scopes.

The only exception: if you iterate by reference
{foreach $arr as &amp;$v}, scope doesn&apos;t apply – references
directly modify the original array, so restoring values after the loop would
break them. Makes sense.

Indent Your Templates
However You Like

You know the drill: you have a &lt;ul&gt; with a
{foreach} inside generating &lt;li&gt;. You naturally
indent it for readability. But that indentation bleeds into the output. Either
you have clean code and ugly HTML, or the other way around.

Unacceptable.

With Feature::Dedent, this dilemma disappears:

$latte-&gt;setFeature(Latte\Feature::Dedent);


Latte automatically removes the common indentation inside paired tags.
So this:

&lt;ul&gt;
    {foreach $items as $item}
        &lt;li&gt;{$item}&lt;/li&gt;
    {/foreach}
&lt;/ul&gt;


generates:

&lt;ul&gt;
    &lt;li&gt;...&lt;/li&gt;
    &lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;


The indentation is gone as if it was never there.

And since dedent is performed at template compilation time, not during each
render, it has zero performance impact. It works for all paired tags –
{if}, {block}, {capture},
{foreach} and more. Nested tags are dedented independently, each at
its own level.

If the indentation is inconsistent – for instance, you mix tabs with
spaces, or a line doesn&apos;t have sufficient indentation – Latte throws a
CompileException with the exact line number. No silent swallowing
of errors.

Filter
|commas

Joining an array into a comma-separated string – easy. But what if you
want “and” instead of a comma before the last element? That&apos;s exactly the
kind of thing where you start writing conditions in the template and suddenly
you have four lines of code instead of one. And all you wanted to say was
“apple, pear and plum”.

{[&apos;apple&apos;, &apos;pear&apos;, &apos;plum&apos;]|commas}        {* apple, pear, plum *}
{[&apos;apple&apos;, &apos;pear&apos;, &apos;plum&apos;]|commas:&apos; and &apos;} {* apple, pear and plum *}
{[&apos;apple&apos;, &apos;pear&apos;, &apos;plum&apos;]|commas:&apos;, or &apos;} {* apple, pear, or plum *}


Without a parameter, it joins with a comma and space. With a parameter, it
uses the given string as the separator between the last two elements – the
rest stays comma-separated. Natural language, not foreach.

Filter
|column

You have an array of user records, say
[[&apos;id&apos; =&gt; 1, &apos;name&apos; =&gt; &apos;John&apos;], [&apos;id&apos; =&gt; 2, &apos;name&apos; =&gt; &apos;Jane&apos;], ...],
and you need just the names? The |column filter extracts values
from a single column – whether it&apos;s an array key or an object property:

{$users|column:&apos;name&apos;|commas}   {* John, Jane, Bob *}


It optionally accepts a second parameter for indexing the results:

{foreach ($users|column:&apos;name&apos;:&apos;id&apos;) as $id =&gt; $name}
    {$id}: {$name}
{/foreach}


It works with iterators too, not just arrays – however, the iterator is
internally converted to an array, so don&apos;t expect any lazy magic.

Filter
|slice for Iterators and New Filter |limit

The |slice filter extracts a portion of an array or string (with
full UTF-8 support for strings). Until now, it only worked with arrays and
strings. Now it also handles iterators and generators – it returns a
generator that reads elements from the source one by one and stops once the
limit is reached. The entire iterator is never loaded into memory:

{foreach ($generator|slice:0:10) as $item}
    {$item}
{/foreach}


On top of that, there&apos;s a new filter
|limit – a more convenient variant for the typical case of
“take the first N elements”. It works with arrays, iterators, and strings
(with UTF-8 support):

{foreach ($items|limit:5) as $item}
    {$item}
{/foreach}

{$description|limit:100}


The difference from |slice is that |limit preserves
the original keys by default.

How to Enable

The new features ScopedLoopVariables and Dedent are
enabled via setFeature():

$latte = new Latte\Engine;
$latte-&gt;setFeature(Latte\Feature::ScopedLoopVariables);
$latte-&gt;setFeature(Latte\Feature::Dedent);


And if you&apos;re using Nette, just enable them in the configuration:

latte:
    scopedLoopVariables: true
    dedent: true


Five small things for life.
</description>
			<content:encoded><![CDATA[
			
		<p class="perex">Latte 3.1 brings five new features – new filters
<code>|column</code>, <code>|commas</code> and <code>|limit</code>, improved
<code>|slice</code> for iterators, and two new engine features. No revolution,
just small things you'll appreciate.</p>

<h2 id="toc-no-more-variable-leaking-from-foreach">No More Variable Leaking from
<code>{foreach}</code></h2>

<p>This has bugged me for years. You write
<code>{foreach $items as $item}</code>, the loop ends, and <code>$item</code>
happily lives on with the value of the last element. Worse, it can overwrite a
variable you were counting on:</p>

<pre
class="language-latte"><code>{var $name = &#039;Yay&#039;}

{foreach $users as $name}
    ...
{/foreach}

{$name} {* oops, no longer &#039;Yay&#039; *}
</code></pre>

<p>Latte has long warned you when foreach overwrote a variable passed to the
template via parameters. But variables created directly in the template, e.g.
via <code>{var}</code>, were overwritten silently. And that's the
sneaky case.</p>

<p>Now you can fix it with a single setting:</p>

<pre
class="language-php"><code>$latte-&gt;setFeature(Latte\Feature::ScopedLoopVariables);
</code></pre>

<p>Variables from <code>{foreach}</code> now exist only inside the loop. After
the loop ends, the original value is restored. And if the variable didn't exist
before? It disappears completely.</p>

<pre
class="language-latte"><code>{var $name = &#039;Yay&#039;}

{foreach $users as $name}
    ...
{/foreach}

{$name} {* outputs &#039;Yay&#039;, yay! *}
</code></pre>

<p>And the old warning about overwritten parameters? It gets disabled with this
feature, because it no longer makes sense – nothing gets overwritten
anymore.</p>

<p>It works with destructuring <code>{foreach $data as [$a, $b]}</code> and of
course with keys <code>{foreach $arr as $k =&gt; $v}</code> – everything is
cleaned up after the loop. Nested loops have independent scopes.</p>

<p>The only exception: if you iterate by reference
<code>{foreach $arr as &amp;$v}</code>, scope doesn't apply – references
directly modify the original array, so restoring values after the loop would
break them. Makes sense.</p>

<h2 id="toc-indent-your-templates-however-you-like">Indent Your Templates
However You Like</h2>

<p>You know the drill: you have a <code>&lt;ul&gt;</code> with a
<code>{foreach}</code> inside generating <code>&lt;li&gt;</code>. You naturally
indent it for readability. But that indentation bleeds into the output. Either
you have clean code and ugly HTML, or the other way around.</p>

<p>Unacceptable.</p>

<p>With <code>Feature::Dedent</code>, this dilemma disappears:</p>

<pre
class="language-php"><code>$latte-&gt;setFeature(Latte\Feature::Dedent);
</code></pre>

<p>Latte automatically removes the common indentation inside paired tags.
So this:</p>

<pre
class="language-latte"><code>&lt;ul&gt;
    {foreach $items as $item}
        &lt;li&gt;{$item}&lt;/li&gt;
    {/foreach}
&lt;/ul&gt;
</code></pre>

<p>generates:</p>

<pre
class="language-latte"><code>&lt;ul&gt;
    &lt;li&gt;...&lt;/li&gt;
    &lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
</code></pre>

<p>The indentation is gone as if it was never there.</p>

<p>And since dedent is performed at template compilation time, not during each
render, it has zero performance impact. It works for all paired tags –
<code>{if}</code>, <code>{block}</code>, <code>{capture}</code>,
<code>{foreach}</code> and more. Nested tags are dedented independently, each at
its own level.</p>

<p>If the indentation is inconsistent – for instance, you mix tabs with
spaces, or a line doesn't have sufficient indentation – Latte throws a
<code>CompileException</code> with the exact line number. No silent swallowing
of errors.</p>

<h2 id="toc-filter-commas"><a
href="https://latte.nette.org/en/filters#toc-commas">Filter
<code>|commas</code></a></h2>

<p>Joining an array into a comma-separated string – easy. But what if you
want “and” instead of a comma before the last element? That's exactly the
kind of thing where you start writing conditions in the template and suddenly
you have four lines of code instead of one. And all you wanted to say was
“apple, pear and plum”.</p>

<pre
class="language-latte"><code>{[&#039;apple&#039;, &#039;pear&#039;, &#039;plum&#039;]|commas}        {* apple, pear, plum *}
{[&#039;apple&#039;, &#039;pear&#039;, &#039;plum&#039;]|commas:&#039; and &#039;} {* apple, pear and plum *}
{[&#039;apple&#039;, &#039;pear&#039;, &#039;plum&#039;]|commas:&#039;, or &#039;} {* apple, pear, or plum *}
</code></pre>

<p>Without a parameter, it joins with a comma and space. With a parameter, it
uses the given string as the separator between the last two elements – the
rest stays comma-separated. Natural language, not foreach.</p>

<h2 id="toc-filter-column"><a
href="https://latte.nette.org/en/filters#toc-column">Filter
<code>|column</code></a></h2>

<p>You have an array of user records, say
<code>[['id' =&gt; 1, 'name' =&gt; 'John'], ['id' =&gt; 2, 'name' =&gt; 'Jane'], ...]</code>,
and you need just the names? The <code>|column</code> filter extracts values
from a single column – whether it's an array key or an object property:</p>

<pre
class="language-latte"><code>{$users|column:&#039;name&#039;|commas}   {* John, Jane, Bob *}
</code></pre>

<p>It optionally accepts a second parameter for indexing the results:</p>

<pre
class="language-latte"><code>{foreach ($users|column:&#039;name&#039;:&#039;id&#039;) as $id =&gt; $name}
    {$id}: {$name}
{/foreach}
</code></pre>

<p>It works with iterators too, not just arrays – however, the iterator is
internally converted to an array, so don't expect any lazy magic.</p>

<h2 id="toc-filter-slice-for-iterators-and-new-filter-limit"><a
href="https://latte.nette.org/en/filters#toc-slice">Filter
<code>|slice</code></a> for Iterators and New <a
href="https://latte.nette.org/en/filters#toc-limit">Filter <code>|limit</code></a></h2>

<p>The <code>|slice</code> filter extracts a portion of an array or string (with
full UTF-8 support for strings). Until now, it only worked with arrays and
strings. Now it also handles iterators and generators – it returns a
generator that reads elements from the source one by one and stops once the
limit is reached. The entire iterator is never loaded into memory:</p>

<pre
class="language-latte"><code>{foreach ($generator|slice:0:10) as $item}
    {$item}
{/foreach}
</code></pre>

<p>On top of that, there's a new <a
href="https://latte.nette.org/en/filters#toc-limit">filter
<code>|limit</code></a> – a more convenient variant for the typical case of
“take the first N elements”. It works with arrays, iterators, and strings
(with UTF-8 support):</p>

<pre
class="language-latte"><code>{foreach ($items|limit:5) as $item}
    {$item}
{/foreach}

{$description|limit:100}
</code></pre>

<p>The difference from <code>|slice</code> is that <code>|limit</code> preserves
the original keys by default.</p>

<h2 id="toc-how-to-enable">How to Enable</h2>

<p>The new features <code>ScopedLoopVariables</code> and <code>Dedent</code> are
enabled via <code>setFeature()</code>:</p>

<pre
class="language-php"><code>$latte = new Latte\Engine;
$latte-&gt;setFeature(Latte\Feature::ScopedLoopVariables);
$latte-&gt;setFeature(Latte\Feature::Dedent);
</code></pre>

<p>And if you're using Nette, just enable them in the configuration:</p>

<pre
class="language-neon"><code>latte:
    scopedLoopVariables: true
    dedent: true
</code></pre>

<p>Five small things for life.</p>

		
			]]></content:encoded>
			<pubDate>Tue, 24 Mar 2026 17:36:00 +0100</pubDate>
			<guid isPermaLink="false">item534@http://blog.nette.org</guid>
		</item>
		<item>
			<title>The Case of the Blocked Pipe</title>
			<link>https://blog.nette.org/en/the-case-of-the-blocked-pipe</link>
			<description>“This job teaches you that every operating system has skeletons in its
closet. Linux? It plays it straight—open source, they call it. But Windows?
Windows is that mysterious beauty in the corner of the bar. Smiling, telling you
everything&apos;s fine, while bodies rot in her basement.”

Chapter 1: The Perfect Crime

This case has been haunting me for fourteen years.

It started in 2012. Nette Tester had
just unlocked the secret of multi-threaded testing. Eight threads working in
perfect harmony by default, like a well-conducted orchestra. It was a
pioneer—back when other libraries were running tests one by one like
pensioners at the post office, Tester was already parallelizing. Sixty seconds
of tests compressed into ten. Beautiful. Elegant.

But from day one, there was a shadow.

The first reports came from Windows users. Tests that flew on Linux were
crawling on Windows. Not walking—crawling. On their bellies. Through
broken glass.

Linux: 3 seconds. Windows: 21.6 seconds.

Someone was murdering parallelism. And they were doing it in broad
daylight.

Chapter 2: The Suspects

I opened the case file and started from the basics.

When Tester runs a test, it spawns a new PHP process. This process runs
independently and writes its results to standard output—stdout. But how does
the main program get those results? Through a pipe. An invisible tube
between processes, through which data flows like water.

On Linux, this pipe is smart. When it&apos;s empty, you say “give me data”
and it replies “got nothing” and you move on. No waiting. No delays.

But on Windows?

I tried everything. First suspect was
stream_set_blocking()—supposed to switch the pipe to
non-blocking mode.

Didn&apos;t work.

Next up was stream_select()—should monitor multiple pipes at
once and tell you which one has data.

Dead too.

Finally, I tried plain old fread(). On Linux, reading from an
empty pipe returns nothing immediately. On Windows? It waits. Waits until
something arrives. Could be forever.

Three functions. Three corpses. Someone had killed non-blocking I/O years
ago. A crime so old, the chalk outline had long faded.

Chapter 3: The Blockade

Picture this:

Eight threads enter a bar. The first one orders — he overdoes it with
shots and gives sleep(3). Nothing unusual. Even a test needs its
beauty sleep sometimes.

But then it gets interesting.

You want to read the output from that first test. You reach into the pipe.
But there&apos;s no output—the test is sleeping, not printing anything. And on
Windows, when you read from an empty pipe…

You wait.

You wait until the test wakes up. Frozen mid-motion with your hand in the
pipe. The other threads stare at you. “Hello? You okay?” But you&apos;ve started
reading and you can&apos;t stop. Seven threads poke at you: “Hey, can we go? We&apos;ve
got tests to run!” But you can&apos;t do anything. Inside, you&apos;re cursing
yourself—if you&apos;d picked a different test, you could be mingling with the
others right now, working, being useful. But no. You picked the sleeper and now
you&apos;re its hostage.

Three seconds of silence. Then the test wakes up, spits out its output, and
you can finally let go.

Eight threads. One stuck. Seven shuffling and waiting.

Twenty-one point six seconds instead of three.

This wasn&apos;t murder. This was torture.

Chapter 4: Files

My job was to solve this problem.

Every detective has that moment. A flash of brilliance. The feeling that
you&apos;ve finally cracked it. Mine came at 2 AM over cold coffee and
frozen code.

“Files,” I whispered. “We&apos;ll write to files.”

A pipe is like a phone line—you have to listen when someone&apos;s talking, or
you miss it. But a file? A file is like an answering machine. The test writes
what it needs, and you read it when you have time. No waiting. No freezing.

proc_open($cmd, [
    [&apos;pipe&apos;, &apos;r&apos;],
    [&apos;file&apos;, $tempFile, &apos;w&apos;],  // Here it is!
    ...
]);


No pipes. No blocking. The test writes to a file, we read it when it&apos;s done.
Clean. Simple. Genius.

27 seconds.

Twenty. Seven. Seconds.

I measured everything:


	proc_open(): 2 ms. Innocent.

	proc_get_status(): 4 ms per thousand calls. Clean too.

	File read + delete: Under 10 ms. Nothing suspicious.


And yet the result was catastrophic.

I stared at those numbers until my eyes watered. Individual operations fine.
The whole thing a disaster. How was this possible?

Then it hit me. Eighty-five tests. Eighty-five temp files. Eighty-five
creates, writes, reads, and deletes. Each operation fast, but Windows filesystem
works differently than Linux. NTFS journaling. Antivirus scanning every new file
like a nervous border agent. And those milliseconds add up.

Death by a thousand cuts. Slow but certain.

Chapter 5: The Gamble

I was running out of ideas. And coffee.

Then something occurred to me. Something dangerous. The kind of idea that
usually gets detectives killed in the third act.

What if we just didn&apos;t read the output right away?

The thought was simple: test is running, we leave it alone. Don&apos;t touch the
pipe, don&apos;t wait, don&apos;t block. When the test finishes—when it *leaves the
bar*—only then do we read what it wrote. Until then, we take care of the other
threads. Work. Live.

if ($status[&apos;running&apos;]) {
    if (PHP_OS_FAMILY !== &apos;Windows&apos;) {
        // On Linux we read continuously, it works there
        $this-&gt;test-&gt;stdout .= stream_get_contents($this-&gt;stdout);
    }
    // On Windows? Don&apos;t read. Don&apos;t touch. Don&apos;t even breathe near the pipe.
    return true;
}


I implemented it. Held my breath. Ran the tests.

3 seconds.

Three. Seconds.

Just like Linux. Just like the good old days. I wanted to cry with joy.
Dance. Run up to the roof and—

The phone rang.

“We have a problem,” said the voice. “Some tests just won&apos;t finish.
Hanging there like laundry.”

My heart stopped.

“Which ones?”

“The ones with lots of output. Tons of echo statements. They write and
write, and then—nothing. Silence. Forever.”

I closed my eyes. Of course. How could I forget.

The pipe buffer. Four kilobytes on Windows. A pipe isn&apos;t
bottomless—it&apos;s a tube with limited capacity. When a test writes and writes
and nobody reads, the buffer fills up. And when the buffer is full, the test
freezes mid-write. Waiting for someone to make room. But we&apos;re not reading.
We&apos;re waiting for the test to finish. It&apos;s waiting for us.

Deadlock.

A classic trap. Two people waiting for each other at a doorway: “After
you.”—“No, after you.”—Forever.

We&apos;d traded one killer for another.

Chapter 6: Last Attempts

The next forty-eight hours were a blur of caffeine and increasingly
desperate ideas.

Timeout:

stream_set_timeout($this-&gt;stdout, 0, 1000); // 1ms timeout
$this-&gt;test-&gt;stdout .= fread($this-&gt;stdout, 8192);


Result: 18.5 seconds. Better. But still blocking. The timeout was just a
polite request that Windows wiped its ass with.

Metadata check:

$meta = stream_get_meta_data($this-&gt;stdout);
if (!empty($meta[&apos;unread_bytes&apos;])) {
    // Only read when there&apos;s something!
}


Result: unread_bytes was always zero. Always. Even when there
was data. Even when there were megabytes. Windows was lying straight to
our faces.

I slammed my fist on the desk.

Every lead hit a wall. Every piece of evidence crumbled in my hands. Every
solution fixed one problem and created another. Like playing chess against an
opponent who redraws the board after every move.

Chapter 7: The Truth

They don&apos;t teach you this in detective school:

Sometimes you don&apos;t catch the killer.

Sometimes the killer is the system itself. Hardcoded into the foundation.
A decision someone made in Redmond twenty years ago. A person who never
imagined PHP processes would want to communicate without waiting.

Windows simply doesn&apos;t support non-blocking I/O on anonymous pipes. Period.
End of story. Case closed.

But then I found an informant. The kind who operates in the gray zone of
documentation.

An undocumented socket descriptor. [&apos;socket&apos;] instead of
[&apos;pipe&apos;, &apos;w&apos;]. PHP can create a TCP socket pair instead of a pipe.
And stream_select() works on sockets. Even on Windows.

proc_open($cmd, [
    [&apos;pipe&apos;, &apos;r&apos;],
    [&apos;socket&apos;],  // Secret weapon!
    [&apos;socket&apos;],
]);


I ran the tests. Held my breath.

Three seconds.

Three seconds on Windows. Just like Linux. I wanted to scream. All eight
threads working simultaneously. No blocking. No waiting. Finally, I felt that
“Linux feeling” on Windows.

But then I noticed something strange.

Some tests reported empty output. Where there should have been hundreds of
lines, there was nothing. As if the witness had forgotten half their
testimony.

I ran the tests again. And again. Out of a hundred runs, seventy were fine.
Thirty had lost output. No pattern. No logic. Pure chance.

I found the answer on php.net. Comment
#128252.


	“Passing a socket handle for stdout/stderr on Windows causes the last
	chunk(s) of output to occasionally get lost…”


And then the sentence that finished me off:


	“This is actually a known bug in Windows itself and Microsoft&apos;s response
	was that CreateProcess() only officially supports anonymous pipes and file
	handles… other handle types will produce ‘undefined behavior.’”


Undefined behavior. Police jargon for “not our
jurisdiction.”

The socket was a witness with a leaky memory. Fast, willing, but sometimes
just forgot what it saw. And I couldn&apos;t risk thirty percent of the evidence
vanishing like tears in rain.

I closed that lead. Another dead end.

Epilogue: The Bitter End

I lit a cigarette I don&apos;t smoke and stared out the window at rain that
wasn&apos;t falling.

The case file sits on my desk. Unsolved. The current implementation—don&apos;t
read while running—is fast as lightning, but carries a bomb in its pocket. Any
test that dares to output more than 4 KB will freeze forever. Trapped in a
digital limbo of its own making.

I closed the file.

Not every case has a happy ending. Not every killer ends up behind bars. And
some operating systems just are what they are —no matter how many
nights you spend on Stack Overflow or how many Microsoft engineers
you curse.

Parallelism on Windows is dead. Long live parallelism.

Case closed. The killer is still out there. Sleep well.




	I&apos;ve seen things you people wouldn&apos;t believe. Attack ships on fire off the
	shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser
	Gate. And I&apos;ve seen PHP try to do non-blocking I/O on Windows pipes. All those
	moments will be lost in time. Like tears in rain.

	— Roy Batty, if he&apos;d been a PHP developer




Update: January 2026

The phone rang.

I&apos;d filed the case away long ago. Unsolved cases. Sometimes I look at it,
dust it off, shake my head. Some crimes just go unpunished.

“We have news,” said the voice. “PHP 8.5. Check out
stream_select().”

I couldn&apos;t believe my eyes. Someone on the PHP core team—some Christoph
from Germany—had fixed
a twenty-year-old problem.

PeekNamedPipe(). Such a simple concept. Before you reach into
the pipe, you peek first. Is something there? Yes—read it. No—move on. No
waiting. No blocking.

// PHP 8.5+: stream_select() finally works on Windows pipes
while (stream_select($read, $w, $e, 0, 0) &gt; 0) {
    $output .= fread($pipe, 8192);
}


I ran the tests.

Three seconds. No blocking. No deadlock. All data
accounted for.

I pulled the file from the archive. On the cover I wrote: SOLVED.
January 2026.

Twenty years. Twenty years that bug waited in the foundations of PHP, like a
time bomb nobody knew how to defuse. And then someone came along who simply knew
where to look.

Not every case ends in a dead end. Sometimes, just sometimes, a cold case
heats up. And dead witnesses find their voice.

Case closed. For real this time.
</description>
			<content:encoded><![CDATA[
			
		<p>“This job teaches you that every operating system has skeletons in its
closet. Linux? It plays it straight—open source, they call it. But Windows?
Windows is that mysterious beauty in the corner of the bar. Smiling, telling you
everything's fine, while bodies rot in her basement.”</p>

<h2 id="toc-chapter-1-the-perfect-crime">Chapter 1: The Perfect Crime</h2>

<p>This case has been haunting me for fourteen years.</p>

<p>It started in 2012. <a href="https://tester.nette.org">Nette Tester</a> had
just unlocked the secret of multi-threaded testing. Eight threads working in
perfect harmony <em>by default</em>, like a well-conducted orchestra. It was a
pioneer—back when other libraries were running tests one by one like
pensioners at the post office, Tester was already parallelizing. Sixty seconds
of tests compressed into ten. Beautiful. Elegant.</p>

<p>But from day one, there was a shadow.</p>

<p>The first reports came from Windows users. Tests that flew on Linux were
crawling on Windows. Not walking—<i>crawling</i>. On their bellies. Through
broken glass.</p>

<p><em>Linux: 3 seconds. Windows: 21.6 seconds.</em></p>

<p>Someone was murdering parallelism. And they were doing it in broad
daylight.</p>

<h2 id="toc-chapter-2-the-suspects">Chapter 2: The Suspects</h2>

<p>I opened the case file and started from the basics.</p>

<p>When Tester runs a test, it spawns a new PHP process. This process runs
independently and writes its results to standard output—stdout. But how does
the main program get those results? Through a <i>pipe</i>. An invisible tube
between processes, through which data flows like water.</p>

<p>On Linux, this pipe is smart. When it's empty, you say “give me data”
and it replies “got nothing” and you move on. No waiting. No delays.</p>

<p>But on Windows?</p>

<p>I tried everything. First suspect was
<code>stream_set_blocking()</code>—supposed to switch the pipe to
non-blocking mode.</p>

<p>Didn't work.</p>

<p>Next up was <code>stream_select()</code>—should monitor multiple pipes at
once and tell you which one has data.</p>

<p>Dead too.</p>

<p>Finally, I tried plain old <code>fread()</code>. On Linux, reading from an
empty pipe returns <i>nothing</i> immediately. On Windows? It waits. Waits until
something arrives. Could be forever.</p>

<p>Three functions. Three corpses. Someone had killed non-blocking I/O years
ago. A crime so old, the chalk outline had long faded.</p>

<h2 id="toc-chapter-3-the-blockade">Chapter 3: The Blockade</h2>

<p>Picture this:</p>

<p>Eight threads enter a bar. The first one orders — he overdoes it with
shots and gives <code>sleep(3)</code>. Nothing unusual. Even a test needs its
beauty sleep sometimes.</p>

<p>But then it gets interesting.</p>

<p>You want to read the output from that first test. You reach into the pipe.
But there's no output—the test is sleeping, not printing anything. And on
Windows, when you read from an empty pipe…</p>

<p><em>You wait.</em></p>

<p>You wait until the test wakes up. Frozen mid-motion with your hand in the
pipe. The other threads stare at you. “Hello? You okay?” But you've started
reading and you can't stop. Seven threads poke at you: “Hey, can we go? We've
got tests to run!” But you can't do anything. Inside, you're cursing
yourself—if you'd picked a different test, you could be mingling with the
others right now, working, being useful. But no. You picked the sleeper and now
you're its hostage.</p>

<p>Three seconds of silence. Then the test wakes up, spits out its output, and
you can finally let go.</p>

<p>Eight threads. One stuck. Seven shuffling and waiting.</p>

<p>Twenty-one point six seconds instead of three.</p>

<p><em>This wasn't murder. This was torture.</em></p>

<h2 id="toc-chapter-4-files">Chapter 4: Files</h2>

<p>My job was to solve this problem.</p>

<p>Every detective has that moment. A flash of brilliance. The feeling that
you've finally cracked it. Mine came at 2 AM over cold coffee and
frozen code.</p>

<p>“Files,” I whispered. “We'll write to files.”</p>

<p>A pipe is like a phone line—you have to listen when someone's talking, or
you miss it. But a file? A file is like an answering machine. The test writes
what it needs, and you read it when you have time. No waiting. No freezing.</p>

<pre
class="language-php"><code>proc_open($cmd, [
    [&#039;pipe&#039;, &#039;r&#039;],
    [&#039;file&#039;, $tempFile, &#039;w&#039;],  // Here it is!
    ...
]);
</code></pre>

<p>No pipes. No blocking. The test writes to a file, we read it when it's done.
Clean. Simple. Genius.</p>

<p><em>27 seconds.</em></p>

<p>Twenty. Seven. Seconds.</p>

<p>I measured everything:</p>

<ul>
	<li><code>proc_open()</code>: 2 ms. Innocent.</li>

	<li><code>proc_get_status()</code>: 4 ms per thousand calls. Clean too.</li>

	<li>File read + delete: Under 10 ms. Nothing suspicious.</li>
</ul>

<p>And yet the result was catastrophic.</p>

<p>I stared at those numbers until my eyes watered. Individual operations fine.
The whole thing a disaster. How was this possible?</p>

<p>Then it hit me. Eighty-five tests. Eighty-five temp files. Eighty-five
creates, writes, reads, and deletes. Each operation fast, but Windows filesystem
works differently than Linux. NTFS journaling. Antivirus scanning every new file
like a nervous border agent. And those milliseconds add up.</p>

<p>Death by a thousand cuts. Slow but certain.</p>

<h2 id="toc-chapter-5-the-gamble">Chapter 5: The Gamble</h2>

<p>I was running out of ideas. And coffee.</p>

<p>Then something occurred to me. Something dangerous. The kind of idea that
usually gets detectives killed in the third act.</p>

<p><em>What if we just didn't read the output right away?</em></p>

<p>The thought was simple: test is running, we leave it alone. Don't touch the
pipe, don't wait, don't block. When the test finishes—when it *leaves the
bar*—only then do we read what it wrote. Until then, we take care of the other
threads. Work. Live.</p>

<pre
class="language-php"><code>if ($status[&#039;running&#039;]) {
    if (PHP_OS_FAMILY !== &#039;Windows&#039;) {
        // On Linux we read continuously, it works there
        $this-&gt;test-&gt;stdout .= stream_get_contents($this-&gt;stdout);
    }
    // On Windows? Don&#039;t read. Don&#039;t touch. Don&#039;t even breathe near the pipe.
    return true;
}
</code></pre>

<p>I implemented it. Held my breath. Ran the tests.</p>

<p><b>3 seconds.</b></p>

<p><em>Three. Seconds.</em></p>

<p>Just like Linux. Just like the good old days. I wanted to cry with joy.
Dance. Run up to the roof and—</p>

<p>The phone rang.</p>

<p>“We have a problem,” said the voice. “Some tests just won't finish.
Hanging there like laundry.”</p>

<p>My heart stopped.</p>

<p>“Which ones?”</p>

<p>“The ones with lots of output. Tons of echo statements. They write and
write, and then—nothing. Silence. Forever.”</p>

<p>I closed my eyes. Of course. How could I forget.</p>

<p>The pipe buffer. Four kilobytes on Windows. A pipe isn't
bottomless—it's a tube with limited capacity. When a test writes and writes
and nobody reads, the buffer fills up. And when the buffer is full, the test
freezes mid-write. Waiting for someone to make room. But we're not reading.
We're waiting for the test to finish. It's waiting for us.</p>

<p><i>Deadlock.</i></p>

<p>A classic trap. Two people waiting for each other at a doorway: “After
you.”—“No, after you.”—Forever.</p>

<p>We'd traded one killer for another.</p>

<h2 id="toc-chapter-6-last-attempts">Chapter 6: Last Attempts</h2>

<p>The next forty-eight hours were a blur of caffeine and increasingly
desperate ideas.</p>

<p><b>Timeout:</b></p>

<pre
class="language-php"><code>stream_set_timeout($this-&gt;stdout, 0, 1000); // 1ms timeout
$this-&gt;test-&gt;stdout .= fread($this-&gt;stdout, 8192);
</code></pre>

<p>Result: 18.5 seconds. Better. But still blocking. The timeout was just a
polite request that Windows wiped its ass with.</p>

<p><b>Metadata check:</b></p>

<pre
class="language-php"><code>$meta = stream_get_meta_data($this-&gt;stdout);
if (!empty($meta[&#039;unread_bytes&#039;])) {
    // Only read when there&#039;s something!
}
</code></pre>

<p>Result: <code>unread_bytes</code> was always zero. Always. Even when there
was data. Even when there were megabytes. Windows was lying straight to
our faces.</p>

<p>I slammed my fist on the desk.</p>

<p>Every lead hit a wall. Every piece of evidence crumbled in my hands. Every
solution fixed one problem and created another. Like playing chess against an
opponent who redraws the board after every move.</p>

<h2 id="toc-chapter-7-the-truth">Chapter 7: The Truth</h2>

<p>They don't teach you this in detective school:</p>

<p>Sometimes you don't catch the killer.</p>

<p>Sometimes the killer is the system itself. Hardcoded into the foundation.
A decision someone made in Redmond twenty years ago. A person who never
imagined PHP processes would want to communicate without waiting.</p>

<p>Windows simply doesn't support non-blocking I/O on anonymous pipes. Period.
End of story. Case closed.</p>

<p>But then I found an informant. The kind who operates in the gray zone of
documentation.</p>

<p>An undocumented socket descriptor. <code>['socket']</code> instead of
<code>['pipe', 'w']</code>. PHP can create a TCP socket pair instead of a pipe.
And <code>stream_select()</code> works on sockets. Even on Windows.</p>

<pre
class="language-php"><code>proc_open($cmd, [
    [&#039;pipe&#039;, &#039;r&#039;],
    [&#039;socket&#039;],  // Secret weapon!
    [&#039;socket&#039;],
]);
</code></pre>

<p>I ran the tests. Held my breath.</p>

<p><em>Three seconds.</em></p>

<p>Three seconds on Windows. Just like Linux. I wanted to scream. All eight
threads working simultaneously. No blocking. No waiting. Finally, I felt that
“Linux feeling” on Windows.</p>

<p>But then I noticed something strange.</p>

<p>Some tests reported empty output. Where there should have been hundreds of
lines, there was nothing. As if the witness had forgotten half their
testimony.</p>

<p>I ran the tests again. And again. Out of a hundred runs, seventy were fine.
Thirty had lost output. No pattern. No logic. Pure chance.</p>

<p>I found the answer on php.net. <a
href="https://www.php.net/manual/en/function.proc-open.php#128252">Comment
#128252</a>.</p>

<blockquote>
	<p>“Passing a socket handle for stdout/stderr on Windows causes the last
	chunk(s) of output to occasionally get lost…”</p>
</blockquote>

<p>And then the sentence that finished me off:</p>

<blockquote>
	<p>“This is actually a known bug in Windows itself and Microsoft's response
	was that CreateProcess() only officially supports anonymous pipes and file
	handles… other handle types will produce ‘undefined behavior.’”</p>
</blockquote>

<p><em>Undefined behavior.</em> Police jargon for “not our
jurisdiction.”</p>

<p>The socket was a witness with a leaky memory. Fast, willing, but sometimes
just forgot what it saw. And I couldn't risk thirty percent of the evidence
vanishing like tears in rain.</p>

<p>I closed that lead. Another dead end.</p>

<h2 id="toc-epilogue-the-bitter-end">Epilogue: The Bitter End</h2>

<p>I lit a cigarette I don't smoke and stared out the window at rain that
wasn't falling.</p>

<p>The case file sits on my desk. Unsolved. The current implementation—don't
read while running—is fast as lightning, but carries a bomb in its pocket. Any
test that dares to output more than 4 KB will freeze forever. Trapped in a
digital limbo of its own making.</p>

<p>I closed the file.</p>

<p>Not every case has a happy ending. Not every killer ends up behind bars. And
some operating systems just <em>are what they are</em> —no matter how many
nights you spend on Stack Overflow or how many Microsoft engineers
you curse.</p>

<p>Parallelism on Windows is dead. Long live parallelism.</p>

<p><em>Case closed. The killer is still out there. Sleep well.</em></p>

<hr>

<blockquote style="text-align:center">
	<p>I've seen things you people wouldn't believe. Attack ships on fire off the
	shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser
	Gate. And I've seen PHP try to do non-blocking I/O on Windows pipes. All those
	moments will be lost in time. Like tears in rain.</p>

	<p>— Roy Batty, if he'd been a PHP developer</p>
</blockquote>

<hr>

<h2 id="toc-update-january-2026">Update: January 2026</h2>

<p>The phone rang.</p>

<p>I'd filed the case away long ago. Unsolved cases. Sometimes I look at it,
dust it off, shake my head. Some crimes just go unpunished.</p>

<p>“We have news,” said the voice. “PHP 8.5. Check out
<code>stream_select()</code>.”</p>

<p>I couldn't believe my eyes. Someone on the PHP core team—some Christoph
from Germany—had <a href="https://github.com/php/php-src/pull/16917">fixed</a>
a twenty-year-old problem.</p>

<p><code>PeekNamedPipe()</code>. Such a simple concept. Before you reach into
the pipe, you peek first. Is something there? Yes—read it. No—move on. No
waiting. No blocking.</p>

<pre
class="language-php"><code>// PHP 8.5+: stream_select() finally works on Windows pipes
while (stream_select($read, $w, $e, 0, 0) &gt; 0) {
    $output .= fread($pipe, 8192);
}
</code></pre>

<p>I ran the tests.</p>

<p><em>Three seconds.</em> No blocking. No deadlock. All data
accounted for.</p>

<p>I pulled the file from the archive. On the cover I wrote: <a
href="https://github.com/nette/tester/commit/d35f75b8e17ff6e7f96d3e9dcf246c37e23fe360">SOLVED.</a>
January 2026.</p>

<p>Twenty years. Twenty years that bug waited in the foundations of PHP, like a
time bomb nobody knew how to defuse. And then someone came along who simply knew
where to look.</p>

<p>Not every case ends in a dead end. Sometimes, just sometimes, a cold case
heats up. And dead witnesses find their voice.</p>

<p><em>Case closed. For real this time.</em></p>

		
			]]></content:encoded>
			<pubDate>Wed, 07 Jan 2026 22:59:00 +0100</pubDate>
			<guid isPermaLink="false">item532@http://blog.nette.org</guid>
		</item>
		<item>
			<title>Less Boilerplate, More Types: Introducing #[TemplateVariable]</title>
			<link>https://blog.nette.org/en/introducing-templatevariable</link>
			<description>Sometimes a presenter works with a specific domain object (like an article,
order, or user) and naturally keeps it in a property. You also need that same
object in your Latte template. Until now, that required an extra line
with $this-&gt;template-&gt;…

In Nette Framework 3.2.9, a neat little feature was introduced: the
#[TemplateVariable] attribute. Simply mark a presenter property
with this attribute (it can&apos;t be private), and it automatically becomes
available in your template under the same name:

&lt;?php
declare(strict_types=1);

namespace App\Presentation\Article;

use Nette\Application\Attributes\TemplateVariable;
use Nette\Application\UI\Presenter;

final class ArticlePresenter extends Presenter
{
	#[TemplateVariable]
	public string $siteName = &apos;My blog&apos;;

	#[TemplateVariable]
	public ?Model\Article $article = null;

	public function actionShow(int $id): void
	{
		$this-&gt;article = $this-&gt;articleFacade-&gt;getById($id);
	}
}


In your template, you can then just use $siteName and
$article directly—no extra wiring needed.

If you do assign a variable with the same name via
$this-&gt;template-&gt;…, though,
#[TemplateVariable] won&apos;t override it. In other words, when a
variable already exists in the template, the attribute leaves it alone.

The Easiest Path to Typed
Templates

Another nice thing about #[TemplateVariable] is that it
naturally encourages using typed properties as your template&apos;s data
source. Types are checked by PHP itself (and picked up by IDEs and static
analysis), so instead of dynamically stuffing values into
$this-&gt;template, you&apos;re working with proper, strongly-typed
values (Article, bool, string…).

Nette offers a complete solution for type-safe templates through custom
template classes, where variable types are explicitly defined and
$this-&gt;template becomes a well-defined object instead of a
generic container.

That said, #[TemplateVariable] can be a nice lightweight
alternative for small to medium-sized use cases: you don&apos;t want the overhead of
creating a dedicated template class, but you still want clean code with proper
type safety. And when your project grows, you can always migrate to the full
strictly-typed template approach.
</description>
			<content:encoded><![CDATA[
			
		<p>Sometimes a presenter works with a specific domain object (like an article,
order, or user) and naturally keeps it in a property. You also need that same
object in your Latte template. Until now, that required an extra line
with <code>$this-&gt;template-&gt;…</code></p>

<p>In Nette Framework 3.2.9, a neat little feature was introduced: the
<code>#[TemplateVariable]</code> attribute. Simply mark a presenter property
with this attribute (it can't be private), and it automatically becomes
available in your template under the same name:</p>

<pre
class="language-php"><code>&lt;?php
declare(strict_types=1);

namespace App\Presentation\Article;

use Nette\Application\Attributes\TemplateVariable;
use Nette\Application\UI\Presenter;

final class ArticlePresenter extends Presenter
{
	#[TemplateVariable]
	public string $siteName = &#039;My blog&#039;;

	#[TemplateVariable]
	public ?Model\Article $article = null;

	public function actionShow(int $id): void
	{
		$this-&gt;article = $this-&gt;articleFacade-&gt;getById($id);
	}
}
</code></pre>

<p>In your template, you can then just use <code>$siteName</code> and
<code>$article</code> directly—no extra wiring needed.</p>

<p>If you do assign a variable with the same name via
<code>$this-&gt;template-&gt;…</code>, though,
<code>#[TemplateVariable]</code> won't override it. In other words, when a
variable already exists in the template, the attribute leaves it alone.</p>

<h2 id="toc-the-easiest-path-to-typed-templates">The Easiest Path to Typed
Templates</h2>

<p>Another nice thing about <code>#[TemplateVariable]</code> is that it
naturally encourages using <b>typed properties</b> as your template's data
source. Types are checked by PHP itself (and picked up by IDEs and static
analysis), so instead of dynamically stuffing values into
<code>$this-&gt;template</code>, you're working with proper, strongly-typed
values (<code>Article</code>, <code>bool</code>, <code>string</code>…).</p>

<p>Nette offers a complete solution for type-safe templates through <a
href="https://doc.nette.org/en/application/templates#toc-type-safe-templates">custom
template classes</a>, where variable types are explicitly defined and
<code>$this-&gt;template</code> becomes a well-defined object instead of a
generic container.</p>

<p>That said, <code>#[TemplateVariable]</code> can be a nice lightweight
alternative for small to medium-sized use cases: you don't want the overhead of
creating a dedicated template class, but you still want clean code with proper
type safety. And when your project grows, you can always migrate to the full
strictly-typed template approach.</p>

		
			]]></content:encoded>
			<pubDate>Mon, 22 Dec 2025 04:12:00 +0100</pubDate>
			<guid isPermaLink="false">item529@http://blog.nette.org</guid>
		</item>
		<item>
			<title>Latte 3.1: When a templating system truly understands HTML</title>
			<link>https://blog.nette.org/en/latte-3-1-when-a-templating-system-truly-understands-html</link>
			<description>Latte has long held the position of the safest templating system for PHP.
It&apos;s not just a marketing phrase – it&apos;s a consequence of the fact that
Latte (unlike Twig or Blade) understands context. It doesn&apos;t just distinguish
between “HTML” and “PHP code”. It sees tags, attributes, values. And in
version 3.1, we pushed this understanding to a level that will radically improve
your DX.

While version 3.0 brought a completely new compiler and parser, version
3.1 focuses on semantics and code cleanliness. It comes with the concept of Smart HTML Attributes,
which eliminates dozens of lines of unnecessary conditions and helpers.

Let&apos;s look at what Latte 3.1 does under the hood and why you&apos;ll want to
use it.

Native mapping of
PHP types to HTML attributes

Until now, the template was a passive string generator. If you sent
null to &lt;span title=&quot;{$title}&quot;&gt;, you got an
empty string title=&quot;&quot;. If you sent an array, you got
title=&quot;Array&quot;. Latte 3.1 changes the rules of the game and
introduces strict rendering logic based on data types.

Null means “attribute does
not exist”

In the DOM, there is a difference between &lt;div title=&quot;&quot;&gt;
(empty title) and &lt;div&gt; (no title). Latte 3.1 respects this
difference.


	Behavior: A null value in any HTML attribute results in
	its complete removal from the output.

	Impact: The end of n:attr or {if}
	conditions inside tags. You just pass data.


Boolean attributes without
magic

Attributes like disabled, checked or
required are binary. Either they are there, or not. Latte now
accepts an expression directly in the attribute value.


	Behavior: Truthy expression ⇒ attribute is rendered. Falsey
	expression ⇒ attribute disappears.

	Advantage: n:attr is a great tool, but for this common
	use-case it was “overkill”. Now the syntax is clean and declarative:


&lt;input disabled={$isDisabled}&gt;


Arrays in class and style

Attributes that expect a list of values (class,
rel, sandbox…) now natively accept arrays. Latte
handles the intelligent rendering.

&lt;div class={[
    btn,
    btn-primary =&gt; $isPrimary, // added only if true
]}&gt;&lt;/div&gt;

&lt;div style={[
	background =&gt; lightblue,
	display =&gt; $isVisible ? block : null,
	font-size =&gt; &apos;16px&apos;,
]}&gt;&lt;/div&gt;


Automatic JSON serialization

You can pass an array or object (stdClass) to
data-* attributes. Latte detects the context and automatically
performs json_encode.

WAI-ARIA compliance without
effort

Accessibility is important, but the WAI-ARIA specification has its specifics.
Unlike standard HTML boolean attributes, where mere presence decides, ARIA
requires explicit text values &quot;true&quot; and &quot;false&quot;.

Latte 3.1 solves this problem for you. It detects the aria-
prefix and automatically ensures correct stringification.

&lt;button aria-expanded={$isExpanded} aria-hidden={=false}&gt;
{* &lt;button aria-expanded=&quot;true&quot; aria-hidden=&quot;false&quot;&gt; *}


Advantage: No more manual writing of ternary operators like
{$val ? &apos;true&apos; : &apos;false&apos;}. Latte guarantees valid output.

Type safety in templates

Latte 3.1 is stricter. And that is good. Common templating systems allow you
to print an array into href, generating a broken link
&lt;a href=&quot;Array&quot;&gt;.

Latte 3.1 introduces Runtime Type Checking for attributes.


	Trying to print a boolean into href? Warning.

	Trying to print an object without __toString into
	title? Warning.


Thanks to this, you detect errors in presentation logic immediately during
development, not when a user complains about broken UI. Moreover, the warning
contains the exact line and column in the template.

Strict Types by Default

We are keeping up with the times. The PHP ecosystem has matured to
strict_types=1 and Latte is not lagging behind. All templates in
Latte 3.1 are compiled with this directive by default. This ensures consistent
behavior with your backend code and prevents unwanted type casting in critical
template logic.

Nullsafe filters:
The Holy Grail for Strict Types

You might be wondering how filters, strict types, and HTML attributes are
related. In Latte 3.1, very closely.

Imagine a situation where you want to print a title in uppercase, but the
variable might be null.

&lt;div title={$title|upper}&gt;


If $title === null, you encounter two problems:


	Strict types: If the upper filter expects a
	string but receives null, a silent conversion to an
	empty string would have occurred previously, but with active strict types, a
	TypeError occurs.

	Loss of “Smart” behavior: Even if the filter accepts
	null, it returns an empty string &quot;&quot;, which for Latte
	is no longer null. The result is &lt;div title=&quot;&quot;&gt;.
	The attribute does not disappear, it is just empty.


The solution is the new Nullsafe filter operator ?|.

It works exactly like ?-&gt; in PHP. If the input value is
null, the filter is not called at all (nor following filters) and
the expression returns null.

&lt;div title={$title?|upper}&gt;


Result? No TypeError, because the filter is not called on null.
And the Smart Attribute works, so title completely disappears from
the HTML.

Syntactic sugar for cleaner
code

We heard the community&apos;s call and added missing pieces of syntax:


	n:elseif: The missing link in chaining conditions using
	n:attributes is finally here.

	n:attributes: Alternative syntax
	&lt;div n:if={$cond}&gt;, thanks to which you can freely use single
	and double quotes inside {...}.


How to upgrade safely?

Changing the behavior of null and boolean attributes is a BC
break, but we thought about that. Latte 3.1 includes a Migration
Mode.


	Call
	$latte-&gt;setFeature(Latte\Feature::MigrationWarnings);.

	Click through the application. Latte will print exactly those places where
	the output differs from version 3.0 to Tracy/log.

	Use the temporary |accept filter to confirm that the new
	behavior (e.g. disappearance of an empty attribute) is desired, or modify
	the code.


Latte 3.1 is not just “another version”. It is a shift towards modern,
type-safe, and semantic HTML generation. Try it out and see how much cleaner
your templates can be.

👉 Complete
migration guide and documentation
</description>
			<content:encoded><![CDATA[
			
		<p>Latte has long held the position of the safest templating system for PHP.
It's not just a marketing phrase – it's a consequence of the fact that
Latte (unlike Twig or Blade) understands context. It doesn't just distinguish
between “HTML” and “PHP code”. It sees tags, attributes, values. And in
version 3.1, we pushed this understanding to a level that will radically improve
your DX.</p>

<p>While version 3.0 brought a completely new compiler and parser, version
3.1 focuses on semantics and code cleanliness. It comes with the concept of <a
href="https://latte.nette.org/en/html-attributes">Smart HTML Attributes</a>,
which eliminates dozens of lines of unnecessary conditions and helpers.</p>

<p>Let's look at what Latte 3.1 does under the hood and why you'll want to
use it.</p>

<h2 id="toc-native-mapping-of-php-types-to-html-attributes">Native mapping of
PHP types to HTML attributes</h2>

<p>Until now, the template was a passive string generator. If you sent
<code>null</code> to <code>&lt;span title="{$title}"&gt;</code>, you got an
empty string <code>title=""</code>. If you sent an array, you got
<code>title="Array"</code>. Latte 3.1 changes the rules of the game and
introduces strict rendering logic based on data types.</p>

<h3 id="toc-null-means-attribute-does-not-exist">Null means “attribute does
not exist”</h3>

<p>In the DOM, there is a difference between <code>&lt;div title=""&gt;</code>
(empty title) and <code>&lt;div&gt;</code> (no title). Latte 3.1 respects this
difference.</p>

<ul>
	<li><b>Behavior:</b> A <code>null</code> value in any HTML attribute results in
	its complete removal from the output.</li>

	<li><b>Impact:</b> The end of <code>n:attr</code> or <code>{if}</code>
	conditions inside tags. You just pass data.</li>
</ul>

<h3 id="toc-boolean-attributes-without-magic">Boolean attributes without
magic</h3>

<p>Attributes like <code>disabled</code>, <code>checked</code> or
<code>required</code> are binary. Either they are there, or not. Latte now
accepts an expression directly in the attribute value.</p>

<ul>
	<li><b>Behavior:</b> Truthy expression ⇒ attribute is rendered. Falsey
	expression ⇒ attribute disappears.</li>

	<li><b>Advantage:</b> <code>n:attr</code> is a great tool, but for this common
	use-case it was “overkill”. Now the syntax is clean and declarative:</li>
</ul>

<pre
class="language-latte"><code>&lt;input disabled={$isDisabled}&gt;
</code></pre>

<h3 id="toc-arrays-in-class-and-style">Arrays in class and style</h3>

<p>Attributes that expect a list of values (<code>class</code>,
<code>rel</code>, <code>sandbox</code>…) now natively accept arrays. Latte
handles the intelligent rendering.</p>

<pre
class="language-latte"><code>&lt;div class={[
    btn,
    btn-primary =&gt; $isPrimary, // added only if true
]}&gt;&lt;/div&gt;

&lt;div style={[
	background =&gt; lightblue,
	display =&gt; $isVisible ? block : null,
	font-size =&gt; &#039;16px&#039;,
]}&gt;&lt;/div&gt;
</code></pre>

<h3 id="toc-automatic-json-serialization">Automatic JSON serialization</h3>

<p>You can pass an array or object (<code>stdClass</code>) to
<code>data-*</code> attributes. Latte detects the context and automatically
performs <code>json_encode</code>.</p>

<h3 id="toc-wai-aria-compliance-without-effort">WAI-ARIA compliance without
effort</h3>

<p>Accessibility is important, but the WAI-ARIA specification has its specifics.
Unlike standard HTML boolean attributes, where mere presence decides, ARIA
requires explicit text values <code>"true"</code> and <code>"false"</code>.</p>

<p>Latte 3.1 solves this problem for you. It detects the <code>aria-</code>
prefix and automatically ensures correct stringification.</p>

<pre
class="language-latte"><code>&lt;button aria-expanded={$isExpanded} aria-hidden={=false}&gt;
{* &lt;button aria-expanded=&quot;true&quot; aria-hidden=&quot;false&quot;&gt; *}
</code></pre>

<p><b>Advantage:</b> No more manual writing of ternary operators like
<code>{$val ? 'true' : 'false'}</code>. Latte guarantees valid output.</p>

<h2 id="toc-type-safety-in-templates">Type safety in templates</h2>

<p>Latte 3.1 is stricter. And that is good. Common templating systems allow you
to print an array into <code>href</code>, generating a broken link
<code>&lt;a href="Array"&gt;</code>.</p>

<p>Latte 3.1 introduces <b>Runtime Type Checking</b> for attributes.</p>

<ul>
	<li>Trying to print a boolean into <code>href</code>? <b>Warning.</b></li>

	<li>Trying to print an object without <code>__toString</code> into
	<code>title</code>? <b>Warning.</b></li>
</ul>

<p>Thanks to this, you detect errors in presentation logic immediately during
development, not when a user complains about broken UI. Moreover, the warning
contains the exact line and column in the template.</p>

<h2 id="toc-strict-types-by-default">Strict Types by Default</h2>

<p>We are keeping up with the times. The PHP ecosystem has matured to
<code>strict_types=1</code> and Latte is not lagging behind. All templates in
Latte 3.1 are compiled with this directive by default. This ensures consistent
behavior with your backend code and prevents unwanted type casting in critical
template logic.</p>

<h2 id="toc-nullsafe-filters-the-holy-grail-for-strict-types">Nullsafe filters:
The Holy Grail for Strict Types</h2>

<p>You might be wondering how filters, strict types, and HTML attributes are
related. In Latte 3.1, very closely.</p>

<p>Imagine a situation where you want to print a title in uppercase, but the
variable might be null.</p>

<pre class="language-latte"><code>&lt;div title={$title|upper}&gt;
</code></pre>

<p>If <code>$title === null</code>, you encounter two problems:</p>

<ol>
	<li><b>Strict types</b>: If the <code>upper</code> filter expects a
	<code>string</code> but receives <code>null</code>, a silent conversion to an
	empty string would have occurred previously, but with active strict types, a
	<code>TypeError</code> occurs.</li>

	<li><b>Loss of “Smart” behavior</b>: Even if the filter accepts
	<code>null</code>, it returns an empty string <code>""</code>, which for Latte
	is no longer <code>null</code>. The result is <code>&lt;div title=""&gt;</code>.
	The attribute does not disappear, it is just empty.</li>
</ol>

<p>The solution is the new Nullsafe filter operator <code>?|</code>.</p>

<p>It works exactly like <code>?-&gt;</code> in PHP. If the input value is
<code>null</code>, the filter is not called at all (nor following filters) and
the expression returns <code>null</code>.</p>

<pre
class="language-latte"><code>&lt;div title={$title?|upper}&gt;
</code></pre>

<p>Result? No <code>TypeError</code>, because the filter is not called on null.
And the Smart Attribute works, so <code>title</code> completely disappears from
the HTML.</p>

<h2 id="toc-syntactic-sugar-for-cleaner-code">Syntactic sugar for cleaner
code</h2>

<p>We heard the community's call and added missing pieces of syntax:</p>

<ul>
	<li><b><code>n:elseif</code></b>: The missing link in chaining conditions using
	n:attributes is finally here.</li>

	<li><b>n:attributes</b>: Alternative syntax
	<code>&lt;div n:if={$cond}&gt;</code>, thanks to which you can freely use single
	and double quotes inside <code>{...}</code>.</li>
</ul>

<h2 id="toc-how-to-upgrade-safely">How to upgrade safely?</h2>

<p>Changing the behavior of <code>null</code> and boolean attributes is a BC
break, but we thought about that. Latte 3.1 includes a <b>Migration
Mode</b>.</p>

<ul>
	<li>Call
	<code>$latte-&gt;setFeature(Latte\Feature::MigrationWarnings);</code>.</li>

	<li>Click through the application. Latte will print exactly those places where
	the output differs from version 3.0 to Tracy/log.</li>

	<li>Use the temporary <code>|accept</code> filter to confirm that the new
	behavior (e.g. disappearance of an empty attribute) is desired, or modify
	the code.</li>
</ul>

<p>Latte 3.1 is not just “another version”. It is a shift towards modern,
type-safe, and semantic HTML generation. Try it out and see how much cleaner
your templates can be.</p>

<p>👉 <b><a
href="https://latte.nette.org/en/cookbook/migration-from-latte-30">Complete
migration guide and documentation</a></b></p>

		
			]]></content:encoded>
			<pubDate>Wed, 26 Nov 2025 19:07:00 +0100</pubDate>
			<guid isPermaLink="false">item527@http://blog.nette.org</guid>
		</item>
		<item>
			<title>Nette Tester: HTTP testing has never been so easy</title>
			<link>https://blog.nette.org/en/nette-tester-http-testing-has-never-been-so-easy</link>
			<description>When was the last time you changed your server configuration, modified
.htaccess, or rewrote nginx rules? And did you check if this
accidentally broke redirects, made robots.txt inaccessible, or exposed a hidden
directory to unauthorized users? Do you check this automatically, or do you just
manually click through the website hoping everything works fine?

With the new HttpAssert class in Nette Tester 2.5.6, you can
automate all these critical checks and never have to worry that a configuration
change might break your website.

Imagine automatically verifying after every nginx configuration change:

// Critical paths are still accessible
HttpAssert::fetch(&apos;https://example.com/robots.txt&apos;)
    -&gt;expectCode(200)
    -&gt;expectHeader(&apos;Content-Type&apos;, contains: &apos;text/plain&apos;);

// Admin section is properly protected
HttpAssert::fetch(&apos;https://example.com/admin&apos;)
    -&gt;expectCode(403);

// API endpoint works
HttpAssert::fetch(&apos;https://example.com/api/health&apos;)
    -&gt;expectCode(200)
    -&gt;expectBody(contains: &apos;&quot;status&quot;:&quot;ok&quot;&apos;);


All validation methods at
your fingertips

HttpAssert allows you to easily perform HTTP requests and verify
their status codes, headers, and response body content. The fetch()
method returns an HttpAssert instance for subsequent verification
using a fluent interface. You can use the same pattern matching you know from
Assert::match().

HttpAssert::fetch(&apos;https://api.example.com/data&apos;)
    // Status codes
    -&gt;expectCode(200)                                   // exact code

    // Headers in every way
    -&gt;expectHeader(&apos;Content-Type&apos;)                      // header must exist
    -&gt;expectHeader(&apos;Content-Type&apos;, &apos;application/json&apos;)  // exact value
    -&gt;expectHeader(&apos;Content-Type&apos;, contains: &apos;json&apos;)    // contains text
    -&gt;expectHeader(&apos;Server&apos;, matches: &apos;nginx %a%&apos;)      // pattern matching

    // Response body
    -&gt;expectBody(&apos;OK&apos;)                                  // exact value
    -&gt;expectBody(contains: &apos;&quot;success&quot;: true&apos;)           // contains text
    -&gt;expectBody(matches: &apos;%A%&quot;users&quot;:[%a%]%A%&apos;);       // matches pattern


The deny* methods are also available and support the same options as their
expect* counterparts:

HttpAssert::fetch(&apos;https://api.example.com/data&apos;)
    -&gt;denyCode(200)                                   // must not be 200
    -&gt;denyHeader(&apos;Content-Type&apos;)                      // header must not exist
    -&gt;denyHeader(&apos;Content-Type&apos;, contains: &apos;json&apos;)    // does not contain text
    -&gt;denyHeader(&apos;Server&apos;, matches: &apos;nginx %a%&apos;)      // does not match pattern
    -&gt;denyBody(&apos;OK&apos;)                                  // body must not have value
    -&gt;denyBody(contains: &apos;&quot;success&quot;: true&apos;)           // does not contain text
    -&gt;denyBody(matches: &apos;~exception|fatal~i&apos;);        // does not match pattern


Callback functions for
advanced validation

Sometimes you need more than just simple comparisons. That&apos;s why HttpAssert
supports callback functions in all verification methods. You can write your own
validation logic while still using the elegant API:

HttpAssert::fetch(&apos;https://api.example.com/data&apos;)
    -&gt;expectCode(fn($code) =&gt; $code === 200 || $code === 201)
    -&gt;expectHeader(&apos;Content-Length&apos;, fn($length) =&gt; $length &gt; 1000)
    -&gt;expectBody(fn($body) =&gt; json_decode($body) !== null);


Intelligent redirect handling

You have full control over redirects – you can follow them or test them
separately:

// Test redirect without following
HttpAssert::fetch(&apos;https://example.com/old-blog&apos;, follow: false)
    -&gt;expectCode(301)
    -&gt;expectHeader(&apos;Location&apos;, &apos;https://example.com/blog&apos;);

// Follow redirects to the end
HttpAssert::fetch(&apos;https://example.com/old-blog&apos;, follow: true)
    -&gt;expectCode(200)
    -&gt;expectBody(contains: &apos;Welcome to our new blog&apos;);


Flexible request configuration

Whether you&apos;re testing GET, POST, PUT, DELETE, or any other HTTP method,
HttpAssert can handle it. And of course you can send a request body, headers, or
cookies:

HttpAssert::fetch(
    &apos;https://api.example.com/protected&apos;,
    method: &apos;POST&apos;,
    headers: [
        &apos;Authorization&apos; =&gt; &apos;Bearer &apos; . $token,  // associative array
        &apos;Accept: application/json&apos;,              // or as string
    ],
    cookies: [&apos;session&apos; =&gt; $sessionId],
    body: json_encode($data)
)
    -&gt;expectCode(201)
    -&gt;expectBody(contains: &apos;created&apos;);


Clear error messages

When a test fails, HttpAssert tells you exactly what
went wrong:

Expected HTTP status code 200 but got 404
Header &apos;Content-Type&apos; should contain &apos;json&apos; but was &apos;text/html&apos;
Body should contain &apos;success&apos;


What else is new in Tester
2.5.6?

Besides HttpAssert, version 2.5.6 also brings support for the
upcoming PHP 8.5, which will be released at the end of the year. Nette Tester
thus always stays one step ahead and ready for the latest PHP versions.

Try HttpAssert and let us know how you like it!
</description>
			<content:encoded><![CDATA[
			
		<p>When was the last time you changed your server configuration, modified
<code>.htaccess</code>, or rewrote nginx rules? And did you check if this
accidentally broke redirects, made robots.txt inaccessible, or exposed a hidden
directory to unauthorized users? Do you check this automatically, or do you just
manually click through the website hoping everything works fine?</p>

<p>With the new <code>HttpAssert</code> class in Nette Tester 2.5.6, you can
automate all these critical checks and never have to worry that a configuration
change might break your website.</p>

<p>Imagine automatically verifying after every nginx configuration change:</p>

<pre
class="language-php"><code>// Critical paths are still accessible
HttpAssert::fetch(&#039;https://example.com/robots.txt&#039;)
    -&gt;expectCode(200)
    -&gt;expectHeader(&#039;Content-Type&#039;, contains: &#039;text/plain&#039;);

// Admin section is properly protected
HttpAssert::fetch(&#039;https://example.com/admin&#039;)
    -&gt;expectCode(403);

// API endpoint works
HttpAssert::fetch(&#039;https://example.com/api/health&#039;)
    -&gt;expectCode(200)
    -&gt;expectBody(contains: &#039;&quot;status&quot;:&quot;ok&quot;&#039;);
</code></pre>

<h2 id="toc-all-validation-methods-at-your-fingertips">All validation methods at
your fingertips</h2>

<p><code>HttpAssert</code> allows you to easily perform HTTP requests and verify
their status codes, headers, and response body content. The <code>fetch()</code>
method returns an <code>HttpAssert</code> instance for subsequent verification
using a fluent interface. You can use the same pattern matching you know from
Assert::match().</p>

<pre
class="language-php"><code>HttpAssert::fetch(&#039;https://api.example.com/data&#039;)
    // Status codes
    -&gt;expectCode(200)                                   // exact code

    // Headers in every way
    -&gt;expectHeader(&#039;Content-Type&#039;)                      // header must exist
    -&gt;expectHeader(&#039;Content-Type&#039;, &#039;application/json&#039;)  // exact value
    -&gt;expectHeader(&#039;Content-Type&#039;, contains: &#039;json&#039;)    // contains text
    -&gt;expectHeader(&#039;Server&#039;, matches: &#039;nginx %a%&#039;)      // pattern matching

    // Response body
    -&gt;expectBody(&#039;OK&#039;)                                  // exact value
    -&gt;expectBody(contains: &#039;&quot;success&quot;: true&#039;)           // contains text
    -&gt;expectBody(matches: &#039;%A%&quot;users&quot;:[%a%]%A%&#039;);       // matches pattern
</code></pre>

<p>The deny* methods are also available and support the same options as their
expect* counterparts:</p>

<pre
class="language-php"><code>HttpAssert::fetch(&#039;https://api.example.com/data&#039;)
    -&gt;denyCode(200)                                   // must not be 200
    -&gt;denyHeader(&#039;Content-Type&#039;)                      // header must not exist
    -&gt;denyHeader(&#039;Content-Type&#039;, contains: &#039;json&#039;)    // does not contain text
    -&gt;denyHeader(&#039;Server&#039;, matches: &#039;nginx %a%&#039;)      // does not match pattern
    -&gt;denyBody(&#039;OK&#039;)                                  // body must not have value
    -&gt;denyBody(contains: &#039;&quot;success&quot;: true&#039;)           // does not contain text
    -&gt;denyBody(matches: &#039;~exception|fatal~i&#039;);        // does not match pattern
</code></pre>

<h2 id="toc-callback-functions-for-advanced-validation">Callback functions for
advanced validation</h2>

<p>Sometimes you need more than just simple comparisons. That's why HttpAssert
supports callback functions in all verification methods. You can write your own
validation logic while still using the elegant API:</p>

<pre
class="language-php"><code>HttpAssert::fetch(&#039;https://api.example.com/data&#039;)
    -&gt;expectCode(fn($code) =&gt; $code === 200 || $code === 201)
    -&gt;expectHeader(&#039;Content-Length&#039;, fn($length) =&gt; $length &gt; 1000)
    -&gt;expectBody(fn($body) =&gt; json_decode($body) !== null);
</code></pre>

<h2 id="toc-intelligent-redirect-handling">Intelligent redirect handling</h2>

<p>You have full control over redirects – you can follow them or test them
separately:</p>

<pre
class="language-php"><code>// Test redirect without following
HttpAssert::fetch(&#039;https://example.com/old-blog&#039;, follow: false)
    -&gt;expectCode(301)
    -&gt;expectHeader(&#039;Location&#039;, &#039;https://example.com/blog&#039;);

// Follow redirects to the end
HttpAssert::fetch(&#039;https://example.com/old-blog&#039;, follow: true)
    -&gt;expectCode(200)
    -&gt;expectBody(contains: &#039;Welcome to our new blog&#039;);
</code></pre>

<h2 id="toc-flexible-request-configuration">Flexible request configuration</h2>

<p>Whether you're testing GET, POST, PUT, DELETE, or any other HTTP method,
HttpAssert can handle it. And of course you can send a request body, headers, or
cookies:</p>

<pre
class="language-php"><code>HttpAssert::fetch(
    &#039;https://api.example.com/protected&#039;,
    method: &#039;POST&#039;,
    headers: [
        &#039;Authorization&#039; =&gt; &#039;Bearer &#039; . $token,  // associative array
        &#039;Accept: application/json&#039;,              // or as string
    ],
    cookies: [&#039;session&#039; =&gt; $sessionId],
    body: json_encode($data)
)
    -&gt;expectCode(201)
    -&gt;expectBody(contains: &#039;created&#039;);
</code></pre>

<h2 id="toc-clear-error-messages">Clear error messages</h2>

<p>When a test fails, <code>HttpAssert</code> tells you exactly what
went wrong:</p>

<pre><code>Expected HTTP status code 200 but got 404
Header &#039;Content-Type&#039; should contain &#039;json&#039; but was &#039;text/html&#039;
Body should contain &#039;success&#039;
</code></pre>

<h2 id="toc-what-else-is-new-in-tester-2-5-6">What else is new in Tester
2.5.6?</h2>

<p>Besides <code>HttpAssert</code>, version 2.5.6 also brings support for the
upcoming PHP 8.5, which will be released at the end of the year. Nette Tester
thus always stays one step ahead and ready for the latest PHP versions.</p>

<p>Try <code>HttpAssert</code> and let us know how you like it!</p>

		
			]]></content:encoded>
			<pubDate>Thu, 07 Aug 2025 01:57:00 +0200</pubDate>
			<guid isPermaLink="false">item525@http://blog.nette.org</guid>
		</item>
		<item>
			<title>{linkBase} brings consistency to linking</title>
			<link>https://blog.nette.org/en/linkbase-brings-consistency-to-linking</link>
			<description>One of the most valuable features of Nette Application is its flexible
directory structure, which seamlessly adapts to a project&apos;s growing needs.
Imagine you&apos;re building an e-shop. You start with a frontend and administration,
each with its own presenters. Gradually, the application expands – perhaps
what was originally a simple OrderPresenter eventually evolves into
a complete order module with presenters like OrderDetail,
OrderEdit, OrderDispatch, and others. Thanks to the
flexible structure, you can make this reorganization very easily and
elegantly.

However, this flexibility can present one developer challenge: what happens
when a single layout suddenly gets used by presenters that are at “different
levels” in the directory structure?

For example, we have @layout.latte for administration, where we
want navigation with links to various sections:

&lt;nav&gt;
    &lt;a n:href=&quot;Dashboard:&quot;&gt;Dashboard&lt;/a&gt;
    &lt;a n:href=&quot;Products:Overview:&quot;&gt;Products&lt;/a&gt;
    &lt;a n:href=&quot;Users:list&quot;&gt;Users&lt;/a&gt;
&lt;/nav&gt;


When this layout is used by the Admin:Dashboard presenter, the
links will correctly lead to Admin:Dashboard:default,
Admin:Users:list, etc. But what if the same layout is used by the
Admin:Products:Detail presenter? Relative links will suddenly be
derived from it and lead to non-existent targets (e.g.,
Admin:Products:Dashboard:default).

Previously, this was solved by writing all links as absolute paths (or using
aliases):

{* Everything absolute *}
&lt;nav&gt;
    &lt;a n:href=&quot;:Admin:Dashboard:&quot;&gt;Dashboard&lt;/a&gt;
    &lt;a n:href=&quot;:Admin:Products:Overview:&quot;&gt;Products&lt;/a&gt;
    &lt;a n:href=&quot;:Admin:Users:list&quot;&gt;Users&lt;/a&gt;
&lt;/nav&gt;


This works, but has its drawbacks. Especially if you later decide to move the
module elsewhere or rename it.

The solution: {linkBase}

Nette Application 3.2.7 introduces an elegant solution in the form of a new
Latte tag {linkBase}. It defines the base from which all relative
links in the template will be derived.

{linkBase Admin}

&lt;nav&gt;
    &lt;a n:href=&quot;Dashboard:&quot;&gt;Dashboard&lt;/a&gt;
    &lt;a n:href=&quot;Products:Overview:&quot;&gt;Products&lt;/a&gt;
    &lt;a n:href=&quot;Users:list&quot;&gt;Users&lt;/a&gt;
&lt;/nav&gt;


Now it doesn&apos;t matter which presenter calls the layout. All relative links
will be derived from the Admin module:


	Dashboard: → Admin:Dashboard:default

	Products:Overview: →
	Admin:Products:Overview:default

	Users:list → Admin:Users:list


{linkBase} only affects relative links – those that don&apos;t
start with a colon. Absolute links (:Admin:Dashboard) and links to
the current presenter (this, show) remain
unchanged.

The tag applies to the entire template and works with all ways of creating
links: {link}, {plink}, n:href.

Bonus: |absoluteUrl filter

Nette Application 3.2.7 also brings another useful addition – the Latte
filter |absoluteUrl. It normalizes URLs to absolute form, which is
useful when you need a guaranteed absolute address:

&lt;meta property=&quot;og:image&quot; content={$imagePath|absoluteUrl}&gt;


Update to Nette Application 3.2.7 and tell us what you think of the new
features.
</description>
			<content:encoded><![CDATA[
			
		<p>One of the most valuable features of Nette Application is its <a
href="https://doc.nette.org/en/application/directory-structure#toc-presenters-and-templates">flexible
directory structure</a>, which seamlessly adapts to a project's growing needs.
Imagine you're building an e-shop. You start with a frontend and administration,
each with its own presenters. Gradually, the application expands – perhaps
what was originally a simple <code>OrderPresenter</code> eventually evolves into
a complete order module with presenters like <code>OrderDetail</code>,
<code>OrderEdit</code>, <code>OrderDispatch</code>, and others. Thanks to the
flexible structure, you can make this reorganization very easily and
elegantly.</p>

<p>However, this flexibility can present one developer challenge: what happens
when a single layout suddenly gets used by presenters that are at “different
levels” in the directory structure?</p>

<p>For example, we have <code>@layout.latte</code> for administration, where we
want navigation with links to various sections:</p>

<pre
class="language-latte"><code>&lt;nav&gt;
    &lt;a n:href=&quot;Dashboard:&quot;&gt;Dashboard&lt;/a&gt;
    &lt;a n:href=&quot;Products:Overview:&quot;&gt;Products&lt;/a&gt;
    &lt;a n:href=&quot;Users:list&quot;&gt;Users&lt;/a&gt;
&lt;/nav&gt;
</code></pre>

<p>When this layout is used by the <code>Admin:Dashboard</code> presenter, the
links will correctly lead to <code>Admin:Dashboard:default</code>,
<code>Admin:Users:list</code>, etc. But what if the same layout is used by the
<code>Admin:Products:Detail</code> presenter? Relative links will suddenly be
derived from it and lead to non-existent targets (e.g.,
<code>Admin:Products:Dashboard:default</code>).</p>

<p>Previously, this was solved by writing all links as absolute paths (or using
<a
href="https://blog.nette.org/en/aliases-a-new-feature-in-application-navigation">aliases</a>):</p>

<pre
class="language-latte"><code>{* Everything absolute *}
&lt;nav&gt;
    &lt;a n:href=&quot;:Admin:Dashboard:&quot;&gt;Dashboard&lt;/a&gt;
    &lt;a n:href=&quot;:Admin:Products:Overview:&quot;&gt;Products&lt;/a&gt;
    &lt;a n:href=&quot;:Admin:Users:list&quot;&gt;Users&lt;/a&gt;
&lt;/nav&gt;
</code></pre>

<p>This works, but has its drawbacks. Especially if you later decide to move the
module elsewhere or rename it.</p>

<h2 id="toc-the-solution-linkbase">The solution: {linkBase}</h2>

<p>Nette Application 3.2.7 introduces an elegant solution in the form of a new
Latte tag <code>{linkBase}</code>. It defines the base from which all relative
links in the template will be derived.</p>

<pre
class="language-latte"><code>{linkBase Admin}

&lt;nav&gt;
    &lt;a n:href=&quot;Dashboard:&quot;&gt;Dashboard&lt;/a&gt;
    &lt;a n:href=&quot;Products:Overview:&quot;&gt;Products&lt;/a&gt;
    &lt;a n:href=&quot;Users:list&quot;&gt;Users&lt;/a&gt;
&lt;/nav&gt;
</code></pre>

<p>Now it doesn't matter which presenter calls the layout. All relative links
will be derived from the <code>Admin</code> module:</p>

<ul>
	<li><code>Dashboard:</code> → <code>Admin:Dashboard:default</code></li>

	<li><code>Products:Overview:</code> →
	<code>Admin:Products:Overview:default</code></li>

	<li><code>Users:list</code> → <code>Admin:Users:list</code></li>
</ul>

<p><code>{linkBase}</code> only affects relative links – those that don't
start with a colon. Absolute links (<code>:Admin:Dashboard</code>) and links to
the current presenter (<code>this</code>, <code>show</code>) remain
unchanged.</p>

<p>The tag applies to the entire template and works with all ways of creating
links: <code>{link}</code>, <code>{plink}</code>, <code>n:href</code>.</p>

<h2 id="toc-bonus-absoluteurl-filter">Bonus: |absoluteUrl filter</h2>

<p>Nette Application 3.2.7 also brings another useful addition – the Latte
filter <code>|absoluteUrl</code>. It normalizes URLs to absolute form, which is
useful when you need a guaranteed absolute address:</p>

<pre
class="language-latte"><code>&lt;meta property=&quot;og:image&quot; content={$imagePath|absoluteUrl}&gt;
</code></pre>

<p>Update to Nette Application 3.2.7 and tell us what you think of the new
features.</p>

		
			]]></content:encoded>
			<pubDate>Fri, 18 Jul 2025 02:40:00 +0200</pubDate>
			<guid isPermaLink="false">item523@http://blog.nette.org</guid>
		</item>
		<item>
			<title>Nette Assets: Finally unified API for everything from images to Vite</title>
			<link>https://blog.nette.org/en/introducing-nette-assets</link>
			<description>Introducing Nette Assets – a
library that completely transforms working with static files in Nette
applications. Tested and proven on dozens of projects.

How many times have you found yourself writing code like this?

&lt;link rel=&quot;stylesheet&quot; href=&quot;{$baseUrl}/css/style.css?v=7&quot;&gt;

&lt;img src=&quot;{$baseUrl}/images/logo.png?v=1699123456&quot; width=&quot;200&quot; height=&quot;100&quot; alt=&quot;Logo&quot;&gt;


You&apos;ve been manually constructing URLs, adding versioning parameters,
determining and adding correct image dimensions and so on? Those days
are over.

Through years of developing web applications, I was looking for a solution
that would be very simple to use, yet infinitely open for extension.
Something I could use across all my websites – from simple presentations to
complex e-shops. After testing various approaches and validating numerous
principles on real projects, Nette Assets was eventually born.

With Nette Assets, the same code changes to:

{asset &apos;css/style.css&apos;}

&lt;img n:asset=&quot;images/logo.png&quot; alt=&quot;Logo&quot;&gt;


And that&apos;s it! The library automatically:

✅ Detects image dimensions and inserts them into HTML
✅ Adds versioning parameters based on file modification time
✅ Has native Vite support with Hot Module Replacement

No configuration!

Let&apos;s start with the simplest scenario: you have a www/assets/
folder full of images, CSS and JS files. You want to include them in pages with
proper versioning and automatic dimensions. Then you don&apos;t need to configure
anything at all, just install Nette Assets:

composer require nette/assets


… and you can start using all the new Latte tags and they
will work immediately:

{asset &apos;css/style.css&apos;}
&lt;img n:asset=&quot;images/logo.png&quot;&gt;


I mainly use the {asset} tag for CSS and scripts, while
preferring n:asset for images since I like having HTML elements
like &lt;img&gt; explicitly visible in templates – but it&apos;s a
matter of personal preference.

This generates code like:

&lt;link rel=&quot;stylesheet&quot; href=&quot;https://example.cz/assets/css/style.css?v=1670133401&quot;&gt;
&lt;img src=&quot;https://example.cz/assets/images/logo.png?v=1699123456&quot; width=&quot;200&quot; height=&quot;100&quot;&gt;


Why the versioning? Browsers cache static files and when
you change a file, the browser may still use the old version. So-called cache
busting solves this problem by adding a parameter like
?v=1699123456, which changes with each file modification and forces
the browser to download a new version. Versioning can be disabled in
configuration.

Let&apos;s add some configuration

Do you prefer a different folder name than assets? In that case
we need to add configuration to common.neon, but just three lines
are enough:

assets:
	mapping:
		default: static  # files in /www/static/


Sometimes I put assets on a separate subdomain, like here on the Nette
website. The configuration then looks like this:

assets:
	mapping:
		default:
			path: %rootDir%/www.files     # files in /www.files/
			url: https://files.nette.org


On the blog I wanted to keep things organized and divide assets logically
into three categories: website design (logo, icons, styles), images in articles
and recorded versions of articles. Each category has its own folder:

assets:
	mapping:
		default: assets       # regular files /www/assets/
		content: media/images # article images in /www/media/images/
		voice: media/audio    # audio files in /www/media/audio/


To access different categories we use colon notation
category:file. If you don&apos;t specify a category, the default
(default) is used. So in the template I have:

{* Article images *}
&lt;img n:asset=&quot;content:hero-photo.jpg&quot; alt=&quot;Hero image&quot;&gt;

{* Audio version of article *}
&lt;audio n:asset=&quot;voice:123.mp3&quot; controls&gt;&lt;/audio&gt;


In real templates we naturally work with dynamic data. You usually have an
article in a variable, say $post, so I can&apos;t write
voice:123.mp3, I simply write:

&lt;audio n:asset=&quot;voice:{$post-&gt;id}.mp3&quot; controls&gt;&lt;/audio&gt;


To avoid worrying about audio formats in templates, I can add automatic
extension completion to the configuration:

assets:
	mapping:
		voice:
			path: media/audio
			extension: mp3  # automatically adds .mp3


Then just write:

&lt;audio n:asset=&quot;voice:{$post-&gt;id}&quot; controls&gt;&lt;/audio&gt;


When I later start using a better M4A format, I&apos;ll just need to modify the
configuration. And since it doesn&apos;t make sense to convert old recordings, I&apos;ll
specify an array of possible extensions – the system will try them in the
given order and use the first one for which a file exists:

assets:
	mapping:
		audio:
			path: media/audio
			extension: [m4a, mp3]  # tries M4A first, then MP3


What if the file simply
doesn&apos;t exist?

Not every article has a recorded version. I need to verify if the file
exists at all, and if not, not render the &lt;audio&gt;
element.

This is solved with a single character! Question mark in the attribute
name n:asset?

{* Shows audio player only if recorded version exists *}
&lt;audio n:asset?=&quot;voice:{$post-&gt;id}&quot; controls&gt;&lt;/audio&gt;


Similarly there&apos;s a question mark variant of the {asset?} tag.
And if you need to wrap an asset in your own HTML structure or add conditional
rendering logic, you can use the asset() function or its
“question mark” variant tryAsset():

{var $voice = tryAsset(&quot;voice:{$post-&gt;id}&quot;)}
&lt;div n:if=&quot;$voice&quot; class=&quot;voice-version&quot;&gt;
	&lt;p&gt;🎧 You can also listen to this article&lt;/p&gt;
	&lt;audio n:asset=$voice controls&gt;&lt;/audio&gt;
&lt;/div&gt;


The tryAsset() function returns an asset object or
null if the file doesn&apos;t exist. The asset() function
always returns an asset object or throws an exception. This enables various
patterns, useful is for example this fallback trick when you want to show a
replacement image:

{var $avatar = tryAsset(&quot;avatar:{$user-&gt;id}&quot;) ?? asset(&apos;avatar:default&apos;)}
&lt;img n:asset=$avatar alt=&quot;Avatar&quot;&gt;


And what objects do these functions actually return? Let&apos;s explore these
objects.

Working with assets in PHP
code

You work with assets not only in templates, but also in PHP code. The
Registry class serves to access them, which is the main interface
of the entire library. We have this service passed via dependency injection:

class ArticlePresenter extends Presenter
{
	public function __construct(
		private Nette\Assets\Registry $assets
	) {}
}


Registry provides getAsset() and tryGetAsset()
methods for getting asset objects. Each object represents some resource. It
doesn&apos;t have to be a physical file – it can be dynamically generated. Asset
carries information about URL and metadata. In the Nette\Assets
namespace you&apos;ll find these classes:


	ImageAsset – images (width, height)

	ScriptAsset – JavaScript files (type for modules)

	StyleAsset – CSS files (media queries)

	AudioAsset / VideoAsset – media (duration, dimensions)

	FontAsset – fonts (proper CORS headers)

	EntryAsset – entry points for Vite

	GenericAsset – other files (basic URL and metadata)


Each asset has readonly properties specific to its type:

// ImageAsset for images
$image = $this-&gt;assets-&gt;getAsset(&apos;photo.jpg&apos;);
echo $image-&gt;url;      // &apos;/assets/photo.jpg?v=1699123456&apos;
echo $image-&gt;width;    // 1920
echo $image-&gt;height;   // 1080
echo $image-&gt;mimeType; // &apos;image/jpeg&apos;

// AudioAsset for audio files
$audio = $this-&gt;assets-&gt;getAsset(&apos;song.mp3&apos;);
echo $audio-&gt;duration;  // duration in seconds

// ScriptAsset for JavaScript
$script = $this-&gt;assets-&gt;getAsset(&apos;app.js&apos;);
echo $script-&gt;type;     // &apos;module&apos; or null


Properties like image dimensions or audio file duration are loaded lazily
(only when you first use them), so the system stays fast even with thousands
of files.

First-class Vite integration

Nette Assets have native support for Vite.
This modern tool handles the need to compile (build) frontend code, because:


	it&apos;s written in other languages (TypeScript, Sass)

	it&apos;s transpiled for older browsers

	it&apos;s bundled into one file

	it&apos;s minified for smaller size


To avoid repeating this entire process with every change in development mode,
Vite serves files directly without bundling and minification. Every change is
thus reflected immediately. And as a bonus it can even do it without page
refresh – you edit CSS or JavaScript and the change is reflected in the
browser within milliseconds, without losing form data or application state. This
is called Hot Module Replacement.

For production, Vite then creates classic optimized builds – bundles,
minifies, splits into chunks and adds versioned names for cache busting.

In today&apos;s JavaScript ecosystem, Vite is the de facto standard for modern
frontend development. That&apos;s why it made sense to include support directly in
Nette Assets. Thanks to the open architecture, you can easily write a mapper for
any other bundler.

You might wonder: “Vite adapters for Nette already exist, what&apos;s different
about this?” Yes, they exist, and I thank their authors for that. The
difference is that Nette Assets is a complete system for managing all static
files, and Vite is one component, but very elegantly integrated.

Just add two words to the common.neon configuration:
type: vite.

assets:
	mapping:
		default:
			type: vite
			path: assets


Vite is known for working even without configuration or being very easy to
configure. However, backend integration adds complexity. That&apos;s why I created
@nette/vite-plugin, which
makes the whole integration simple again.

In the vite.config.ts configuration file, just activate the
plugin and specify the path to entry
points:

import { defineConfig } from &apos;vite&apos;;
import nette from &apos;@nette/vite-plugin&apos;;

export default defineConfig({
	plugins: [
		nette({
			entry: &apos;app.js&apos;,  // entry point
		}),
	],
});


And in the layout template we load the entry point:

&lt;!doctype html&gt;
&lt;head&gt;
	{asset &apos;app.js&apos;}
&lt;/head&gt;


And that&apos;s it! The system automatically takes care of the rest:


	In development mode loads files from Vite dev server with Hot Module
	Replacement

	In production uses compiled, optimized files


No configuration, no conditions in templates. It just works.

Power of custom mappers: Real
examples

A mapper is a component that knows where to find files and how to
create URLs for loading them. You can have multiple mappers for different
purposes – local files, CDN, cloud storage or build tools. The Nette Assets
system is completely open for creating custom mappers for anything.

Mapper for product images

On an e-shop you need product images in different sizes. You usually already
have a service that handles resize and image optimization. Simply wrap it with a
Nette Assets mapper.

The getAsset() method of your mapper must return the appropriate
asset type. In our case ImageAsset. If we additionally give the
constructor a file parameter with the path to the local file, it
automatically loads image dimensions (width, height) and MIME type. If the
requested file doesn&apos;t exist, we throw an AssetNotFoundException
exception:

class ProductImageMapper implements Mapper
{
	public function getAsset(string $reference, array $options = []): Asset
	{
		// $reference is product ID
		$product = $this-&gt;database-&gt;getProduct((int) $reference);
		if (!$product) {
			throw new Nette\Assets\AssetNotFoundException(&quot;Product $reference not found&quot;);
		}

		$size = $options[&apos;size&apos;] ?? &apos;medium&apos;; // small, medium, large

		// We leverage our existing image service
		return new Nette\Assets\ImageAsset(
			url: $this-&gt;imageService-&gt;getProductUrl($product, $size),
			file: $this-&gt;imageService-&gt;getProductFile($product, $size)
		);
	}
}


Registration in configuration:

assets:
	mapping:
		product: App\ProductImageMapper()


Usage in templates:

{* Different product sizes *}
&lt;img n:asset=&quot;product:{$product-&gt;id}, size: small&quot; alt=&quot;Product thumbnail&quot;&gt;
&lt;img n:asset=&quot;product:{$product-&gt;id}, size: large&quot; alt=&quot;Product detail&quot;&gt;


Automatic OG image generation

OG (Open Graph) images are displayed when sharing on social networks. Instead
of manual creation, you can have the system generate them automatically based on
content type.

In this case, the $reference parameter determines the image
context. For static pages (e.g. homepage, about)
pre-prepared files are used. For articles, the image is generated dynamically
based on the article title. The mapper first checks cache – if the image
already exists, it returns it. Otherwise it generates it and saves it to cache
for next use:

class OgImageMapper implements Mapper
{
	public function getAsset(string $reference, array $options = []): Asset
	{
		// For articles we generate dynamically based on title
		if ($reference === &apos;article&apos;) {
			$title = $options[&apos;title&apos;] ?? throw new LogicException(&apos;Missing option title for article&apos;);
			$filename = &apos;/generated/&apos; . md5(&quot;article-{$title}&quot;) . &apos;.png&apos;;
			$path = $this-&gt;staticDir . $filename;
			if (!file_exists($path)) {
				file_put_contents($path, $this-&gt;ogGenerator-&gt;createArticleImage($title));
			}

		} else { // For static pages we use pre-prepared files
			$filename = &quot;/{$reference}.png&quot;;
			$path = $this-&gt;staticDir . $filename;
			if (!file_exists($path)) {
				throw new Nette\Assets\AssetNotFoundException(&quot;Static OG image &apos;$reference&apos; not found&quot;);
			}
		}

		return new Nette\Assets\ImageAsset($this-&gt;baseUrl . $filename, file: $path);
	}
}


Then in the template we add to the header:

{* For static page *}
{var $ogImage = asset(&apos;og:homepage&apos;)}

{* For article with dynamic generation *}
{var $ogImage = asset(&apos;og:article&apos;, title: $article-&gt;title)}
&lt;meta property=&quot;og:image&quot; content={$ogImage}&gt;
&lt;meta property=&quot;og:image:width&quot; content={$ogImage-&gt;width}&gt;
&lt;meta property=&quot;og:image:height&quot; content={$ogImage-&gt;height}&gt;



Thanks to this approach you have automatically generated OG images for every
article, which are created only once and then cached for further use.

Nette Assets: more
elegant asset management

Nette Assets represent an elegant system that takes care of almost everything
automatically.


	Eliminates boilerplate code – no more manual URL construction

	Supports modern workflow – Vite, TypeScript, code splitting

	Is flexible – custom mappers, various storage systems

	Works immediately – no complex configuration


Complete description can be found in documentation and code on GitHub.

You&apos;ll never write
&lt;img src=&quot;/images/photo.jpg?v=123456&quot;&gt; manually
again!
</description>
			<content:encoded><![CDATA[
			
		<p>Introducing <a href="https://doc.nette.org/cs/assets">Nette Assets</a> – a
library that completely transforms working with static files in Nette
applications. Tested and proven on dozens of projects.</p>

<p>How many times have you found yourself writing code like this?</p>

<pre
class="language-latte"><code>&lt;link rel=&quot;stylesheet&quot; href=&quot;{$baseUrl}/css/style.css?v=7&quot;&gt;

&lt;img src=&quot;{$baseUrl}/images/logo.png?v=1699123456&quot; width=&quot;200&quot; height=&quot;100&quot; alt=&quot;Logo&quot;&gt;
</code></pre>

<p>You've been manually constructing URLs, adding versioning parameters,
determining and adding correct image dimensions and so on? <b>Those days
are over.</b></p>

<p>Through years of developing web applications, I was looking for a solution
that would be <b>very simple to use, yet infinitely open for extension</b>.
Something I could use across all my websites – from simple presentations to
complex e-shops. After testing various approaches and validating numerous
principles on real projects, <b>Nette Assets</b> was eventually born.</p>

<p>With Nette Assets, the same code changes to:</p>

<pre
class="language-latte"><code>{asset &#039;css/style.css&#039;}

&lt;img n:asset=&quot;images/logo.png&quot; alt=&quot;Logo&quot;&gt;
</code></pre>

<p><b>And that's it!</b> The library automatically:</p>

<p>✅ Detects image dimensions and inserts them into HTML<br>
✅ Adds versioning parameters based on file modification time<br>
✅ Has native Vite support with Hot Module Replacement</p>

<h2 id="toc-no-configuration">No configuration!</h2>

<p>Let's start with the simplest scenario: you have a <code>www/assets/</code>
folder full of images, CSS and JS files. You want to include them in pages with
proper versioning and automatic dimensions. Then you don't need to configure
anything at all, just install Nette Assets:</p>

<pre class="language-shell"><code>composer require nette/assets
</code></pre>

<p>… and you can start using all the <a
href="https://doc.nette.org/cs/assets#toc-asset">new Latte tags</a> and they
will work immediately:</p>

<pre
class="language-latte"><code>{asset &#039;css/style.css&#039;}
&lt;img n:asset=&quot;images/logo.png&quot;&gt;
</code></pre>

<p>I mainly use the <code>{asset}</code> tag for CSS and scripts, while
preferring <code>n:asset</code> for images since I like having HTML elements
like <code>&lt;img&gt;</code> explicitly visible in templates – but it's a
matter of personal preference.</p>

<p>This generates code like:</p>

<pre
class="language-latte"><code>&lt;link rel=&quot;stylesheet&quot; href=&quot;https://example.cz/assets/css/style.css?v=1670133401&quot;&gt;
&lt;img src=&quot;https://example.cz/assets/images/logo.png?v=1699123456&quot; width=&quot;200&quot; height=&quot;100&quot;&gt;
</code></pre>

<p class="note"><b>Why the versioning?</b> Browsers cache static files and when
you change a file, the browser may still use the old version. So-called cache
busting solves this problem by adding a parameter like
<code>?v=1699123456</code>, which changes with each file modification and forces
the browser to download a new version. Versioning can be disabled in
configuration.</p>

<h2 id="toc-let-s-add-some-configuration">Let's add some configuration</h2>

<p>Do you prefer a different folder name than <code>assets</code>? In that case
we need to add configuration to <code>common.neon</code>, but just three lines
are enough:</p>

<pre
class="language-neon"><code>assets:
	mapping:
		default: static  # files in /www/static/
</code></pre>

<p>Sometimes I put assets on a separate subdomain, like here on the Nette
website. The configuration then looks like this:</p>

<pre
class="language-neon"><code>assets:
	mapping:
		default:
			path: %rootDir%/www.files     # files in /www.files/
			url: https://files.nette.org
</code></pre>

<p>On the blog I wanted to keep things organized and divide assets logically
into three categories: website design (logo, icons, styles), images in articles
and recorded versions of articles. Each category has its own folder:</p>

<pre
class="language-neon"><code>assets:
	mapping:
		default: assets       # regular files /www/assets/
		content: media/images # article images in /www/media/images/
		voice: media/audio    # audio files in /www/media/audio/
</code></pre>

<p>To access different categories we use colon notation
<code>category:file</code>. If you don't specify a category, the default
(<code>default</code>) is used. So in the template I have:</p>

<pre
class="language-latte"><code>{* Article images *}
&lt;img n:asset=&quot;content:hero-photo.jpg&quot; alt=&quot;Hero image&quot;&gt;

{* Audio version of article *}
&lt;audio n:asset=&quot;voice:123.mp3&quot; controls&gt;&lt;/audio&gt;
</code></pre>

<p>In real templates we naturally work with dynamic data. You usually have an
article in a variable, say <code>$post</code>, so I can't write
<code>voice:123.mp3</code>, I simply write:</p>

<pre
class="language-latte"><code>&lt;audio n:asset=&quot;voice:{$post-&gt;id}.mp3&quot; controls&gt;&lt;/audio&gt;
</code></pre>

<p>To avoid worrying about audio formats in templates, I can add automatic
extension completion to the configuration:</p>

<pre
class="language-neon"><code>assets:
	mapping:
		voice:
			path: media/audio
			extension: mp3  # automatically adds .mp3
</code></pre>

<p>Then just write:</p>

<pre
class="language-latte"><code>&lt;audio n:asset=&quot;voice:{$post-&gt;id}&quot; controls&gt;&lt;/audio&gt;
</code></pre>

<p>When I later start using a better M4A format, I'll just need to modify the
configuration. And since it doesn't make sense to convert old recordings, I'll
specify an array of possible extensions – the system will try them in the
given order and use the first one for which a file exists:</p>

<pre
class="language-neon"><code>assets:
	mapping:
		audio:
			path: media/audio
			extension: [m4a, mp3]  # tries M4A first, then MP3
</code></pre>

<h2 id="toc-what-if-the-file-simply-doesn-t-exist">What if the file simply
doesn't exist?</h2>

<p>Not every article has a recorded version. I need to verify if the file
exists at all, and if not, not render the <code>&lt;audio&gt;</code>
element.</p>

<p><b>This is solved with a single character!</b> Question mark in the attribute
name <code>n:asset?</code></p>

<pre
class="language-latte"><code>{* Shows audio player only if recorded version exists *}
&lt;audio n:asset?=&quot;voice:{$post-&gt;id}&quot; controls&gt;&lt;/audio&gt;
</code></pre>

<p>Similarly there's a question mark variant of the <code>{asset?}</code> tag.
And if you need to wrap an asset in your own HTML structure or add conditional
rendering logic, you can use the <code>asset()</code> function or its
“question mark” variant <code>tryAsset()</code>:</p>

<pre
class="language-latte"><code>{var $voice = tryAsset(&quot;voice:{$post-&gt;id}&quot;)}
&lt;div n:if=&quot;$voice&quot; class=&quot;voice-version&quot;&gt;
	&lt;p&gt;🎧 You can also listen to this article&lt;/p&gt;
	&lt;audio n:asset=$voice controls&gt;&lt;/audio&gt;
&lt;/div&gt;
</code></pre>

<p>The <code>tryAsset()</code> function returns an asset object or
<code>null</code> if the file doesn't exist. The <code>asset()</code> function
always returns an asset object or throws an exception. This enables various
patterns, useful is for example this fallback trick when you want to show a
replacement image:</p>

<pre
class="language-latte"><code>{var $avatar = tryAsset(&quot;avatar:{$user-&gt;id}&quot;) ?? asset(&#039;avatar:default&#039;)}
&lt;img n:asset=$avatar alt=&quot;Avatar&quot;&gt;
</code></pre>

<p>And what objects do these functions actually return? Let's explore these
objects.</p>

<h2 id="toc-working-with-assets-in-php-code">Working with assets in PHP
code</h2>

<p>You work with assets not only in templates, but also in PHP code. The
<code>Registry</code> class serves to access them, which is the main interface
of the entire library. We have this service passed via dependency injection:</p>

<pre
class="language-php"><code>class ArticlePresenter extends Presenter
{
	public function __construct(
		private Nette\Assets\Registry $assets
	) {}
}
</code></pre>

<p>Registry provides <code>getAsset()</code> and <code>tryGetAsset()</code>
methods for getting asset objects. Each object represents some resource. It
doesn't have to be a physical file – it can be dynamically generated. Asset
carries information about URL and metadata. In the <code>Nette\Assets</code>
namespace you'll find these classes:</p>

<ul>
	<li><b>ImageAsset</b> – images (width, height)</li>

	<li><b>ScriptAsset</b> – JavaScript files (type for modules)</li>

	<li><b>StyleAsset</b> – CSS files (media queries)</li>

	<li><b>AudioAsset</b> / <b>VideoAsset</b> – media (duration, dimensions)</li>

	<li><b>FontAsset</b> – fonts (proper CORS headers)</li>

	<li><b>EntryAsset</b> – entry points for Vite</li>

	<li><b>GenericAsset</b> – other files (basic URL and metadata)</li>
</ul>

<p>Each asset has readonly properties specific to its type:</p>

<pre
class="language-php"><code>// ImageAsset for images
$image = $this-&gt;assets-&gt;getAsset(&#039;photo.jpg&#039;);
echo $image-&gt;url;      // &#039;/assets/photo.jpg?v=1699123456&#039;
echo $image-&gt;width;    // 1920
echo $image-&gt;height;   // 1080
echo $image-&gt;mimeType; // &#039;image/jpeg&#039;

// AudioAsset for audio files
$audio = $this-&gt;assets-&gt;getAsset(&#039;song.mp3&#039;);
echo $audio-&gt;duration;  // duration in seconds

// ScriptAsset for JavaScript
$script = $this-&gt;assets-&gt;getAsset(&#039;app.js&#039;);
echo $script-&gt;type;     // &#039;module&#039; or null
</code></pre>

<p>Properties like image dimensions or audio file duration are loaded lazily
(only when you first use them), so the system stays fast even with thousands
of files.</p>

<h2 id="toc-first-class-vite-integration">First-class Vite integration</h2>

<p>Nette Assets have native support for <a href="https://vite.dev">Vite</a>.
This modern tool handles the need to compile (build) frontend code, because:</p>

<ul>
	<li>it's written in other languages (TypeScript, Sass)</li>

	<li>it's transpiled for older browsers</li>

	<li>it's bundled into one file</li>

	<li>it's minified for smaller size</li>
</ul>

<p>To avoid repeating this entire process with every change in development mode,
Vite serves files directly without bundling and minification. Every change is
thus reflected immediately. And as a bonus it can even do it without page
refresh – you edit CSS or JavaScript and the change is reflected in the
browser within milliseconds, without losing form data or application state. This
is called Hot Module Replacement.</p>

<p>For production, Vite then creates classic optimized builds – bundles,
minifies, splits into chunks and adds versioned names for cache busting.</p>

<p>In today's JavaScript ecosystem, Vite is the de facto standard for modern
frontend development. That's why it made sense to include support directly in
Nette Assets. Thanks to the open architecture, you can easily write a mapper for
any other bundler.</p>

<p>You might wonder: “Vite adapters for Nette already exist, what's different
about this?” Yes, they exist, and I thank their authors for that. The
difference is that Nette Assets is a complete system for managing all static
files, and Vite is one component, but very elegantly integrated.</p>

<p>Just add two words to the <code>common.neon</code> configuration:
<code>type: vite</code>.</p>

<pre
class="language-neon"><code>assets:
	mapping:
		default:
			type: vite
			path: assets
</code></pre>

<p>Vite is known for working even without configuration or being very easy to
configure. However, backend integration adds complexity. That's why I created
<a href="https://github.com/nette/vite-plugin">@nette/vite-plugin</a>, which
makes the whole integration simple again.</p>

<p>In the <code>vite.config.ts</code> configuration file, just activate the
plugin and specify the path to <a
href="https://doc.nette.org/cs/assets/vite#toc-vstupni-body">entry
points</a>:</p>

<pre
class="language-js"><code>import { defineConfig } from &#039;vite&#039;;
import nette from &#039;@nette/vite-plugin&#039;;

export default defineConfig({
	plugins: [
		nette({
			entry: &#039;app.js&#039;,  // entry point
		}),
	],
});
</code></pre>

<p>And in the layout template we load the entry point:</p>

<pre
class="language-latte"><code>&lt;!doctype html&gt;
&lt;head&gt;
	{asset &#039;app.js&#039;}
&lt;/head&gt;
</code></pre>

<p><b>And that's it!</b> The system automatically takes care of the rest:</p>

<ul>
	<li><b>In development mode</b> loads files from Vite dev server with Hot Module
	Replacement</li>

	<li><b>In production</b> uses compiled, optimized files</li>
</ul>

<p>No configuration, no conditions in templates. It just works.</p>

<h2 id="toc-power-of-custom-mappers-real-examples">Power of custom mappers: Real
examples</h2>

<p>A <b>mapper</b> is a component that knows where to find files and how to
create URLs for loading them. You can have multiple mappers for different
purposes – local files, CDN, cloud storage or build tools. The Nette Assets
system is completely open for creating custom mappers for anything.</p>

<p><b>Mapper for product images</b></p>

<p>On an e-shop you need product images in different sizes. You usually already
have a service that handles resize and image optimization. Simply wrap it with a
Nette Assets mapper.</p>

<p>The <code>getAsset()</code> method of your mapper must return the appropriate
asset type. In our case <code>ImageAsset</code>. If we additionally give the
constructor a <code>file</code> parameter with the path to the local file, it
automatically loads image dimensions (width, height) and MIME type. If the
requested file doesn't exist, we throw an <code>AssetNotFoundException</code>
exception:</p>

<pre
class="language-php"><code>class ProductImageMapper implements Mapper
{
	public function getAsset(string $reference, array $options = []): Asset
	{
		// $reference is product ID
		$product = $this-&gt;database-&gt;getProduct((int) $reference);
		if (!$product) {
			throw new Nette\Assets\AssetNotFoundException(&quot;Product $reference not found&quot;);
		}

		$size = $options[&#039;size&#039;] ?? &#039;medium&#039;; // small, medium, large

		// We leverage our existing image service
		return new Nette\Assets\ImageAsset(
			url: $this-&gt;imageService-&gt;getProductUrl($product, $size),
			file: $this-&gt;imageService-&gt;getProductFile($product, $size)
		);
	}
}
</code></pre>

<p>Registration in configuration:</p>

<pre
class="language-neon"><code>assets:
	mapping:
		product: App\ProductImageMapper()
</code></pre>

<p>Usage in templates:</p>

<pre
class="language-latte"><code>{* Different product sizes *}
&lt;img n:asset=&quot;product:{$product-&gt;id}, size: small&quot; alt=&quot;Product thumbnail&quot;&gt;
&lt;img n:asset=&quot;product:{$product-&gt;id}, size: large&quot; alt=&quot;Product detail&quot;&gt;
</code></pre>

<p><b>Automatic OG image generation</b></p>

<p>OG (Open Graph) images are displayed when sharing on social networks. Instead
of manual creation, you can have the system generate them automatically based on
content type.</p>

<p>In this case, the <code>$reference</code> parameter determines the image
context. For static pages (e.g. <code>homepage</code>, <code>about</code>)
pre-prepared files are used. For articles, the image is generated dynamically
based on the article title. The mapper first checks cache – if the image
already exists, it returns it. Otherwise it generates it and saves it to cache
for next use:</p>

<pre
class="language-php"><code>class OgImageMapper implements Mapper
{
	public function getAsset(string $reference, array $options = []): Asset
	{
		// For articles we generate dynamically based on title
		if ($reference === &#039;article&#039;) {
			$title = $options[&#039;title&#039;] ?? throw new LogicException(&#039;Missing option title for article&#039;);
			$filename = &#039;/generated/&#039; . md5(&quot;article-{$title}&quot;) . &#039;.png&#039;;
			$path = $this-&gt;staticDir . $filename;
			if (!file_exists($path)) {
				file_put_contents($path, $this-&gt;ogGenerator-&gt;createArticleImage($title));
			}

		} else { // For static pages we use pre-prepared files
			$filename = &quot;/{$reference}.png&quot;;
			$path = $this-&gt;staticDir . $filename;
			if (!file_exists($path)) {
				throw new Nette\Assets\AssetNotFoundException(&quot;Static OG image &#039;$reference&#039; not found&quot;);
			}
		}

		return new Nette\Assets\ImageAsset($this-&gt;baseUrl . $filename, file: $path);
	}
}
</code></pre>

<p>Then in the template we add to the header:</p>

<pre
class="language-latte"><code>{* For static page *}
{var $ogImage = asset(&#039;og:homepage&#039;)}

{* For article with dynamic generation *}
{var $ogImage = asset(&#039;og:article&#039;, title: $article-&gt;title)}
&lt;meta property=&quot;og:image&quot; content={$ogImage}&gt;
&lt;meta property=&quot;og:image:width&quot; content={$ogImage-&gt;width}&gt;
&lt;meta property=&quot;og:image:height&quot; content={$ogImage-&gt;height}&gt;

</code></pre>

<p>Thanks to this approach you have automatically generated OG images for every
article, which are created only once and then cached for further use.</p>

<h2 id="toc-nette-assets-more-elegant-asset-management">Nette Assets: more
elegant asset management</h2>

<p>Nette Assets represent an elegant system that takes care of almost everything
automatically.</p>

<ul>
	<li><b>Eliminates boilerplate code</b> – no more manual URL construction</li>

	<li><b>Supports modern workflow</b> – Vite, TypeScript, code splitting</li>

	<li><b>Is flexible</b> – custom mappers, various storage systems</li>

	<li><b>Works immediately</b> – no complex configuration</li>
</ul>

<p>Complete description can be found <a
href="https://doc.nette.org/cs/assets">in documentation</a> and <a
href="https://github.com/nette/assets">code on GitHub</a>.</p>

<p><em>You'll never write
<code>&lt;img src="/images/photo.jpg?v=123456"&gt;</code> manually
again!</em></p>

		
			]]></content:encoded>
			<pubDate>Thu, 05 Jun 2025 19:25:00 +0200</pubDate>
			<guid isPermaLink="false">item521@http://blog.nette.org</guid>
		</item>
		<item>
			<title>Architecture That Grows with Your Project</title>
			<link>https://blog.nette.org/en/architecture-that-grows-with-your-project</link>
			<description>One of the most common challenges in PHP application development is proper
code organization. Where should presenters go? Where should individual classes
be located? And how to ensure the project structure grows naturally with its
development?

Nette documentation brings a comprehensive
guide to directory structure that offers answers to all these questions.

The quality of code organization significantly affects its readability. At
first glance at a new project, you should quickly understand its purpose. Take a
look at this app/Model/ directory:

app/Model/
├── Services/
├── Repositories/
└── Entities/


What does this structure tell you about the application? Almost nothing.
Compare with this alternative:

app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/


It&apos;s immediately clear that this is an e-commerce application. That&apos;s the
power of domain-oriented structure that the document presents.

The guide also shows how to naturally evolve the structure as your project
grows. Whether you&apos;re starting a new project or want to improve an existing
application, you&apos;ll find principles for making informed decisions about code
organization.

Read the Application
Directory Structure chapter in Nette documentation and blogpost Elegant Presenter
Structuring.
</description>
			<content:encoded><![CDATA[
			
		<p>One of the most common challenges in PHP application development is proper
code organization. Where should presenters go? Where should individual classes
be located? And how to ensure the project structure grows naturally with its
development?</p>

<p>Nette documentation brings a <a
href="https://doc.nette.org/en/application/directory-structure">comprehensive
guide to directory structure</a> that offers answers to all these questions.</p>

<p>The quality of code organization significantly affects its readability. At
first glance at a new project, you should quickly understand its purpose. Take a
look at this <code>app/Model/</code> directory:</p>

<pre><code>app/Model/
├── Services/
├── Repositories/
└── Entities/
</code></pre>

<p>What does this structure tell you about the application? Almost nothing.
Compare with this alternative:</p>

<pre><code>app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/
</code></pre>

<p>It's immediately clear that this is an e-commerce application. That's the
power of domain-oriented structure that the document presents.</p>

<p>The guide also shows how to naturally evolve the structure as your project
grows. Whether you're starting a new project or want to improve an existing
application, you'll find principles for making informed decisions about code
organization.</p>

<p>Read the <a
href="https://doc.nette.org/en/application/directory-structure">Application
Directory Structure</a> chapter in Nette documentation and blogpost <a
href="https://blog.nette.org/en/elegant-presenter-structuring">Elegant Presenter
Structuring</a>.</p>

		
			]]></content:encoded>
			<pubDate>Thu, 23 Jan 2025 15:10:00 +0100</pubDate>
			<guid isPermaLink="false">item519@http://blog.nette.org</guid>
		</item>
		<item>
			<title>One line in configuration will speed up your Nette application. How is that possible?</title>
			<link>https://blog.nette.org/en/one-line-in-configuration-will-speed-up-your-nette-application-how-is-that-possible</link>
			<description>Imagine you have a large application with dozens of services – database,
logger, mailer, cache, and many others. During each HTTP request, all these
services are created, even if you don&apos;t use them at all. This unnecessarily
slows down your application. The new version of PHP 8.4 and Nette brings an
elegant solution to this problem using so-called lazy objects.

What are lazy objects?

A lazy object is a special type of object that looks and behaves exactly
like the actual service from your code&apos;s perspective, but in reality, it delays
its initialization until it&apos;s actually needed. When we request a database
service from the DI container, for example, we get an object that externally
looks like a regular instance of Database, but it hasn&apos;t actually
established a database connection yet. This happens only when we first actually
use the service.

Example:

// We get an object that looks like Database but hasn&apos;t established a connection yet
$database = $container-&gt;getByType(Database::class);
// The database connection is created HERE, during first actual use
$database-&gt;query(&apos;SELECT * FROM users&apos;);


How to enable it?

In the new version of Nette DI 3.2.4, all you need is one line in
configuration:

di:
    lazy: true


That&apos;s all! From now on, all services in the DI container will be created
“lazily”.

We can also set lazy creation for individual services:

services:
    newsletter:
        create: Newsletter
        lazy: false   # this service will be created immediately, even if lazy is globally enabled
    database:
        create: Database
        lazy: true    # this service will be lazy, even if lazy is globally disabled


Benefits in practice


	Faster application startup – only services you actually need are
	created

	Lower memory consumption – unused services don&apos;t take up space

	Simple implementation – just one line in configuration


It&apos;s important to note that lazy objects logically change how service
configuration errors manifest themselves. For example, incorrect database
credentials won&apos;t show up at application startup, but only during the first
actual database connection.

Hidden
Advantage – Solving Circular Dependencies

Lazy objects bring yet another interesting advantage – they elegantly
solve circular dependencies between services. Imagine a situation where service
A needs service B and service B simultaneously needs service A. The Nette DI
container immediately detects such a loop and throws a “Circular reference
detected” exception instead of letting PHP cycle in an infinite loop. With
lazy objects, this problem practically disappears. Service A receives a lazy
proxy of service B during creation, which is only initialized when actually
used, at which point service A already exists and can be passed to service B as
a dependency. Although circular dependencies aren&apos;t something we should
deliberately create when designing an application, it&apos;s interesting to realize
that with lazy objects, this problem disappears.

Limitations and
recommendations


	Lazy objects work only for your own classes

	They cannot be used for internal PHP classes

	Requires PHP 8.4 or higher


Lazy objects represent a significant step forward in PHP application
optimization. Thanks to them, your Nette applications can be faster and more
efficient without changing a single line of your code.
</description>
			<content:encoded><![CDATA[
			
		<p>Imagine you have a large application with dozens of services – database,
logger, mailer, cache, and many others. During each HTTP request, all these
services are created, even if you don't use them at all. This unnecessarily
slows down your application. The new version of PHP 8.4 and Nette brings an
elegant solution to this problem using so-called lazy objects.</p>

<h2 id="toc-what-are-lazy-objects">What are lazy objects?</h2>

<p>A lazy object is a special type of object that looks and behaves exactly
like the actual service from your code's perspective, but in reality, it delays
its initialization until it's actually needed. When we request a database
service from the DI container, for example, we get an object that externally
looks like a regular instance of <code>Database</code>, but it hasn't actually
established a database connection yet. This happens only when we first actually
use the service.</p>

<p>Example:</p>

<pre
class="language-php"><code>// We get an object that looks like Database but hasn&#039;t established a connection yet
$database = $container-&gt;getByType(Database::class);
// The database connection is created HERE, during first actual use
$database-&gt;query(&#039;SELECT * FROM users&#039;);
</code></pre>

<h2 id="toc-how-to-enable-it">How to enable it?</h2>

<p>In the new version of Nette DI 3.2.4, all you need is <b>one line in
configuration</b>:</p>

<pre class="language-neon"><code>di:
    lazy: true
</code></pre>

<p>That's all! From now on, all services in the DI container will be created
“lazily”.</p>

<p>We can also set lazy creation for individual services:</p>

<pre
class="language-neon"><code>services:
    newsletter:
        create: Newsletter
        lazy: false   # this service will be created immediately, even if lazy is globally enabled
    database:
        create: Database
        lazy: true    # this service will be lazy, even if lazy is globally disabled
</code></pre>

<h2 id="toc-benefits-in-practice">Benefits in practice</h2>

<ol>
	<li>Faster application startup – only services you actually need are
	created</li>

	<li>Lower memory consumption – unused services don't take up space</li>

	<li>Simple implementation – just one line in configuration</li>
</ol>

<p>It's important to note that lazy objects logically change how service
configuration errors manifest themselves. For example, incorrect database
credentials won't show up at application startup, but only during the first
actual database connection.</p>

<h2 id="toc-hidden-advantage-solving-circular-dependencies">Hidden
Advantage – Solving Circular Dependencies</h2>

<p>Lazy objects bring yet another interesting advantage – they elegantly
solve circular dependencies between services. Imagine a situation where service
A needs service B and service B simultaneously needs service A. The Nette DI
container immediately detects such a loop and throws a “Circular reference
detected” exception instead of letting PHP cycle in an infinite loop. With
lazy objects, this problem practically disappears. Service A receives a lazy
proxy of service B during creation, which is only initialized when actually
used, at which point service A already exists and can be passed to service B as
a dependency. Although circular dependencies aren't something we should
deliberately create when designing an application, it's interesting to realize
that with lazy objects, this problem disappears.</p>

<h2 id="toc-limitations-and-recommendations">Limitations and
recommendations</h2>

<ul>
	<li>Lazy objects work only for your own classes</li>

	<li>They cannot be used for internal PHP classes</li>

	<li>Requires PHP 8.4 or higher</li>
</ul>

<p>Lazy objects represent a significant step forward in PHP application
optimization. Thanks to them, your Nette applications can be faster and more
efficient without changing a single line of your code.</p>

		
			]]></content:encoded>
			<pubDate>Fri, 10 Jan 2025 06:46:00 +0100</pubDate>
			<guid isPermaLink="false">item517@http://blog.nette.org</guid>
		</item>
		<item>
			<title>Write Safer Code with the New Nette Database Documentation</title>
			<link>https://blog.nette.org/en/write-safer-code-with-the-new-nette-database-documentation</link>
			<description>Nette Database has finally received what it has long
deserved—a documentation worthy of its potential. The completely rewritten
text not only describes all the functions in detail but also opens developers’
eyes to the security of database operations.

Documentation That Packs a
Punch

High-quality documentation is the Achilles&apos; heel of many open-source
projects. Not so with the Nette Framework. Nette boasts something that many
competitors lack—precise, clear, and engaging documentation that guides
developers from their first steps to advanced concepts. For example, the Dependency Injection
documentation not only explains Nette DI itself but also provides an accessible
theoretical introduction to the topic, enriched by contributions from Miško
Hevery, the author of the Angular framework. The Latte documentation offers an
interactive
insight into escaping, explaining why it is the only safe templating system
in PHP. Nette&apos;s commitment to its users is further demonstrated by the
existence of a complete introduction
to object-oriented programming.

No matter where in the world you’re coding, Nette speaks your language. All
documentation is available in impressive 16 world languages: English, German, Spanish, French, Italian, Hungarian, Polish, Portuguese, Romanian, Slovenian, Turkish, Greek, Bulgarian, Russian, and Ukrainian, and Czech.

A New Era for Database
Documentation

There were, however, two areas that slightly marred an otherwise perfect
score—documentation for Nette Database and the Tracy debugger. This is now
changing for the Database. Check out the revamped and expanded version of the new documentation.

The content is clearly divided into two logical sections based on approaches
to working with databases:


	SQL Approach for
	developers who prefer direct control over their queries

	Explorer for those
	who value development speed and the convenience of automation


It includes numerous examples and code snippets that demonstrate the
library&apos;s real capabilities and inspire effective use. Each concept is
illustrated with practical use cases, enabling quick understanding and immediate
application in your projects.

Security First
(Because Your Sleep Depends on It)

In an age where database attacks are commonplace, the brand-new section of
the documentation offers a thorough technical analysis of security
risks. Here, you’ll find practical demonstrations of real-world threats
and their prevention:


	A detailed explanation of SQL injection and its dangers

	Practical examples of safe parameterized queries

	A comprehensive approach to input data validation

	Proper use of dynamic identifiers


The new documentation combines function descriptions with a detailed
explanation of security aspects and best practices. The result is a guide that
helps you write not only functional but also secure code. Start
reading today!
</description>
			<content:encoded><![CDATA[
			
		<p class="perex">Nette Database has finally received what it has long
deserved—a documentation worthy of its potential. The completely rewritten
text not only describes all the functions in detail but also opens developers’
eyes to the security of database operations.</p>

<h2 id="toc-documentation-that-packs-a-punch">Documentation That Packs a
Punch</h2>

<p>High-quality documentation is the Achilles' heel of many open-source
projects. Not so with the Nette Framework. Nette boasts something that many
competitors lack—precise, clear, and engaging documentation that guides
developers from their first steps to advanced concepts. For example, the <a
href="https://doc.nette.org/en/dependency-injection">Dependency Injection</a>
documentation not only explains Nette DI itself but also provides an accessible
theoretical introduction to the topic, enriched by contributions from Miško
Hevery, the author of the Angular framework. The Latte documentation offers an
<a
href="https://latte.nette.org/en/safety-first#toc-latte-vs-naive-systems">interactive
insight</a> into escaping, explaining why it is the only safe templating system
in PHP. Nette's commitment to its users is further demonstrated by the
existence of a complete <a
href="https://doc.nette.org/en/introduction-to-object-oriented-programming">introduction
to object-oriented programming</a>.</p>

<p>No matter where in the world you’re coding, Nette speaks your language. All
documentation is available in impressive 16 world languages: <a
href="https://doc.nette.org/en/">English</a>, <a
href="https://doc.nette.org/de/">German</a>, <a
href="https://doc.nette.org/es/">Spanish</a>, <a
href="https://doc.nette.org/fr/">French</a>, <a
href="https://doc.nette.org/it/">Italian</a>, <a
href="https://doc.nette.org/hu/">Hungarian</a>, <a
href="https://doc.nette.org/pl/">Polish</a>, <a
href="https://doc.nette.org/pt/">Portuguese</a>, <a
href="https://doc.nette.org/ro/">Romanian</a>, <a
href="https://doc.nette.org/sl/">Slovenian</a>, <a
href="https://doc.nette.org/tr/">Turkish</a>, <a
href="https://doc.nette.org/el/">Greek</a>, <a
href="https://doc.nette.org/bg/">Bulgarian</a>, <a
href="https://doc.nette.org/ru/">Russian</a>, and <a
href="https://doc.nette.org/uk/">Ukrainian</a>, and <a
href="https://doc.nette.org/cs/">Czech</a>.</p>

<h2 id="toc-a-new-era-for-database-documentation">A New Era for Database
Documentation</h2>

<p>There were, however, two areas that slightly marred an otherwise perfect
score—documentation for Nette Database and the Tracy debugger. This is now
changing for the Database. Check out the revamped and expanded version of the <a
href="https://doc.nette.org/en/database/guide">new documentation</a>.</p>

<p>The content is clearly divided into two logical sections based on approaches
to working with databases:</p>

<ul>
	<li><a href="https://doc.nette.org/en/database/sql-way">SQL Approach</a> for
	developers who prefer direct control over their queries</li>

	<li><a href="https://doc.nette.org/en/database/explorer">Explorer</a> for those
	who value development speed and the convenience of automation</li>
</ul>

<p>It includes numerous examples and code snippets that demonstrate the
library's real capabilities and inspire effective use. Each concept is
illustrated with practical use cases, enabling quick understanding and immediate
application in your projects.</p>

<h2 id="toc-security-first-because-your-sleep-depends-on-it">Security First
(Because Your Sleep Depends on It)</h2>

<p>In an age where database attacks are commonplace, the brand-new section of
the documentation offers a thorough <a
href="https://doc.nette.org/en/database/security">technical analysis of security
risks</a>. Here, you’ll find practical demonstrations of real-world threats
and their prevention:</p>

<ul>
	<li>A detailed explanation of SQL injection and its dangers</li>

	<li>Practical examples of safe parameterized queries</li>

	<li>A comprehensive approach to input data validation</li>

	<li>Proper use of dynamic identifiers</li>
</ul>

<p>The new documentation combines function descriptions with a detailed
explanation of security aspects and best practices. The result is a guide that
helps you write not only functional but also secure code. Start
reading today!</p>

		
			]]></content:encoded>
			<pubDate>Thu, 09 Jan 2025 16:13:00 +0100</pubDate>
			<guid isPermaLink="false">item515@http://blog.nette.org</guid>
		</item>

</channel>
</rss>
