<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
  <channel>
    <title>Blog entries tagged php :: mwop.net</title>
    <description>Blog entries tagged php :: mwop.net</description>
    <pubDate>Thu, 16 Apr 2026 10:30:00 -0500</pubDate>
    <generator>Laminas_Feed_Writer 2 (https://getlaminas.org)</generator>
    <link>https://mwop.net/blog/tag/php</link>
    <atom:link rel="self" type="application/rss+xml" href="https://mwop.net/blog/tag/php/rss.xml"/>
    <item>
      <title>PHP DateTimeImmutable::createFromFormat Reset Character</title>
      <pubDate>Thu, 16 Apr 2026 10:30:00 -0500</pubDate>
      <link>https://mwop.net/blog/2026-04-16-php-create-from-format-reset.html</link>
      <guid>https://mwop.net/blog/2026-04-16-php-create-from-format-reset.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I was recently building something that was taking date input from an HTML form field, and casting it to a PHP <code>DateTimeImmutable</code>. I was then comparing that to another date, and got thrown off during testing when I compared the resulting instance to <code>new DateTimeImmutable('today')</code>; the instances were not considered equal.</p>


<p>To recreate the conditions, you can try the following:</p>
<pre><code class="language-php hljs php" data-lang="php">$date     = <span class="hljs-string">'2016-06-16'</span>;
$fromForm = DateTimeImmutable::createFromFormat(<span class="hljs-string">'Y-m-d'</span>, $date);
$today    = <span class="hljs-keyword">new</span> DateTimeImmutable(<span class="hljs-string">'today'</span>);
<span class="hljs-keyword">echo</span> $fromForm == $today ? <span class="hljs-string">'Equal'</span> : <span class="hljs-string">'Not equal'</span>; <span class="hljs-comment">// outputs "Not equal"</span>
</code></pre>
<p>What's happening? Well, if you were to echo the results of each of <code>$fromForm-&gt;format('c')</code> and <code>$today-&gt;format('c')</code>, the difference is clear: the <code>$fromForm</code> value includes the <em>time</em> when the instance was created, while <code>$today</code> has the time set to midnight.</p>
<p>So, how do you zero out the time when using <code>createFromFormat()</code>?</p>
<p>It turns out that one of the format characters you can use is the <code>|</code> operator. When you include this at the end of your format string, any fields not included in the format are zero'ed out:</p>
<pre><code class="language-php hljs php" data-lang="php">$fromForm = DateTimeImmutable::createFromFormat(<span class="hljs-string">'Y-m-d|'</span>);
</code></pre>
<hr />
<h4>Reference</h4>
<ul>
<li><a href="https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters">DateTimeImmutable::createFromFormat() Parameters</a></li>
</ul>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2026-04-16-php-create-from-format-reset.html">PHP DateTimeImmutable::createFromFormat Reset Character</a> was originally
    published <time class="dt-published" datetime="2026-04-16T10:30:00-05:00">16 April 2026</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Handling PHP-FPM using Caddy</title>
      <pubDate>Fri, 21 Mar 2025 11:59:06 -0500</pubDate>
      <link>https://mwop.net/blog/2025-03-21-caddy-php-fpm.html</link>
      <guid>https://mwop.net/blog/2025-03-21-caddy-php-fpm.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I've been using Caddy for years as a reverse proxy in front of other services, but recently wanted to use it to directly route traffic to PHP-FPM.
Caddy has a specialized reverse proxy directive for PHP-FPM, <code>php_fastcgi</code>, which seemed like it would do the trick, but I found that no traffic was ever getting routed to my FPM pool.</p>


<h2>What was happening</h2>
<p>There's a <a href="https://stackoverflow.com/a/77024172">fantastic StackOverflow answer describing what happens</a>, and it's great reading.</p>
<p>The long and the short of it is that the <code>php_fastcgi</code> directive looks for matching files on the disk, and in the case of paths that look like directories, an <code>index.php</code> under that directory.
In other words, <em>it cannot match arbitrary paths (aka pretty URLs)</em>, just files.
Which doesn't work with basically any fraṁework-based application; as the author of that SO answer posits, the directive was likely written to target Wordpress.
Additionally, this causes issues if the PHP-FPM web pool is using a different root directory than you're using in your Caddy server, as Caddy will look for the files in the root defined for the server, and return a 404 if it can't find it, <em>without ever passing it on to PHP-FPM</em>.</p>
<h2>The solution</h2>
<p>The solution is to use the standard <code>reverse_proxy</code> directive, and have it use the <code>fastcgi</code> transport.
You wrap this in a <code>handle</code> directive so that you can also specify a different filesystem root to pass to PHP-FPM.</p>
<p>As an example, let's assume:</p>
<ul>
<li>I'm running PHP-FPM on port 9000 of the host &quot;app&quot;.</li>
<li>The filesystem root for my PHP application is in <code>/var/www/app.example.org/public</code></li>
<li>I want to route all requests to <code>index.php</code> in that root so that they are handled by the application.</li>
</ul>
<p>I can write the Caddyfile as follows:</p>
<pre><code class="language-lua hljs lua" data-lang="lua">app.example.org {
    # Note that this is the filesystem root <span class="hljs-keyword">for</span> the _server_
    root * /static/app.example.org

    file_server

    handle {
        # This is the filesystem root <span class="hljs-keyword">for</span> the FPM pool
        root * /var/www/app.example.org/public
        rewrite index.php
        reverse_proxy app:<span class="hljs-number">9000</span> {
            transport fastcgi {
                split .php
                capture_stderr
            }
        }
    }
}
</code></pre>
<p>If the file is not found in the filesystem, it will try to pass it on to PHP-FPM.
When it does, it rewrites to <code>index.php</code> in the PHP-FPM root (which is a different root than Caddy uses for its own file server!); PHP-FPM will use the matched path as the <code>PATH_INFO</code>, and your framework remains happy.</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-03-21-caddy-php-fpm.html">Handling PHP-FPM using Caddy</a> was originally
    published <time class="dt-published" datetime="2025-03-21T11:59:06-05:00">21 March 2025</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>SQL Nested Queries or Sub Queries with Doctrine DBAL</title>
      <pubDate>Mon, 10 Mar 2025 08:24:34 -0500</pubDate>
      <link>https://mwop.net/blog/2025-03-06-dbal-sub-query.html</link>
      <guid>https://mwop.net/blog/2025-03-06-dbal-sub-query.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I recently ran into a problem with my website for which the solution was a nested query (sometimes termed a subquery).
However, I use <a href="https://www.doctrine-project.org/projects/doctrine-dbal/en/4.2/index.html">Doctrine DBAL</a> for creating my dynamic queries, and there's no documentation on how to do them.</p>


<h3>The problem</h3>
<p>For my art gallery, the raw SQL looks something like the following:</p>
<pre><code class="language-sql hljs sql" data-lang="sql"><span class="hljs-keyword">SELECT</span>
    p.filename,
    p.description,
    p.created,
    array_agg(<span class="hljs-keyword">DISTINCT</span> t.tag) <span class="hljs-keyword">as</span> tags
<span class="hljs-keyword">FROM</span>
    photos p
<span class="hljs-keyword">LEFT</span> <span class="hljs-keyword">JOIN</span> tags t <span class="hljs-keyword">ON</span> p.filename = t.content_id
<span class="hljs-keyword">WHERE</span>
    p.filename = :filename
    <span class="hljs-keyword">AND</span> t.content_type = :content_type
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span>
    p.filename
</code></pre>
<p>I started noticing an odd issue where, post insertion of an image into my gallery, I'd get no error, but the form would not appear to have processed.
On further inspection, using <a href="https://www.zend.com/products/zendphp-enterprise/zendhq#tab-panel-16522">Z-Ray from ZendHQ</a>, I realized that the insertion was successful, but that the redirect to view the inserted image was returning a 404.
I grabbed the executed SQL from Z-Ray for retrieving the image, and it returned no rows.</p>
<p>I started thinking about why this image wasn't posting, when others were, and realized there was one trivial difference: I wasn't including any hashtags in my description, which meant no tags.</p>
<p>Hopefully you can see where this is leading.</p>
<p>A <code>LEFT JOIN</code> normally will not cause the entire query to fail if it finds no matching rows on the joined table; that's the behavior of <code>INNER JOIN</code>.
However, if you put a condition that is based on a joined table outside the join itself, it essentially acts like an <code>INNER JOIN</code>, as this is now a condition of the <code>SELECT</code> query.</p>
<p>Sure enough, when I removed the <code>LEFT JOIN</code> and the <code>array_agg</code> column, I got a hit.</p>
<h3>&quot;Obvious&quot; solution: move the condition</h3>
<p>As a commenter on this post noted, the immediate solution is to move the <code>AND t.content_type = :content_type</code> clause to the <code>JOIN</code>:</p>
<pre><code class="language-sql hljs sql" data-lang="sql"><span class="hljs-keyword">SELECT</span>
    p.filename,
    p.description,
    p.created,
    array_agg(<span class="hljs-keyword">DISTINCT</span> t.tag) <span class="hljs-keyword">as</span> tags
<span class="hljs-keyword">FROM</span>
    photos p
<span class="hljs-keyword">LEFT</span> <span class="hljs-keyword">JOIN</span> tags t <span class="hljs-keyword">ON</span> p.filename = t.content_id <span class="hljs-keyword">AND</span> t.content_type = :content_type
<span class="hljs-keyword">WHERE</span>
    p.filename = :filename
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span>
    p.filename
</code></pre>
<p>This does work, and requires no huge changes to the DBAL query builder; I just move the condition into the <code>joinLeft()</code>, and carry on.</p>
<h3>Preferred solution: nested query</h3>
<p>The solution I chose was to do a nested query, and to aggregate those results as an array.
This makes it more clear when reading the query as to the intent: I want to select all distinct tags for this image and assign them as an array to a column.
I'm using PostgreSQL, so the query looks like this:</p>
<pre><code class="language-sql hljs sql" data-lang="sql"><span class="hljs-keyword">SELECT</span>
    p.filename,
    p.description,
    p.created,
    (<span class="hljs-keyword">SELECT</span> <span class="hljs-built_in">ARRAY</span>(
        <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">DISTINCT</span> tag <span class="hljs-keyword">FROM</span> tags <span class="hljs-keyword">WHERE</span> p.filename = content_id <span class="hljs-keyword">AND</span> content_type = :content_type
        )) <span class="hljs-keyword">as</span> tags
<span class="hljs-keyword">FROM</span>
    photos p
<span class="hljs-keyword">WHERE</span>
    p.filename = :filename
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span>
    p.filename
</code></pre>
<p>With this approach, if no rows are returned from the <code>tags</code> table, an empty array is created; otherwise an array of the tag values that match is returned.</p>
<p>However, I didn't know how to create this query using the Doctrine DBAL query builder.</p>
<h3>DBAL solution</h3>
<p>When creating a <code>SELECT</code> using the DBAL query builder, you do something like this:</p>
<pre><code class="language-php hljs php" data-lang="php">$select = $dbal-&gt;createQueryBuilder();
$select
    -&gt;select(
        <span class="hljs-comment">// one argument per column to select</span>
    )
    -&gt;from(<span class="hljs-string">'table_name'</span>, <span class="hljs-string">'t'</span>) <span class="hljs-comment">// alias the table</span>
</code></pre>
<p>The arguments to <code>select()</code> are expected to be strings, and any given string can be an arbitrary SQL expression.</p>
<p>Creating the subselect is easy; you do it like any other query:</p>
<pre><code class="language-php hljs php" data-lang="php">$tags = $dbal-&gt;createQueryBuilder();
$tags
    -&gt;select(<span class="hljs-string">'tag'</span>)
    -&gt;distinct()
    -&gt;from(<span class="hljs-string">'tags'</span>)
    -&gt;where(<span class="hljs-string">'content_id = p.filename'</span>)
    -&gt;andWhere(<span class="hljs-string">'content_type = :content_type'</span>);
</code></pre>
<p>Now, how do I get that into a column string for a select?</p>
<p>The <code>getSQL()</code> method of a query builder will spit out the SQL sent.
Moreover, it <strong>does not</strong> replace placeholders, so even if you set a bound parameter, it won't be injected into the generated SQL.</p>
<p>Knowing all this, I did the following:</p>
<pre><code class="language-php hljs php" data-lang="php">$tags = $dbal-&gt;createQueryBuilder();
$tags
    -&gt;select(<span class="hljs-string">'tag'</span>)
    -&gt;distinct()
    -&gt;from(<span class="hljs-string">'tags'</span>)
    -&gt;where(<span class="hljs-string">'content_id = p.filename'</span>)
    -&gt;andWhere(<span class="hljs-string">'content_type = :content_type'</span>);

$select = $dbal-&gt;createQueryBuilder();
$select
    -&gt;select(
        <span class="hljs-string">'p.filename'</span>,
        <span class="hljs-string">'p.description'</span>,
        <span class="hljs-string">'p.created'</span>,
        sprintf(<span class="hljs-string">'(SELECT ARRAY(%s)) as tags'</span>, $tags-&gt;getSQL()),
    )
    -&gt;from(<span class="hljs-string">'photos'</span>, <span class="hljs-string">'p'</span>)
    -&gt;where(<span class="hljs-string">'p.filename = :filename'</span>)
    -&gt;groupBy(<span class="hljs-string">'p.filename'</span>)
    -&gt;setParameter(<span class="hljs-string">'filename'</span>, $filename, ParameterType::STRING)
    -&gt;setParameter(<span class="hljs-string">'content_type'</span>, <span class="hljs-string">'photo'</span>, ParameterType::STRING);
</code></pre>
<p>(Where <code>ParameterType</code> is imported from the namespace <code>Doctrine\DBAL</code>.)</p>
<p>This approach worked immediately, and generated exactly the same result as the raw SQL I had tested.</p>
<h3>Changelog</h3>
<ul>
<li>2025-03-07: clarified that the solution was targeting the DBAL query builder. DBAL can consume raw SQL as well, and does not require usage of the query builder.</li>
<li>2025-03-10: noted that a <code>LEFT JOIN</code> will still work, as long as the <code>t.content_type = :content_type</code> condition is moved from the <code>SELECT</code> to the <code>LEFT JOIN</code>.</li>
</ul>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-03-06-dbal-sub-query.html">SQL Nested Queries or Sub Queries with Doctrine DBAL</a> was originally
    published <time class="dt-published" datetime="2025-03-06T10:35:34-06:00">6 March 2025</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Roundup of PHP 8.4 Posts</title>
      <pubDate>Fri, 13 Dec 2024 11:00:23 -0600</pubDate>
      <link>https://mwop.net/blog/2024-12-04-php-8.4-roundup.html</link>
      <guid>https://mwop.net/blog/2024-12-04-php-8.4-roundup.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I've recently written several blog posts for <a href="https://www.zend.com">Zend</a> (one of the brands I help manage at <a href="https://www.perforce.com">Perforce</a>) covering changes in the recently released <a href="https://www.php.net/releases/8.4/en.php">PHP 8.4</a>. If you're curious what to look out for, and how to use some of the new major features, they're worth a read:</p>
<ul>
<li><a href="https://www.zend.com/blog/php-8-4">What's New in PHP 8.4: Features, Changes, and Deprecations</a></li>
<li><a href="https://www.zend.com/blog/php-8-4-property-hooks">A Guide to PHP 8.4 Property Hooks</a></li>
<li><a href="https://www.zend.com/blog/php-asymmetric-visibility">Asymmetric Visibility in PHP 8.4: What It Means for PHP Teams</a></li>
<li><a href="https://www.zend.com/blog/http-verbs-php-8-4">HTTP Verbs Changes in PHP 8.4</a></li>
</ul>




<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-12-04-php-8.4-roundup.html">Roundup of PHP 8.4 Posts</a> was originally
    published <time class="dt-published" datetime="2024-12-04T14:03:23-06:00">4 December 2024</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Configuring PHP.INI settings in a PHP-FPM pool</title>
      <pubDate>Tue, 27 Aug 2024 17:37:30 -0500</pubDate>
      <link>https://mwop.net/blog/2024-08-27-til-php-fpm-admin-value.html</link>
      <guid>https://mwop.net/blog/2024-08-27-til-php-fpm-admin-value.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I consume PHP via Docker primarily, and to keep it manageable, I generally use a PHP-FPM container, with a web server sitting in front of it. I learned something new about PHP configuration recently that (a) made my day, and (b) kept me humble, as I should have known this all along.</p>


<p>What was it? quite simply, the <code>php_admin_value</code> struct can be used to configure <code>php.ini</code> settings for the pool. This is a great alternative to <em>also</em> adding PHP configuration settings via <code>php.ini</code> (or an include file for <code>php.ini</code>), as it allows you to keep the settings specific to that pool. That way, if you <em>must</em> have multiple pools (e.g., to serve multiple applications from the same machine and/or same PHP version), you can still have separate configuration for each.</p>
<p>How does it work? In your pool configuration, add values to that struct:</p>
<pre><code class="language-ini hljs ini" data-lang="ini"><span class="hljs-attr">php_admin_value[memory_limit]</span> = <span class="hljs-number">32</span>M
<span class="hljs-attr">php_admin_flag[error_reporting]</span> = E_ALL &amp; ~E_NOTICE &amp; ~E_DEPRECATED
<span class="hljs-attr">php_admin_flat[track_errors]</span> = <span class="hljs-literal">Off</span>
<span class="hljs-comment">; etc</span>
</code></pre>
<p>With <a href="https://www.zend.com/products/zendphp-enterprise">ZendPHP</a>, we just launched some Ansible tooling, which operates on the assumption that you are deploying PHP-FPM — and as part of its operation, it creates a template for the FPM pool configuration, but not for the PHP SAPI. And this is fine! Because you can use the <code>php_admin_value</code> settings to configure the pool for the application you're deploying!</p>
<p>Looking forward to simplifying a few of my deployments with this!</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-08-27-til-php-fpm-admin-value.html">Configuring PHP.INI settings in a PHP-FPM pool</a> was originally
    published <time class="dt-published" datetime="2024-08-27T17:37:30-05:00">27 August 2024</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Initializing ZendHQ JobQueue During Application Deployment</title>
      <pubDate>Tue, 07 May 2024 15:19:07 -0500</pubDate>
      <link>https://mwop.net/blog/2024-05-07-zendhq-jq-warmup.html</link>
      <guid>https://mwop.net/blog/2024-05-07-zendhq-jq-warmup.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>In the past few years, I've transitioned from engineering into product management at <a href="https://www.zend.com">Zend</a>, and it's been a hugely rewarding experience to be able to toss ideas over the fence to my own engineering team, and have them do all the fiddly tricky bits of actually implementing them!</p>
<p>Besides packaging long-term support versions of PHP, we also are publishing a product called ZendHQ. This is a combination of a PHP extension, and an independent service that PHP instances communicate with to do things like monitoring and queue management.</p>
<p>It's this latter I want to talk about a bit here, as (a) I think it's a really excellent tool, and (b) in using it, I've found some interesting patterns for prepping it during deployment.</p>


<h3>What does it do?</h3>
<p>ZendHQ's JobQueue feature provides the ability to defer work, schedule it to process at a future date and time, and to schedule recurring work. Jobs themselves can be either command-line processes, or webhooks that JobQueue will call when the job runs.</p>
<p>Why would you use this over, say, a custom queue runner managed by supervisord, or a tool like Beanstalk, or cronjobs?</p>
<p>There's a few reasons:</p>
<ul>
<li>Queue management and insight. Most of these tools do not provide any way to inspect what jobs are queued, running, or complete, or even if they failed. You can <em>add</em> those features, but they're not built in.</li>
<li>If you are using monitoring tools with PHP... queue workers used with these tools generally cannot be monitored. If I run my jobs as web jobs, these can run within the same cluster and communicate to the same ZendHQ instance, giving me monitoring and code traces for free.</li>
<li>Speaking of using web workers, this means I can also re-use technologies that are stable and provide worker management that I already know: php-fpm and mod_php. This is less to learn, and something I already have running.</li>
<li>Retries. JobQueue allows you to configure the ability to retry a job, and how long to wait between retries. A lot of jobs, particularly if they rely on other web services, will have transient failures, and being able to retry can make them far more reliable.</li>
</ul>
<h3>So, what about queue warmup?</h3>
<p>When using recurring jobs, you'll (a) want to ensure your queue is defined, and (b) define any recurring jobs at application deployment. You don't want to be checking on each and every request to see if the queues are present, or if the recurring jobs are present. Ideally, this should only happen on application deployment.</p>
<p>When deploying my applications, I generally have some startup scripts I fire off. Assuming that the PHP CLI is configured with the ZendHQ extension and can reach the ZendHQ instance, these scripts can (a) check for and create queues, and (b) check for and create recurring jobs.</p>
<p>As a quick example:</p>
<pre><code class="language-php hljs php" data-lang="php"><span class="hljs-keyword">use</span> <span class="hljs-title">ZendHQ</span>\<span class="hljs-title">JobQueue</span>\<span class="hljs-title">HTTPJob</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">ZendHQ</span>\<span class="hljs-title">JobQueue</span>\<span class="hljs-title">JobOptions</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">ZendHQ</span>\<span class="hljs-title">JobQueue</span>\<span class="hljs-title">JobQueue</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">ZendHQ</span>\<span class="hljs-title">JobQueue</span>\<span class="hljs-title">Queue</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">ZendHQ</span>\<span class="hljs-title">JobQueue</span>\<span class="hljs-title">QueueDefinition</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">ZendHQ</span>\<span class="hljs-title">JobQueue</span>\<span class="hljs-title">RecurringSchedule</span>;

$jq = <span class="hljs-keyword">new</span> ZendHQ\JobQueue();

<span class="hljs-comment">// Lazily create the queue "mastodon"</span>
$queue = $jq-&gt;hasQueue(<span class="hljs-string">'mastodon'</span>)
    ? $jq-&gt;getQueue(<span class="hljs-string">'mastodon'</span>)
    ? $jq-&gt;addQueue(<span class="hljs-string">'mastodon'</span>, <span class="hljs-keyword">new</span> QueueDefinition(
        QueueDefinition::PRIORITY_NORMAL,
        <span class="hljs-keyword">new</span> JobOptions(
            JobOptions::PRIORITY_NORMAL,
            <span class="hljs-number">60</span>, <span class="hljs-comment">// timeout</span>
            <span class="hljs-number">3</span>, <span class="hljs-comment">// allowed retries</span>
            <span class="hljs-number">30</span>, <span class="hljs-comment">// retry wait time</span>
            JobOptions::PERSIST_OUTPUT_ERROR,
            <span class="hljs-keyword">false</span>, <span class="hljs-comment">// validate SSL</span>
    ));

<span class="hljs-comment">// Look for jobs named "timeline"</span>
$jobs = $queue-&gt;getJobsByName(<span class="hljs-string">'timeline'</span>);
<span class="hljs-keyword">if</span> (count($jobs) === <span class="hljs-number">0</span>) {
    <span class="hljs-comment">// Job does not exist; create it</span>
    $job = <span class="hljs-keyword">new</span> HTTPJob(<span class="hljs-string">'http://worker/mastodon/timeline'</span>, HTTPJob::HTTP_METHOD_POST);
    $job-&gt;setName(<span class="hljs-string">'timeline'</span>);
    $job-&gt;addHeader(<span class="hljs-string">'Content-Type'</span>, <span class="hljs-string">'application/my-site-jq+json'</span>);
    $job-&gt;setRawBody(json_encode([
        <span class="hljs-string">'type'</span> =&gt; MyApp\Mastodon\Timeline::class,
        <span class="hljs-string">'data'</span> =&gt; [ <span class="hljs-comment">/* ... */</span> ],
    ]);

    <span class="hljs-comment">// Schedule to run every 15 minutes</span>
    $queue-&gt;scheduleJob($job, <span class="hljs-keyword">new</span> RecurringSchedule(<span class="hljs-string">'* */15 * * * *'</span>));
}
</code></pre>
<p>That's literally it.</p>
<p>The takeaway points:</p>
<ul>
<li>You can check for an existing queue and use it, and only define it if it's not there. You could also decide to suspend the queue and delete it before creating it, if you know that the existing jobs will not run with the current deployment.</li>
<li>If you give a job a name (you don't actually have to, but it helps you identify related jobs far easier if you do), you can search for it. In the example above, if I find any jobs with that name, I know it's already setup, and I can skip the step of scheduling the job.</li>
</ul>
<h3>Running one-off jobs on deployment</h3>
<p>Something else I also like to do is run one-off tasks at deployment. Often these are related to recurring tasks, and I might want to fetch the content at initialization rather than waiting for the schedule. In other cases, I might want to do things like reset caches.</p>
<p>Because these scripts run <em>before</em> deployment, which might mean restarting the web server, or, more often, waiting for the php-fpm container and/or web server to be healthy, I cannot run the jobs <em>immediately</em>, because there's nothing to answer them.</p>
<p>The answer to this is to queue a job <em>in the future</em>:</p>
<pre><code class="language-php hljs php" data-lang="php"><span class="hljs-keyword">use</span> <span class="hljs-title">DateTimeImmutable</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">ZendHQ</span>\<span class="hljs-title">JobQueue</span>\<span class="hljs-title">ScheduledTime</span>;

$queue-&gt;scheduleJob($job, <span class="hljs-keyword">new</span> ScheduledTime(<span class="hljs-keyword">new</span> DateTimeImmutable(<span class="hljs-string">'+1 minute'</span>)));
</code></pre>
<p>(I find it usually takes less than a minute for my FPM pool and/or web server to be online after running these scripts.)</p>
<p>The beauty of this approach is that my bootstrapping scripts now tend to be very fast, as I'm not trying to do all of this stuff before launching the site updates. The jobs then execute very soon after the site is up, and there's no noticeable differences in content or behavior.</p>
<h3>Closing notes</h3>
<p>I know I'm biased around ZendHQ. I'm also generally one of my own biggest critics. I had my team re-implement a lot of features that were present in Zend Server that I was never terribly keen on, and was hugely worried that we were going to make some of the same mistakes I felt we'd made with that product. However, the end result has been something that I am delighted to use, and which has opened up a ton of possibilities for how I build sites. The ability to warm my queues and manage them <em>just like the rest of my PHP application</em> is hugely powerful. I'm looking forward to seeing what others build with it!</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-05-07-zendhq-jq-warmup.html">Initializing ZendHQ JobQueue During Application Deployment</a> was originally
    published <time class="dt-published" datetime="2024-05-07T15:19:07-05:00">7 May 2024</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Advent 2023: PSR-15</title>
      <pubDate>Thu, 14 Dec 2023 17:21:00 -0600</pubDate>
      <link>https://mwop.net/blog/2023-12-14-advent-psr-15.html</link>
      <guid>https://mwop.net/blog/2023-12-14-advent-psr-15.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I've mentioned a few times over the course of this <a href="https://mwop.net/blog/tag/advent2023">2023 Advent series</a> that the longer I'm in the tech field, the more I appreciate and favor <em>simple</em> solutions.
I was reminded of this yesterday when I <a href="https://masteringlaravel.io/daily/2023-12-13-why-we-dont-use-return-types-on-controller-actions">read this article on return types in Laravel controllers</a> by <a href="https://joelclermont.com/">Joel Clermont</a>.</p>


<blockquote>
<h4>Request</h4>
<p>Please, <em>please</em>, <strong><em>please</em></strong> do not take this as an attack on Laravel or on Joel.
I have nothing but respect for Joel, and while I'm not a fan of Laravel, I'm also not a hater.
It's never a bad thing to have a popular framework that brings folks to a language; Laravel has done that in spades for PHP.</p>
</blockquote>
<h3>Summarize the article, already...</h3>
<p>In the article, Joel notes the problem with providing return types in a Laravel controller is due to the fact that it could return a view, a JSON response, an array, a redirect, or more.
If there are multiple types that could be returned, based on the request context, you would need to provide a union type.
And if you refactor or make changes to the controller later that result in new types being returned, you now need to remember to change the return type declaration.</p>
<p>In other words, it introduces brittleness.</p>
<h3>So what?</h3>
<p>I've worked on multiple iterations of a major MVC framework, and I ran into these same issues.
As PHP's type system got incrementally better, the cracks in how frameworks interact with controllers became more evident.
Personally, I find the increasing number of type capabilities in PHP to be a huge boon in helping the correctness of applications, and preventing whole classes of errors.
But if the framework <em>prevents</em> you from using the type system, or makes adding type declarations into a situation that can now introduce errors, it puts the developer and maintainer of an application into a problematic situation.</p>
<h3>What are the alternatives?</h3>
<p>I worked for quite some time on <a href="https://www.php-fig.org/psr/psr-7">PSR-7 HTTP Message Interfaces</a>, largely so that we could have a proper HTTP message abstraction in PHP on which to build a better foundation for applications and frameworks.
From this emerged <a href="https://www.php-fig.org/psr/psr-15">PSR-15 HTTP Server Request Handlers</a> (which I sponsored and collaborated on, but was not primary author of).</p>
<p>What I love about PSR-15 is that there is no ambiguity about what you return from middleware or a handler.
You return a response.
That's <em>all</em> you can return.</p>
<p>This means there's no magic about different return values resulting in different behavior from the framework.
You don't need to keep a mental map about what will happen, or do a deep dive into the framework internals to understand the ramifications of returning a view versus an array.</p>
<p>Instead, your handler will <em>create a response</em>, and provide the logic for how that is done.
If you need HTML, you render a template, and feed it to the response.
If you need JSON, you serialize data to JSON, and feed it to the response.
If you need a redirect, you create a response with the appropriate status code and <code>Location</code> header.
And so on and on.</p>
<p>Yes, this can lead to a little extra code at times, but:</p>
<ul>
<li>You can see <em>exactly</em> what you intend to return to the user, and <em>why</em>.</li>
<li>If you try and return anything <em>but</em> a response, it'll result in a <code>TypeError</code>.</li>
<li>You can test all of the different possible returns easily, by doing assertions on the returned response based on different requests provided to the handler or middleware.</li>
</ul>
<p>But should you do <em>everything</em> in a handler?
What about things that will happen for whole sections of the site, or will be repeated in many locations, like initializing a session, or checking for an authenticated user, or validating headers, or caching?</p>
<p>For those things, PSR-15 provides <em>middleware</em>.
These are expected to be chained together, like a pipeline or a command bus, and the request is passed down through them, and a response returned on the way back up.
They're a powerful way to provide re-usable pieces of functionality to your application.</p>
<p>What's more, using middleware is often far easier to understand than how and when various events will intercept a request.
You can see the list of middleware for a given handler, and understand that they act either as filters on the incoming request (authentication, caching, etc.), or as decorators on the response (e.g. encoding or compressing the response, caching, etc.).
Since each does exactly one thing (ideally), you can test how each works, and understand how and when to compose each, and how they might work in combination.</p>
<p>Building complex behavior via piping one thing to another is hugely powerful.
There's a reason that the <a href="https://en.wikipedia.org/wiki/Unix_philosophy">Unix Philosophy</a> has existed as long as it has, and I can appreciate an approach to web development that builds on it.</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2023-12-14-advent-psr-15.html">Advent 2023: PSR-15</a> was originally
    published <time class="dt-published" datetime="2023-12-14T17:21:00-06:00">14 December 2023</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Advent 2023: Doctrine DBAL</title>
      <pubDate>Mon, 11 Dec 2023 09:21:00 -0600</pubDate>
      <link>https://mwop.net/blog/2023-12-10-advent-dbal.html</link>
      <guid>https://mwop.net/blog/2023-12-10-advent-dbal.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I've mostly taken database abstraction for granted since I started at Zend.
We had a decent abstraction layer in ZF1, and improved it for ZF2.
There were a lot quirks to it — you really had to dive in and look at the various SQL abstraction classes to understand how to do more complex stuff — but it worked, and was always right there and available in the projects I worked on.</p>
<p>In the last couple of years, though, we came to the realization in the Laminas Project that we didn't really have anybody with the expertise or time to maintain it.
We've marked it security-only twice now, and while we've managed to keep it updated to each new PHP version, it's becoming harder and harder, and whenever there's a CI issue, it's anybody's guess as to whether or not we'll be able to get it resolved.</p>
<p>My alternatives have been straight PDO, or Doctrine DBAL, with the latter being my preference.</p>


<h3>Doctrine <em>what</em>?</h3>
<p>When most folks who use PHP hear &quot;Doctrine&quot;, they immediately think &quot;<a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping">ORM</a>&quot;; it's how most folks use it, and what it's best known for.</p>
<p>Underlying the ORM is its database abstraction layer (hence &quot;DBAL&quot;).
This library exposes an API that will work across any database it supports; this is essentially what zend-db, and later laminas-db, were doing as well.
What most folks don't realize is that you can use the DBAL <em>by itself</em>, without the ORM.</p>
<h3>Why no ORM?</h3>
<p>ORMs are fine.
Really.
But they add an additional layer of complexity to understanding what you are actually doing.
Additionally, if you want to do something that doesn't quite fit how the ORM works, you'll need to drop down to the DBAL anyways.
So my take has always been: why not just use the DBAL from the beginning?</p>
<p>So, how does <em>Matthew</em> write code that interacts with the database?</p>
<p>I start by writing value objects that represent discrete aspects of the application.
Most of my work will be in consuming or creating these.
From there, I write a <em><a href="https://martinfowler.com/eaaCatalog/repository.html">repository</a></em> class that I use for purposes of persisting and retrieving them.
I can usually extract an interface from this, which aids in my testing, or if I decide I need a different approach to persistence later.</p>
<p>I push the work of mapping the data from the database to these objects, and vice versa, either in the repository, or in the value objects themselves (often via a <a href="https://verraes.net/2014/06/named-constructors-in-php/">named constructor</a>).
Using these approaches creates lean code that can be easily tested, and for which there's no real need to understand the underlying system; it's all right there in what I've written for the application.</p>
<h3>Some gripes about the documentation, and some tips</h3>
<p>The <a href="https://www.doctrine-project.org/projects/doctrine-dbal/en/current/index.html">Doctrine DBAL docs</a> are a bit sparse, particularly when it comes to its <a href="https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/query-builder.html">SQL abstraction</a>.
And there's no &quot;getting started&quot; or &quot;basic usage&quot; guide.
In fact, it's not until the third page within the docs that you get any code examples; thankfully, at that point they give you information on how to get a database connection:</p>
<pre><code class="language-php hljs php" data-lang="php"><span class="hljs-keyword">use</span> <span class="hljs-title">Doctrine</span>\<span class="hljs-title">DBAL</span>\<span class="hljs-title">DriverManager</span>;

$connectionParams = [
    <span class="hljs-string">'dbname'</span>   =&gt; <span class="hljs-string">'mydb'</span>,
    <span class="hljs-string">'user'</span>     =&gt; <span class="hljs-string">'user'</span>,
    <span class="hljs-string">'password'</span> =&gt; <span class="hljs-string">'secret'</span>,
    <span class="hljs-string">'host'</span>     =&gt; <span class="hljs-string">'localhost'</span>,
    <span class="hljs-string">'driver'</span>   =&gt; <span class="hljs-string">'pdo_mysql'</span>,
];
$conn = DriverManager::getConnection($connectionParams);
</code></pre>
<p>They also provide a number of other approaches, including using a DSN (an acronym they never explain, but based on using PDO, likely means &quot;data source name&quot;).</p>
<p>Once you have a connection, what do you do?
Well the DBAL connection allows you to prepare and execute queries, including via the use of prepared statements.
It provides a variety of methods for fetching individual or multiple rows, with a variety of options for how the data is returned (indexed arrays, associative arrays, individual columns, individual values, etc.).
These retrieval methods are mirrored in the result instances returned when executing prepared statements as well.</p>
<p>And that brings me to the SQL abstraction.</p>
<p>First, it's really, really good.
It's minimal, but it covers just about anything you need to do.
If you need to write something complex, you probably can; the beauty is that if you can't, you can always fall back to a SQL query, and using the connection's API for binding values.</p>
<p>But the documentation could be better.</p>
<p>It felt like it was written by a database admin who has forgotten more than most people ever learn about databases, and never considered that others might not know as much as them.
The fact that it starts with architecture and not usage feels hugely antagonistic for somebody coming in just wanting to know how to connect to the database, build a query, and fetch some results.
(The irony is not lost on me that this is almost exactly how Laminas and Mezzio docs are written, and, yes, I recognize we could all do better!)</p>
<blockquote>
<p>Before folks start grousing, yes, I have on my TODO list an item for contributing to the DBAL docs.
I'm trying to work up an outline of what I would have found useful, what acronyms need explanation, and some examples of common patterns before I make any suggestions, however.</p>
</blockquote>
<p>First, they have a whole documentation page related to the SQL query builder, and a lot of examples.
But not a single one details <em>how to actually execute the query</em>!
So, for those wondering:</p>
<pre><code class="language-php hljs php" data-lang="php">$sql = $conn-&gt;createQueryBuilder();

<span class="hljs-comment">// ... build your query ...</span>

<span class="hljs-comment">// Execute a query that will retrieve results (generally SELECT queries):</span>
$result = $sql-&gt;executeQuery();

<span class="hljs-comment">// Execute a query that produces changes (INSERT, UPDATE, DELETE, etc.):</span>
$count = $sql-&gt;executeStatement();
</code></pre>
<p>Query results have a variety of <code>fetch*()</code> operations on them, while executing a statement returns an integer indicating the number of rows affected (assuming the database supports this).</p>
<p>Second, when I started doing joins, the argument names were confusing, and made it harder to understand what was needed.
I eventually figured it out, but it was really easy to flip the arguments for the different tables being joined.
The usage below illustrates names that would better describe how to use it:</p>
<pre><code class="language-php hljs php" data-lang="php">$sql-&gt;innerJoin(
    $primaryTableOrItsAliasIfYouSpecifiedOne, <span class="hljs-comment">// e.g. "user" or "u"</span>
    $newTableToJoin,                          <span class="hljs-comment">// e.g. "address"</span>
    $aliasForNewTableToJoin,                  <span class="hljs-comment">// e.g. "a"</span>
    $conditionToJoinOn                        <span class="hljs-comment">// e.g. "u.id = a.uid"</span>
);
</code></pre>
<p>Third, there's some odd differences in the API between INSERT and UPDATE operations.,
When setting a value, one takes <code>setValue()</code>, while the other takes <code>set()</code>, and only one of these is valid for a given operation (it's <code>setValue()</code> for INSERT operations, and <code>set()</code> for UPDATE operations, in case you were wondering).
This is especially confusing when using bound parameters, because <em>both</em> can use the <code>setParameter()</code> method for binding positional placeholder values.</p>
<p>Speaking of plaeholders, the docs don't do a great job of detailing how to handle <em>placeholders</em> gracefully.</p>
<p>The documentation suggests patterns like this:</p>
<pre><code class="language-php hljs php" data-lang="php">$queryBuilder
    -&gt;select(<span class="hljs-string">'id'</span>, <span class="hljs-string">'name'</span>)
    -&gt;from(<span class="hljs-string">'users'</span>)
    -&gt;where(<span class="hljs-string">'email = ?'</span>)
    -&gt;setParameter(<span class="hljs-number">0</span>, $userInputEmail);
</code></pre>
<p>Which is fine when there's only one parameterized value, but what if you have several, or if you're dynamically building the query (e.g., looping through user-supplied sorting or criteria, etc.), and you don't know their exact position in the final query?
And what if you want to use named parameters instead of positional parameters, but you're not sure if your database supports them?</p>
<p>The answer is in the docs, but the various <em>examples</em> don't use the pattern (other than in the discussion of the methods), which is infuriating.
The above can also be written as follows:</p>
<pre><code class="language-php hljs php" data-lang="php">$queryBuilder
    -&gt;select(<span class="hljs-string">'id'</span>, <span class="hljs-string">'name'</span>)
    -&gt;from(<span class="hljs-string">'users'</span>)
    -&gt;where(<span class="hljs-string">'email = '</span> . $queryBuilder-&gt;createNamedParameter($userInputEmail));
</code></pre>
<p>There's also a <code>createPositionalParameter()</code> method.
Both accept an optional second argument, where you can specify the value <em>type</em>, which can help ensure that values are quoted correctly for the SQL type they will map to.
This also allows you to do <code>IN()</code> operations, and each value will be quoted correctly, with the appropriate list separator for the database.</p>
<p>Once you know this approach, it's easy to remember and use, but it took me a few times through the docs before I stumbled across it.</p>
<p>The SQL it generates, though, is great, and when I've used tools like ZendHQ's Z-Ray to introspect queries, I'm always impressed by what was actually sent over the wire.</p>
<blockquote>
<h4>2023-12-11 Update</h4>
<p><a href="https://mastodon.social/@nesl247">Alexander Kim</a> pointed out to me that you can use named parameters within the query builder, along with the <code>setParameter()</code> method.
That usage looks like this:</p>
<pre><code class="language-php hljs php" data-lang="php"><span class="hljs-keyword">use</span> <span class="hljs-title">Doctrine</span>\<span class="hljs-title">DBAL</span>\<span class="hljs-title">ParameterType</span>;

$queryBuilder
    -&gt;select(<span class="hljs-string">'id'</span>, <span class="hljs-string">'name'</span>)
    -&gt;from(<span class="hljs-string">'users'</span>)
    -&gt;where(<span class="hljs-string">'email = :email'</span>)
    -&gt;setParameter(<span class="hljs-string">'email'</span>, $userInputEmail, ParameterType::STRING);
</code></pre>
<p>You can also specify named parameters when using <code>set()</code> and <code>setValue()</code>, though I'd argue that using <code>createNamedParameter()</code> is easier in those contexts.</p>
</blockquote>
<p>But for all these issues, the fact is that the docs generally give you <em>enough</em>, and the API is so clean and reasonably documented that you can generally figure out how things work just from your IDE hints and autocompletion.
Yes, I have gripes, but the library is <em>very</em> solid, <em>very</em> well written, and absolutely something I can depend on.</p>
<h3>Final Thoughts</h3>
<p>I've often used straight PDO for projects, and it works fine.
However, having a tool available like Doctrine DBAL has been a huge boon in ensuring I can switch from SQLite while prototyping to MySQL for production, and know that things will &quot;just work&quot;.</p>
<p>I also find the way it juggles <em>types</em> to be really useful.
I know that if a value is typed in the database as a NULL or as text or as a float or integer, I'll actually get those types back when I query; the same is true for when I send data to the database.
There's no magic involved, and I don't have to remember to do type conversions to and from the database.
That's <em>exactly</em> the type of functionality I want from a DBAL.</p>
<p>Yes, writing database-centric code is cumbersome, and there's a reason folks use ORMs, ActiveRecord, and the like.
However, it generally only needs to be written once, with occasional updates.
Having a good DBAL available helps keep complexity of your application down and gives you the tools to communicate securely with your database.</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2023-12-10-advent-dbal.html">Advent 2023: Doctrine DBAL</a> was originally
    published <time class="dt-published" datetime="2023-12-10T11:00:00-06:00">10 December 2023</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Advent 2023: Forms</title>
      <pubDate>Sat, 09 Dec 2023 11:00:00 -0600</pubDate>
      <link>https://mwop.net/blog/2023-12-09-advent-forms.html</link>
      <guid>https://mwop.net/blog/2023-12-09-advent-forms.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>The first thing I was tasked with after I moved full time to the Zend Framework team (17 years ago! Yikes!) was to create a forms library.
Like all the work I did for ZF in the early days, I first created a working group, gathered requirements, and prioritized features.
There were a <em>lot</em> of requests:</p>
<ul>
<li>Ability to normalize values</li>
<li>Ability to validate values</li>
<li>Ability to get validation error messages</li>
<li>Ability to render HTML forms, and have customizable markup</li>
<li>Ability to do nested values</li>
<li>Ability to handle optional values</li>
<li>Ability to report missing values</li>
</ul>
<p>and quite a lot more.
But those are some of the things that stuck out that I can remember off the top of my head.</p>
<p><a href="https://framework.zend.com/manual/1.12/en/zend.form.html">Zend_Form</a> was considered a big enough new feature that we actually bumped the version from 1.0 to 1.5 to call it out.</p>
<p>And, honestly, in hindsight, it was a mistake.</p>


<h3>A mistake?</h3>
<p>Considering the timeframe when I was developing Zend_Form, it was actually a good effort, and it's still one of those features that folks tell me sold them on the framework.
But within a year or two, I was able to see some of the drawbacks.</p>
<p>I first realized the issues when we started integrating the <a href="https://dojotoolkit.org/">Dojo Toolkit</a> with ZF.
We ended up having to create first a suite of Dojo-specific form elements, and second a whole bnch of Dojo-specific <em>decorators</em>, which were what we used to render form elements.
While the library gave us this flexibility, I saw a few issues:</p>
<ul>
<li><strong>Duplication.</strong>
We had multiple versions of the same form elements, and it was actually possible to get the wrong version for your form context.
And with duplication comes increased maintenance: any time we fixed an issue in one element, we had to check to see if the same issue existed with the Dojo versions, and fix them there as well.</li>
<li><strong>Javascript</strong>.
One of the reasons for integrating Dojo was to allow doing fun things like client-side validation; this allowed giving early feedback, without a round-trip to the server.
But this also meant that we had validation logic duplicated between the server-side and client-side logic.
And more interestingly: the form might be sent as a request <em>by javascript</em>, instead of a standard form request, which meant that we needed to validate it only, and then serialize validation status and messages.
Basically, all the rendering aspects of the form were irrelevant in this scenario.
Which brings me to...</li>
<li><strong>APIs.</strong>
Around this time, APIs started trending.
It would be a few years before REST became popular and commonly understood by developers, but folks were starting to see that we'd be needing them for the nascent mobile application markets, and that they were going to be a useful way to conduct business-to-business transactions.
Once you start having APIs in the mix, a library centered on <em>web forms</em> becomes less interesting.</li>
</ul>
<p>By the time we started planning for version 2 of ZF, we realized we'd need to reconsider how we did forms.
The first step we took was splitting the validation aspect from the form aspect, and created <code>Zend\InputFilter</code> to address the first, and <code>Zend\Form</code> to address the second.
Input filters encapsulated how to filter, normalize, and validate incoming data.
Forms composed an input filter, and then provided hints for the view layer to allow rendering the elements.
This separation helped a fair bit: you could re-use input filters for handling API or JS requests easily, while the form layer helped with rendering HTML forms.</p>
<p>But I still feel we didn't get it right:</p>
<ul>
<li>Our validation component and our input filter component were each <em>stateful</em>.
When you performed validation, each would store the values, validation status, and validation messages as part of the state.
This makes re-use within the same request more difficult (it was not uncommon to use the same validator with multiple elements, and this now required multiple instances), makes testing more difficult, and makes it harder to understand if the instance represents the definition, or the results of validation.</li>
<li>The longer I've worked in web development, the more I've realized that while the HTML generation aspects of these form libraries are useful for prototyping, they inevitably cannot be used for the final production code.
Designers, user experience experts, and accessibility developers will each want different features represented, and these will <em>never</em> fall into the defaults the framework provides.
Even if the framework provides customization features, the end result is <em>more</em> programming effort.
It's almost always better to code the HTML markup in your templates, and then feed state (e.g., element IDs/names, validation state, whether or not to display placeholders and/or error messages, etc.) from some object representing form or element state.</li>
</ul>
<p>A few years back, I started an RFC for Laminas to create an idempotent validation library, one that would not even consider web form integration, but never quite hit on a good design.
What with my work role changing, and having more and more varied interests outside work, I essentially abandoned it.</p>
<h3>Uh oh, I did it again</h3>
<p>Until recently.</p>
<p>I develop a number of internal tools for work to support some of the different functional teams with whom I work.
These often require validation at some point, with varying amounts of complexity.
As such, I've used these tools as a way for me to play with some of these ideas around validation and forms.</p>
<p>In developing the last couple of tools, I found a pattern that was working.
I decided to extract it, and then iterated on it some more.
Each iteration, I'd update one of these applications to see how it worked, what it enabled, and what was getting in the way.</p>
<p>I came up with a few goals:</p>
<ul>
<li>Provide an idempotent way to validate individual items and/or data sets.</li>
<li>Provide an extensible framework for developing validation rules.</li>
<li>Allow handling optional data, with default values.</li>
<li>Allow reporting validation error messages.</li>
<li>Ensure missing required values are reported as validation failures.</li>
<li>Use as few dependencies as possible.</li>
</ul>
<p>I also came up with some explicit <em>non-goals</em>:</p>
<ul>
<li>Creating an extensive set of validation rule classes.</li>
<li>Providing extensive mechanisms for validating and returning nested data sets.</li>
<li>Providing a configuration-driven mechanism for creating rule sets.</li>
<li>Providing HTML form input representations or all metadata required to create HTML form input representations.</li>
</ul>
<p>What I wanted was something that could validate an incoming data set, return a validation result, and then use that result to report back to the user.
In the case of an API, for an invalid result, I'd be able to get the validation error messages, which could then be used to seed a <a href="https://www.rfc-editor.org/rfc/rfc7807">Problem Details for HTTP APIs</a> message.
In the case of a web form, I'd be able to extract values, validation status, and validation error messages.</p>
<p>One thing I realized early on was that it was also useful to be able to represent a form's <em>initial state</em>.
This would allow using the same template for both the initial form, as well as reporting form validation errors later.</p>
<p>Finally, I wanted a solution that reported types and would play nicely with static analysis.
If I'm pulling a result out of a result set, I want to know that the value <em>type</em> is what I expect it to be.
This helps with testing, provides IDE hinting, and helps ensure I'm using the features correctly.
I think I ended up spending more time on this aspect than anything.</p>
<p>The result is my <a href="https://github.com/phly/phly-rule-validation">phly/phly-rule-validation</a> library.
I developed it for PHP 8.2 and up, as I wanted to use some specific features (though the ones specific to 8.2 and up... I ended up having to remove, so it would likely work on 8.0 or 8.1 as well).
It's a little over 600 lines of code in total, and has no additional dependencies.
It's also incredibly sparse; I only include 2 default validation rules.</p>
<p>The basic idea is:</p>
<ul>
<li>You create a <em>rule set</em>, consisting of <em>rules</em>.</li>
<li>A <em>rule</em> defines:
<ul>
<li>The <em>key</em> it maps to in the data set being validated.</li>
<li>A method for <em>validating</em> a value, which produces a <em>result</em>.</li>
<li>A way to produce <em>results</em> for each of a <em>default</em> value, and when the value is <em>missing</em>.</li>
</ul>
</li>
<li>Rule validation produces a <em>result</em>, which composes:
<ul>
<li>The <em>key</em> associated with the result.</li>
<li>The <em>value</em> associated with the result.
The validation routine <em>can</em> normalize the result if desired, so this value might not be 1:1 with what was submitted.
This approach allowed me to not require splitting filtering/normalization from validation, as it becomes an implementation detail.</li>
<li>The <em>validation state</em>: is it valid, or not?</li>
<li>The validation <em>message</em>: this will generally only be populated for <em>invalid</em> values, and representes a validation <em>error message</em>.</li>
</ul>
</li>
<li>A <em>rule set</em> produces a <em>result set</em>, which is a collection of <em>results</em>.</li>
</ul>
<p>In all cases, there are static analysis templates provided to allow defining the <em>types</em>.
A validation result allows defining the <em>value type</em>, and a result set allows mapping keys to specific result types.
Rules return result types.
And so on.</p>
<p>A rule set can produce a <em>valid result set</em>, and this can be used to seed the initial state of a form.
And I built support for <em>nested results</em>, which allows having forms that have groups of data.</p>
<p>The library provides usage examples, and I wrote <a href="https://github.com/phly/phly-rule-validation/tree/0.2.x/docs">quite a bit of documentation</a>, if you want to see how it works.</p>
<h3>Some thoughts</h3>
<p>Is the result perfect?
Probably not.
I know that folks used to things like ZF, Laminas, Symfony, or Laravel forms will likely dislike the approach, as it does not allow for quick prototyping of web forms.
I don't find that to be a detriment, however; as I noted earlier, the final production version of a form is likely going to be created by a designer, and won't work well with the HTML generation aspects of these systems anyways.
For folks who only want to validate API payloads, while it will be a nice, lightweight approach, it doesn't provide a lot of defaults.
Again, that's by design, as it allows developers to customize their validation logic and, more importantly, test it independently.</p>
<p>I've updated some of my applications to use this library.
In some cases, I had a net reduction of code.
In others, I ended up with more, but a far clearer understanding of what's in a form, how each item is validated, and what types are expected.
And since the bulk of phly-rule-validation is around interfaces, it means that I'm not concerned about <em>how the library works</em>; it's pretty clear how it <em>will</em> work just from viewing the classes I've created.</p>
<p>One benefit of creating the library is that it helped me better understand <a href="https://psalm.dev">Psalm</a> and type templates.
There are definitely limitations, and some things produce WTF moments, but when it all comes together, it's kind of magical.
In some forms I built, it was amazing to be in a view template and get completion for everything, along with an understanding of what various types were, and warnings when I was doing an operation that couldn't use the type for a given element.</p>
<p>And these are the reasons I developed the library.
I wanted something explicit, idempotent, and static analysis friendly, as these would make testing and IDE integration more straight-forward.
I think I succeeded in that goal.</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2023-12-09-advent-forms.html">Advent 2023: Forms</a> was originally
    published <time class="dt-published" datetime="2023-12-09T11:00:00-06:00">9 December 2023</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Goodbye Twitter</title>
      <pubDate>Mon, 28 Nov 2022 14:20:00 -0600</pubDate>
      <link>https://mwop.net/blog/2022-11-28-goodbye-twitter.html</link>
      <guid>https://mwop.net/blog/2022-11-28-goodbye-twitter.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>This is a long, personal post.</p>
<p><strong>tl;dr</strong>: I'm leaving Twitter.
You can find me in the <a href="https://fediverse.info/">Fediverse</a> as @<a href="mailto:matthew@mwop.net">matthew@mwop.net</a>.</p>


<h2>In the beginning</h2>
<p>I started using Twitter because of ZendCon 2007.
<a href="https://calevans.com">Cal Evans</a> had the idea that if folks attending the conference were to tweet about it, those who were unable to attend would get an idea of what the conference was about, get links to slides if speakers posted them, and more; it would both feed FOMO, and respond to it.
(It also became an unofficial way for many of us to organize non-conference events during the evenings.)</p>
<p>Once the conference was done, I wasn't quite sure what to do with it.
There was a bit of engagement, but not a ton.
Hash tags, replies, retweets, quote tweets — none of these existed yet.
Hell, even direct messages were just a specially formatted tweet, and heaven forbid you get the initial character sequence wrong!
We started creating conventions, many of which later became codified into Twitter itself.</p>
<p>Over the next year or two, I found it became my &quot;virtual watercooler.&quot;
Being somebody who worked remotely, from home, I didn't have office conversations.
A few of my colleagues and collaborators were on IRC, but back then, that was about it.
If I wanted to talk to a larger group, or somebody not in my regular channels... Twitter became that place.</p>
<p>I made friends.
I got job offers.
I learned about places to visit on my travels.
When abroad, I could coordinate meet-ups with friends.</p>
<p>When I realized folks couldn't spell my handle, I reached out <em>on Twitter</em> to see if I knew somebody <em>at Twitter</em>, or if somebody had a friend at Twitter, to see if I could change my handle, as somebody was squatting on &quot;mwop&quot;.
A friend of a friend made it happen — and I made a new friend in the process.</p>
<p>That was the honeymoon period, it seems.</p>
<h2>The start of the fall</h2>
<p>Sometime in the early 2010s, I began seeing the ugly side of Twitter.
You know the folks, the ones who slide into your mentions or DMs when you post an opinion, the ones who ask for receipts and links or push whatabout-isms nonstop until you give in or stop replying (which they also take as victory).
The ones who treat your lived experience as invalid, because it does not match theirs.
The ones who cannot even imagine a valid experience outside their own.
The ones who would not even allow another person's beliefs, body, heritage, circumstances to exist if they had their way.</p>
<p>Before muting and blocking existed on Twitter, the service was quickly becoming somewhere I did not want to engage.
Somewhere I only felt comfortable posting non-revealing content about things like my open source projects, or retweeting work-related content.
(I haven't posted anything about my family in years.)
When Twitter allowed you to limit DMs to people you mutually followed, that helped a bit.
But even then, I'd get folks in my mentions arguing or trolling; I cannot tell you how many times I was told the projects I worked on were crap, should die in a fire, that I should be embarrassed to even share them, that I should quit and get a different job, preferably in a different field.
And this is only a fraction of what I see in the replies to women, people of color, LGBTQ+, people with accessibility issues — where the very act of existing as who they are is evidently an egregious offence.
It's easy to see why so many leave the service, even though it can be hugely powerful at connecting you to others in your chosen community.</p>
<p>With muting and blocking, the service became more bearable, but only barely.
I'd still get the tweets, replies, and quote tweets, but now the first time somebody spewed vitriol at me, it would be their last.</p>
<p>But I still had to see them at least once.</p>
<h2>Crumbling</h2>
<p>And then 2016 came along.</p>
<p>I am a liberal.
My wife and I laugh at the assertion that you become more conservative with age.
If anything, we've become <em>more</em> liberal.</p>
<p>And the run-up to the 2016 US elections broke us.</p>
<p>On Twitter, I was seeing either tons of right-wing hate spewed by folks, or reactions from others to that hate.
The few times I addressed it were horrible; the amount of vitriol in my mentions shocked me.
Some people have the energy and mental reserves to fight back.
I'm not one of those; I internalize the attack, and it replays in my mind over and over.
It tears me apart.</p>
<p>So following the election, I started pulling back.</p>
<p>I created a couple lists that I'd check daily, mostly those of authors or artists I like and admire.
This created a little oasis for me, and made things somewhat manageable.</p>
<p>But here's the thing: we are <em>all</em> political.
Living in a society means we engage with politics.
And this meant that, even following creators, I was still seeing politics; the politics of the era affect us all.
And I was seeing how people responded, reacted, attacked these people I love and admire, and that was somehow even worse than when it was directed at me.</p>
<p>I started checking Twitter less and less frequently.</p>
<p>I started using Instagram in 2019, primarily to share my Zentangle-inspired art.
Oh, my, was that a breath of fresh air.
Yes, there are ads, but I would open it, and be greeted by primarily screen after screen after screen of art.
It was bliss.</p>
<p>I was also increasingly using Facebook, mostly in private groups for, you guessed it, sharing Zentangle-inspired art.
When one of these moved to <a href="https://circle.so">circle.so</a>, I was amazed at how much better the experience was on a private platform.
We could engage directly with each other, and the website facilitated meaningful interaction better.
It showed me that social media doesn't have to be algorithmically determined, and that decent tooling could facilitate better quality interactions.
And I re-discovered that smaller is better; I don't need the entire world at my fingertips all the time.</p>
<p>But I also kept checking my creator lists on Twitter; it was a habit I couldn't quite shake.</p>
<h2>Chaos</h2>
<p>And then Musk came along, and threw his ego and money around, announcing his intent to buy Twitter.</p>
<p>When he announced his intent in April of this year, I remembered Mastodon, and remembered that the <a href="https://phpc.social">PHP community had an instance</a>.
I joined, and started using that as my goto microblogging location, even setting up ways to send tweets when I posted certain keywords.
The community then was small (around 200 folks), and I could even follow our local server timeline quickly each day, which would give me recommendations for new folks to follow.</p>
<p>But I still kept checking my creator lists on Twitter; I missed these creators, and wanted to follow their work.</p>
<p>And then the Twitter deal closed, and now Musk was &quot;in charge&quot;.
(The quotes are deliberate; his flailing hardly feels like somebody in charge.)</p>
<p>And I just can't.</p>
<p>Allowing Trump back on (even if he hasn't yet rejoined); sending dog whistles and outright overtures to white nationalists and antisemitism and outright fascists; unbanning people banned for those exact beliefs; playing roulette with the verification system until it becomes meaningless; firing the very people that made the service work for so many years, and who worked to improve its community safety (though these efforts still needed a lot of work); and and and and</p>
<p>I just can't.</p>
<p>I don't want to provide content for a billionaire to make money off of.
I don't want to engage with the ads that will help pay for this new version of the service.
As much as I cherish free speech, I'm not a free speech absolutist; hate speech should not be protected, and certainly not amplified.
I don't want to be in the &quot;world's town hall&quot; if that means having to argue with people who do not bring arguments to the table in good faith.</p>
<h2>Where I'll be</h2>
<p>And... I don't have to.</p>
<p>Since Twitter transferred ownership to Musk, the Masto instance I'm on, phpc.social, has grown to over 1600 users.
The majority of the creators I follow are at least trying Mastodon out, usually on one of the big instances.
Many friends I've not communicated with in years are also moving, to many different hosts.
Where I was able to follow my instance timeline easily each day, I've now already given up on even following my home timeline, and am, in fact, needing to segregate into lists again... but now not out of a desire to limit what I see, but instead to allow me to dive into conversations around topics when I have the time and interest.</p>
<p>Look, Mastodon isn't perfect.
It could use some UX designers and experts.
And there's a moderation problem, particularly if you're on a big instance; in particular, BIPOC users are reporting issues with moderation and discrimination that need to be addressed head on.
And while the design of ActivityPub, the protocol underlying Mastodon, is such that while &quot;spin up your own instance&quot; is a valid answer, the fact is that if your audience grows, you can DDoS your instance really quickly any time you post.</p>
<p>But it's also important to note that Mastodon has been chugging along for years <em>already</em>, powering communities that are largely queer and neurodivergent, and you can quickly find communities that share your values, and which are small and inviting, and fiercely protective.
These are the same communities that, when each of Gab and Truth Social came online (both of which are built on Mastodon), immediately defederated them, ensuring they would not show up in their timelines, or any of the servers they federate with.</p>
<p>It's good enough, at least for now.</p>
<p>A week ago, I decided I was done with Twitter.
I requested my archive.
I deleted my one remaining Twitter-related app from my phone (I loathed the &quot;official&quot; app).
I modified my browser such that the words &quot;twitter&quot; or &quot;tweetdeck&quot; now suggest the <a href="https://pinafore.social">site I use with my Mastodon accounts</a>.
I setup <a href="https://webfinger.net/">WebFinger</a> on my website, such that &quot;@<a href="mailto:matthew@mwop.net">matthew@mwop.net</a>&quot; will resolve to wherever I am microblogging.
And I removed all references to Twitter on my website; no more &quot;Tweet this&quot; links, no more embedded tweet streams.</p>
<p>I'll continue occasionally posting to Twitter, <em>via my Mastodon account</em>.
But I'm no longer going to visit it to read timelines or mentions.
It'll be like sending the odd communique to the wilderness for now.</p>
<p>Goodbye, Twitter.
You were fun, until you weren't.</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2022-11-28-goodbye-twitter.html">Goodbye Twitter</a> was originally
    published <time class="dt-published" datetime="2022-11-28T14:20:00-06:00">28 November 2022</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
  </channel>
</rss>
