<?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/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
  <channel>
    <title>Blog by Dominik Chrástecký</title>
    <description>Blog about anything and everything by Dominik Chrástecký</description>
    <pubDate>Fri, 09 Jan 2026 16:33:00 +0000</pubDate>
    <generator>Laminas_Feed_Writer 2 (https://getlaminas.org)</generator>
    <link>https://chrastecky.dev</link>
    <atom:link rel="self" type="application/rss+xml" href="https://chrastecky.dev/rss.xml"/>
    <item>
      <title>Making Your Angular App SEO-Friendly with SSR</title>
      <description><![CDATA[Unless you’ve been living under a rock, you’ve probably noticed that Angular has supported server-side rendering (SSR) for quite a while now. Let’s look at what it can actually do for SEO!]]></description>
      <pubDate>Sat, 10 Jan 2026 18:00:10 +0000</pubDate>
      <link>https://chrastecky.dev/programming/making-your-angular-app-seo-friendly-with-ssr</link>
      <guid isPermaLink="false">25</guid>
      <enclosure type="image/png" length="1954594" url="https://chrastecky.dev/i/l/44/post_detail/angular-seo.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[Unless you’ve been living under a rock, you’ve probably noticed that Angular has supported server-side rendering (SSR) for quite a while now. Let’s look at what it can actually do for SEO!

<p>SSR support in Angular makes it suitable for a whole new category of apps: content websites that need SEO and other metadata rendered directly in the HTML source instead of client-side at runtime.</p>

<p>And the best part is that most of the time you don&rsquo;t need to do anything special!</p>

<h2>Changing the title</h2>

<p>You&rsquo;ve most likely done this already, and SSR doesn&rsquo;t change a thing. You still do it the same way you always did: inject the <code>Title</code> service from Angular and set the title:</p>

<pre>
<code class="language-typescript">import {Title} from "@angular/platform-browser";

// then in ngOnInit or somewhere like that

this.title.setTitle('Cool New Title');
</code></pre>

<p>And that&rsquo;s it &mdash; it works automatically both client-side and server-side.</p>

<h2>Changing meta tags</h2>

<p>Changing meta tags follows the same principle &mdash; Angular has a service for exactly that. Unsurprisingly, it&rsquo;s simply called <code>Meta</code>, and you can import it from <code>@angular/platform-browser</code>:</p>

<pre>
<code class="language-typescript">import {Meta} from "@angular/platform-browser";</code></pre>

<p>Then you can create a meta tag:</p>

<pre>
<code class="language-typescript">this.meta.addTag({
  name: 'description',
  content: 'This is a description',
});</code></pre>

<p>Or use a property instead of a name:</p>

<pre>
<code class="language-typescript">this.meta.addTag({
  property: 'og:title',
  content: 'Cool New Title',
});</code></pre>

<p>This approach, however, has a small caveat: all tags are added one by one as you navigate through the pages, which means you end up with multiple duplicate descriptions or <code>og:title</code> tags. That would be fine if all search bots were server-side only, but some (like Googlebot) actually render client-side JavaScript, and they could get confused. Instead, you can first search for the tag and create it only if it doesn&rsquo;t exist:</p>

<pre>
<code class="language-typescript">let description = this.meta.getTag("name='description'");
if (!description) {
  description = this.meta.addTag({
    name: 'description',
  });
}
description!.content = 'This is a description';</code></pre>

<p>The syntax for <code>getTag()</code> is <code>attributeName=&#39;attribute value&#39;</code>, so you can search by pretty much anything. While there should only be one description tag, there might be other tags with multiple instances, and then searching by name is not going to help. In that case, when creating the tag, you can assign a unique ID or another attribute that makes it easy to look up later.</p>

<h2>Changing head link tags</h2>

<p>The most common SEO use case here is setting the canonical URL. Another common one is linking to an alternate language or version.</p>

<p>This time we don&rsquo;t get any ready-made Angular service, but it&rsquo;s easy enough to handle by appending elements directly to the DOM. However, we won&rsquo;t use the <code>window.document</code> browser API (because it doesn&rsquo;t exist on the server), but an Angular wrapper instead. You must inject it using an injection token called <code>DOCUMENT</code>:</p>

<pre>
<code class="language-typescript">import {DOCUMENT} from '@angular/core';
// if using constructor injection, also import Inject
import {Inject} from '@angular/core';
// if using the inject function, import it
import {inject} from '@angular/core';

// then inject it in some way
private readonly document = inject(DOCUMENT);

// or constructor injection
constructor(
  @Inject(DOCUMENT) private readonly document: Document,
) {
}</code></pre>

<p>After getting a reference to the document object, you can use it the same way as you would use the native <code>window.document</code> object:</p>

<pre>
<code class="language-typescript">const head = this.document.getElementsByTagName('head')[0];
let element: HTMLLinkElement | null = this.document.querySelector(`link[rel='canonical']`) || null;
if (element == null) {
  element = this.document.createElement('link') as HTMLLinkElement;
  element.rel = 'canonical';
  head.appendChild(element);
}
element.href = 'https://example.com';</code></pre>

<p>The above code does the following:</p>

<ul>
	<li>gets a reference to the <code>&lt;head&gt;</code> tag</li>
	<li>checks whether a <code>&lt;link&gt;</code> element with <code>rel=&quot;canonical&quot;</code> exists
	<ul>
		<li>if not, it creates one</li>
	</ul>
	</li>
	<li>assigns <em>https://example.com</em> as the canonical link</li>
</ul>

<p>That way, when you navigate to a different page, the existing canonical link gets updated instead of creating another one.</p>

<h3>Getting the current URL</h3>

<p>Manually assigning the URL doesn&rsquo;t make much sense. Instead, you want to get the current URL and normalize it in some way. Here the client side differs slightly from the server side, and you need a solution specific to the server side. For canonical links you can even skip the client side entirely, but I&rsquo;ll include it anyway.</p>

<p>To modify server-side behavior, you need to inject a <code>Request</code> object with the help of the <code>REQUEST</code> injection token:</p>

<blockquote>
<p>You can of course use the <code>inject()</code> function if that&rsquo;s your thing. From now on I&rsquo;ll be using constructor injection only, but the <code>inject()</code> function can be used interchangeably with it.</p>
</blockquote>

<pre>
<code class="language-typescript">import {Inject, Optional, REQUEST} from '@angular/core';

constructor(
  @Optional() @Inject(REQUEST) private readonly request: Request | null,
) {
}</code></pre>

<blockquote>
<p>The above only works if you use the <code>AngularNodeAppEngine</code> in your <strong>server.ts</strong>. If your app is older and was bootstrapped with the <code>CommonEngine</code>, you need to provide the request object manually.</p>
</blockquote>

<p>Additionally, you need to detect whether you&rsquo;re on the server or in the browser, so inject the platform ID:</p>

<pre>
<code class="language-typescript">import {Inject, PLATFORM_ID} from '@angular/core';

  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
  ) {
  }</code></pre>

<p>Then replace the <em>example.com</em> assignment from above with the following:</p>

<pre>
<code class="language-typescript">if (isPlatformServer(this.platformId) &amp;&amp; this.request) {
  const url = new URL(this.request.url);
  element.href = url.toString();
}</code></pre>

<p>This first makes sure the code only runs when you&rsquo;re on the server and the request object is not <code>null</code> (which it <em>shouldn&rsquo;t</em> be if you&rsquo;re on the server, but if you need to write generic code that should work across various environments, it&rsquo;s better to be safe).</p>

<p>Then it creates a new <code>URL</code> object and assigns the canonical URL to its value. Why wrap it in a <code>URL</code> object? Because we&rsquo;re going to do some normalization!</p>

<p>Before assigning the URL to the canonical link element, you might want to remove some marketing query parameters:</p>

<pre>
<code class="language-typescript">const url = new URL(this.request.url);

const bannedParams = ['utm_medium', 'utm_source', 'utm_content', 'fbclid']; // add more if you want
for (const param of bannedParams) {
  if (url.searchParams.has(param)) {
    url.searchParams.delete(param);
  }
}

element.href = url.toString();</code></pre>

<p>Or you might want to make sure your canonical hostname is used:</p>

<pre>
<code class="language-typescript">if (url.host === 'www.chrastecky.dev') {
  url.host = 'chrastecky.dev';
}</code></pre>

<p>Or any other combination of rules to make your URL the canonical version.</p>

<h3>CommonEngine</h3>

<p>If you&rsquo;re not yet using the new(ish) <code>AngularNodeAppEngine</code>, you&rsquo;re probably using the <code>CommonEngine</code>, which doesn&rsquo;t include the request and response objects. Your <strong>server.ts</strong> should contain a call to <code>commonEngine.render()</code>, which should already include some providers in the <code>providers</code> array. Simply add new ones there:</p>

<pre>
<code class="language-typescript">commonEngine.render({
  // other parameters
  providers: [
    // other providers
    {provide: REQUEST, useValue: createWebRequestFromNodeRequest(req)},
  ],
})</code></pre>

<h3>Getting the URL client-side</h3>

<p>If you want to update the canonical link client-side for any reason, you can subscribe to the router events like this:</p>

<pre>
<code class="language-typescript">if (isPlatformBrowser(this.platformId)) {
  this.router.events.subscribe(event =&gt; {
    if (event instanceof NavigationEnd) {
      const canonicalUrl = new URL(`https://${window.location.host}/${event.urlAfterRedirects}`);
      // todo update the link element
    }
  });
}</code></pre>

<p>The above subscribes to all router events and, when it&rsquo;s a <code>NavigationEnd</code> event, it checks what the URL after redirects is. Note that <code>urlAfterRedirects</code> only contains the path + query + fragment, not the whole URL, so you need to get the hostname from somewhere. Since we already checked that we&rsquo;re in a browser, it&rsquo;s safe to get it from <code>window.location</code>.</p>

<p>You could even mostly use this on the server side as well, but currently there is a problem with query parameters &mdash; the above code simply doesn&rsquo;t provide them on the server side (I&rsquo;m not sure whether that&rsquo;s intended or a bug).</p>

<h2>Returning correct status codes</h2>

<p>By default, Angular always returns the status code 200, which means &ldquo;OK&rdquo;, and that&rsquo;s not ideal. For example, if I link to a nonsense page (like <a href="https://chrastecky.dev/this-page-does-not-exist" rel="nofollow">https://chrastecky.dev/this-page-does-not-exist</a>), it&rsquo;s not enough that it shows an error &mdash; search engines need to actually see the 404 status code so they know not to index it.</p>

<p>Another great use case might be creating an HTTP interceptor for your API that automatically checks when the API returns 502/503 and returns the same status code, so that bots crawling your site know there&rsquo;s a temporary hiccup and they should try again later.</p>

<blockquote>
<p>If you&rsquo;re using <code>CommonEngine</code>, skip the following section &mdash; the approach is a little bit different (more on that below).</p>
</blockquote>

<p>First, inject the <code>ResponseInit</code> object:</p>

<pre>
<code class="language-typescript">import {Inject, RESPONSE_INIT} from '@angular/core';

constructor(
  @Inject(RESPONSE_INIT) private readonly responseInit: ResponseInit | null,
) {
}</code></pre>

<p>Then make sure it&rsquo;s present and you&rsquo;re on the server before setting the status code:</p>

<pre>
<code class="language-typescript">if (isPlatformServer(this.platformId) &amp;&amp; this.responseInit) {
  this.responseInit.status = 404;
}</code></pre>

<h3>Status codes in CommonEngine</h3>

<p>In <code>CommonEngine</code> you need to provide the whole <code>Response</code> object instead of <code>ResponseInit</code>. To do so, add a new line to the <strong>server.ts</strong> providers:</p>

<pre>
<code class="language-typescript">commonEngine.render({
  // other parameters
  providers: [
    // other providers
    { provide: RESPONSE, useValue: res },
  ],
})</code></pre>

<p>You will quickly notice that the <code>RESPONSE</code> injection token doesn&rsquo;t actually exist, so we have to provide our own. You can put this anywhere you like; I usually have a single <strong>injection-tokens.ts</strong> file:</p>

<pre>
<code class="language-typescript">export const RESPONSE = new InjectionToken&lt;Response | null&gt;('RESPONSE');
</code></pre>

<p>And then inject it as usual:</p>

<pre>
<code class="language-typescript">constructor(
  @Optional() @Inject(RESPONSE) private readonly response: Response | null,
) {}</code></pre>

<p>Finally, set the correct status code:</p>

<pre>
<code class="language-typescript">if (isPlatformServer(this.platformId) &amp;&amp; this.response) {
  this.response.status(404)
}</code></pre>

<h2>Redirects</h2>

<p>Redirects are an important part of SEO because they tell crawlers that a page has moved somewhere else. In principle, they&rsquo;re the same as the 404 responses above, and you could implement a redirect like that in a component, but you can also do it without involving the Angular runtime at all &mdash; by adding a new route handler directly in <strong>server.ts</strong>. I&rsquo;ll be using a static config in this example, but you can obtain it any way you like.</p>

<h3>The config</h3>

<p>It&rsquo;s pretty simple:</p>

<pre>
<code class="language-typescript">export type RedirectStatus = 301 | 302;

export interface RedirectRule {
  from: string;
  to: string;
  status?: RedirectStatus;
}

export const redirectRules: RedirectRule[] = [
  { from: '/old1', to: '/new1', status: 301 },
  { from: '/old2', to: '/new2', status: 301 },
  { from: '/old3', to: '/new3', status: 301 },
];
</code></pre>

<p>You can put this anywhere you like, for example in a new <strong>redirects.ts</strong> file.</p>

<h3>Redirecting</h3>

<p>Somewhere close to the top, add a new route handler. This example is the same for both Angular engines because they both use an Express server, and this part kicks in before either of the two engines runs &mdash; meaning before any part of Angular is executed:</p>

<pre>
<code class="language-typescript">app.get('*', (req, res, next) =&gt; {
  const requestPath = req.path;
  const rule = redirectRules.find((item) =&gt; item.from === requestPath);
  if (!rule) {
    next();
    return;
  }
  const status = rule.status ?? 302;
  res.redirect(status, rule.to);
});</code></pre>

<p>This is a classic middleware pattern where you get the request object, the response object, and a <code>next</code> handler. Step by step, this code:</p>

<ul>
	<li>registers a <code>GET</code> handler for all routes</li>
	<li>checks whether any of the redirect rules matches the request path</li>
	<li>if none matches, calls <code>next()</code> and does nothing else</li>
	<li>otherwise, sets a redirect on the response using the status code and URL from the config</li>
</ul>

<h2>Conclusion</h2>

<p>There are obviously many more important SEO topics, but the basic principles are mostly covered here. For example, adding structured data is just a variation of adding the canonical URL, except you add a <code>&lt;script&gt;</code> tag instead of a <code>&lt;link&gt;</code> one.</p>

<p>For me personally, the addition of SSR made it possible to use Angular pretty much everywhere, including the very blog site I&rsquo;m writing this on. How about you &mdash; do you see yourself building your next content-heavy website in Angular?</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Fun with PHP: Changing Readonly Properties and Other Shenanigans</title>
      <description><![CDATA[Did you know that PHP’s userland lets you change a readonly property multiple times? Or that you can modify an enum’s value? Or even change internal object properties that should never be changed? Let me show you some truly cursed PHP tricks.]]></description>
      <pubDate>Wed, 05 Nov 2025 09:22:50 +0000</pubDate>
      <link>https://chrastecky.dev/programming/fun-with-php-changing-readonly-properties-and-other-shenanigans</link>
      <guid isPermaLink="false">24</guid>
      <enclosure type="image/png" length="302589" url="https://chrastecky.dev/i/l/43/post_detail/php-abuse.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[Did you know that PHP’s userland lets you change a readonly property multiple times? Or that you can modify an enum’s value? Or even change internal object properties that should never be changed? Let me show you some truly cursed PHP tricks.

<h2>Changing a Readonly Property</h2>

<p>So, you know how <code>readonly</code> properties are, well&hellip; read-only? Turns out they&rsquo;re not!</p>

<p>I stumbled upon this mechanism just as PHP started deprecating it &mdash; but hey, if you ignore the deprecation warnings, you can still use it up until PHP 9!</p>

<p><del>A bit of theory first:&nbsp;<code>readonly</code> properties can only be assigned inside a class constructor. After that, they&rsquo;re supposed to be immutable.</del></p>

<p>Readonly properties can actually be set anywhere in the class and since 8.4 from anywhere.&nbsp;</p>

<pre>
<code class="language-php">final readonly class ReadonlyClass
{
    public string $someProp;

    public function __construct()
    {
        $this-&gt;someProp = 'unchangeable!';
    }
}
</code></pre>

<p>The only <em>official</em> way to set such a property outside the class (pre 8.4) is via reflection &mdash; but even then, only if the property hasn&rsquo;t been initialized yet:</p>

<pre>
<code class="language-php">final readonly class ReadonlyClass
{
    public string $someProp;
}
$test = new ReadonlyClass();
$reflection = new ReflectionClass(ReadonlyClass::class)-&gt;getProperty('someProp');
$reflection-&gt;setValue($test, 'changed once!');
var_dump($test-&gt;someProp);
$reflection-&gt;setValue($test, 'changed twice?');</code></pre>

<p>This produces the predictable result:</p>

<pre>
<code>string(13) "changed once!"

Fatal error: Uncaught Error: Cannot modify readonly property ReadonlyClass::$someProp</code></pre>

<blockquote>
<p>You get the same error no matter whether you do it in a constructor, set it in a different method or simply use reflection. As soon as you change the value in any official way more than once, you get an error.</p>
</blockquote>

<h2>Changing It Multiple Times</h2>

<p>Enough stalling &mdash; let&rsquo;s dive in! The magical object that can modify a <code>readonly</code> property (and much more) is <code>ArrayObject</code>.</p>

<p>Normally, you&rsquo;d use <code>ArrayObject</code> to wrap an array. But it also accepts <em>any object</em> as the backing value &mdash; and that&rsquo;s where the fun begins. Once you know how PHP stores properties internally (which is actually pretty simple), chaos follows.</p>

<p>Let&rsquo;s start with this class:</p>

<pre>
<code class="language-php">final readonly class ReadonlyClass
{
    public string $someProp;
    private string $somePrivateProp;
    protected string $someProtectedProp;

    public function __construct()
    {
        $this-&gt;someProp = 'unchangeable?';
        $this-&gt;somePrivateProp = 'unchangeable?';
        $this-&gt;someProtectedProp = 'unchangeable?';
    }

    public function getSomePrivateProp(): string
    {
        return $this-&gt;somePrivateProp;
    }

    public function getSomeProtectedProp(): string
    {
        return $this-&gt;someProtectedProp;
    }
}</code></pre>

<p>Now we create an instance and wrap it in an <code>ArrayObject</code>:</p>

<pre>
<code class="language-php">$instance = new ReadonlyClass();
$arrayObj = new ArrayObject($instance);
</code></pre>

<p>And now comes the fun part:</p>

<pre>
<code class="language-php">// simply use the property name for public properties
$arrayObj['someProp'] = 'changeable public!';
// use "\0[FQN]\0[Property name]" for private properties
$arrayObj["\0ReadonlyClass\0somePrivateProp"] = 'changeable private!';
// use "\0*\0[Property name]" for protected properties
$arrayObj["\0*\0someProtectedProp"] = 'changeable protected!';

var_dump($instance-&gt;someProp, $instance-&gt;getSomePrivateProp(), $instance-&gt;getSomeProtectedProp());</code></pre>

<p>This prints:</p>

<pre>
<code>string(18) "changeable public!"
string(19) "changeable private!"
string(21) "changeable protected!"</code></pre>

<p>And just like that, you&rsquo;ve changed an <em>unchangeable</em> property. You can modify it as many times as you want. So&hellip; what other arcane tricks are possible?</p>

<h2>Changing an Enum Value</h2>

<p>Enums are basically fancy objects that represent a specific named instance &mdash; optionally with a value. The key difference from old userland implementations is that PHP guarantees every enum case is a unique instance that&rsquo;s always equal to itself, no matter where it&rsquo;s referenced from.</p>

<p>In other words, an enum is really just an object, and <code>-&gt;value</code> or <code>-&gt;name</code> are plain properties.</p>

<pre>
<code class="language-php">enum MyEnum: string {
    case A = 'a';
    case B = 'b';
}

$arrayObj = new ArrayObject(MyEnum::A);
$arrayObj['value'] = 'b';
$arrayObj['name'] = 'C';

var_dump(MyEnum::A-&gt;value);
var_dump(MyEnum::A-&gt;name);</code></pre>

<p>This prints exactly what you&rsquo;d expect after reading the previous example:</p>

<pre>
<code>string(1) "b"
string(1) "C"</code></pre>

<p>Even more amusing: Running <code>var_dump(MyEnum::A);</code> now prints <code>enum(MyEnum::C)</code>.</p>

<p>It won&rsquo;t actually make it equal to another enum case, but if you use the value somewhere and reconstruct it using <code>MyEnum::from()</code>, you&rsquo;ll get back <code>MyEnum::B</code>.</p>

<p>If you try to serialize and deserialize it, you&rsquo;ll get an error &mdash; because <code>MyEnum::C</code> doesn&rsquo;t exist:</p>

<pre>
<code class="language-php">var_dump(MyEnum::from(MyEnum::A-&gt;value));
var_dump(unserialize(serialize(MyEnum::A)));</code></pre>

<p>The first prints <code>enum(MyEnum::B)</code>, while the second throws a warning:&nbsp;<code>Undefined constant MyEnum::C</code>.</p>

<h2>Breaking Types</h2>

<p><code>ArrayObject</code> is so powerful that even the type system trembles before it. Types? Mere suggestions!</p>

<pre>
<code class="language-php">final class TestTypedClass
{
    public string $str = 'test';
    public bool $bool = true;
    public int $int = 42;
}

$instance = new TestTypedClass();
$arrayObj = new ArrayObject($instance);

$arrayObj['str'] = 5;
$arrayObj['bool'] = 'hello';
$arrayObj['int'] = new stdClass();

var_dump($instance-&gt;str, $instance-&gt;bool, $instance-&gt;int);</code></pre>

<p>Output:</p>

<pre>
<code>int(5)
string(5) "hello"
object(stdClass)#3 (0) {
}</code></pre>

<p>So if you ever thought <em>&ldquo;Hmm, this boolean could really use more than two possible values&rdquo;</em> &mdash; now you know how!</p>

<h2>Dynamic Properties Everywhere</h2>

<p>Some internal classes like <code>Closure</code>, <code>Generator</code>, and <code>DateTime</code> disallow dynamic properties. Nevermore!</p>

<pre>
<code class="language-php">$closure = fn () =&gt; true;
$arrayObject = new ArrayObject($closure);
$arrayObject['test'] = 'hello';

var_dump($closure-&gt;test);
// prints string(5) "hello"</code></pre>

<h2>Crashing PHP</h2>

<p>And finally &mdash; my favourite one! Ever wanted to cause a segmentation fault? Try this:</p>

<pre>
<code class="language-php">$exception = new Exception("Hello there!");
$arrayObject = new ArrayObject($exception);
$arrayObject["\0Exception\0trace"] = -1;

var_dump($exception-&gt;getTraceAsString());</code></pre>

<p>That gave me one beautiful&nbsp;<code>Segmentation fault (core dumped)</code>!</p>

<p>So, how did you like these all-powerful <code>ArrayObject</code> shenanigans?</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>New in PHP 8.5: Small Features, Big Impact</title>
      <description><![CDATA[The smaller features in PHP 8.5 that may not make headlines but definitely deserve a mention.]]></description>
      <pubDate>Sat, 25 Oct 2025 10:44:23 +0000</pubDate>
      <link>https://chrastecky.dev/programming/new-in-php-8-5-small-features-big-impact</link>
      <guid isPermaLink="false">23</guid>
      <enclosure type="image/png" length="124362" url="https://chrastecky.dev/i/l/42/post_detail/the_small_stuff.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[The smaller features in PHP 8.5 that may not make headlines but definitely deserve a mention.

<p>Originally, I intended to write an article about every single change in PHP 8.5, but then I realized that some of them don&rsquo;t really warrant a full post. <a href="/programming/new-in-php-8-5-asymmetric-visibility-for-static-properties">That didn&rsquo;t stop me from trying, though</a>.</p>

<p>Anyway, I&rsquo;m older and wiser now (it&rsquo;s been whole three months, after all), so here&rsquo;s a single article covering the rest of the new features &mdash; the ones that might not justify a deep dive but are still worth knowing about.</p>

<h2>OPcache Is Now a Mandatory Part of PHP</h2>

<p>What many developers might not realize is that OPcache has been optional for the past decade, even though you&rsquo;d be hard-pressed to find a production (or even development) server running PHP without it. That&rsquo;s changing in PHP 8.5 &mdash; OPcache is now officially part of PHP itself and will no longer be bundled as a separate extension.</p>

<h2>Final Property Promotion</h2>

<p>I&rsquo;ve already covered this in a <a href="/programming/new-in-php-8-5-final-promoted-properties">separate article</a>, but to summarize: promoted properties (the ones defined in a constructor&rsquo;s parameter list) can now be declared <code>final</code>.</p>

<h2>Attributes on Constants</h2>

<p>Non-class compile-time constants (those declared with <code>const</code>, not <code>define()</code>) can now have attributes. Alongside this comes a new <code>Attribute::TARGET_CONSTANT</code> target and a new <code>ReflectionConstant::getAttributes()</code> method. You can also now use the built-in <code>#[Deprecated]</code> attribute on constants.</p>

<h2>Asymmetric Visibility for Static Properties</h2>

<p>This brings static properties in line with instance properties in terms of visibility. The same asymmetric-visibility rules now apply to both.</p>

<h2>Easier Access to Error and Exception Handlers</h2>

<p>Until now, PHP only provided setter functions for error and exception handlers, which returned the previous handler. To retrieve the current handler, developers had to resort to a small hack like this:</p>

<pre>
<code class="language-php">$currentHandler = set_error_handler('must_be_a_valid_callable');
restore_error_handler();</code></pre>

<p>In PHP 8.5, you can use the new <code>get_error_handler()</code> and <code>get_exception_handler()</code> functions instead. Both return exactly the same value that was originally passed to their respective setter, with a return type of <code>?callable</code>.</p>

<h2>New array_first() and array_last() Functions</h2>

<p>No more <code>$array[array_key_first($array)]</code> or, worse, <code>reset($array)</code>! These two new functions complement <code>array_key_first()</code> and <code>array_key_last()</code> introduced in PHP 7.3. They both return <code>null</code> for empty arrays.</p>

<h2>A Saner Directory Class</h2>

<p>The <code>Directory</code> class (returned, for example, by the <a href="https://www.php.net/manual/en/function.dir.php" rel="noopener noreferrer" target="_blank"><code>dir()</code></a> function) is what&rsquo;s known as a resource object &mdash; a class-like wrapper for what used to be old-style resource types.</p>

<p>These resource objects typically can&rsquo;t be instantiated with <code>new</code>, serialized, or cloned, and generally don&rsquo;t behave like regular classes. The <code>Directory</code> class was the odd one out &mdash; it allowed all of that for historical reasons, though doing so never resulted in a valid instance, and directories created that way couldn&rsquo;t actually be used.</p>

<p>In PHP 8.5, <code>Directory</code> joins the rest of its siblings and becomes a proper resource object, behaving consistently with the others.</p>

<h2>#[Override] Can Now Apply to Properties</h2>

<p>Just as you could previously mark methods that override a parent&rsquo;s implementation, you can now apply <code>#[Override]</code> to properties. As with methods, PHP will throw an error if the property doesn&rsquo;t actually override anything.</p>

<h2>#[Deprecated] Can Be Used on Traits</h2>

<p>This allows you to deprecate an entire trait. Whenever a class uses a deprecated trait, PHP will emit a deprecation notice.</p>

<h2>Deprecations</h2>

<p>As usual, PHP 8.5 brings a few deprecations. Besides some more obscure ones (did you know you could use a semicolon instead of a colon in a <code>case</code> statement?), here are the ones more likely to affect real-world codebases:</p>

<ul>
	<li><strong>Backtick operator:</strong> The backtick operator is now deprecated. If you&rsquo;re unfamiliar, it&rsquo;s a shorthand for <code>shell_exec()</code> &mdash; for example, <code>echo `whoami`;</code> is equivalent to <code>echo shell_exec(&#39;whoami&#39;);</code>. Some older codebases still use it, so keep an eye out for that.</li>
	<li><strong><code>__sleep()</code> and <code>__wakeup()</code>:</strong> Following the deprecation of the <code>Serializable</code> interface, PHP is now deprecating these legacy serialization hooks as well. Going forward, use <code>__serialize()</code> and <code>__unserialize()</code> instead.</li>
	<li><strong><code>setAccessible()</code> in Reflection:</strong> These methods have done nothing since PHP 8.1 but remained for backward compatibility. In 8.5, they&rsquo;ll now trigger a deprecation notice.</li>
	<li><strong><code>SplObjectStorage</code> methods:</strong> The <code>contains()</code>, <code>attach()</code>, and <code>detach()</code> methods are deprecated. Use the <code>ArrayAccess</code> equivalents &mdash; <code>offsetExists()</code>, <code>offsetSet()</code>, and <code>offsetUnset()</code> &mdash; instead.</li>
</ul>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>New in PHP 8.5: Closures as Constant Expressions</title>
      <description><![CDATA[Another exciting PHP 8.5 feature: Closures can now be used as constant expressions, allowing them to appear as default parameters or attribute values.]]></description>
      <pubDate>Fri, 15 Aug 2025 01:58:56 +0000</pubDate>
      <link>https://chrastecky.dev/programming/new-in-php-8-5-closures-as-constant-expressions</link>
      <guid isPermaLink="false">22</guid>
      <enclosure type="image/png" length="132090" url="https://chrastecky.dev/i/l/41/post_detail/closures-constant-expressions.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[Another exciting PHP 8.5 feature: Closures can now be used as constant expressions, allowing them to appear as default parameters or attribute values.

<p>Ever wanted to set a closure as a default parameter value in PHP, only having to come up with workarounds? In PHP 8.5, that frustration is gone. Closures can now be constant expressions &mdash; meaning they work anywhere you&rsquo;d use a literal value.</p>

<p>I&rsquo;ve been bitten by this limitation before. Many times. Now, you can use closures in places where you could previously only use values like integers or strings:</p>

<ul>
	<li>Default parameter values</li>
	<li>Constant values</li>
	<li>Property default values</li>
	<li>Attribute parameter values</li>
	<li>And more</li>
</ul>

<h2>Default values</h2>

<p>In the past, I&rsquo;ve written code like this:</p>

<pre>
<code class="language-php">function someFunction(mixed $someValue, ?callable $callback = null): bool
{
    $callback ??= fn () =&gt; true;
    return $callback($someValue);
}</code></pre>

<p>Or this:</p>

<pre>
<code class="language-php">final class SomeClass
{
    private Closure $someCallable;

    public function __construct()
    {
        $this-&gt;someCallable = function (mixed $value): bool {
            // todo
            return true;
        };
    }
}</code></pre>

<p>With closures now being constant expressions, both examples can be simplified to:</p>

<pre>
<code class="language-php">function someFunction(
    mixed $someValue,
    callable $callback = static function () { return true; },
): bool {
    return $callback($someValue);
}

final class SomeClass
{
    private Closure $someCallable = static function (mixed $value): bool {
        // todo
        return true;
    };
}</code></pre>

<p>No more <code>$callback ??=</code> gymnastics. Using closures directly as default parameter values is something I do fairly often, so being able to tighten the public interface by avoiding nonsense values like <code>null</code> is a great improvement.</p>

<h2>Attributes</h2>

<p>This is another great change &mdash; you can now define functions directly within attributes. For example:</p>

<pre>
<code class="language-php">#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class TruthyValidator
{
    public function __construct(
        public Closure $truthyValidator = static function(mixed $value): bool {
            return (bool) $value;
        }
    ) {
    }
}</code></pre>

<p>Here&rsquo;s a simple validator attribute that checks whether the value is truthy, with the default implementation just casting it to a boolean and letting PHP handle the conversion. But say you want to consider the string <code>&#39;0&#39;</code> as truthy:</p>

<pre>
<code class="language-php">    #[TruthyValidator(truthyValidator: static function(string|int|null $value): bool {
        return $value === '0' || $value;
    })]
    public string|int|null $someProperty = null;</code></pre>

<h2>First-Class Callables</h2>

<blockquote>
<p>This is technically a separate RFC, but it was split for voting reasons rather than technical ones, so I&rsquo;m covering both in the same article.</p>
</blockquote>

<p>In addition to standard closures where you define the function body inline, you can now also use first-class callables as constant expressions. This means all of the above examples also work with them.</p>

<pre>
<code class="language-php">&lt;?php

// define a default validator
function defaultValidatorFunction(mixed $value): bool
{
    return (bool) $value;
}

// define the validator class
#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class TruthyValidator
{
    public function __construct(
        // and assign the default validator using the first-class callable syntax
        public Closure $truthyValidator = defaultValidatorFunction(...),
    ) {
    }
}

// define our custom validation function
function truthyValidatorWithoutZeroString(string|int|null $value): bool
{
    return $value === '0' || $value;
}

class SomeClassToBeValidated
{
    // and use it as a first-class callable
    #[TruthyValidator(truthyValidator: truthyValidatorWithoutZeroString(...))]
    public string|int|null $someProperty = null;
}
</code></pre>

<h2>Conclusion</h2>

<p>I really like this addition because it &mdash; like many other recent improvements &mdash; moves PHP toward a cleaner, more consistent language with fewer hacks and a saner syntax.</p>

<p>Where will you use this first? Drop your examples in the comments &mdash; I&rsquo;m curious what creative cases you come up with.</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>New in PHP 8.5: The Pipe Operator</title>
      <description><![CDATA[One of the most exciting additions in PHP 8.5 is the pipe operator. It enables more readable and expressive code when working with nested function calls.]]></description>
      <pubDate>Wed, 23 Jul 2025 18:38:04 +0000</pubDate>
      <link>https://chrastecky.dev/programming/new-in-php-8-5-the-pipe-operator</link>
      <guid isPermaLink="false">17</guid>
      <enclosure type="image/png" length="127663" url="https://chrastecky.dev/i/l/32/post_detail/pipe-operator.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[One of the most exciting additions in PHP 8.5 is the pipe operator. It enables more readable and expressive code when working with nested function calls.

<p>One of PHP&#39;s longstanding limitations is that scalar values (strings, integers, arrays, etc.) cannot have methods. As a result, deeply nested operations often end up looking cluttered and hard to read:</p>

<pre>
<code class="language-php">echo ucfirst(strtolower(preg_replace('@\s+@', '', "HELLO THERE!")));
</code></pre>

<p>There are a few workarounds, such as using intermediate variables:</p>

<pre>
<code class="language-php">$temp = "HELLO THERE!";
$temp = preg_replace('@\s+@', '', $temp);
$temp = strtolower($temp);
$temp = ucfirst($temp);

echo $temp;</code></pre>

<p>Or using indentation to improve readability:</p>

<pre>
<code class="language-php">echo ucfirst(
    strtolower(
        preg_replace(
            '@\s+@',
            '',
            "HELLO THERE!"
        ),
    ),
);</code></pre>

<p>These approaches work, but they&#39;re clunky. The new pipe operator offers a more elegant solution by allowing you to chain the result of the left-hand expression into a callable on the right:</p>

<pre>
<code class="language-php">echo "HELLO THERE!"
    |&gt; fn (string $str) =&gt; preg_replace('@\s+@', '', $str)
    |&gt; strtolower(...)
    |&gt; ucfirst(...)
;</code></pre>

<p>If you&#39;ve ever worked with fluent setters, this pattern should feel very familiar.</p>

<h2>How It Works</h2>

<p>The pipe operator passes the result of the left-hand expression as an argument to the callable on the right. That callable must accept exactly one required parameter. It pairs perfectly with first-class callables (<code>ucfirst(...)</code>) and short arrow functions (<code>fn($x) =&gt; $x</code>).</p>

<p>It&rsquo;s a full expression, so you can use it wherever any expression is allowed&mdash;assignments, return statements, conditionals, etc.</p>

<h2>Operator Precedence</h2>

<p>The pipe operator is left-associative, just like most arithmetic operators. That means expressions are evaluated from left to right:</p>

<pre>
<code class="language-php">// correctly evaluates to 2
$result = 2 + 2 |&gt; sqrt(...);

// equivalent to this expression with parentheses
$result = (2 + 2) |&gt; sqrt(...);</code></pre>

<p>You can, of course, use parentheses to alter evaluation order:</p>

<pre>
<code class="language-php">// will evaluate to something like 3.4142135623731
$result = 2 + (2 |&gt; sqrt(...));

// equivalent to
$result = 2 + sqrt(2);</code></pre>

<p>The pipe operator has higher precedence than comparison operators, but lower than arithmetic ones. So:</p>

<pre>
<code class="language-php">$result = 2 + 2 |&gt; sqrt(...) &gt; 5;
// is equivalent to
$result = ((2 + 2) |&gt; sqrt(...)) &gt; 5
// is equivalent to
$result = sqrt(2 + 2) &gt; 5;</code></pre>

<p>The rules are intuitive, but one case where parentheses are often necessary is with the null coalescing operator:</p>

<pre>
<code class="language-php">$result = 5 |&gt; trueOrNullFunction(...) ?? false;
// equivalent to
$result = (5 |&gt; trueOrNullFunction(...)) ?? false;
// equivalent to
$result = trueOrNullFunction(5) ?? false;</code></pre>

<p>And if you&#39;re providing an optional callable, parentheses are required:</p>

<pre>
<code class="language-php">// this is wrong without parentheses
$result = 5 |&gt; $possiblyNullCallable ?? fn ($x) =&gt; true;
// this is correct
$result = 5 |&gt; ($possiblyNullCallable ?? fn ($x) =&gt; true);
</code></pre>

<h2>Higher-Order Functions</h2>

<p>The pipe operator really shines when used with higher-order functions&mdash;functions that return other functions:</p>

<pre>
<code class="language-php">function map(callable $mapper): Closure
{
    return fn (array $array) =&gt; array_map($mapper, $array);
}

function filter(callable $filter): Closure
{
    return fn (array $array) =&gt; array_filter($array, $filter);
}

// assume is_odd and pow2 exist
$result = [1, 2, 3, 4, 5]
    |&gt; filter(is_odd(...))
    |&gt; map(pow2(...))
;
</code></pre>

<h2>Caveats</h2>

<p>The pipe operator is highly optimized and introduces virtually no overhead compared to traditional function calls. However, it does have some limitations:</p>

<ul>
	<li>It only works with callables that accept exactly one required argument.</li>
	<li>Callables that require additional arguments must be wrapped in a closure.</li>
</ul>

<pre>
<code class="language-php">$result = [1, 2, 3]
    |&gt; fn (array $array) =&gt; array_filter($array, fn (int $num) =&gt; $num % 2 === 0)
    |&gt; fn (array $array) =&gt; array_map(fn (int $num) =&gt; $num ** 2, $array)
;</code></pre>

<p>This adds a tiny performance cost, though it&rsquo;s usually negligible. If the <a href="https://wiki.php.net/rfc/partial_function_application" rel="noopener noreferrer" target="_blank">Partial Function Application RFC</a> is accepted, it will make this kind of usage even cleaner.</p>

<p>p&gt;One major limitation is that the pipe operator doesn&rsquo;t support references. For example, the following code will throw an error:</p>

<p>&nbsp;</p>

<pre>
<code class="language-php">function square(int &amp;$number): void
{
    $number **= 2;
}

$num = 2;
$num |&gt; square(...); // ❌ This will fail</code></pre>

<p>Some functions in PHP can accept both references and values, depending on how they&#39;re called (something I didn&#39;t even know was possible before writing this article). These will work with the pipe operator, but the values will always be passed <em>by value</em>, not by reference.</p>

<h2>Conclusion</h2>

<p>The new pipe operator is a welcome addition to PHP 8.5 that makes functional-style programming much cleaner and more readable. Whether you&#39;re cleaning up strings, transforming arrays, or composing complex logic, it allows you to express intent without sacrificing clarity.</p>

<p>While it has a few limitations&mdash;such as lack of reference support and the need for wrappers around multi-argument functions&mdash;these are relatively minor compared to the readability gains. And with future features like partial function application potentially on the way, the story will only get better.</p>

<p>What do you think? Will the pipe operator find a place in your workflow, or do you prefer more traditional patterns?</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Go Meets PHP: Enhancing Your PHP Applications with Go via FFI</title>
      <description><![CDATA[Use PHP’s FFI to supercharge your application by offloading compute-heavy work to Go!]]></description>
      <pubDate>Sat, 19 Jul 2025 15:19:47 +0000</pubDate>
      <link>https://chrastecky.dev/programming/go-meets-php-enhancing-your-php-applications-with-go-via-ffi</link>
      <guid isPermaLink="false">19</guid>
      <enclosure type="image/png" length="395351" url="https://chrastecky.dev/i/l/34/post_detail/php-ffi-go.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[Use PHP’s FFI to supercharge your application by offloading compute-heavy work to Go!

<p>As an interpreted language, PHP has inherent performance limitations, especially when it comes to CPU-bound tasks. Go, on the other hand, is a compiled language known for its speed and efficiency. By leveraging PHP&rsquo;s Foreign Function Interface (FFI), we can call Go functions from PHP via a shared C layer and achieve significant performance improvements in the right scenarios.</p>

<h2>Before We Start</h2>

<p>There are a few caveats to keep in mind:</p>

<ul>
	<li>This approach only benefits CPU-bound tasks &mdash; it won&rsquo;t help with I/O-bound operations like database queries or API calls.</li>
	<li>FFI adds overhead. For simple tasks, PHP may still be faster despite Go&rsquo;s raw speed.</li>
	<li>We&rsquo;re using Go&rsquo;s C bindings, which add an extra layer. For the absolute best performance, writing in C directly is faster.</li>
	<li>Cross-platform support can be tricky &mdash; you&rsquo;ll need to compile your Go shared library separately for each target platform and architecture.</li>
	<li>Memory management between PHP and Go requires care &mdash; you need to handle allocation and freeing of memory correctly on both sides.</li>
</ul>

<p>That said, for the right use cases, this technique can be extremely powerful without the complexity of writing low-level C code.</p>

<h2>Hello World!</h2>

<p>No tutorial would be complete without a &ldquo;Hello, World!&rdquo; example &mdash; but let&rsquo;s skip the static string and jump straight into a personalized greeting.</p>

<p>In Go, it&rsquo;s as simple as:</p>

<pre>
<code class="language-go">package main

import "fmt"

func HelloWorld(name string) {
	fmt.Printf("Hello %s!\n", name)
}</code></pre>

<p>Calling it is straightforward:</p>

<pre>
<code class="language-go">	HelloWorld("Dominik")
</code></pre>

<p>Which prints: <code>Hello Dominik!</code></p>

<p>To make this callable from PHP, we&rsquo;ll need to export it as a C function. Here&#39;s a basic binding:</p>

<pre>
<code class="language-php">package main

import "C"
import (
	"fmt"
)

//export HelloWorldC
func HelloWorldC(name *C.char) {
	result := C.GoString(name)
	fmt.Printf("Hello %s!\n", result)
}</code></pre>

<p>However, mixing conversion and logic can get messy. A cleaner approach is to separate concerns:</p>

<pre>
<code class="language-go">package main

import "C"
import (
	"fmt"
)

//export HelloWorld
func HelloWorld(name *C.char) {
	HelloWorldGo(C.GoString(name))
}

func HelloWorldGo(name string) {
	fmt.Printf("Hello %s!\n", name)
}

func main() {}
</code></pre>

<p>Now we have a clear boundary: <code>HelloWorld</code> handles data conversion, and <code>HelloWorldGo</code> contains the business logic.</p>

<blockquote>
<p>The <code>//export</code> comment is essential &mdash; without it, Go won&rsquo;t export the function. You also need an empty <code>main()</code> function to satisfy the Go compiler when building shared libraries in the main package.</p>
</blockquote>

<p>Build it with:</p>

<pre>
<code>go build -buildmode=c-shared -o hello.so hello.go</code></pre>

<p>This generates two files: <code>hello.so</code> and <code>hello.h</code>, both of which we&rsquo;ll need on the PHP side.</p>

<h3>Wiring It Up in PHP</h3>

<p>Create an FFI instance in PHP:</p>

<pre>
<code class="language-php">&lt;?php

$ffi = FFI::cdef(
    file_get_contents(__DIR__ . '/hello.h'),
    __DIR__ . '/hello.so',
);</code></pre>

<p>However, PHP uses a non-standard C header parser, so we&rsquo;ll need to trim <code>hello.h</code> to just this:</p>

<pre>
<code class="language-cpp">extern void HelloWorld(char* name);</code></pre>

<p>Once that&rsquo;s done, you can call it directly:</p>

<pre>
<code class="language-php">$ffi-&gt;HelloWorld("Dominik");</code></pre>

<p>Which outputs: <code>Hello Dominik!</code></p>

<h2>The FFI Overhead</h2>

<p>Before we dive deeper, let&rsquo;s compare the performance of this FFI approach against a native PHP function. For simple functions like this, the FFI overhead is <strong>significant</strong>, and using Go wouldn&rsquo;t make much sense.</p>

<p>Running the following code, we compare the performance of calling the Go function via FFI a thousand times versus calling a native PHP function:</p>

<pre>
<code class="language-php">&lt;?php

$ffi = FFI::cdef(
    file_get_contents(__DIR__ . '/hello.h'),
    __DIR__ . '/hello.so',
);

function HelloWorld(string $name): void
{
    echo "Hello {$name}!", PHP_EOL;
}

$start = microtime(true);
for ($i = 0; $i &lt; 1000; $i++) {
    $ffi-&gt;HelloWorld("Dominik");
}
$end = microtime(true);

$timeGo = $end - $start;

$start = microtime(true);
for ($i = 0; $i &lt; 1000; $i++) {
    HelloWorld("Dominik");
}
$end = microtime(true);
$timePhp =  $end - $start;

echo "Go version took {$timeGo} seconds.", PHP_EOL;
echo "PHP version took {$timePhp} seconds.", PHP_EOL;
</code></pre>

<p>The results:</p>

<pre>
<code>Go version took 0.51009082794189 seconds.
PHP version took 0.0016758441925049 seconds.</code></pre>

<p>As you can see, the Go version is <strong>much slower</strong> here &mdash; over 300 times slower than native PHP. That&rsquo;s not because Go is slow, but because FFI incurs a high cost per call. Each of those 1,000 calls crosses the PHP&ndash;C&ndash;Go boundary.</p>

<p>Now let&rsquo;s move the loop inside Go to reduce the number of boundary crossings. Here&rsquo;s the updated Go function:</p>

<pre>
<code class="language-go">func HelloWorldGo(name string) {
	for range 1000 {
		fmt.Printf("Hello, %s!\n", name)
	}
}</code></pre>

<p>And an equivalent PHP function for fairness:</p>

<pre>
<code class="language-php">function HelloWorld(string $name): void
{
    for ($i = 0; $i &lt; 1000; $i++) {
        echo "Hello {$name}!", PHP_EOL;
    }
}</code></pre>

<p>The results now look very different:</p>

<pre>
<code>Go version took 0.0031590461730957 seconds.
PHP version took 0.012860059738159 seconds.</code></pre>

<p>This time, the Go version is clearly faster. Why? Because we&rsquo;ve reduced the number of PHP&ndash;FFI&ndash;Go context switches from 1,000 down to just 1. This highlights the most important performance tip when using FFI: <strong>minimize the number of boundary crossings</strong>. Let Go do as much as possible once you&rsquo;re there.</p>

<h2>Fibonacci</h2>

<p>Now that we&rsquo;ve seen how performance improves with fewer context switches, let&rsquo;s try something that&rsquo;s inherently CPU-bound: calculating the nth number in the Fibonacci sequence. We&rsquo;ll stick with a naive recursive implementation to keep things simple (and CPU-intensive).</p>

<p>Here&rsquo;s the Go version:</p>

<pre>
<code class="language-go">//export Fibonacci
func Fibonacci(n C.int) C.int {
	return C.int(fibonacciGo(int(n)))
}

func fibonacciGo(n int) int {
	if n &lt;= 1 {
		return n
	}
	return fibonacciGo(n-1) + fibonacciGo(n-2)
}</code></pre>

<p>And here&rsquo;s the equivalent PHP version:</p>

<pre>
<code class="language-php">function fibonacci(int $n): int
{
    if ($n &lt;= 1) {
        return $n;
    }

    return fibonacci($n - 1) + fibonacci($n - 2);
}</code></pre>

<p>To benchmark both implementations:</p>

<pre>
<code class="language-php">&lt;?php

$ffi = FFI::cdef(
    file_get_contents(__DIR__ . '/hello.h'),
    __DIR__ . '/hello.so',
);

function fibonacci(int $n): int
{
    if ($n &lt;= 1) {
        return $n;
    }

    return fibonacci($n - 1) + fibonacci($n - 2);
}

$start = microtime(true);
$result = $ffi-&gt;Fibonacci(35);
$end = microtime(true);
$time = $end - $start;

echo "Go result: {$result}. It took {$time} seconds to compute.", PHP_EOL;

$start = microtime(true);
$result = fibonacci(35);
$end = microtime(true);
$time = $end - $start;

echo "PHP result: {$result}. It took {$time} seconds to compute.", PHP_EOL;
</code></pre>

<p>The output:</p>

<pre>
<code class="language-php">Go result: 9227465. It took 0.041604042053223 seconds to compute.
PHP result: 9227465. It took 3.975930929184 seconds to compute.</code></pre>

<p>Same result, but Go is almost <strong>100 times faster</strong>. And the difference gets even more dramatic with larger inputs. Here&rsquo;s what happens with <code>fibonacci(40)</code>:</p>

<pre>
<code>Go result: 102334155. It took 0.39231300354004 seconds to compute.
PHP result: 102334155. It took 44.720011949539 seconds to compute.</code></pre>

<p>That&rsquo;s nearly 45 seconds for PHP versus less than half a second for Go. It&rsquo;s a striking example of why you&rsquo;d want to offload compute-heavy tasks to Go via FFI.</p>

<h2>Where It Makes Sense</h2>

<p>Some potential real-world use cases:</p>

<ul>
	<li>Sorting large in-memory datasets</li>
	<li>Matrix operations and other complex math</li>
	<li>Cryptographic algorithms not natively supported by PHP (e.g., BLAKE3)</li>
	<li>Custom sorters (e.g., geo distance, radix sort)</li>
	<li>Compression formats unsupported by PHP extensions</li>
	<li>Working with XLS files (via Go libraries)</li>
	<li>Concurrent workloads</li>
</ul>

<h2>Concurrent Work</h2>

<p>Let&rsquo;s now explore one of Go&rsquo;s major strengths: concurrency. As an example, imagine a user uploads multiple images and your application needs to generate thumbnails for them. We&rsquo;ll simulate the image processing step using <code>time.Sleep</code> to represent a long-running operation.</p>

<p>Here&rsquo;s a simplified image processing function in Go:</p>

<pre>
<code class="language-php">func ResizeImage(path string) error {
	time.Sleep(300 * time.Millisecond)

	if rand.Int()%2 == 0 {
		return errors.New("test")
	}

	return nil
}</code></pre>

<p>In Go, returning an <code>error</code> is a common idiom. Returning <code>nil</code> (similar to <code>null</code> in other languages) indicates success.</p>

<p>Now let&rsquo;s look at the function we&rsquo;ll be calling from PHP:</p>

<pre>
<code class="language-go">func ResizeImagesGo(paths []string) []string {
	var waitGroup sync.WaitGroup // create a wait group - once it's empty, everything has been processed
	var mutex sync.Mutex         // a mutex to safely write into the failed slice below
	failed := make([]string, 0)  // create a slice that can contain strings and has initial length of zero

	for _, path := range paths { // iterate over all paths
		path := path     // this recreates the path variable inside the current scope to avoid race conditions
		waitGroup.Add(1) // add one to the wait group
		go func() {      // run this in a goroutine (similar to threads in other languages)
			defer waitGroup.Done() // after this function finishes, waitGroup.Done() will be called
			err := ResizeImage(path)
			if err != nil { // if we have an error
				mutex.Lock()                  // lock the mutex to make sure only one goroutine is writing to the failed slice
				failed = append(failed, path) // add a new path to the list of failed paths
				mutex.Unlock()                // unlock the mutex so that any other goroutine can lock it again
			}
		}()
	}

	waitGroup.Wait() // wait until all wait groups are done

	return failed
}</code></pre>

<p>I&rsquo;ve commented the code heavily, but here&rsquo;s the high-level flow:</p>

<ul>
	<li>Accept a list of image paths</li>
	<li>Process each image in its own goroutine (like a lightweight thread)</li>
	<li>Safely track which images failed using a mutex</li>
	<li>Wait for all images to finish processing</li>
	<li>Return the list of failed paths</li>
</ul>

<p>Now comes the only messy part &mdash; the C binding. Unfortunately, that&rsquo;s just how FFI works at this level:</p>

<pre>
<code class="language-go">//export ResizeImages
func ResizeImages(input **C.char, count C.int, failedOut ***C.char, failedCount *C.int) {
	// because this is a C binding and C doesn't have any nice structures built-in,
	// we have to pass the data as a char[] pointer and provide the count of items as
	// a second parameter

	// to avoid having to create a custom struct, we return the data by having them passed as references
	// the triple asterisk means it's a pointer to char array, the single asterisk means it's a pointer to
	// an integer

	paths := unsafe.Slice(input, int(count)) // we have to make a slice out of the input
	goPaths := make([]string, count)         // create a new Go slice with the correct length
	for i, path := range paths {
		goPaths[i] = C.GoString(path) // convert the C-strings to Go-strings
	}

	failed := ResizeImagesGo(goPaths) // call the Go function and assign the result

	// the parts below are some C-level shenanigans, basically you need to allocate (C.malloc) enough memory
	// to hold the amount of pointers that will be assigned, which is the length of the failed slice
	failedAmount := len(failed)
	ptrSize := unsafe.Sizeof(uintptr(0))
	cArray := C.malloc(C.size_t(failedAmount) * C.size_t(ptrSize))
	cStrs := unsafe.Slice((**C.char)(cArray), failedAmount)

	for i, str := range failed { // iterate over the failed paths
		cStrs[i] = C.CString(str) // and assign it to the C array
	}

	*failedOut = (**C.char)(cArray)    // assign the array to the reference input parameter
	*failedCount = C.int(failedAmount) // assign the count of failed items to the reference input parameter
}</code></pre>

<p>Yes, it&rsquo;s a bit messy &mdash; but that&rsquo;s standard practice when working with low-level bindings in Go or C. The important part is that we&rsquo;ve isolated the complexity into this layer. Imagine writing the actual business logic in C &mdash; suddenly Go feels a lot more pleasant.</p>

<p>Now, after rebuilding the library, you&rsquo;ll need to update <code>hello.h</code> to include:</p>

<pre>
<code class="language-cpp">extern void ResizeImages(char** input, int count, char*** failedOut, int* failedCount);
</code></pre>

<h3>PHP Integration</h3>

<p>Let&rsquo;s now call this function from PHP. Here&rsquo;s the full example:</p>

<pre>
<code class="language-php">&lt;?php

$ffi = FFI::cdef(
    file_get_contents(__DIR__ . '/hello.h'),
    __DIR__ . '/hello.so',
);

$imagePaths = [
    "pathA",
    "pathB",
    "pathC",
    "pathD",
];
$imagesCount = count($imagePaths);

$cArray = FFI::new("char*[" . count($imagePaths) . "]"); // create a new array with fixed size
$buffers = []; // this will just hold variables to prevent PHP's garbage collection

foreach ($imagePaths as $i =&gt; $path) {
    $size = strlen($path); // the size to allocate in bytes
    $buffer = FFI::new("char[" . ($size + 1) . "]"); // create a new C string of length +1 to add space for null terminator
    FFI::memcpy($buffer, $path, $size); // copy the content of $path to memory at $buffer with size $size
    $cArray[$i] = FFI::cast("char*", $buffer); // cast it to a C char*, aka a string
    $buffers[] = $buffer; // assigning it to the $buffers array ensures it doesn't go out of scope and PHP cannot garbage collect it
}

$failedOut = FFI::new("char**"); // create a string array in C, this will be passed as reference
$failedCount = FFI::new("int"); // create an integer which will be passed as reference

$start = microtime(true);
$ffi-&gt;ResizeImages(
    $cArray,
    count($imagePaths),
    FFI::addr($failedOut),
    FFI::addr($failedCount),
);
$end = microtime(true);
$time = $end - $start;

$count = $failedCount-&gt;cdata; // fetch the count of failed items

echo "Failed items: {$count}", PHP_EOL;
for ($i = 0; $i &lt; $count; $i++) {
    echo " - ", FFI::string($failedOut[$i]), PHP_EOL; // cast each item to a php string and print it
}
echo "Processing took: {$time} seconds", PHP_EOL;
</code></pre>

<p>Depending on randomness, you&rsquo;ll see output similar to:</p>

<pre>
<code>Failed items: 4
 - pathA
 - pathC
 - pathD
 - pathB
Processing took: 0.30362796783447 seconds
</code></pre>

<p>Two things to notice:</p>

<ul>
	<li>The failed items are out of order &mdash; a clear sign the operations ran in parallel. Each image was processed in its own goroutine and reported failure as soon as it was done.</li>
	<li>Total time is around 300 ms &mdash; the time it takes to process <em>a single image</em>, despite processing four at once. This shows we achieved true concurrency.</li>
</ul>

<h3>Memory Management</h3>

<p>The previous example contains a memory leak &mdash; something you typically don&rsquo;t have to worry about in PHP or Go, since both languages have garbage collectors. But once you introduce C into the mix, you&rsquo;re responsible for manually managing memory.</p>

<p>Whether this matters depends on how you run your PHP code. If you use the traditional <em>execute-and-die</em> model (e.g. a web server spawns a PHP process that dies at the end of each request), then memory leaks are mostly harmless &mdash; the operating system will reclaim all memory when the process exits.</p>

<p>However, if you&#39;re using modern alternatives like <strong>RoadRunner</strong>, <strong>Swoole</strong>, <strong>AMPHP</strong>, <strong>ReactPHP</strong>, or any long-running PHP worker (<strong>Symfony Messenger</strong>), memory leaks will accumulate across requests and eventually exhaust system memory.</p>

<p>The rule of thumb is simple: <strong>if your C glue code allocates memory, you must free it once it&rsquo;s no longer needed</strong>. In our case, both the outer array and the individual strings are allocated in Go:</p>

<pre>
<code class="language-go">// C.malloc is a direct allocation of memory
cArray := C.malloc(C.size_t(failedAmount) * C.size_t(ptrSize))

for i, str := range failed {
    // C.CString uses malloc in the background
	cStrs[i] = C.CString(str) 
}</code></pre>

<p>To free this memory in PHP, you can use <code>FFI::free()</code> directly:</p>

<pre>
<code class="language-php">echo "Failed items: {$count}", PHP_EOL;
for ($i = 0; $i &lt; $count; $i++) {
    echo " - ", FFI::string($failedOut[$i]), PHP_EOL;
    FFI::free($failedOut[$i]); // free each string after use
}
FFI::free($failedOut); // finally free the array itself</code></pre>

<p>Again, if you&#39;re only using short-lived PHP processes, this isn&#39;t a concern. But in long-running environments, proper memory management is essential to avoid leaks and unpredictable crashes.</p>

<h2>Conclusion</h2>

<p>The C-level glue code can be verbose and awkward, but once it&rsquo;s in place, combining Go and PHP can unlock performance that&rsquo;s hard to beat &mdash; all while keeping most of your code in modern, high-level languages.</p>

<p>What do you think? Would you consider using Go alongside PHP for performance-critical workloads?</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>New in PHP 8.5: Asymmetric Visibility for Static Properties</title>
      <description><![CDATA[A small but meaningful update in PHP 8.5 introduces asymmetric visibility for static properties.]]></description>
      <pubDate>Wed, 02 Jul 2025 19:54:49 +0000</pubDate>
      <link>https://chrastecky.dev/programming/new-in-php-8-5-asymmetric-visibility-for-static-properties</link>
      <guid isPermaLink="false">18</guid>
      <enclosure type="image/png" length="128133" url="https://chrastecky.dev/i/l/33/post_detail/asymmetric-visiblity-static.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[A small but meaningful update in PHP 8.5 introduces asymmetric visibility for static properties.

<p>This minor addition brings asymmetric visibility&mdash;already available for instance properties&mdash;to static properties as well.</p>

<p>Previously, this was valid syntax:</p>

<pre>
 <code class="language-php">final class PublicPrivateSetClass {
    public private(set) string $instanceProperty;
} </code></pre>

<p>As of PHP 8.5, you can now do the same with static properties:</p>

<pre>
 <code class="language-php">final class PublicPrivateSetClass {
    public private(set) static string $staticProperty;
} </code></pre>

<p>While not the most groundbreaking feature, it improves consistency in the language&mdash;which is always a welcome change.</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>New in PHP 8.5: Final Promoted Properties</title>
      <description><![CDATA[PHP 8.5 adds support for final properties using constructor promotion. In this (very short) article, I’ll show you everything you need to know about this new addition.]]></description>
      <pubDate>Tue, 24 Jun 2025 20:45:11 +0000</pubDate>
      <link>https://chrastecky.dev/programming/new-in-php-8-5-final-promoted-properties</link>
      <guid isPermaLink="false">16</guid>
      <enclosure type="image/png" length="134538" url="https://chrastecky.dev/i/l/31/post_detail/final-promoted-properties.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[PHP 8.5 adds support for final properties using constructor promotion. In this (very short) article, I’ll show you everything you need to know about this new addition.

<p>Starting with PHP 8.5, you&#39;ll be able to do the following:</p>

<pre>
 <code class="language-php">public function __construct(
    final public string $someProperty,
) {}</code></pre>

<p>This wasn&#39;t possible before, as promoted properties couldn&#39;t be declared <code>final</code>.</p>

<p>Perhaps the more interesting part is that you can now omit the visibility modifier if you include <code>final</code>. In that case, the property will default to <code>public</code>:</p>

<pre>
 <code class="language-php">public function __construct(
    final string $someProperty, // this property will be public
) {}</code></pre>

<p>Personally, I&rsquo;m not a fan of this behavior &mdash; I prefer explicit over implicit. Fortunately, it can be enforced by third-party tools like code style fixers. Still, I would have preferred if the core required the visibility to be specified.</p>

<p><strong>What do you think?</strong> Do you like this change, or would you have preferred a stricter approach?</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>New in PHP 8.5: Levenshtein Comparison for UTF-8 Strings</title>
      <description><![CDATA[PHP 8.5 adds a new function for calculating the Levenshtein distance between strings — now with proper UTF-8 support.]]></description>
      <pubDate>Mon, 30 Jun 2025 03:53:28 +0000</pubDate>
      <link>https://chrastecky.dev/programming/new-in-php-8-5-levenshtein-comparison-for-utf-8-strings</link>
      <guid isPermaLink="false">15</guid>
      <enclosure type="image/png" length="133156" url="https://chrastecky.dev/i/l/30/post_detail/levenshtein.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[PHP 8.5 adds a new function for calculating the Levenshtein distance between strings — now with proper UTF-8 support.

<p>PHP has long had a <a href="https://www.php.net/manual/en/function.levenshtein.php" rel="noopener noreferrer" target="_blank">levenshtein()</a> function, but it comes with a significant limitation: it doesn&rsquo;t support UTF-8.</p>

<blockquote>
<p>If you&rsquo;re not familiar with the <a href="https://en.wikipedia.org/wiki/Levenshtein_distance" rel="noopener noreferrer" target="_blank">Levenshtein distance</a>, it&rsquo;s a way to measure how different two strings are &mdash; by counting the minimum number of single-character edits (insertions, deletions, or substitutions) required to change one string into another.</p>
</blockquote>

<p>For example, the following code returns <code>2</code> instead of the correct result, <code>1</code>:</p>

<pre>
<code class="language-php">var_dump(levenshtein('göthe', 'gothe'));</code></pre>

<p>There are workarounds &mdash; such as using a pure PHP implementation or converting strings to a custom single-byte encoding &mdash; but they come with downsides, like slower performance or non-standard behavior.</p>

<p>With the new <strong><code>grapheme_levenshtein()</code></strong> function in PHP 8.5, the code above now correctly returns <code>1</code>.</p>

<h2>Grapheme-Based Comparison</h2>

<p>What makes this new function especially powerful is that it operates on <em>graphemes</em>, not bytes or code points. For instance, the character <strong>&eacute;</strong> (accented &#39;e&#39;) can be represented in two ways: as a single code point (<code>U+00E9</code>) or as a combination of the letter <strong>e</strong> (<code>U+0065</code>) and a combining accent (<code>U+0301</code>). In PHP, you can write these as:</p>

<pre>
<code class="language-php">$string1 = "\u{00e9}";
$string2 = "\u{0065}\u{0301}";</code></pre>

<p>Even though these strings are technically different at the byte level, they represent the same grapheme. The new <code>grapheme_levenshtein()</code> function correctly recognizes this and returns <code>0</code> &mdash; meaning no difference.</p>

<p>This is particularly useful when working with complex scripts such as Japanese, Chinese, or Korean, where grapheme clusters play a bigger role than in Latin or Cyrillic alphabets.</p>

<p>Just for fun: what do you think the original <code>levenshtein()</code> function will return for the example above?</p>

<pre>
<code class="language-php">var_dump(levenshtein("\u{0065}\u{0301}", "\u{00e9}"));</code></pre>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>New in PHP 8.5: Attributes on Constants</title>
      <description><![CDATA[PHP 8.5 introduces support for attributes on non-class constants. This article walks you through everything you need to know about this new feature!]]></description>
      <pubDate>Mon, 23 Jun 2025 19:36:34 +0000</pubDate>
      <link>https://chrastecky.dev/programming/new-in-php-8-5-attributes-on-constants</link>
      <guid isPermaLink="false">13</guid>
      <enclosure type="image/png" length="131337" url="https://chrastecky.dev/i/l/28/post_detail/const-attributes.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[PHP 8.5 introduces support for attributes on non-class constants. This article walks you through everything you need to know about this new feature!

<p>This change is quite straightforward, so this won&rsquo;t be a long article. PHP 8.5 adds support for annotating non-class, compile-time constants with attributes. Compile-time constants are those defined using the <code>const</code> keyword, not the <code>define()</code> function.</p>

<p>Attributes can now include <code>Attribute::TARGET_CONSTANT</code> among their valid targets. Additionally, as the name suggests, <code>Attribute::TARGET_ALL</code> now includes constants as well. The <code>ReflectionConstant</code> class has been updated with a new method, <code>getAttributes()</code>, to support retrieving these annotations.</p>

<p>One particularly useful aspect of this change is that the built-in <code>#[Deprecated]</code> attribute can now be applied to compile-time constants.</p>

<p>As promised, this was a short post, since the change is relatively simple. See you next time&mdash;hopefully with a more exciting new feature in PHP 8.5!</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>New in PHP 8.5: Marking Return Values as Important</title>
      <description><![CDATA[Today, we'll explore one of the exciting features coming with PHP 8.5—the new #[NoDiscard] attribute to indicate important return values.]]></description>
      <pubDate>Wed, 04 Jun 2025 16:38:19 +0000</pubDate>
      <link>https://chrastecky.dev/programming/new-in-php-8-5-marking-return-values-as-important</link>
      <guid isPermaLink="false">12</guid>
      <enclosure type="image/png" length="193633" url="https://chrastecky.dev/i/l/27/post_detail/nodiscard.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[Today, we'll explore one of the exciting features coming with PHP 8.5—the new #[NoDiscard] attribute to indicate important return values.

<p><strong>PHP 8.5</strong> introduces a variety of compelling features. As a library author, I&#39;m particularly thrilled by the addition of the built-in <code>#[NoDiscard]</code> attribute, enabling developers to mark a function or method&#39;s return value as important.</p>

<blockquote>
<p><strong>Tip:</strong> You can read the full RFC at <a href="https://wiki.php.net/rfc/marking_return_value_as_important" rel="noopener noreferrer" target="_blank">wiki.php.net</a>.</p>
</blockquote>

<h2>How does the NoDiscard attribute work?</h2>

<p>Using <code>#[NoDiscard]</code> is straightforward&mdash;simply annotate your function or method. For example, marking a function that returns critical operation results, such as status flags or error messages, helps prevent accidental omission or unnoticed errors:</p>

<pre>
<code class="language-php">&lt;?php

#[NoDiscard]
function processStuff(): array
{
    return [];
}</code></pre>

<p>Now, if the function is called without using its return value, PHP generates the following warning:</p>

<pre>
<code>The return value of function processStuff() should either be used or intentionally ignored by casting it as (void).</code></pre>

<h3>Customizing the Warning Message</h3>

<p>You can provide a custom message for greater clarity:</p>

<pre>
<code class="language-php">&lt;?php

#[NoDiscard("because this is a batch processing function, and if any of the items fail, it returns the error details in an array instead of throwing an exception.")]
function processStuff(): array
{
    return [];
}</code></pre>

<p>This results in a more personalized warning:</p>

<pre>
<code>The return value of function processStuff() is expected to be consumed, because this is a batch processing function, and if any of the items fail, it returns the error details in an array instead of throwing an exception.</code></pre>

<h2>Suppressing the Warning</h2>

<p>Besides using the returned value (assigning or otherwise processing it), you can suppress this warning in several ways:</p>

<pre>
<code class="language-php">&lt;?php

@processStuff();          // Error suppression operator
(void)processStuff();     // Explicit void cast
$_ = processStuff();      // If you're coming from Go ;)</code></pre>

<p>However, <strong>beware of OPCache optimizations</strong>. If OPCache detects an unused instruction, it might optimize away the call, leading to inconsistencies between development and production environments.</p>

<pre>
<code class="language-php">&lt;?php

(bool)processStuff(); // OPCache may ignore this because the result isn't used.</code></pre>

<p>Using <code>(void)</code> explicitly is safe since OPCache will not optimize away explicit void casts.</p>

<h2>Where is it used?</h2>

<p>In addition to being usable in your own code, the <code>#[NoDiscard]</code> attribute is automatically applied to specific core PHP functions/methods, notably:</p>

<ul>
	<li><a href="https://www.php.net/flock" rel="noopener noreferrer" target="_blank"><code>flock()</code></a>: Ignoring its false return value can lead to difficult-to-diagnose concurrency issues.</li>
	<li>Setters of <a href="https://www.php.net/manual/en/class.datetimeimmutable.php" rel="noopener noreferrer" target="_blank"><code>DateTimeImmutable</code></a>: These methods do not modify the original instance but return a new one. Ignoring this return value does nothing, a common pitfall for new developers.</li>
</ul>

<h2>When is the Warning Triggered?</h2>

<p>PHP triggers the warning immediately before the function call executes, offering a significant safety benefit. If your application converts warnings into exceptions (as Symfony does by default), the potentially dangerous code never executes:</p>

<pre>
<code class="language-php">&lt;?php

set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});

$file = fopen("/tmp/test.txt", "r+");
flock($file, LOCK_EX);
// Safe to write to the file! Or is it? We don't know because we ignored the return value of flock()
fwrite($file, "Hello world!");
fclose($file);
</code></pre>

<p>In this example, the flock() is never called! The warning prevents the execution of potentially harmful code, ensuring issues are caught early during development.</p>

<h2>Constraints</h2>

<p>The use of <code>#[NoDiscard]</code> comes with some logical constraints:</p>

<ul>
	<li>It cannot be applied to functions with a <code>void</code> or <code>never</code> return type, as enforcing usage of a non-existent return value doesn&#39;t make sense.</li>
	<li>It cannot be used on property hooks (getters/setters), as reading a property inherently means you&#39;re working with its value. Ignoring it would lead to unnecessary confusion and complexity.</li>
</ul>

<hr />
<p>So, what do you think? I personally find the <code>#[NoDiscard]</code> attribute powerful and particularly valuable for library authors. I&#39;m eagerly awaiting PHP 8.5 to incorporate this new feature into my own projects!</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Creating a Simple Encrypted Matrix Bot in Go</title>
      <description><![CDATA[Creating a basic Matrix bot is straightforward—generate an access token, send an HTTP request, and you're done. But posting messages to an encrypted room with verified sessions is a bit more involved. This guide will walk you step-by-step through creating your own encrypted Matrix bot using Golang.]]></description>
      <pubDate>Tue, 24 Jun 2025 12:18:39 +0000</pubDate>
      <link>https://chrastecky.dev/programming/creating-a-simple-encrypted-matrix-bot-in-go</link>
      <guid isPermaLink="false">11</guid>
      <enclosure type="image/png" length="211277" url="https://chrastecky.dev/i/l/23/post_detail/GolangMatrixBot.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[Creating a basic Matrix bot is straightforward—generate an access token, send an HTTP request, and you're done. But posting messages to an encrypted room with verified sessions is a bit more involved. This guide will walk you step-by-step through creating your own encrypted Matrix bot using Golang.

<p>I&#39;ve created Matrix bots before, and sending simple unencrypted messages is so easy it doesn&#39;t even require a library. Typically, you&#39;d get your room ID, username, and password, then perform two HTTP requests: one for login, and one for sending the message.</p>

<p>But recently, I wanted to do things the proper way. We&#39;re migrating from Slack to Matrix for a project I&#39;m working on with some friends, and we&#39;ve decided that all rooms, including our server notifications channel, should be encrypted. This meant I had to find a suitable library with end-to-end encryption support in a language I&#39;m comfortable with. Eventually, I settled on <a href="https://github.com/mautrix/go" rel="noopener noreferrer" target="_blank">mautrix-go</a>.</p>

<h2>Setting Up Your Matrix Bot</h2>

<p>We&#39;ll create a straightforward proof-of-concept bot that logs in, sends a single message, and exits. Later, we&#39;ll enhance it by adding encryption support.</p>

<h3>Installation</h3>

<p>First, install the mautrix-go library:</p>

<pre>
<code>go get maunium.net/go/mautrix</code></pre>

<h3>Defining Constants</h3>

<p>We&#39;ll use some constants for simplicity in this example. Remember: <strong>never</strong> store sensitive credentials like this in production code.</p>

<pre>
<code class="language-go">const homeserver = "https://matrix.exapmle.com" // replace with your server
const username = "test_bot"
const password = "super-secret-cool-password"
const roomID = "!okfsAqlvVqyZZRgPWy:example.com"

const userId = ""
const accessToken = ""
const deviceId = ""
</code></pre>

<p>Initially, the user ID, access token, and device ID are empty because the bot needs to log in and retrieve these values. Usually, you&#39;d store them securely in a database or similar storage.</p>

<h3>Initializing the Client</h3>

<p>Now, let&#39;s create the Matrix client:</p>

<pre>
<code class="language-go">func main() {
	client, err := mautrix.NewClient(homeserver, userId, accessToken)
	if err != nil {
		panic(err)
	}
}</code></pre>

<h3>Logging In</h3>

<p>If your credentials aren&#39;t set, log in to obtain them:</p>

<pre>
<code class="language-go">    if deviceId == "" || userId == "" || accessToken == "" {
		resp, err := client.Login(context.Background(), &amp;mautrix.ReqLogin{
			Type: mautrix.AuthTypePassword,
			Identifier: mautrix.UserIdentifier{
				User: username,
				Type: mautrix.IdentifierTypeUser,
			},
			Password:         password,
			StoreCredentials: true,
		})
		if err != nil {
			panic(err)
		}

		log.Println(resp.DeviceID)
		log.Println(resp.AccessToken)
		log.Println(resp.UserID)

		return
	}</code></pre>

<p>The printed values will look something like this:</p>

<pre>
<code>2025/04/19 15:57:50 AQWFKLSBNJ
2025/04/19 15:57:50 syt_dgVzdF7ibFQ_GurkyhAWzEpTGgSBemjL_2JdxlO
2025/04/19 15:57:50 @test_bot:example.com</code></pre>

<p>Copy these values back into your constants.</p>

<h3>Sending an Unencrypted Message</h3>

<p>Now we can send a basic message:</p>

<pre>
<code class="language-go">	client.DeviceID = deviceId
	content := event.MessageEventContent{
		MsgType: event.MsgText,
		Body:    "Hello world from Go!",
	}

	_, err = client.SendMessageEvent(context.Background(), roomID, event.EventMessage, content)
	if err != nil {
		panic(err)
	}</code></pre>

<p>At this stage, your message will arrive in the Matrix room&mdash;but it&#39;s not encrypted yet:</p>

<p><img alt=" Screenshot of a Matrix room showing a message from “Test bot” saying “Hello world from Go!”, marked as not encrypted." src="https://chrastecky.dev/uploads/5/55/551a8ab836dd0106937e98a99cbbd689d5599794.png" style="height:132px; width:311px" /></p>

<p>Here&#39;s the full code so far:</p>

<pre>
<code class="language-go">import (
	"context"
	"log"
	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/event"
)

const homeserver = "https://matrix.exapmle.com" // replace with your server
const username = "test_bot"
const password = "super-secret-cool-password"
const roomID = "!okfsAqlvVqyZZRgPWy:example.com"

const userId = "@test_bot:example.com"
const accessToken = "syt_dgVzdF7ibFQ_GurkyhAWzEpTGgSBemjL_2JdxlO"
const deviceId = "AQWFKLSBNJ"

func main() {
	client, err := mautrix.NewClient(homeserver, userId, accessToken)
	if err != nil {
		panic(err)
	}

	if deviceId == "" || userId == "" || accessToken == "" {
		resp, err := client.Login(context.Background(), &amp;mautrix.ReqLogin{
			Type: mautrix.AuthTypePassword,
			Identifier: mautrix.UserIdentifier{
				User: username,
				Type: mautrix.IdentifierTypeUser,
			},
			Password:         password,
			StoreCredentials: true,
		})
		if err != nil {
			panic(err)
		}

		log.Println(resp.DeviceID)
		log.Println(resp.AccessToken)
		log.Println(resp.UserID)

		return
	}

	client.DeviceID = deviceId
	content := event.MessageEventContent{
		MsgType: event.MsgText,
		Body:    "Hello world from Go!",
	}

	_, err = client.SendMessageEvent(context.Background(), roomID, event.EventMessage, content)
	if err != nil {
		panic(err)
	}
}</code></pre>

<h2>Sending Encrypted Messages</h2>

<p>Encrypting messages involves syncing with the server and setting up cryptography, but don&#39;t worry&mdash;it&#39;s still quite straightforward. Let&#39;s see how easily this can be done using mautrix-go.</p>

<h3>Create a Cryptography Helper</h3>

<p>We&#39;ll first create a secure key (&quot;pickle key&quot;) and helper function. Make sure to keep this key completely secret and never share it publicly:</p>

<pre>
<code class="language-go">// note that the key doesn't have to be a string, you can directly generate random bytes and store them somewhere in a binary form
const pickleKeyString = "NnSHJguDSW7vtSshQJh2Yny4zQHc6Wyf"

func setupCryptoHelper(cli *mautrix.Client) (*cryptohelper.CryptoHelper, error) {
	// remember to use a secure key for the pickle key in production
	pickleKey := []byte(pickleKeyString)

	// this is a path to the SQLite database you will use to store various data about your bot
	dbPath := "crypto.db"

	helper, err := cryptohelper.NewCryptoHelper(cli, pickleKey, dbPath)
	if err != nil {
		return nil, err
	}

	// initialize the database and other stuff
	err = helper.Init(context.Background())
	if err != nil {
		return nil, err
	}

	return helper, nil
}</code></pre>

<h3>Syncing the Client</h3>

<p>First, we create the syncer and assign it to the client:</p>

<pre>
<code class="language-go">	syncer := mautrix.NewDefaultSyncer()
	client.Syncer = syncer
</code></pre>

<p>Then we create and assign the crypto helper:</p>

<pre>
<code class="language-go">	cryptoHelper, err := setupCryptoHelper(client)
	if err != nil {
		panic(err)
	}
	client.Crypto = cryptoHelper</code></pre>

<p>The syncer is needed to listen to events from synchronization, which is what we&#39;ll implement next:</p>

<pre>
<code class="language-go">	go func() {
		if err := client.Sync(); err != nil {
			panic(err)
		}
	}()</code></pre>

<p>The <code>Sync()</code> method is a blocking call and runs until an error occurs, so we run it in a goroutine. Now we&#39;ll use a channel to wait for the first event from the syncer to make sure everything&#39;s initialized:</p>

<pre>
<code class="language-go">    readyChan := make(chan bool)
	var once sync.Once
	syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
		once.Do(func() {
			close(readyChan)
		})

		return true
	})</code></pre>

<p>The <code>sync.Once</code> ensures the channel gets closed only once, even if multiple sync events come in in different threads. Finally, we wait for the first sync:</p>

<pre>
<code class="language-go">	log.Println("Waiting for sync to receive first event from the encrypted room...")
	&lt;-readyChan
	log.Println("Sync received")</code></pre>

<p>Now your client is ready to send encrypted messages! The full section we just created looks like this:</p>

<pre>
<code class="language-go">    readyChan := make(chan bool)
	var once sync.Once
	syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
		once.Do(func() {
			close(readyChan)
		})

		return true
	})

	go func() {
		if err := client.Sync(); err != nil {
			panic(err)
		}
	}()

	log.Println("Waiting for sync to receive first event from the encrypted room...")
	&lt;-readyChan
	log.Println("Sync received")</code></pre>

<p>And just to confirm everything worked, here&#39;s what the message looks like in the Matrix room:</p>

<p><img alt=" Screenshot of a Matrix room showing an encrypted message with a warning that the sending device hasn’t been verified." src="https://chrastecky.dev/uploads/0/03/03ae5cb2f1c51d2ff292053a80012072081181d1.png" style="height:91px; width:423px" /></p>

<p>As you can see, the message was encrypted successfully, but the session still isn&#39;t verified yet&mdash;hence the warning. We&#39;ll fix that next.</p>

<p>Here&#39;s the full source code so far:</p>

<pre>
<code class="language-go">import (
	"context"
	"log"
	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/crypto/cryptohelper"
	"maunium.net/go/mautrix/event"
	"sync"
)

const homeserver = "https://matrix.exapmle.com" // replace with your server
const username = "test_bot"
const password = "super-secret-cool-password"
const roomID = "!okfsAqlvVqyZZRgPWy:example.com"

const userId = "@test_bot:example.com"
const accessToken = "syt_dgVzdF7ibFQ_GurkyhAWzEpTGgSBemjL_2JdxlO"
const deviceId = "AQWFKLSBNJ"
const pickleKeyString = "NnSHJguDSW7vtSshQJh2Yny4zQHc6Wyf"

func setupCryptoHelper(cli *mautrix.Client) (*cryptohelper.CryptoHelper, error) {
	// remember to use a secure key for the pickle key in production
	pickleKey := []byte(pickleKeyString)

	// this is a path to the SQLite database you will use to store various data about your bot
	dbPath := "crypto.db"

	helper, err := cryptohelper.NewCryptoHelper(cli, pickleKey, dbPath)
	if err != nil {
		return nil, err
	}

	// initialize the database and other stuff
	err = helper.Init(context.Background())
	if err != nil {
		return nil, err
	}

	return helper, nil
}

func main() {
	client, err := mautrix.NewClient(homeserver, userId, accessToken)
	if err != nil {
		panic(err)
	}

	if deviceId == "" || userId == "" || accessToken == "" {
		resp, err := client.Login(context.Background(), &amp;mautrix.ReqLogin{
			Type: mautrix.AuthTypePassword,
			Identifier: mautrix.UserIdentifier{
				User: username,
				Type: mautrix.IdentifierTypeUser,
			},
			Password:         password,
			StoreCredentials: true,
		})
		if err != nil {
			panic(err)
		}

		log.Println(resp.DeviceID)
		log.Println(resp.AccessToken)
		log.Println(resp.UserID)

		return
	}
	client.DeviceID = deviceId

	syncer := mautrix.NewDefaultSyncer()
	client.Syncer = syncer

	cryptoHelper, err := setupCryptoHelper(client)
	if err != nil {
		panic(err)
	}
	client.Crypto = cryptoHelper

	readyChan := make(chan bool)
	var once sync.Once
	syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
		once.Do(func() {
			close(readyChan)
		})

		return true
	})

	go func() {
		if err := client.Sync(); err != nil {
			panic(err)
		}
	}()

	log.Println("Waiting for sync to receive first event from the encrypted room...")
	&lt;-readyChan
	log.Println("Sync received")

	content := event.MessageEventContent{
		MsgType: event.MsgText,
		Body:    "Hello world from Go!",
	}

	_, err = client.SendMessageEvent(context.Background(), roomID, event.EventMessage, content)
	if err != nil {
		panic(err)
	}
}</code></pre>

<h2>Verifying the Session</h2>

<p>For verified encryption, you&#39;ll need a recovery key (obtainable via Element). Store it securely. I have to admit, this part wasn&#39;t as intuitive for me&mdash;I had to look at some existing projects because it dives a bit deeper into Matrix internals than I usually go. Still, the method names are quite descriptive, so even without deep knowledge, it&#39;s not too hard to follow:</p>

<pre>
<code class="language-go">const recoveryKey = "EsUF NQce e4BW teUM Kf7W iZqD Nj3f 56qj GuN5 s3aw aut7 div2"
</code></pre>

<p>Just like the pickle key, the recovery key should be treated as <strong>highly sensitive</strong>&mdash;do not share or hardcode it in production environments.</p>

<p>Then, create this helper function:</p>

<pre>
<code class="language-go">func verifyWithRecoveryKey(machine *crypto.OlmMachine) (err error) {
	ctx := context.Background()

	keyId, keyData, err := machine.SSSS.GetDefaultKeyData(ctx)
	if err != nil {
		return
	}
	key, err := keyData.VerifyRecoveryKey(keyId, recoveryKey)
	if err != nil {
		return
	}
	err = machine.FetchCrossSigningKeysFromSSSS(ctx, key)
	if err != nil {
		return
	}
	err = machine.SignOwnDevice(ctx, machine.OwnIdentity())
	if err != nil {
		return
	}
	err = machine.SignOwnMasterKey(ctx)

	return
}</code></pre>

<p>Call this function after synchronization&mdash;back in the <code>main()</code> function:</p>

<pre>
<code class="language-go">	err = verifyWithRecoveryKey(cryptoHelper.Machine())
	if err != nil {
		panic(err)
	}</code></pre>

<p>Now, your messages will be encrypted, verified, and free of security warnings.</p>

<p>And just to confirm, here&#39;s what that looks like in the Matrix room&mdash;notice that the warning icon is gone:</p>

<p><img alt=" Screenshot of the same message in a Matrix room, now encrypted and verified with no warning displayed." src="https://chrastecky.dev/uploads/9/98/982cf88f5b8a95d1d62a6d201dee61e2929dd1a9.png" style="height:104px; width:328px" /></p>

<p>Here&#39;s the full source code:</p>

<pre>
<code class="language-go">import (
	"context"
	"log"
	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/crypto"
	"maunium.net/go/mautrix/crypto/cryptohelper"
	"maunium.net/go/mautrix/event"
	"sync"
)

const homeserver = "https://matrix.exapmle.com" // replace with your server
const username = "test_bot"
const password = "super-secret-cool-password"
const roomID = "!okfsAqlvVqyZZRgPWy:example.com"

const userId = "@test_bot:example.com"
const accessToken = "syt_dgVzdF7ibFQ_GurkyhAWzEpTGgSBemjL_2JdxlO"
const deviceId = "AQWFKLSBNJ"
const pickleKeyString = "NnSHJguDSW7vtSshQJh2Yny4zQHc6Wyf"

func setupCryptoHelper(cli *mautrix.Client) (*cryptohelper.CryptoHelper, error) {
	// remember to use a secure key for the pickle key in production
	pickleKey := []byte(pickleKeyString)

	// this is a path to the SQLite database you will use to store various data about your bot
	dbPath := "crypto.db"

	helper, err := cryptohelper.NewCryptoHelper(cli, pickleKey, dbPath)
	if err != nil {
		return nil, err
	}

	// initialize the database and other stuff
	err = helper.Init(context.Background())
	if err != nil {
		return nil, err
	}

	return helper, nil
}

func verifyWithRecoveryKey(machine *crypto.OlmMachine) (err error) {
	ctx := context.Background()

	keyId, keyData, err := machine.SSSS.GetDefaultKeyData(ctx)
	if err != nil {
		return
	}
	key, err := keyData.VerifyRecoveryKey(keyId, recoveryKey)
	if err != nil {
		return
	}
	err = machine.FetchCrossSigningKeysFromSSSS(ctx, key)
	if err != nil {
		return
	}
	err = machine.SignOwnDevice(ctx, machine.OwnIdentity())
	if err != nil {
		return
	}
	err = machine.SignOwnMasterKey(ctx)

	return
}

func main() {
	client, err := mautrix.NewClient(homeserver, userId, accessToken)
	if err != nil {
		panic(err)
	}

	if deviceId == "" || userId == "" || accessToken == "" {
		resp, err := client.Login(context.Background(), &amp;mautrix.ReqLogin{
			Type: mautrix.AuthTypePassword,
			Identifier: mautrix.UserIdentifier{
				User: username,
				Type: mautrix.IdentifierTypeUser,
			},
			Password:         password,
			StoreCredentials: true,
		})
		if err != nil {
			panic(err)
		}

		log.Println(resp.DeviceID)
		log.Println(resp.AccessToken)
		log.Println(resp.UserID)

		return
	}
	client.DeviceID = deviceId

	syncer := mautrix.NewDefaultSyncer()
	client.Syncer = syncer

	cryptoHelper, err := setupCryptoHelper(client)
	if err != nil {
		panic(err)
	}
	client.Crypto = cryptoHelper

	readyChan := make(chan bool)
	var once sync.Once
	syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
		once.Do(func() {
			close(readyChan)
		})

		return true
	})

	go func() {
		if err := client.Sync(); err != nil {
			panic(err)
		}
	}()

	log.Println("Waiting for sync to receive first event from the encrypted room...")
	&lt;-readyChan
	log.Println("Sync received")

	err = verifyWithRecoveryKey(cryptoHelper.Machine())
	if err != nil {
		panic(err)
	}

	content := event.MessageEventContent{
		MsgType: event.MsgText,
		Body:    "Hello world from Go!",
	}

	_, err = client.SendMessageEvent(context.Background(), roomID, event.EventMessage, content)
	if err != nil {
		panic(err)
	}
}</code></pre>

<h2>Conclusion</h2>

<p>With this approach, your Matrix bot securely communicates within encrypted rooms. Remember to securely store credentials, use secure keys, and manage device verification properly in production. Happy coding!</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>ActivityPub: The Good, the Bad and the Ugly</title>
      <description><![CDATA[ActivityPub is the best protocol available for interoperable federated social media—primarily because no other current federated protocols target social media, offer interoperability, and aren't deprecated.]]></description>
      <pubDate>Sun, 18 May 2025 09:45:27 +0000</pubDate>
      <link>https://chrastecky.dev/technology/activity-pub-the-good-the-bad-and-the-ugly</link>
      <guid isPermaLink="false">10</guid>
      <enclosure type="image/png" length="2335212" url="https://chrastecky.dev/i/l/19/post_detail/ActivityPub.png"/>
      <category><![CDATA[Technology]]></category>
      <content:encoded><![CDATA[ActivityPub is the best protocol available for interoperable federated social media—primarily because no other current federated protocols target social media, offer interoperability, and aren't deprecated.

<p>I know that title might seem controversial, so let&#39;s dive in. Right now, here&rsquo;s the landscape of federated protocols:</p>

<ul>
	<li><strong>ActivityPub</strong> - The shining star of this article.</li>
	<li><strong>OStatus</strong> - Mostly deprecated in favour of ActivityPub.</li>
	<li><strong>Diaspora</strong> - Exclusive to Diaspora, limiting interoperability.</li>
	<li><strong>Nostr</strong> - Infamous due to problematic user behaviour, unlikely to achieve significant interoperability.</li>
	<li><strong>Matrix</strong> - Could theoretically support social media use-cases but currently doesn&#39;t.</li>
	<li><strong>ATProto</strong> - Technically promising, yet hampered by corporate handling and mistrust from the open-source community.</li>
</ul>

<p>Ultimately, that leaves us with <a href="https://www.w3.org/TR/activitypub/" rel="noopener noreferrer" target="_blank">ActivityPub</a>. Everything else either lacks widespread adoption, doesn&rsquo;t support common social media scenarios, or is effectively proprietary despite being open-source. For those who prioritize open-source solutions, ActivityPub is essentially the only viable option.</p>

<h2>The Good</h2>

<p>While I&rsquo;m about to critique ActivityPub extensively, it undeniably has strong points:</p>

<ul>
	<li><strong>Interoperability</strong>: The core idea is genuinely powerful. Using your Mastodon account to comment on a Lemmy post&mdash;or even reading this blog post on your preferred instance&mdash;is genuinely amazing. Different instances can display content uniquely, allowing users to interact in a way tailored to their platform.</li>
	<li><strong>Human-readable JSON</strong>: While some might underestimate this, JSON&#39;s human readability makes debugging and understanding ActivityPub interactions straightforward.</li>
	<li><strong>Extensible</strong>: Custom properties and types can extend functionality beyond initial design limitations, ensuring future flexibility.</li>
</ul>

<h2>The Bad</h2>

<p>Most issues with ActivityPub stem from one critical flaw: much of its behaviour is undefined. The core types and activities allow interactions that make little practical sense. For instance, the <code>Like</code> activity should be simple&mdash;you like something. But, in ActivityPub, you can &quot;like&quot; another <code>Like</code> activity, creating infinite loops of nonsensical interactions.</p>

<p>This flexibility leads to a significant problem: no two implementations behave identically. Developers resort to hacks and guesswork to interpret undefined behaviour. Ideally, ActivityPub would strictly define interactions for core types, enabling implementations (Mastodon, Lemmy, Pleroma, etc.) to focus solely on presentation or extending functionality, knowing basic interactions remain consistent across platforms.</p>

<p>A practical example is the confusion around private messages. Two competing methods have emerged: a custom <code>ChatMessage</code> type not officially supported by ActivityPub (used by Lemmy, Pleroma and others), and an alternate &quot;standard&quot; using a <code>Note</code> object that excludes the public audience but explicitly mentions recipients (used by Mastodon and others). This ambiguity creates compatibility nightmares.</p>

<p>Another example I personally encountered was a frustrating issue while implementing ActivityPub for this blog: updating a post propagated to Lemmy but not Mastodon. Despite the <code>Update</code> activity being accepted, Mastodon silently rejected it unless the <code>updated</code> timestamp changed&mdash;a logical but unofficial requirement. Developers must track down subtle implementation details that aren&#39;t formally documented, significantly complicating adoption and usage.</p>

<h2>The Ugly</h2>

<p>Privacy is virtually non-existent. When another server federates with yours, it receives all public activities, which might seem harmless initially. However, what happens if you mistakenly share sensitive information publicly? In theory, deleting a post propagates across the network, but real-world scenarios vary greatly&mdash;from technical glitches to outright malicious actors ignoring delete requests. Ensuring robust privacy requires substantial protocol-level changes, such as introducing end-to-end encryption&mdash;something notoriously complex to implement, as evidenced by Matrix&rsquo;s struggles.</p>

<p>Another significant flaw is impersonation vulnerability. ActivityPub itself has no built-in authentication mechanism, meaning anyone could theoretically impersonate any user. Although most implementations use the <a href="https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures" rel="noopener noreferrer" target="_blank">HTTP Signatures</a> standard to address this, ActivityPub itself remains incomplete in terms of essential security features. The standard openly acknowledges:</p>

<blockquote>
<p>Unfortunately at the time of standardization, there are no strongly agreed upon mechanisms for authentication.</p>
</blockquote>

<h2>Conclusion</h2>

<p>ActivityPub, particularly its vocabulary rules (<a href="https://www.w3.org/TR/activitystreams-core/" rel="noopener noreferrer" target="_blank">ActivityStreams</a>), remains a half-finished protocol. Its effectiveness depends heavily on individual implementation choices, creating problematic discrepancies&mdash;such as the inability to reliably send private messages between Mastodon and Lemmy users. Moreover, simple human errors or software oversights can unintentionally expose private information, as recently demonstrated when new Fediverse software mishandled Mastodon-style private messages and displayed them publicly.</p>

<p>The solution? ActivityPub needs a clearly defined second iteration&mdash;an ActivityPub v2&mdash;that eliminates ambiguity, standardizes behaviour strictly, and provides essential security measures. Certain issues, especially privacy, may never be fully resolved within the protocol, but increased clarity and stricter rules would significantly mitigate existing risks.</p>

<p>This doesn&rsquo;t mean we should abandon ActivityPub, but rather, we must work collectively to standardize it further, making it more secure and less error-prone.</p>

<p>What are your thoughts on ActivityPub? Have you developed something using it? Are you planning to? Let me know in the comments!</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Static Typing for the AWS SDK for PHP</title>
      <description><![CDATA[If you’ve ever worked with both PHPStan and the official AWS SDK for PHP, you know they don’t get along particularly well. In this article, I’ll show you a package I created that addresses this exact issue.]]></description>
      <pubDate>Thu, 13 Mar 2025 00:33:24 +0000</pubDate>
      <link>https://chrastecky.dev/programming/static-typing-for-the-aws-sdk-for-php</link>
      <guid isPermaLink="false">9</guid>
      <enclosure type="image/png" length="31805" url="https://chrastecky.dev/i/l/18/post_detail/PHPStan%20AWS.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[If you’ve ever worked with both PHPStan and the official AWS SDK for PHP, you know they don’t get along particularly well. In this article, I’ll show you a package I created that addresses this exact issue.

<blockquote>
<p>If you just want to install it without reading the whole article, you can install it via Composer: <a href="https://packagist.org/packages/rikudou/aws-sdk-phpstan" rel="noopener noreferrer" target="_blank">rikudou/aws-sdk-phpstan</a>.</p>
</blockquote>

<p>When using PHPStan alongside AWS, you end up making a lot of type assertions, because the automatically generated AWS SDK doesn&rsquo;t strictly define types, so everything defaults to <code>mixed</code>. Fortunately, the SDK package includes the source data used to generate itself, so I reused that information to create a PHPStan extension. This extension provides precise type definitions, catches all type-related errors for return types, and allows you to remove those otherwise unnecessary type assertions.</p>

<h2>How It&rsquo;s Made</h2>

<p>As mentioned earlier, if all you want is to install and use the package, you don&rsquo;t really need this article. But if you want a closer look at how it works, read on.</p>

<p>The first step was to make the <code>Result</code> class (which is returned by all API calls) generic by providing a custom stub&mdash;particularly for its <code>get()</code> method:</p>

<pre>
<code class="language-php">/**
 * @template T of array&lt;string, mixed&gt;
 * @implements ResultInterface&lt;T&gt;
 */
class Result implements ResultInterface, MonitoringEventsInterface
{
    /**
     * @template TKey of (key-of&lt;T&gt;|string)
     * @param TKey $key
     * @return (TKey is key-of&lt;T&gt; ? T[TKey] : null)
     */
    public function get(string $key): mixed {}
}</code></pre>

<p>The class itself is generic, constrained to an array. The <code>get()</code> method is also made generic based on the key. If the key is a known key of <code>T</code>, the method returns its corresponding value; otherwise, it returns <code>null</code>. Essentially, if the response type expects the property, it&rsquo;s returned&mdash;if not, <code>null</code> is returned.</p>

<h2>The Individual Clients</h2>

<p>All the client classes are generated from the type definitions in the <a href="https://github.com/aws/aws-sdk-php/tree/master/src/data" rel="noopener noreferrer" target="_blank">src/data directory</a> of the official AWS SDK for PHP. Each client&rsquo;s definitions come in two files, such as:</p>

<ul>
	<li><a href="https://github.com/aws/aws-sdk-php/blob/master/src/data/s3/2006-03-01/api-2.json" rel="noopener noreferrer" target="_blank">api-2.json</a></li>
	<li><a href="https://github.com/aws/aws-sdk-php/blob/master/src/data/s3/2006-03-01/api-2.json.php" rel="noopener noreferrer" target="_blank">api-2.json.php</a></li>
</ul>

<p>(These map one-to-one with the PHP client methods. You&rsquo;ll notice that the actual PHP client just uses <code>__call()</code> for these methods.)</p>

<p>For example, in the JSON definition for S3Client, you might see:</p>

<pre>
<code class="language-json">{
    "GetObject":{
      "name":"GetObject",
      "http":{
        "method":"GET",
        "requestUri":"/{Bucket}/{Key+}"
      },
      "input":{"shape":"GetObjectRequest"},
      "output":{"shape":"GetObjectOutput"},
      "errors":[
        {"shape":"NoSuchKey"},
        {"shape":"InvalidObjectState"}
      ],
      "documentationUrl":"http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectGET.html",
      "httpChecksum":{
        "requestValidationModeMember":"ChecksumMode",
        "responseAlgorithms":[
          "CRC64NVME",
          "CRC32",
          "CRC32C",
          "SHA256",
          "SHA1"
        ]
      }
    }
}</code></pre>

<p>And in PHP:</p>

<pre>
<code class="language-php">use Aws\S3\S3Client;

$client = new S3Client([]);
$object = $client-&gt;getObject([
    'Bucket' =&gt; 'test',
    'Key' =&gt; 'test',
]);
</code></pre>

<p>In reality, these methods don&rsquo;t actually exist in the client class; they&rsquo;re invoked through <code>__call()</code> under the hood.</p>

<p>Going back to the JSON definitions, each operation has an <code>input</code> and an <code>output</code> shape. The package currently only focuses on the <code>output</code> shape, although I plan to add <code>input</code> shape support in the future. For the <code>GetObjectOutput</code>, the relevant shape might be:</p>

<pre>
<code class="language-json">{
    "GetObjectOutput":{
      "type":"structure",
      "members":{
        "Body":{
          "shape":"Body",
          "streaming":true
        },
        "DeleteMarker":{
          "shape":"DeleteMarker",
          "location":"header",
          "locationName":"x-amz-delete-marker"
        },
        "AcceptRanges":{
          "shape":"AcceptRanges",
          "location":"header",
          "locationName":"accept-ranges"
        },
        "Expiration":{
          "shape":"Expiration",
          "location":"header",
          "locationName":"x-amz-expiration"
        },
        "Restore":{
          "shape":"Restore",
          "location":"header",
          "locationName":"x-amz-restore"
        },
        "LastModified":{
          "shape":"LastModified",
          "location":"header",
          "locationName":"Last-Modified"
        }
    }
}</code></pre>

<blockquote>
<p><em>(Note: The actual shape is larger, but I&rsquo;ve omitted some fields to keep this article shorter.)</em></p>
</blockquote>

<h2>Generating Type Extensions</h2>

<p>PHPStan lets you add extensions that generate return types based on the method call and its input parameters. I decided to take that approach, though there are other possibilities (such as generating a stub file for each client).</p>

<p>For every client, a class like <code>{ShortAwsClientClassName}TypeExtension</code> is generated, for example, <a href="https://github.com/RikudouSage/AwsSdkPhpstan/blob/master/src/Types/S3ClientReturnTypeExtension.php" rel="noopener noreferrer" target="_blank">S3ClientReturnTypeExtension</a>. The <code>isMethodSupported()</code> method just checks if the method name matches one of the operations defined in the JSON file. Then there&rsquo;s a <code>getTypeFromMethodCall()</code> method that uses a <code>match</code> expression to call a private method of the same name.</p>

<p>Those private methods return PHPStan types derived from the shapes in the JSON data. The generator supports lists, maps, nested structures, binary blobs, date/time objects, enums, and simple types (strings, booleans, integers, floats), including unions, nested arrays, and more.</p>

<p>If you want to dive into the code:</p>

<ul>
	<li><a href="https://github.com/RikudouSage/AwsSdkPhpstan/blob/master/src/Generator/TypeExtensionGenerator.php#L225" rel="noopener noreferrer" target="_blank">The method that generates each API operation</a></li>
	<li><a href="https://github.com/RikudouSage/AwsSdkPhpstan/blob/master/src/Generator/TypeExtensionGenerator.php#L271" rel="noopener noreferrer" target="_blank">The code that transforms each shape into a PHP type</a></li>
</ul>

<p>As a final touch, the <code>extension.neon</code> file (which registers extensions with PHPStan) is populated automatically with each generated class.</p>

<h2>Performance</h2>

<p>The performance isn&rsquo;t ideal if you include every single client class by default&mdash;which is understandable considering there are around 400 classes, each containing thousands of lines. Most projects likely won&rsquo;t use all 400 AWS services in one codebase. That&rsquo;s why I provided a generation script as a Composer binary, along with support for specifying only the clients you need by updating your <code>composer.json</code>:</p>

<pre>
<code class="language-json">{
  "extra": {
    "aws-sdk-phpstan": {
      "only": [
        "Aws\\S3\\S3Client",
        "Aws\\CloudFront\\CloudFrontClient"
      ]
    }
  }
}</code></pre>

<p>After that, run <code>vendor/bin/generate-aws-phpstan</code> to regenerate the classes. The script deletes all existing type extensions first, then generates only for <code>S3Client</code> and <code>CloudFrontClient</code>. It also updates the <code>extensions.neon</code> file with just those extensions. With only a few extensions active, there&rsquo;s no noticeable slowdown in PHPStan.</p>

<p>You can also leverage Composer&#39;s script events to run the binary automatically:</p>

<pre>
<code class="language-json">{
  "extra": {
    "aws-sdk-phpstan": {
      "only": [
        "Aws\\S3\\S3Client",
        "Aws\\CloudFront\\CloudFrontClient"
      ]
    }
  },
  "scripts": {
    "post-install-cmd": [
      "generate-aws-phpstan"
    ],
    "post-update-cmd": [
      "generate-aws-phpstan"
    ]
  }
}</code></pre>

<p>Ideally, the official SDK itself would include type definitions (it shouldn&rsquo;t be too difficult, given they already generate the SDK from these JSON files). In the meantime, though, I&rsquo;m pretty happy with how this little project turned out.</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Transpiling PHP for older versions</title>
      <description><![CDATA[Transpiling lets you write your package or application using the latest PHP features—even if your runtime is older. This is especially useful for libraries (where you can’t just bump the PHP version without inconveniencing your users) and can be a lifesaver when upgrading legacy apps.]]></description>
      <pubDate>Sun, 23 Feb 2025 20:02:44 +0000</pubDate>
      <link>https://chrastecky.dev/programming/transpiling-php-for-older-versions</link>
      <guid isPermaLink="false">3</guid>
      <enclosure type="image/png" length="210499" url="https://chrastecky.dev/i/l/6/post_detail/Transpiling.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[Transpiling lets you write your package or application using the latest PHP features—even if your runtime is older. This is especially useful for libraries (where you can’t just bump the PHP version without inconveniencing your users) and can be a lifesaver when upgrading legacy apps.

<h2>The Problem</h2>

<p>Every developer wants to use the latest and greatest features of their tools, and PHP is no exception. But sometimes you simply can&rsquo;t upgrade&mdash;whether because of project constraints or because your users are still on an older PHP version. For instance, if you&rsquo;re building a library, you&rsquo;ll often need to target a version that&rsquo;s a few releases behind the latest, so you&rsquo;re not forcing your users to upgrade before they&rsquo;re ready.</p>

<h2>The Solution</h2>

<p>Transpiling! Instead of writing code that only works on a modern PHP version, you write it using the newest features and then transpile it down to your target PHP version. One of the best tools for this job is <a href="https://github.com/rectorphp/rector" rel="noopener noreferrer" target="_blank">Rector</a>. You might know Rector as the tool that automatically upgrades your code to a newer version of PHP&mdash;but it works in reverse as well. Downgrading is just as easy. For example, to downgrade your code to PHP 7.4, your <code>rector.php</code> file can be as simple as this:</p>

<pre>
<code class="language-php">&lt;?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    -&gt;withPaths([
        __DIR__ . '/src',
    ])
    -&gt;withDowngradeSets(php74: true)
;
</code></pre>

<p>Now, simply run Rector as you normally would (for example, <code>vendor/bin/rector process</code>), and you&rsquo;re all set..</p>

<p>As an example, here&rsquo;s a class that uses many modern PHP features:</p>

<pre>
<code class="language-php">final readonly class ModernClass
{
    final protected const string TYPED_FINAL_CONSTANT = 'some-string';

    public function __construct(
        public int $promotedProperty,
        private stdClass $data = new stdClass(),
    ) {
        // new without parenthesis
        $selfName = new ReflectionClass($this)-&gt;getName();
        // named parameters and the new rounding mode enum
        $rounded = round(5.5, mode: RoundingMode::HalfTowardsZero);

        // previously those functions only worked with Traversable instances, in PHP 8.2 they work with both Traversable and array instances
        $array = [1, 2, 3];
        $count = iterator_count($array);
        $array = iterator_to_array($array);

        $callable = $this-&gt;methodThatReturnsNever(...);
        $callable();
    }

    private function methodThatReturnsNever(): never
    {
        throw new Exception();
    }

    // standalone false/true/null type
    public function returnTrue(): true
    {
        return true;
    }
    public function returnFalse(): false
    {
        return false;
    }
    public function returnNull(): null
    {
        return null;
    }
}
</code></pre>

<p>And here&rsquo;s what it looks like after downgrading:</p>

<pre>
<code class="language-php">final class ModernClass
{
    /**
     * @readonly
     */
    public int $promotedProperty;
    /**
     * @readonly
     */
    private stdClass $data;
    /**
     * @var string
     */
    protected const TYPED_FINAL_CONSTANT = 'some-string';

    public function __construct(
        int $promotedProperty,
        ?stdClass $data = null
    ) {
        $data ??= new stdClass();
        $this-&gt;promotedProperty = $promotedProperty;
        $this-&gt;data = $data;
        // new without parenthesis
        $selfName = (new ReflectionClass($this))-&gt;getName();
        // named parameters and the new rounding mode enum
        $rounded = round(5.5, 0, \PHP_ROUND_HALF_DOWN);

        // previously those functions only worked with Traversable instances, in PHP 8.2 they work with both Traversable and array instances
        $array = [1, 2, 3];
        $count = iterator_count(is_array($array) ? new \ArrayIterator($array) : $array);
        $array = iterator_to_array(is_array($array) ? new \ArrayIterator($array) : $array);

        $callable = \Closure::fromCallable([$this, 'methodThatReturnsNever']);
        $callable();
    }

    /**
     * @return never
     */
    private function methodThatReturnsNever()
    {
        throw new Exception();
    }

    // standalone false/true/null type
    /**
     * @return true
     */
    public function returnTrue(): bool
    {
        return true;
    }
    /**
     * @return false
     */
    public function returnFalse(): bool
    {
        return false;
    }
    /**
     * @return null
     */
    public function returnNull()
    {
        return null;
    }
}</code></pre>

<p>This is now a perfectly valid PHP 7.4 class. It&rsquo;s amazing to see how much PHP has evolved since 7.4&mdash;not to mention compared to the old 5.x days. I personally can&rsquo;t live without property promotion anymore.</p>

<blockquote>
<p><strong>Note:</strong> Not every piece of modern PHP code can be downgraded automatically. For example, Rector leaves the following property definitions unchanged:</p>

<pre>
<code class="language-php">    public bool $hooked {
        get =&gt; $this-&gt;hooked;
    }
    public private(set) bool $asymmetric = true;</code></pre>

<p>I assume support for downgrading asymmetric visibility will eventually be added, but hooked properties are very hard to downgrade in general&mdash;even though in some specialized cases they could be converted to readonly properties.</p>
</blockquote>

<h2>Downgrading Your Composer Package</h2>

<p>If you want to write your package using modern PHP features but still support older PHP versions, you need a way to let Composer know which version to install. One simple approach would be to publish a separate package for each PHP version&mdash;say, the main package as <code>vendor/package</code> and additional ones like <code>vendor/package-82</code>, <code>vendor/package-74</code>, etc. While this works, it has a drawback. For instance, if you&rsquo;re on PHP 8.3 and later upgrade your main package to PHP 8.4, you&rsquo;d have to force users to switch to a new package (say, <code>vendor/package-83</code>), rendering the package incompatible for anyone still on an older PHP version.</p>

<p>Instead, I leverage two behaviors of Composer:</p>

<ol>
	<li>It always tries to install the newest version that matches your version constraints.</li>
	<li>It picks the latest version that is supported by the current environment.</li>
</ol>

<p>This means you can add a suffix to each transpiled version. For version 1.2.0, you might have:</p>

<ul>
	<li>1.2.084 (for PHP 8.4)</li>
	<li>1.2.083 (for PHP 8.3)</li>
	<li>1.2.082 (for PHP 8.2)</li>
	<li>1.2.081 (for PHP 8.1)</li>
	<li>1.2.080 (for PHP 8.0)</li>
	<li>1.2.074 (for PHP 7.4)</li>
</ul>

<p>When someone runs <code>composer require vendor/package</code>, Composer will select the version with the highest version number that is compatible with their PHP runtime. So, a user on PHP 8.4 gets 1.2.084, while one on PHP 8.2 gets 1.2.082. If you use the caret (<code>^</code>) or greater-than-or-equal (<code>&gt;=</code>) operator in your <code>composer.json</code>, you also future-proof your package: if someone with a hypothetical PHP 8.5 tries to install it, they&rsquo;ll still get the highest compatible version (in this case, 1.2.084).</p>

<p>Of course, you&rsquo;ll need to run the transpilation before each release and automatically update your <code>composer.json</code> file. For older PHP versions, you might also have to make additional adjustments. In one package I worked on, I had to include extra polyfills for PHP 7.2 and even downgrade PHPUnit&mdash;but overall, the process works really well.</p>

<p>You can see this approach in action in the <a href="https://github.com/Unleash/unleash-client-php" rel="noopener" target="_new">Unleash PHP SDK</a>. More specifically, check out <a href="https://github.com/Unleash/unleash-client-php/blob/main/.github/workflows/release.yaml#L40" rel="noopener" target="_new">this workflow file</a> and, for example, <a href="https://github.com/Unleash/unleash-client-php/commit/9921a1e6529c701d7df520574700de11cc379d48" rel="noopener" target="_new">this commit</a> which shows all the changes involved when transpiling code from PHP 8.3 down to PHP 7.2.</p>

<blockquote>
<p><strong>Caveat:</strong> One important downside of this approach is that if a user installs the package in an environment that initially has a newer PHP version than the one where the code will eventually run (or where dependencies will be installed), Composer might install a version of the package that the actual runtime cannot handle.</p>
</blockquote>

<p>I believe this approach offers the best of both worlds when writing packages. You get to enjoy all the modern PHP features (I can&rsquo;t live without constructor property promotion, and public readonly properties are fantastic for writing simple DTOs), while still supporting users who aren&rsquo;t able&mdash;or ready&mdash;to upgrade immediately.</p>

<p>It&rsquo;s also a powerful tool if your development team can upgrade PHP versions faster than your server administrators. You can write your app using the latest syntax and features, and then transpile it to work on the servers that are actually in use.</p>

<p>So, what do you think? Is this an approach you or your team might consider?</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>OpenSCAD configurable calendar 3D model</title>
      <description><![CDATA[I love creating configurable generic models using OpenSCAD, and this is the most complex one I've created to date—an easy-to-configure calendar using some clever algorithms to render correctly.]]></description>
      <pubDate>Mon, 24 Mar 2025 22:06:19 +0000</pubDate>
      <link>https://chrastecky.dev/3d-printing/open-scad-configurable-calendar-3d-model</link>
      <guid isPermaLink="false">8</guid>
      <enclosure type="image/png" length="167028" url="https://chrastecky.dev/i/l/13/post_detail/Calendar.png"/>
      <category><![CDATA[3D printing]]></category>
      <content:encoded><![CDATA[I love creating configurable generic models using OpenSCAD, and this is the most complex one I've created to date—an easy-to-configure calendar using some clever algorithms to render correctly.

<p>OpenSCAD is truly amazing in a way that no other 3D modeling software is, including those with limited scripting abilities.</p>

<p>You can implement standard algorithms from general-purpose languages, like the impressive <a href="https://en.wikipedia.org/wiki/Zeller%27s_congruence" rel="noopener noreferrer" target="_blank">Zeller&#39;s Congruence</a> used to calculate the day of the week for any given date. I utilized this to make the calendar automatically adjust the date offset. Simply change the year number in the configurator, and the model remains accurate:</p>

<p><a href="/i/o/14/calendar-year-2025.png" target="_blank"><img alt="A calendar model screenshot for year 2025, showing that the Jan 1st is a Wednesday" src="https://chrastecky.dev/i/l/14/post_detail/calendar-year-2025.png" style="height:494px; width:1152px" /></a></p>

<p>According to my computer, Jan 1st, 2025, is indeed a Wednesday.</p>

<p><a href="/i/o/15/calendar-year-2056.png" target="_blank"><img alt="A screenshot of a 3D model showing that Jan 1st 2056 is a Saturday" src="https://chrastecky.dev/i/l/15/post_detail/calendar-year-2056.png" style="height:486px; width:1152px" /></a></p>

<p>A quick calendar check confirms that Jan 1st, 2056, is a Saturday!</p>

<p>Here&rsquo;s the OpenSCAD function:</p>

<pre>
<code class="language-openscad">function getFirstDay(year, month, day = 1) = 
    let (
        q = day,
        m = month &lt; 3 ? month + 12 : month,
        adjusted_year = month &lt; 3 ? year - 1 : year,
        K = (adjusted_year) % 100,
        J = floor((adjusted_year) / 100)
    )
    (
        let (
            h = (q + floor((13 * (m + 1)) / 5) + K + floor(K / 4) + floor(J / 4) + 5 * J) % 7
        )
        ((h + 5) % 7) + 1
    );</code></pre>

<p>I kept the variable names consistent with the Wikipedia page for easier verification.</p>

<p>Additionally, I included a generic leap year check and a function to get the correct number of days in a month:</p>

<pre>
<code class="language-openscad">function daysAmount(month) = month == 2 
    ? (year % 4 == 0 &amp;&amp; (year % 400 == 0 || year % 100 != 0)) ? 29 : 28 
    : (month % 2 == 0 ? (month &gt;= 8 ? 31 : 30) : (month &gt;= 8 ? 30 : 31));</code></pre>

<p>Working with dates is always a &ldquo;pleasure,&rdquo; but doing so in a language with no built-in date support was especially interesting!</p>

<p>This project is highly user-friendly with multiple configurable options, including:</p>

<ul>
	<li>Selection of months to render, column layout, and layer height adjustments for multi-material printing.</li>
	<li>Custom holiday markings, such as highlighting Saturdays in red and adding holidays through a comma-separated list.</li>
	<li>Full translation support for titles, month names, and day names.</li>
	<li>Configurable holes for magnets and screws to mount on fridges or walls.</li>
</ul>

<p>Some options leverage libraries like <a href="https://github.com/JustinSDK/dotSCAD" rel="noopener noreferrer" target="_blank">JustinSDK/dotSCAD</a> and <a href="https://github.com/davidson16807/relativity.scad" rel="noopener noreferrer" target="_blank">davidson16807/relativity.scad</a> lor string manipulation (e.g., replacing <code>%year</code> in the title with the selected year or splitting holiday dates).</p>

<p>The model is available on <a href="https://makerworld.com/en/models/511591" target="_blank">Makerworld</a>. If it ever gets taken down (possibly due to my dissatisfaction with the recent Bambu firmware changes), here&rsquo;s the full source code:</p>

<pre>
<code class="language-openscad">/**
 * MIT License
 *
 * Copyright (c) 2025 Dominik Chrástecký
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/* [What to render] */
// Whether to render the red parts (holidays, Sundays, Saturdays if enabled)
redParts = true;
// Whether to render the white parts (background)
whiteParts = true;
// Whether to render the black parts (dates, text)
blackParts = true;
// Whether to render the blue parts (background behind month names)
blueParts = true;

/* [General] */
// The year to generate the calendar for
year = 2024;
// The start month, useful if you want to print the calendar in multiple parts
startMonth = 1;
// The end month, useful if you want to print the calendar in multiple parts
endMonth = 12;
// comma separated holiday dates with day first and month second, for example: 1.1,8.5,5.7,6.7 (means Jan 1st, May 8th, Jul 5th, Jul 6th)
holidays = "";
// Whether you want to print using AMS, MMU or a similar system, or a single extruder version
multiMaterial = true;
// The height of the calendar
calendarHeight = 3.2;
// a number between 10 and 360, the higher the better quality
quality = 60; // [10:360]
// whether Saturdays should be rendered in red in addition to Sundays
saturdayRedColor = false;
// how many months to put on a single row
monthsPerRow = 3;

/* [Hook and magnet holes] */
// Enable hook holes?
hookHole = true;
// Enable magnet hole?
magnetHole = true;
// How much to add to the various sizes, if your printer is not well calibrated, you might need to make the tolerances larger
tolerances = 0.2;
// The diameter of the lower part of the hook hole
hookHoleDiameter = 5.6;
// The width of the upper part of the hook hole
hookHoleUpperPartWidth = 3;
// Whether the magnet is round or square
roundMagnet = true;
// The diameter of the magnet, ignored if the magnet is not round
magnetDiameter = 10;
// The width of the magnet, ignored if the magnet is round
magnetWidth = 10;
// The depth of the magnet, ignored if the magnet is round
magnetDepth = 10;
// The height of the magnet hole. Please make sure the calendarHeight is larger than the magnet hole, otherwise weird stuff might happen
magnetHeight = 2;
// When checked, the magnet hole will be hidden inside the calendar and you will have to pause the print to insert the magnet, if unchecked, the magnet hole will be visible on the back
hiddenMagnet = true;

/* [Text settings] */
// The name of the font to use
font = "Liberation Mono:style=Bold";
// The size of the month names
monthFontSize = 5.01;
// The size of the font for name days
dayNameFontSize = 2.51;
// The size of the font for calendar title
titleFontSize = 10.01;

/* [Calendar title] */
// The title of the calendar, %year will be replaced with the current year
calendarTitle = "Calendar %year";
// The space around the calendar title, make larger if your magnet is too big to fit
titleSpace = 15;

/* [Day names] */
// Your language version for Monday
monday = "MON";
// Your language version for Tuesday
tuesday = "TUE";
// Your language version for Wednesday
wednesday = "WED";
// Your language version for Thursday
thursday = "THU";
// Your language version for Friday
friday = "FRI";
// Your language version for Saturday
saturday = "SAT";
// Your language version for Sunday
sunday = "SUN";

/* [Month names] */
// Your language version for January
january = "JANUARY";
// Your language version for February
february = "FEBRUARY";
// Your language version for March
march = "MARCH";
// Your language version for April
april = "APRIL";
// Your language version for May
may = "MAY";
// Your language version for June
june = "JUNE";
// Your language version for July
july = "JULY";
// Your language version for August
august = "AUGUST";
// Your language version for September
september = "SEPTEMBER";
// Your language version for October
october = "OCTOBER";
// Your language version for November
november = "NOVEMBER";
// Your language version for December
december = "DECEMBER";

function getFirstDay(year, month, day = 1) = 
    let (
        q = day,
        m = month &lt; 3 ? month + 12 : month,
        adjusted_year = month &lt; 3 ? year - 1 : year,
        K = (adjusted_year) % 100,
        J = floor((adjusted_year) / 100)
    )
    (
        let (
            h = (q + floor((13 * (m + 1)) / 5) + K + floor(K / 4) + floor(J / 4) + 5 * J) % 7
        )
        ((h + 5) % 7) + 1
    );

// from https://github.com/JustinSDK/dotSCAD/blob/master/src/util/_impl/_split_str_impl.scad
function sub_str(t, begin, end) =
    let(
        ed = is_undef(end) ? len(t) : end,
        cum = [
            for (i = begin, s = t[i], is_continue = i &lt; ed;
            is_continue;
            i = i + 1, is_continue = i &lt; ed, s = is_continue ? str(s, t[i]) : undef) s
        ]
    )
    cum[len(cum) - 1];

function _split_t_by(idxs, t) =
    let(leng = len(idxs))
    [sub_str(t, 0, idxs[0]), each [for (i = 0; i &lt; leng; i = i + 1) sub_str(t, idxs[i] + 1, idxs[i + 1])]];

function daysAmount(month) = month == 2 
    ? (year % 4 == 0 &amp;&amp; (year % 400 == 0 || year % 100 != 0)) ? 29 : 28 
    : (month % 2 == 0 ? (month &gt;= 8 ? 31 : 30) : (month &gt;= 8 ? 30 : 31));

function split_str(t, delimiter) = len(search(delimiter, t)) == 0 ? [t] : _split_t_by(search(delimiter, t, 0)[0], t);
    
function contains(value, array) = 
    count_true([for (element = array) element == value]) &gt; 0;

function count_true(values) = 
    sum([for (v = values) v ? 1 : 0]);

function sum(values) = 
    sum_helper(values, 0);

function sum_helper(values, i) = 
    i &lt; len(values) ? values[i] + sum_helper(values, i + 1) : 0;
    
    
// from https://github.com/davidson16807/relativity.scad/blob/master/strings.scad
function replace(string, replaced, replacement, ignore_case=false, regex=false) = 
	_replace(string, replacement, index_of(string, replaced, ignore_case=ignore_case, regex=regex));
    
function _replace(string, replacement, indices, i=0) = 
    i &gt;= len(indices)?
        after(string, indices[len(indices)-1].y-1)
    : i == 0?
        str( before(string, indices[0].x), replacement, _replace(string, replacement, indices, i+1) )
    :
        str( between(string, indices[i-1].y, indices[i].x), replacement, _replace(string, replacement, indices, i+1) )
    ;
    
function after(string, index=0) =
	string == undef?
		undef
	: index == undef?
		undef
	: index &lt; 0?
		string
	: index &gt;= len(string)-1?
		""
	:
        join([for (i=[index+1:len(string)-1]) string[i]])
	;
function before(string, index=0) = 
	string == undef?
		undef
	: index == undef?
		undef
	: index &gt; len(string)?
		string
	: index &lt;= 0?
		""
	: 
        join([for (i=[0:index-1]) string[i]])
	;
function join(strings, delimeter="") = 
	strings == undef?
		undef
	: strings == []?
		""
	: _join(strings, len(strings)-1, delimeter);
function _join(strings, index, delimeter) = 
	index==0 ? 
		strings[index] 
	: str(_join(strings, index-1, delimeter), delimeter, strings[index]) ;
        
function index_of(string, pattern, ignore_case=false, regex=false) = 
	_index_of(string, 
        regex? _parse_rx(pattern) : pattern, 
        regex=regex, 
        ignore_case=ignore_case);
function _index_of(string, pattern, pos=0, regex=false, ignore_case=false) = 		//[start,end]
	pos == undef?
        undef
	: pos &gt;= len(string)?
		[]
	:
        _index_of_recurse(string, pattern, 
            _index_of_first(string, pattern, pos=pos, regex=regex, ignore_case=ignore_case),
            pos, regex, ignore_case)
	;
    
function _index_of_recurse(string, pattern, index_of_first, pos, regex, ignore_case) = 
    index_of_first == undef?
        []
    : concat(
        [index_of_first],
        _coalesce_on(
            _index_of(string, pattern, 
                    pos = index_of_first.y,
                    regex=regex,
                    ignore_case=ignore_case),
            undef,
            [])
    );
function _index_of_first(string, pattern, pos=0, ignore_case=false, regex=false) =
	pos == undef?
        undef
    : pos &gt;= len(string)?
		undef
	: _coalesce_on([pos, _match(string, pattern, pos, regex=regex, ignore_case=ignore_case)], 
		[pos, undef],
		_index_of_first(string, pattern, pos+1, regex=regex, ignore_case=ignore_case))
    ;

function _coalesce_on(value, error, fallback) = 
	value == error?
		fallback
	: 
		value
	;
function _match(string, pattern, pos, regex=false, ignore_case=false) = 
    regex?
    	_match_parsed_peg(string, undef, pos, peg_op=pattern, ignore_case=ignore_case)[_POS]
    : starts_with(string, pattern, pos, ignore_case=ignore_case)? 
        pos+len(pattern) 
    : 
        undef
    ;
function starts_with(string, start, pos=0, ignore_case=false, regex=false) = 
	regex?
		_match_parsed_peg(string,
			undef,
			pos, 
			_parse_rx(start), 
			ignore_case=ignore_case) != undef
	:
		equals(	substring(string, pos, len(start)), 
			start, 
			ignore_case=ignore_case)
	;
function equals(this, that, ignore_case=false) = 
	ignore_case?
		lower(this) == lower(that)
	:
		this==that
	;
function substring(string, start, length=undef) = 
	length == undef? 
		between(string, start, len(string)) 
	: 
		between(string, start, length+start)
	;
function between(string, start, end) = 
	string == undef?
		undef
	: start == undef?
		undef
	: start &gt; len(string)?
		undef
	: start &lt; 0?
		before(string, end)
	: end == undef?
		undef
	: end &lt; 0?
		undef
	: end &gt; len(string)?
		after(string, start-1)
	: start &gt; end?
		undef
	: start == end ? 
		"" 
	: 
        join([for (i=[start:end-1]) string[i]])
	;


module _radiusCorner(depth, radius) {
    difference(){
       translate([radius / 2 + 0.1, radius / 2 + 0.1, 0]){
          cube([radius + 0.2, radius + 0.1, depth + 0.2], center=true);
       }

       cylinder(h = depth + 0.2, r = radius, center=true);
    }   
}

module roundedRectangle(width, height, depth, radius, leftTop = true, leftBottom = true, rightTop = true, rightBottom = true) {
    translate([width / 2, height / 2, depth / 2])
    difference() {
        cube([
            width, 
            height, 
            depth,
        ], center = true);
        if (rightTop) {
            translate([width / 2 - radius, height / 2 - radius]) {
                rotate(0) {
                    _radiusCorner(depth, radius);   
                }
            }
        }
        if (leftTop) {
            translate([-width / 2 + radius, height / 2 - radius]) {
                rotate(90) {
                    _radiusCorner(depth, radius);
                }
            }
        }
        if (leftBottom) {
            translate([-width / 2 + radius, -height / 2 + radius]) {
                rotate(180) {
                    _radiusCorner(depth, radius);
                }
            }
        }
        if (rightBottom) {            
            translate([width / 2 - radius, -height / 2 + radius]) {
                rotate(270) {
                    _radiusCorner(depth, radius);
                }
            }
        }
    }   
}

$fn = quality;

holidaysArray = split_str(holidays, ",");
hasHolidays = !(len(holidaysArray) == 1 &amp;&amp; holidaysArray[0] == "");

plateWidth = 80;

colorWhite = "#ffffff";
colorBlue = "#2323F7";
colorBlack = "#000000";
colorRed = "#ff0000";

noMmuBlueOffset = 0.4;
noMmuBlackOffset = 0.8;
noMmuRedOffset = 1.2;
noMmuWhiteOffset = 1.6;

module monthBg(plateWidth, plateDepth, depth, margin) {
    height = 0.6;
    radius = 4;
    
    translate([
        margin,
        plateDepth - depth - 5, 
        calendarHeight - height + 0.01
    ])
    roundedRectangle(
        plateWidth - margin * 2, 
        depth, 
        height + (multiMaterial ? 0 : noMmuBlueOffset), 
        radius
    );
}

module monthName(month, plateWidth, plateDepth, bgDepth) {
    height = 0.6;
    
    monthNames = [january, february, march, april, may, june, july, august, september, october, november, december];
    
    color(colorWhite)
    translate([
        plateWidth / 2,
        plateDepth - bgDepth - 3,
        calendarHeight - height + 0.02
    ])
    linear_extrude(height + (multiMaterial ? 0 : noMmuWhiteOffset))
    text(monthNames[month - 1], size = monthFontSize, font = font, halign = "center");
}

module dayName(day, margin, plateWidth, plateDepth) {
    height = 0.6;
    days = [monday, tuesday, wednesday, thursday, friday, saturday, sunday];
    
    space = (plateWidth - margin * 2) / 7 + 0.4;
    
    translate([
        margin + (day - 1) * space,
        plateDepth - 20,
        calendarHeight - height + 0.01
    ])
    linear_extrude(height + (multiMaterial ? 0 : (day == 7 ? noMmuRedOffset : noMmuBlackOffset)))
    text(days[day - 1], size = dayNameFontSize, font = font);
}

module dayNumber(day, month, startOffset, plateWidth, plateDepth, margin) {
    height = 0.6;
    space = (plateWidth - margin * 2) / 7 + 0.4;
    
    index = (startOffset + day) % 7;
    stringDate = str(day, ".", month);
    
    isRed = index == 0 || saturdayRedColor &amp;&amp; index == 6 || (hasHolidays &amp;&amp; contains(stringDate, holidaysArray));
    
    translate([
        margin + ((startOffset + day - 1) % 7) * space,
        plateDepth - 25 - floor((startOffset + day - 1) / 7) * 5,
        calendarHeight - height + 0.01
    ])
    linear_extrude(height + (multiMaterial ? 0 : (isRed ? noMmuRedOffset : noMmuBlackOffset)))
    text(str(day), size = dayNameFontSize, font = font);
}

module monthPlate(year, month) {
    plateDepth = 55;
    monthBgDepth = 9;
    margin = 5;
    
    if (whiteParts) {
        difference() {
            color(colorWhite)
            cube([plateWidth, plateDepth, calendarHeight]);
         
            monthBg(plateWidth, plateDepth, monthBgDepth, margin = margin);   
            
            for (day = [1:7]) {
                dayName(day, margin = margin, plateWidth = plateWidth, plateDepth = plateDepth);
            }
            
            for (day = [1:daysAmount(month)]) {
                startOffset = getFirstDay(year, month) - 1;
                dayNumber(day, month, startOffset, plateWidth = plateWidth, margin = margin, plateDepth = plateDepth);
            }
        }
        
        monthName(month, plateWidth, plateDepth, monthBgDepth);
    }
    if (blueParts) {
        difference() {
            color(colorBlue)
            monthBg(plateWidth, plateDepth, monthBgDepth, margin = margin);
            monthName(month, plateWidth, plateDepth, monthBgDepth);
        }
    }
    
    for (day = [1:7]) {
        if (((day == 7 || day == 6 &amp;&amp; saturdayRedColor) &amp;&amp; redParts) || (!(day == 7 || day == 6 &amp;&amp; saturdayRedColor) &amp;&amp; blackParts)) {
            color(day == 7 || day == 6 &amp;&amp; saturdayRedColor ? colorRed : colorBlack)
            dayName(day, margin = margin, plateWidth = plateWidth, plateDepth = plateDepth);
        }
    }

    for (day = [1:daysAmount(month)]) {
        startOffset = getFirstDay(year, month) - 1;
        index = (startOffset + day) % 7;
        
        stringDate = str(day, ".", month);
        isRed = index == 0 || saturdayRedColor &amp;&amp; index == 6 || (hasHolidays &amp;&amp; contains(stringDate, holidaysArray));
        
        if ((isRed &amp;&amp; redParts) || (!isRed &amp;&amp; blackParts)) {
            color(isRed ? colorRed : colorBlack)
            dayNumber(day, month, startOffset, plateWidth = plateWidth, margin = margin, plateDepth = plateDepth);
        }
    }
}

module title(bgHeight) {
    height = 0.6;
    
    translate([
        (plateWidth * monthsPerRow) / 2,
        bgHeight / 2,
        calendarHeight - height + 0.01
    ])
    linear_extrude(height + (multiMaterial ? 0 : noMmuBlackOffset))
    text(replace(calendarTitle, "%year", year), size = titleFontSize, halign = "center", valign = "center");
}

module hookHole() {
    height = calendarHeight + 1;
    translate([hookHoleDiameter / 2, hookHoleDiameter / 2, -0.01]) {
        translate([-hookHoleUpperPartWidth / 2, hookHoleDiameter / 5.6, 0])
        roundedRectangle(hookHoleUpperPartWidth + tolerances, 6, height, 1.5);
        cylinder(h = height, d = hookHoleDiameter + tolerances);        
    }
}

for (month = [startMonth:endMonth]) {
    translate([
        ((month - startMonth) % monthsPerRow) * plateWidth,
        -(ceil((month - startMonth + 1) / monthsPerRow)) * 55,
        0
    ])
    monthPlate(year, month);   
}

titleHeight = titleSpace;

if (whiteParts) {
    
    color(colorWhite)
    difference() {
        cube([plateWidth * monthsPerRow, titleHeight, calendarHeight]);
        title(titleHeight);

        if (hookHole) {
            margin = 10;
            
            translate([margin, 3])
            hookHole();
            
            translate([plateWidth * monthsPerRow - margin - hookHoleDiameter, 3])
            hookHole();
        }
        
        if (magnetHole) {
            translate([0, 0, hiddenMagnet ? 0.4 : 0]) {
                if (roundMagnet) {
                    translate([
                        (plateWidth * monthsPerRow) / 2, 
                        magnetDiameter / 2 + 1,
                        -0.01
                    ])
                    cylinder(h = magnetHeight + tolerances, d = magnetDiameter + tolerances);
                } else {
                    translate([
                        (plateWidth * monthsPerRow) / 2 - magnetWidth / 2,
                        magnetDepth / 2,
                        -0.01
                    ])
                    cube([magnetWidth + tolerances, magnetDepth + tolerances, magnetHeight + tolerances]);
                }   
            }
        }
    }
}
if (blackParts) {
    color(colorBlack)
    title(titleHeight);
}</code></pre>

<p>In a future update, I plan to implement an algorithm to calculate Easter, allowing it to be added to holidays with a single toggle. If you know of any algorithm that could be easily implemented in OpenSCAD, let me know!</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Persistent packages on Steam Deck using Nix</title>
      <description><![CDATA[The Steam Deck (and SteamOS) delivers a fantastic console-like experience on a Linux system. However, its immutable filesystem means that installing packages that persist across system upgrades isn’t straightforward. Fortunately, with a little Nix magic, you can work around that limitation.]]></description>
      <pubDate>Tue, 06 Jan 2026 08:49:21 +0000</pubDate>
      <link>https://chrastecky.dev/gaming/persistent-packages-on-steam-deck-using-nix</link>
      <guid isPermaLink="false">7</guid>
      <enclosure type="image/png" length="875409" url="https://chrastecky.dev/i/l/10/post_detail/Steam%20Deck%20-%20Nix.png"/>
      <category><![CDATA[Gaming]]></category>
      <content:encoded><![CDATA[The Steam Deck (and SteamOS) delivers a fantastic console-like experience on a Linux system. However, its immutable filesystem means that installing packages that persist across system upgrades isn’t straightforward. Fortunately, with a little Nix magic, you can work around that limitation.

<p>Immutable systems offer many benefits&mdash;until you need to customize your filesystem by installing packages. While installing software isn&rsquo;t difficult per se, SteamOS&rsquo;s design means that most customizations are wiped during system upgrades. About a year ago, Valve added <code>/nix</code> to the list of directories that remain intact during updates, and that&rsquo;s where Nix stores all of its packages.</p>

<blockquote>
<p>If you&rsquo;re not familiar with Nix: it&rsquo;s a package manager that uses declarative definitions for your software instead of commands like <code>apt install</code> or <code>dnf install</code>. You simply list all your desired packages in a configuration file, and Nix takes care of installing them. Additionally, the handy <code>nix-shell</code> utility lets you spawn temporary shells with the packages you specify.</p>
</blockquote>

<p>There are two primary ways to work with Nix comfortably: you can either run <a href="https://nixos.org/" rel="noopener noreferrer" target="_blank">NixOS</a> (which isn&rsquo;t ideal on a Steam Deck) or use <a href="https://nix-community.github.io/home-manager/" rel="noopener noreferrer" target="_blank">Home Manager</a>.</p>

<h2>Installing Nix</h2>

<p>Switch to Desktop Mode and open Konsole for the following steps. First, install Nix itself using this command (see the <a href="https://nixos.org/download/" rel="noopener noreferrer" target="_blank">official installation instructions</a>):</p>

<p><code>sh &lt;(curl -L https://nixos.org/nix/install) --no-daemon</code></p>

<p>This command installs Nix in single-user mode (<code>--no-daemon</code>), which is a good fit for SteamOS since it may not require <code>sudo</code> for most operations. (If it does ask for sudo, you&rsquo;ll need to <a href="https://www.gamingonlinux.com/guides/view/how-to-set-change-and-reset-your-steamos-steam-deck-desktop-sudo-password/" rel="noopener noreferrer" target="_blank">set up sudo on your Steam Deck</a>.)</p>

<p>Next, load Nix into your current terminal session:</p>

<p><code>source .bash_profile</code></p>

<p>By default, Nix uses the unstable branch of packages. To switch to the stable channel, run:</p>

<p><code>nix-channel --add https://nixos.org/channels/nixos-24.11 nixpkgs</code></p>

<p>This command sets your <code>nixpkgs</code> channel to the latest stable version (in this example, 24.11). In the future, check the current stable version on the <a href="https://nixos.org/" rel="noopener noreferrer" target="_blank">NixOS homepage</a>.</p>

<p>Nix is now installed&mdash;but without Home Manager, it isn&rsquo;t very user-friendly.</p>

<h2>Installing Home Manager</h2>

<p>First, add the Home Manager channel to your Nix configuration:</p>

<p><code>nix-channel --add https://github.com/nix-community/home-manager/archive/release-24.11.tar.gz home-manager</code></p>

<p><strong>Note:</strong> Ensure that the version for both Nix and Home Manager match. In this example, both are 24.11.</p>

<blockquote>
<p>If you prefer the unstable branch, you can instead run: <code>nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager</code></p>
</blockquote>

<p>Update your channels to include these changes:</p>

<p><code>nix-channel --update</code></p>

<p>Before proceeding, back up your Bash configuration files:</p>

<ul>
	<li><code>mv .bash_profile .bash_profile.bckp</code></li>
	<li><code>mv .bashrc .bashrc.bckp</code></li>
</ul>

<blockquote>
<p>If you choose not to back them up, you&rsquo;ll need to remove them because Home Manager creates these files during installation and will fail if they already exist.</p>
</blockquote>

<p>Now, run the Home Manager installation:</p>

<p><code>nix-shell &#39;&lt;home-manager&gt;&#39; -A install</code></p>

<p>Once the installation completes, create your Home Manager configuration file using a text editor:</p>

<p><code>kate ~/.config/home-manager/home.nix</code></p>

<p>Paste in the following configuration:</p>

<pre>
<code class="language-nix">{ config, pkgs, ... }:
{
  home.username = "deck";
  home.homeDirectory = "/home/deck";

  programs.bash = {
    enable = true;
    initExtra = ''
      if [ -e $HOME/.nix-profile/etc/profile.d/nix.sh ]; then . $HOME/.nix-profile/etc/profile.d/nix.sh; fi

      export NIX_SHELL_PRESERVE_PROMPT=1
      if [[ -n "$IN_NIX_SHELL" ]]; then
        export PS1="$PS1(nix-shell) "
      fi
    '';
  };

  home.stateVersion = "24.11"; # don't change this even if you upgrade your channel in the future, this should stay the same as the version you first installed nix on

  home.packages = with pkgs; [
    
  ];

  programs.home-manager.enable = true;
}</code></pre>

<p>This configuration does the following:</p>

<ul>
	<li>Sets your username to <strong>deck</strong> (the default on Steam Deck).</li>
	<li>Specifies the correct path to your home directory.</li>
	<li>Enables Home Manager to manage your Bash shell and ensures the Nix environment is loaded automatically&mdash;so you won&rsquo;t have to source it manually each time.</li>
	<li>Adds a <code>(nix-shell)</code> suffix to your terminal prompt when you&rsquo;re in a Nix shell, which is a subtle but useful improvement over the default behavior.</li>
	<li>Defines the <code>home.stateVersion</code>, which should remain the same as when you first installed Nix (even if you later change your channels). <strong>You should never change it after the initial Nix installation</strong></li>
	<li>Enables Home Manager itself.</li>
	<li>Provides an empty list (<code>home.packages</code>) where you can later add your desired packages.</li>
</ul>

<p>Apply your new configuration by running:</p>

<p><code>home-manager switch</code></p>

<p>This is the basic workflow for managing your environment with Nix: update your configuration file and then run <code>home-manager switch</code> to apply the changes.</p>

<p>After closing and reopening your terminal, test the setup by running <code>nix-shell</code>. If you see an error indicating that <code>default.nix</code> is missing, everything is working as expected. (If the command isn&rsquo;t found at all, something went wrong.)</p>

<h2>Installing packages</h2>

<p>To install packages, simply add them to the <code>home.packages</code> list in your configuration file. For example, to install <code>nmap</code> (for network scanning) and <code>cowsay</code> (because a cow makes everything better), update your configuration as follows:</p>

<pre>
<code class="language-nix">  home.packages = with pkgs; [
      nmap
      cowsay
  ];</code></pre>

<p>Keep the rest of the file unchanged, then apply the new configuration with <code>home-manager switch</code>. You can test the setup by running:</p>

<p><code>echo &quot;Hello from my Steam Deck!&quot; | cowsay</code></p>

<p>You should see this beauty in your terminal:</p>

<pre>
<code> ___________________________ 
&lt; Hello from my Steam Deck! &gt;
 --------------------------- 
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
</code></pre>

<p>Running <code>nmap</code> should display its usage instructions. If you decide to remove <code>nmap</code> (you&#39;re keeping <code>cowsay</code>, right?), just delete it from the configuration file and run <code>home-manager switch</code> again.</p>

<h2>Tips</h2>

<ul>
	<li>Create a desktop shortcut to your configuration file:
	<ul>
		<li><code>ln -s ~/.config/home-manager/home.nix ~/Desktop/Nix_Config</code></li>
	</ul>
	</li>
	<li>Run <code>nix-collect-garbage</code> periodically to remove unused packages and free up space.</li>
	<li>Install the <code>comma</code> package. This nifty tool lets you run any package on the fly by simply prefixing the command with a comma.
	<ul>
		<li>For example, instead of adding <code>nmap</code> to your configuration, you could run <code>, nmap</code> to temporarily use it. (notice the comma in front of nmap)</li>
	</ul>
	</li>
	<li>Nix can do much more than just manage packages&mdash;for instance, you can use it to create environment variables, shell aliases, systemd services, files, and more.</li>
</ul>

<p><small>Cover image sources:&nbsp;<a href="https://commons.wikimedia.org/wiki/File:Steam_Deck_(front).png" rel="noopener noreferrer" target="_blank">Wikimedia Commons</a>, <a href="https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-nineish-catppuccin-frappe-alt.png" rel="noopener noreferrer" target="_blank">NixOS</a> </small></p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Lazy objects in PHP 8.4</title>
      <description><![CDATA[Lazy objects are a fantastic addition to the already impressive PHP 8.4 release. In this article, we'll explore how to use them.]]></description>
      <pubDate>Tue, 02 Sep 2025 08:45:45 +0000</pubDate>
      <link>https://chrastecky.dev/programming/lazy-objects-in-php-8-4</link>
      <guid isPermaLink="false">6</guid>
      <enclosure type="image/png" length="95142" url="https://chrastecky.dev/i/l/9/post_detail/Lazy%20ghost%20objects.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[Lazy objects are a fantastic addition to the already impressive PHP 8.4 release. In this article, we'll explore how to use them.

<p>Lazy objects allow you to delay initialization until it&rsquo;s absolutely necessary. This is particularly useful when an object depends on I/O operations&mdash;such as accessing a database or making an external HTTP request. Although you could previously implement lazy loading in userland, there were significant caveats. For example, you couldn&rsquo;t declare the proxied class as final, because the lazy proxy must extend it to satisfy type checks. If you&rsquo;ve ever used Doctrine, you might have noticed that entities cannot be declared final for precisely this reason.</p>

<p>Without further ado, let&#39;s dive right in!</p>

<h2>Lazy deserializer</h2>

<p>For this project, I created a simple DTO:</p>

<pre>
<code class="language-php">final readonly class Product
{
    public function __construct(
        public string $name,
        public string $description,
        public float $price,
    ) {
    }
}</code></pre>

<p>Notice that the class is declared as both <code>final</code> and <code>readonly</code>&mdash;something that wouldn&rsquo;t have been possible with a pure userland implementation. Here&rsquo;s what the deserializer looks like:</p>

<pre>
<code class="language-php">final readonly class LazyDeserializer
{
    /**
     * @template T of object
     * @param class-string&lt;T&gt; $class
     * @return T
     */
    public function deserialize(array $data, string $class): object
    {
        // todo
    }
}</code></pre>

<p>This setup lets us write code like the following:</p>

<pre>
<code class="language-php">$data = [
    'name' =&gt; 'Door knob',
    'description' =&gt; "The coolest door knob you've ever seen!",
    'price' =&gt; 123.45,
];

$deserializer = new LazyDeserializer();
$object = $deserializer-&gt;deserialize($data, Product::class);

var_dump($object);</code></pre>

<h3>Implementing the deserializer</h3>

<p>I split the implementation into multiple methods for better maintainability. Let&rsquo;s start with the single public method whose signature we just saw:</p>

<pre>
<code class="language-php">    /**
     * @template T of object
     * @param class-string&lt;T&gt; $class
     * @return T
     */
    public function deserialize(array $data, string $class): object
    {
        $reflection = new ReflectionClass($class);

        return $reflection-&gt;newLazyGhost(function (object $object) use ($data): void {
            $this-&gt;deserializeObject($data, $object);
        });
    }</code></pre>

<p>First, we obtain a reflection of the target class and then call its <code>newLazyGhost</code> method. The lazy ghost is responsible for creating the lazily initialized object. It accepts a single callback that receives an instance of the target object (which remains uninitialized) and uses it to set up the properties in the <code>deserializeObject</code> method.</p>

<p>At this point, the method returns an object of the target class (specified by the <code>$class</code> parameter) with all its properties uninitialized. These properties will be initialized only when you access them. For example, if you <code>var_dump</code> the resulting object right now, you might see something like:</p>

<pre>
<code>lazy ghost object(App\Dto\Product)#7 (0) {
  ["name"]=&gt;
  uninitialized(string)
  ["description"]=&gt;
  uninitialized(string)
  ["price"]=&gt;
  uninitialized(float)
}
</code></pre>

<p>Notice that it doesn&rsquo;t matter that the private <code>deserializeObject</code> method isn&rsquo;t implemented yet&mdash;the object remains truly lazy. Any errors related to initialization will only appear when you try to access one of its uninitialized properties.</p>

<p>Here&#39;s an implementation of the private method:</p>

<pre>
<code class="language-php">    private function deserializeObject(array $data, object $object): void
    {
        $reflection = new ReflectionObject($object);

        foreach ($reflection-&gt;getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
            if (!isset($data[$property-&gt;getName()])) {
                if ($property-&gt;getType()?-&gt;allowsNull()) {
                    $property-&gt;setValue($object, null);
                }
                continue;
            }

            $property-&gt;setValue($object, $data[$property-&gt;getName()]);
            unset($data[$property-&gt;getName()]);
        }

        if (count($data)) {
            throw new LogicException('There are left-over data in the array which could not be deserialized into any property.');
        }
    }</code></pre>

<p>I&rsquo;m using reflection here because the object is marked as <code>readonly</code>&mdash;this is the only way to set a readonly property outside the constructor. If the properties weren&rsquo;t readonly, you could simply assign values directly (e.g. <code>$object-&gt;$propertyName = $value</code>).</p>

<p>The process is straightforward: we iterate over each public property of the class, assign the corresponding value from the data array, and if a property is missing (and its type allows <code>null</code>), we set it to <code>null</code>. Finally, we ensure there&rsquo;s no leftover data, which would indicate a mismatch between the data and the model. (Note that this is a naive implementation; real-world deserializers tend to be more robust.)</p>

<p>Now, let&rsquo;s modify the previous example slightly to trigger the initialization of the model:</p>

<pre>
<code class="language-php">$data = [
    'name' =&gt; 'Door knob',
    'description' =&gt; "The coolest door knob you've ever seen!",
    'price' =&gt; 123.45,
];

$deserializer = new LazyDeserializer();
$object = $deserializer-&gt;deserialize($data, Product::class);

var_dump($object); // this will print the uninitialized model

$object-&gt;name; // simply calling a property will force the object to initialize

var_dump($object); // this now prints:

// object(App\Dto\Product)#7 (3) {
//  ["name"]=&gt;
//  string(9) "Door knob"
//  ["description"]=&gt;
//  string(39) "The coolest door knob you've ever seen!"
//  ["price"]=&gt;
//  float(123.45)
//}</code></pre>

<p>Note that this implementation isn&rsquo;t very useful on its own since it merely assigns properties from a static array&mdash;there&rsquo;s no I/O involved. Let&rsquo;s enhance it to support deserializing more complex values, such as enums, nested objects, and (most importantly) I/O-bound entities (which we&rsquo;ll simulate with an HTTP request). First, instead of directly assigning the value, I add another private method:</p>

<pre>
<code class="language-php">$property-&gt;setValue($object, $this-&gt;assignValue($property, $data[$property-&gt;getName()]));
</code></pre>

<p>Now, let&rsquo;s implement <code>assignValue</code>:</p>

<pre>
<code class="language-php">    private function assignValue(ReflectionProperty $property, mixed $value): mixed
    {
        $type = $property-&gt;getType();
        if (!$type) {
            return $value;
        }
        if ($value === null &amp;&amp; $type-&gt;allowsNull()) {
            return null;
        }
        if (!$type instanceof ReflectionNamedType) {
            throw new LogicException('Only a single type is allowed');
        }

        $typeName = $type-&gt;getName();
        if (is_a($typeName, BackedEnum::class, true)) {
            return $typeName::from($value);
        } else if (is_array($value) &amp;&amp; class_exists($typeName)) {
            return $this-&gt;deserialize($value, $typeName);
        } else if ($this-&gt;isHttpEntity($typeName) &amp;&amp; is_string($value)) {
            return $this-&gt;fetchHttpEntity($typeName, $value);
        }

        return $value;
    }</code></pre>

<p>Here&rsquo;s what happens in <code>assignValue</code>:</p>

<ul>
	<li>If the property has no type, the value is returned as is.</li>
	<li>If the value is <code>null</code> and the type is nullable, <code>null</code> is returned.</li>
	<li>An exception is thrown if the type isn&rsquo;t a single named type (supporting multiple types would add too much complexity for this example).</li>
	<li>Three cases are then handled:
	<ul>
		<li>If the type is a backed enum, we convert the value using its built-in <code>from</code> method.</li>
		<li>If the value is an array and the type corresponds to an existing class, we recursively call <code>deserialize</code> to support nested objects.</li>
		<li>If the type is marked as a HTTP entity (using the <code>HttpEntity</code> attribute) and the value is a string, we assume it represents an ID and fetch the entity.</li>
	</ul>
	</li>
</ul>

<p>Here are some more objects that the deserializer now supports:</p>

<pre>
<code class="language-php">enum Availability: int
{
    case InStock = 1;
    case OnTheWay = 2;
    case OutOfStock = 3;
}

final readonly class ProductVariant
{
    public function __construct(
        public string $color,
        public string $size,
    ) {
    }
}

#[HttpEntity]
final readonly class Seller
{
    public function __construct(
        public string $id,
        public string $name,
        public float $rating,
    ) {
    }
}</code></pre>

<p>For completeness, here&rsquo;s the definition of the <code>HttpEntity</code> attribute and a helper method to check for it:</p>

<pre>
<code class="language-php">#[Attribute(Attribute::TARGET_CLASS)]
final readonly class HttpEntity
{
}


private function isHttpEntity(string $typeName): bool
{
    if (!class_exists($typeName)) {
        return false;
    }

    $reflection = new ReflectionClass($typeName);
    $attributes = $reflection-&gt;getAttributes(HttpEntity::class);

    return count($attributes) &gt; 0;
}</code></pre>

<p>The enum and the non-HTTP entity class work out of the box. For example:</p>

<pre>
<code class="language-php">final readonly class Product
{
    public function __construct(
        public string $name,
        public string $description,
        public float $price,
        public Availability $availability,
        public ?ProductVariant $variant = null,
    ) {
    }
}

$data = [
    'name' =&gt; 'Door knob',
    'description' =&gt; "The coolest door knob you've ever seen!",
    'price' =&gt; 123.45,
    'availability' =&gt; 2,
    'variant' =&gt; [
        'color' =&gt; 'golden',
        'size' =&gt; '3',
    ],
];

$deserializer = new LazyDeserializer();
$object = $deserializer-&gt;deserialize($data, Product::class);

var_dump($object);

// lazy ghost object(App\Dto\Product)#7 (0) {
//  ["name"]=&gt;
//  uninitialized(string)
//  ["description"]=&gt;
//  uninitialized(string)
//  ["price"]=&gt;
//  uninitialized(float)
//  ["availability"]=&gt;
//  uninitialized(App\Enum\Availability)
//  ["variant"]=&gt;
//  uninitialized(?App\Dto\ProductVariant)
//}

$object-&gt;name;

var_dump($object);

// object(App\Dto\Product)#7 (5) {
//  ["name"]=&gt;
//  string(9) "Door knob"
//  ["description"]=&gt;
//  string(39) "The coolest door knob you've ever seen!"
//  ["price"]=&gt;
//  float(123.45)
//  ["availability"]=&gt;
//  enum(App\Enum\Availability::OnTheWay)
//  ["variant"]=&gt;
//  lazy ghost object(App\Dto\ProductVariant)#19 (0) {
//    ["color"]=&gt;
//    uninitialized(string)
//    ["size"]=&gt;
//    uninitialized(string)
//  }
//}

$object-&gt;variant-&gt;color;

// object(App\Dto\Product)#7 (5) {
//  ["name"]=&gt;
//  string(9) "Door knob"
//  ["description"]=&gt;
//  string(39) "The coolest door knob you've ever seen!"
//  ["price"]=&gt;
//  float(123.45)
//  ["availability"]=&gt;
//  enum(App\Enum\Availability::OnTheWay)
//  ["variant"]=&gt;
//  object(App\Dto\ProductVariant)#19 (2) {
//    ["color"]=&gt;
//    string(6) "golden"
//    ["size"]=&gt;
//    string(1) "3"
//  }
//}</code></pre>

<p>Notice that the <code>variant</code> property is also lazily initialized&mdash;which is pretty neat. Every nested object is handled lazily.</p>

<h3>I/O bound entities</h3>

<p>Now, let&rsquo;s move on to HTTP entities. We&rsquo;ll create a service that &ldquo;fetches&rdquo; them (in this case, we&rsquo;ll simulate the fetch):</p>

<pre>
<code class="language-php">final readonly class HttpEntityFetcher
{
    public function fetchRawByIdAndType(string $id, string $type): ?array
    {
        sleep(1);
        return [
            'id' =&gt; $id,
            'name' =&gt; 'Cool seller',
            'rating' =&gt; 4.9,
        ];
    }
}</code></pre>

<p>Here, I simulate a slow HTTP request that takes one second to complete and returns JSON data (already decoded into an array). Note that for this example the fetch always returns a seller.</p>

<p>Now all that&rsquo;s missing is the <code>LazyDeserializer::fetchHttpEntity()</code> method:</p>

<pre>
<code class="language-php">public function __construct(
    private HttpEntityFetcher $entityFetcher,
) {
}

/**
 * @template T of object
 *
 * @param class-string&lt;T&gt; $typeName
 * @return T|null
 */
private function fetchHttpEntity(string $typeName, string $id): ?object
{
    return new ReflectionClass($typeName)-&gt;newLazyGhost(function (object $object) use ($typeName, $id): void {
        $data = $this-&gt;entityFetcher-&gt;fetchRawByIdAndType($id, $object::class);
        if (!is_array($data)) {
            throw new InvalidArgumentException('An object of type ' . $typeName . ' with id ' . $id . ' could not be fetched.');
        }

        $this-&gt;deserializeObject($data, $object);
    });
}</code></pre>

<p>This lazy ghost postpones the HTTP request until one of the object&rsquo;s properties is actually accessed. Next, let&rsquo;s add the seller property to our product:</p>

<pre>
<code class="language-php">final readonly class Product
{
    public function __construct(
        public string $name,
        public string $description,
        public float $price,
        public Availability $availability,
        public Seller $seller,
        public ?ProductVariant $variant = null,
    ) {
    }
}</code></pre>

<p>And here&rsquo;s an example that adds some timing measurements to our deserialization:</p>

<pre>
<code class="language-php">$data = [
    'name' =&gt; 'Door knob',
    'description' =&gt; "The coolest door knob you've ever seen!",
    'price' =&gt; 123.45,
    'availability' =&gt; 2,
    'variant' =&gt; [
        'color' =&gt; 'golden',
        'size' =&gt; '3',
    ],
    'seller' =&gt; 'some-seller-id',
];

$deserializer = new LazyDeserializer(new HttpEntityFetcher());
$start = microtime(true);
$object = $deserializer-&gt;deserialize($data, Product::class);
$end = microtime(true);

echo "Deserializer took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;

$start = microtime(true);
$object-&gt;seller-&gt;name;
$end = microtime(true);

echo "Fetching seller id took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;</code></pre>

<p>On my PC, this prints:</p>

<pre>
<code>Deserializer took: 0.0000250340 seconds
Fetching seller name took: 1.0002360344 seconds</code></pre>

<p>The deserialization is nearly instantaneous&mdash;the delay comes when the HTTP request is eventually executed during initialization.</p>

<h3>Partially initializing ghost objects</h3>

<p>In the example above, there&rsquo;s one piece of information we already know about the seller even before any HTTP request is made: its ID. Triggering a network call just to obtain the ID is unnecessary. Fortunately, we can initialize that property immediately:</p>

<pre>
<code class="language-php">/**
 * @template T of object
 *
 * @param class-string&lt;T&gt; $typeName
 * @return T|null
 */
private function fetchHttpEntity(string $typeName, string $id): ?object
{
    $reflection = new ReflectionClass($typeName);
    $entity = $reflection-&gt;newLazyGhost(function (object $object) use ($typeName, $id): void {
        $data = $this-&gt;entityFetcher-&gt;fetchRawByIdAndType($id, $object::class);
        if (!is_array($data)) {
            throw new InvalidArgumentException('An object of type ' . $typeName . ' with id ' . $id . ' could not be fetched.');
        }

        unset($data['id']);
        $this-&gt;deserializeObject($data, $object);
    });
    $reflection-&gt;getProperty('id')-&gt;setRawValueWithoutLazyInitialization($entity, $id);

    return $entity;
}</code></pre>

<p>The <code>setRawValueWithoutLazyInitialization</code> method (a catchy name, right?) lets you assign a value to a property without forcing the rest of the object to be initialized.</p>

<pre>
<code class="language-php">$start = microtime(true);
$object = $deserializer-&gt;deserialize($data, Product::class);
$end = microtime(true);

echo "Deserializer took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;
var_dump($object-&gt;seller);

$start = microtime(true);
$object-&gt;seller-&gt;id;
$end = microtime(true);

echo "Fetching seller id took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;
var_dump($object-&gt;seller);


$start = microtime(true);
$object-&gt;seller-&gt;name;
$end = microtime(true);

echo "Fetching seller name took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;
var_dump($object-&gt;seller);</code></pre>

<p>This prints timings similar to:</p>

<pre>
<code>Deserializer took: 0.0000338554 seconds
Fetching seller id took: 0.0000009537 seconds
Fetching seller name took: 1.0001599789 seconds</code></pre>

<p>As you can see, accessing the ID is immediate, while accessing another property (like the name) triggers the full initialization.</p>

<pre>
<code>lazy ghost object(App\Entity\Seller)#20 (1) {
  ["id"]=&gt;
  string(14) "some-seller-id"
  ["name"]=&gt;
  uninitialized(string)
  ["rating"]=&gt;
  uninitialized(float)
}


lazy ghost object(App\Entity\Seller)#20 (1) {
  ["id"]=&gt;
  string(14) "some-seller-id"
  ["name"]=&gt;
  uninitialized(string)
  ["rating"]=&gt;
  uninitialized(float)
}

object(App\Entity\Seller)#20 (3) {
  ["id"]=&gt;
  string(14) "some-seller-id"
  ["name"]=&gt;
  string(11) "Cool seller"
  ["rating"]=&gt;
  float(4.9)
}</code></pre>

<p>That&rsquo;s it for the deserializer example! It&rsquo;s a simplified implementation, but I imagine that Doctrine may eventually replace its userland proxy approach with these core lazy objects once they target PHP 8.4 and later.</p>

<p><em>Update 2025-09-02: Doctrine can now optionally use native lazy objects as of <a href="https://www.doctrine-project.org/2025/06/28/orm-3.4.0-released.html" rel="noopener" target="_blank">version 3.4.0</a>.</em></p>

<h2>Private key generating example</h2>

<p>As a bonus, here&rsquo;s an additional example&mdash;a private key generator that I&rsquo;ve actually used in one of my libraries. (<a href="https://github.com/RikudouSage/ActivityPub/blob/master/src/Server/KeyGenerator/OpenSslActorKeyGenerator.php" rel="noopener noreferrer" target="_blank">View on GitHub</a>)</p>

<pre>
<code class="language-php">public function generate(int $bits = 4096): KeyPair
{
    $reflection = new ReflectionClass(KeyPair::class);
    $keyPair = $reflection-&gt;newLazyGhost(function (KeyPair $keyPair) use ($bits) {
        $config = [
            'private_key_type' =&gt; OPENSSL_KEYTYPE_RSA,
            'private_key_bits' =&gt; $bits,
        ];
        $resource = openssl_pkey_new($config) ?: throw new CryptographyException('Failed generating new private key');

        $privateKeyPem = '';
        openssl_pkey_export($resource, $privateKeyPem);
        assert(is_string($privateKeyPem));

        $details = openssl_pkey_get_details($resource) ?: throw new CryptographyException('Failed decoding the private key');
        $publicKeyPem = $details['key'];
        assert(is_string($publicKeyPem));

        $reflection = new ReflectionObject($keyPair);
        $reflection-&gt;getProperty('privateKey')-&gt;setValue($keyPair, $privateKeyPem);
        $reflection-&gt;getProperty('publicKey')-&gt;setValue($keyPair, $publicKeyPem);
    });
    assert($keyPair instanceof KeyPair);

    return $keyPair;
}</code></pre>

<p>This postpones the expensive operation (generating a 4096 bits private key) until it&#39;s actually needed.</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Strongly typed ng-template in Angular</title>
      <description><![CDATA[If you're like me and feel uneasy whenever something is typed as 'any' in TypeScript, this article is for you! I'll show you a neat trick to help both the compiler and your IDE understand the exact type of data being passed to your ng-template.]]></description>
      <pubDate>Sun, 09 Feb 2025 10:35:19 +0000</pubDate>
      <link>https://chrastecky.dev/programming/strongly-typed-ng-template-in-angular</link>
      <guid isPermaLink="false">4</guid>
      <enclosure type="image/png" length="140595" url="https://chrastecky.dev/i/l/7/post_detail/angular_wordmark_gradient.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[If you're like me and feel uneasy whenever something is typed as 'any' in TypeScript, this article is for you! I'll show you a neat trick to help both the compiler and your IDE understand the exact type of data being passed to your ng-template.

<h2>The problem</h2>

<p>When you have a <code>&lt;ng-template&gt;</code> that accepts parameters via context, you usually lose TypeScript&#39;s type safety, reverting to the prehistoric age of JavaScript with no type enforcement:</p>

<pre>
<code class="language-xml">&lt;ng-template #someTemplate let-someVariable="someVariable"&gt;
  {{Math.abs(someVariable)}} &lt;!-- compiler and IDE have no idea that the variable is a string --&gt;
&lt;/ng-template&gt;</code></pre>

<p>With this approach, you can perform any operation on <code>someVariable</code>, and the compiler won&#39;t warn you&mdash;even if it results in runtime errors.</p>

<h2>The solution</h2>

<p>To ensure type safety, we can create a <strong>type assertion guard directive</strong>:</p>

<pre>
<code class="language-typescript">@Directive({
  selector: 'ng-template[some-template]',
  standalone: true,
})
export class SomeTemplateNgTemplate {
  static ngTemplateContextGuard(
    directive: SomeTemplateNgTemplate,
    context: unknown
  ): context is {someVariable: string} {
    return true;
  }
}</code></pre>

<h3>Explanation</h3>

<ol>
	<li><strong>Directive setup</strong>

	<ul>
		<li>This directive applies to <code>&lt;ng-template&gt;</code> elements that include the <code>some-template</code> attribute (<code>ng-template[some-template]</code> in the selector).</li>
		<li>It&#39;s marked as <code>standalone</code>, which is the recommended approach in modern Angular.</li>
	</ul>
	</li>
	<li><strong>Type Context Guard</strong>
	<ul>
		<li>The class name is not important and can be anything.</li>
		<li>The <code>static ngTemplateContextGuard</code> function is where the magic happens.</li>
		<li>It must accept two parameters:
		<ul>
			<li>An instance of itself (<code>directive: SomeTemplateNgTemplate</code>).</li>
			<li>The <code>context</code> (which is typed as <code>unknown</code> which is a more type-safe <code>any</code>).</li>
		</ul>
		</li>
		<li>The return type uses a <strong>TypeScript type predicate</strong>, which tells the compiler: <em>If this function returns <code>true</code>, then the <code>context</code> must match the given type <code>{ someVariable: string }</code>.</em></li>
	</ul>
	</li>
</ol>

<p>Since this function always returns <code>true</code>, TypeScript will assume that every template using this directive has the expected type.</p>

<blockquote>
<p><strong>Important note:</strong> As with all TypeScript type assertions, this is a compile-time safety measure&mdash;it <strong>does not</strong> enforce types at runtime. You can still pass invalid values, but TypeScript will warn you beforehand.</p>
</blockquote>

<h2>Applying the Directive</h2>

<p>Now, update your template to use the directive:</p>

<pre>
<code class="language-xml">&lt;ng-template some-template #someTemplate let-someVariable="someVariable"&gt;
  {{Math.abs(someVariable)}}
&lt;/ng-template&gt;</code></pre>

<h3>The result</h3>

<p>With the <code>some-template</code> directive in place, Angular now correctly infers the type of <code>someVariable</code>. If you try to use <code>Math.abs(someVariable)</code>, TypeScript will now show an error:</p>

<p><code>NG5: Argument of type &#39;string&#39; is not assignable to parameter of type &#39;number&#39;.</code></p>

<h2>Conclusion</h2>

<p>By leveraging <code>ngTemplateContextGuard</code>, you can enforce strong typing within <code>ng-template</code> contexts, making your Angular code safer and more maintainable. This simple trick helps catch potential errors at compile time rather than at runtime&mdash;ensuring better developer experience and fewer unexpected bugs.</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Unleash: Feature flags in PHP</title>
      <description><![CDATA[Feature flags (also known as feature toggles) let you enable or disable specific features or code paths at runtime without deploying new code. In this article, we'll explore Unleash, my favourite open-source tool for managing feature flags efficiently.]]></description>
      <pubDate>Fri, 14 Feb 2025 20:35:09 +0000</pubDate>
      <link>https://chrastecky.dev/programming/unleash-feature-flags-in-php</link>
      <guid isPermaLink="false">2</guid>
      <enclosure type="image/png" length="466248" url="https://chrastecky.dev/i/l/3/post_detail/feature-toggle.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[Feature flags (also known as feature toggles) let you enable or disable specific features or code paths at runtime without deploying new code. In this article, we'll explore Unleash, my favourite open-source tool for managing feature flags efficiently.

<p>If you&#39;re unsure where you could (or why you should) use feature flags in your project, this section is for you, otherwise feel free to skip this part.</p>

<h2>What are feature flags</h2>

<p>Feature flags are runtime switches that enable or disable specific code paths dynamically. You might already be using them without realizing it! If your system allows enabling or disabling functionality via database settings (e.g., toggling registrations, comments, or user uploads), you&#39;re already using a basic form of feature flags. But these self-built options are rarely as thought-out as dedicated feature flagging systems.</p>

<h3>Dedicated feature flagging systems</h3>

<p>Dedicated feature flagging systems provide a standardized way to manage feature toggles and unlock additional use cases, such as:</p>

<ul>
	<li>Gradually roll out features to a subset of users, such as internal users or beta testers.
	<ul>
		<li>Makes it possible to do a gradual rollout to test out the reactions without deploying a feature to everyone</li>
		<li>Enable features based on the region of the user (like GDPR, CCPA)</li>
	</ul>
	</li>
	<li>Create experimental features without maintaining separate branches</li>
	<li>A/B test multiple versions of a new feature</li>
	<li>Implement a kill switch to turn off some parts of the code in case of emergency (attack, data corruption...)</li>
	<li>Replace your built-in permission system</li>
	<li>Create toggleable features that are only needed in certain cases (for example, enable a high verbosity logging if you run into issues)</li>
	<li>Rollback features if they&#39;re broken</li>
	<li>and many more</li>
</ul>

<h2>Unleash</h2>

<blockquote>
<p>Disclaimer: I originally wrote the open-source Unleash PHP SDK, which was later adopted as the official Unleash SDK. While I&rsquo;m paid to maintain it, this article is not sponsored (and I&#39;m not an employee of Unleash). I&rsquo;m writing it for the same reasons I originally created the SDK: I love how Unleash is implemented and think more people should use it!</p>
</blockquote>

<p><a href="https://www.getunleash.io" rel="noopener noreferrer" target="_blank">Unleash</a> is one such system. Unleash offers both a paid plan and a self-hosted open-source version. While the open-source version lacks some premium features, since the release of the <strong>constraints</strong> feature to the OSS version it&#39;s feature-complete for my needs.</p>

<p>What makes Unleash unique is the way the feature evaluation is handled: everything happens locally, meaning your app does not leak any data to Unleash. Your application also avoids performance overhead from unnecessary HTTP requests. Usually these systems do the evaluation on the server and just return a yes/no response. With Unleash, you instead get the whole configuration as a simple JSON and the SDK does evaluation locally (to the point that you could even use the SDK without Unleash at all, you can simply provide a static JSON). Furthermore, the features are cached locally for half a minute or so, thus the only I/O overhead Unleash adds is 2 http requests a minute. And another cool feature is that they support pretty much every major programming language. Now that my fanboying is over, let&#39;s go over Unleash in PHP!</p>

<h2>Unleash in PHP</h2>

<p>Installing the SDK is straightforward, simply run <code>composer require unleash/client</code>. The documentation can be found at <a href="https://packagist.org/packages/unleash/client" rel="noopener noreferrer" target="_blank">Packagist</a> or <a href="https://github.com/Unleash/unleash-client-php" rel="noopener noreferrer" target="_blank">GitHub</a>. It supports PHP versions as old as 7.2. Afterwards you create an instance of the Unleash object that you will use throughout your code:</p>

<pre>
<code class="language-php">$unleash = UnleashBuilder::create()
    -&gt;withAppName('Some app name')
    -&gt;withAppUrl('https://my-unleash-server.com/api/')
    -&gt;withInstanceId('Some instance id')
    -&gt;build();</code></pre>

<p>The app name and instance ID are used to identify clients. The app URL is the Unleash server endpoint, which you can find in the settings page.</p>

<p>Once you&#39;ve set up the Unleash object, using it is extremely simple:</p>

<pre>
<code class="language-php">if ($unleash-&gt;isEnabled('new-product-page')) {
  // do one thing
} else if ($unleash-&gt;isEnabled('semi-new-product-page')) {
  // do other thing
} else {
  // do yet another thing
}</code></pre>

<p>If you do A/B testing, you can configure variants like this:</p>

<pre>
<code class="language-php">$topMenuVariant = $unleash-&gt;getVariant('top-menu');
if (!$topMenuVariant-&gt;isEnabled()) {
  // todo the user does not have access to the feature at all
} else {
  $payload = $topMenuVariant-&gt;getPayload();
  // let's assume the payload is a JSON
  assert($payload-&gt;getType() === VariantPayloadType::JSON);
  $payloadData = $payload-&gt;fromJson();

  // todo display the menu based on the received payload
}</code></pre>

<h3>Configuring the features</h3>

<p>All of the above must be configured somewhere and that place is the Unleash UI. You can test out their <a href="https://app.unleash-hosted.com/demo/login" rel="noopener noreferrer" target="_blank">official demo</a> (just put whatever email in there, it doesn&#39;t even have to be real, there&#39;s no confirmation) if you don&#39;t want to install Unleash locally.</p>

<p>Each feature has multiple environments, by default a <code>development</code> and <code>production</code> one (I think in the open source version you cannot create more, though I successfully did so by fiddling directly with the database) and each environment must have one or more strategies (unless the environment is disabled). Strategies is what controls whether the feature is enabled for a user or not. I&#39;ll go briefly over the simple strategies and then write a bit more about the complex ones (and custom ones).</p>

<ol>
	<li><strong>Standard</strong> - simple yes/no strategy, no configuration, just enabled or disabled</li>
	<li><strong>User IDs</strong> - enable the feature for specific user IDs</li>
	<li><strong>IPs</strong> and <strong>Hosts</strong> - enable the feature for specific IP addresses and hostnames respectively</li>
</ol>

<p>Unleash doesn&rsquo;t automatically know your app&rsquo;s user IDs&mdash;you need to provide them via an Unleash context:</p>

<pre>
<code class="language-php">$context = new UnleashContext(currentUserId: '123');

if ($unleash-&gt;isEnabled('some-feature', $context)) {
  // todo
}</code></pre>

<p>Or more likely, if you don&#39;t want to pass around a manually created context all the time, just create a provider that will create the default context:</p>

<pre>
<code class="language-php">final class MyContextProvider implements UnleashContextProvider 
{
    public function getContext(): Context
    {
        $context = new UnleashContext();
        $context-&gt;setCurrentUserId('user id from my app');
        
        return $context;     
    }
}

$unleash = UnleashBuilder::create()
    -&gt;withAppName('Some app name')
    -&gt;withAppUrl('https://my-unleash-server.com/api/')
    -&gt;withInstanceId('Some instance id')
    -&gt;withContextProvider(new MyContextProvider())
    -&gt;build();

if ($unleash-&gt;isEnabled('some-feature')) {
  // todo
}</code></pre>

<h3>The Gradual rollout strategy</h3>

<p>This powerful strategy allows you to roll out features to a percentage of users based on a chosen context field (e.g., user ID, IP address, or any custom attribute). With the help of constraints you can configure very complex access scenarios thanks to the many operators that are available (various string, array, date, numeric and version operators) for each of your context fields. So in short, you create arbitrary fields in your context which you can then validate with any of the supported operators.</p>

<blockquote>
<p>This is sort of becoming the catch-all default strategy because it can do everything the others can with the help of constraints. If you want to emulate the <strong>Standard</strong> strategy, just make it always available to 100% of your users. Emulating <strong>User IDs</strong> strategy can be done by having it available to 100% of your userbase and adding a constraint that the <em>userId</em> must be one of the specified values. And so on.</p>
</blockquote>

<h3>Custom strategies</h3>

<p>Need even more flexibility? You can create custom strategies! Here&rsquo;s a real-world example from one of my projects:</p>

<pre>
<code class="language-php">&lt;?php

namespace App\Service\Unleash;

use InvalidArgumentException;
use Unleash\Client\Configuration\Context;
use Unleash\Client\DTO\Strategy;
use Unleash\Client\Strategy\AbstractStrategyHandler;
use Override;

final class AccountIdUnleashStrategy extends AbstractStrategyHandler
{
    public const string CONTEXT_NAME = 'currentAccountId';

    #[Override]
    public function getStrategyName(): string
    {
        return 'accountId';
    }

    #[Override]
    public function isEnabled(Strategy $strategy, Context $context): bool
    {
        $allowedAccountIds = $this-&gt;findParameter('accountIds', $strategy);
        if (!$allowedAccountIds) {
            return false;
        }

        try {
            $currentCompanyAccountId = $context-&gt;getCustomProperty(self::CONTEXT_NAME);
        } catch (InvalidArgumentException) {
            return false;
        }

        $allowedAccountIds = array_map('trim', explode(',', $allowedAccountIds));
        $enabled = in_array($currentCompanyAccountId, $allowedAccountIds, true);

        if (!$enabled) {
            return false;
        }

        return $this-&gt;validateConstraints($strategy, $context);
    }
}
</code></pre>

<p>Then simply register it:</p>

<pre>
<code class="language-php">$unleash = UnleashBuilder::create()
    -&gt;withAppName('Some app name')
    -&gt;withAppUrl('https://my-unleash-server.com/api/')
    -&gt;withInstanceId('Some instance id')
    -&gt;withContextProvider(new MyContextProvider())
    -&gt;withStrategy(new AccountIdUnleashStrategy())
    -&gt;build();</code></pre>

<p>The strategy is then simply created in Unleash where you add an <code>accountIds</code> field of type <code>list</code> and mark it as required. Note that this strategy could also be defined using a <strong>Gradual rollout</strong> strategy with constraints, but I think having a custom one like that provides a better developer experience.</p>

<p>One downside to custom strategies is that if you use them in different projects, you need to create them in each project and the behavior must be the same (meaning the same context fields and the same implementation even across languages).</p>

<h2>Unleash in Symfony</h2>

<p>The <a href="https://github.com/Unleash/unleash-client-symfony" rel="noopener noreferrer" target="_blank">Unleash Symfony bundle</a> handles most of the configuration for you and offers additional features, such as:</p>

<ul>
	<li><code>#[IsEnabled]</code> attribute for controller routes</li>
	<li>Automatic user ID if the Symfony Security component is configured</li>
	<li>Automatic integration with the Symfony http request object, like fetching the remote IP from it instead of from the $_SERVER array</li>
	<li>Automatic environment context value based on the kernel environment</li>
	<li>Custom context properties configured either as static values, as Expression Language expressions or provided via an event listener</li>
	<li>Twig functions, tags, tests and filters</li>
	<li>Automatically registered custom strategies, you simply implement them and Unleash knows about them</li>
	<li>and more</li>
</ul>

<h2>Additional notes</h2>

<p>There are many other Unleash features I haven&rsquo;t covered, such as the frontend proxy (which handles evaluation and prevents client-side state leakage). Some advanced features are better suited for official <a href="https://github.com/Unleash/unleash-client-php" rel="noopener noreferrer" target="_blank">documentation</a> rather than a blog post.</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Doctrine and SQLite migrations: How to disable foreign keys in PHP 8.4</title>
      <description><![CDATA[SQLite has limited capabilities for modifying tables—often, you must drop the original table and recreate it, which can break your foreign keys. PHP 8.4 provides a cool solution for that.]]></description>
      <pubDate>Sat, 08 Mar 2025 17:56:36 +0000</pubDate>
      <link>https://chrastecky.dev/programming/doctrine-and-sq-lite-migrations-how-to-disable-foreign-keys-in-php-8-4</link>
      <guid isPermaLink="false">1</guid>
      <enclosure type="image/png" length="96788" url="https://chrastecky.dev/i/l/1/post_detail/Screenshot_20250116_154723.png"/>
      <category><![CDATA[Programming]]></category>
      <content:encoded><![CDATA[SQLite has limited capabilities for modifying tables—often, you must drop the original table and recreate it, which can break your foreign keys. PHP 8.4 provides a cool solution for that.

<h2>The problem</h2>

<p>If you use a SQLite database in a Doctrine project and enable foreign key checks, you&rsquo;ll run into an issue with table-modifying migrations: You often need to drop and fully recreate the table. If that table is referenced by others, the migration will fail unless you disable the foreign key checks. Furthermore, the entire migration runs inside a transaction, and SQLite doesn&rsquo;t allow changing foreign key checks during a transaction.</p>

<h2>The solution</h2>

<p>There are several possible solutions, but here&rsquo;s a particularly neat one made possible by PHP 8.4&rsquo;s new property hooks:</p>

<pre>
<code class="language-php">final class VersionXXXXXXXXXXXXXX extends AbstractMigration
{
    protected $connection {
        get {
            $this-&gt;connection-&gt;executeStatement('PRAGMA foreign_keys = OFF');
            return $this-&gt;connection;
        }
        set =&gt; $this-&gt;connection = $value;
    }

    public function up(Schema $schema): void
    {
        // TODO create migration
    }

    public function down(Schema $schema): void
    {
        // TODO create migration
    }
}</code></pre>

<p>The code above overrides the <code>$connection</code> property from the parent class with a property hook, so every time the migration system requests a connection, the foreign key checks are disabled.</p>]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
  </channel>
</rss>
