<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text">Blog entries tagged php :: mwop.net</title>
  <updated>2026-04-16T10:30:00-05:00</updated>
  <generator uri="https://getlaminas.org" version="2">Laminas_Feed_Writer</generator>
  <link rel="alternate" type="text/html" href="https://mwop.net/blog/tag/php"/>
  <link rel="self" type="application/atom+xml" href="https://mwop.net/blog/tag/php/atom.xml"/>
  <id>https://mwop.net/blog/tag/php</id>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[PHP DateTimeImmutable::createFromFormat Reset Character]]></title>
    <published>2026-04-16T10:30:00-05:00</published>
    <updated>2026-04-16T10:30:00-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2026-04-16-php-create-from-format-reset.html"/>
    <id>https://mwop.net/blog/2026-04-16-php-create-from-format-reset.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>I was recently building something that was taking date input
from an HTML form field, and casting it to a PHP
<xhtml:code>DateTimeImmutable</xhtml:code>. I was then comparing that to
another date, and got thrown off during testing when I compared the
resulting instance to <xhtml:code>new DateTimeImmutable('today')</xhtml:code>;
the instances were not considered equal.</xhtml:p>
<xhtml:p>To recreate the conditions, you can try the following:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php">$date     = <xhtml:span class="hljs-string">'2016-06-16'</xhtml:span>;
$fromForm = DateTimeImmutable::createFromFormat(<xhtml:span class="hljs-string">'Y-m-d'</xhtml:span>, $date);
$today    = <xhtml:span class="hljs-keyword">new</xhtml:span> DateTimeImmutable(<xhtml:span class="hljs-string">'today'</xhtml:span>);
<xhtml:span class="hljs-keyword">echo</xhtml:span> $fromForm == $today ? <xhtml:span class="hljs-string">'Equal'</xhtml:span> : <xhtml:span class="hljs-string">'Not equal'</xhtml:span>; <xhtml:span class="hljs-comment">// outputs "Not equal"</xhtml:span>
</xhtml:code></xhtml:pre>
<xhtml:p>What's happening? Well, if you were to echo the results of each
of <xhtml:code>$fromForm-&gt;format('c')</xhtml:code> and
<xhtml:code>$today-&gt;format('c')</xhtml:code>, the difference is clear: the
<xhtml:code>$fromForm</xhtml:code> value includes the <xhtml:em>time</xhtml:em> when the
instance was created, while <xhtml:code>$today</xhtml:code> has the time set to
midnight.</xhtml:p>
<xhtml:p>So, how do you zero out the time when using
<xhtml:code>createFromFormat()</xhtml:code>?</xhtml:p>
<xhtml:p>It turns out that one of the format characters you can use is
the <xhtml:code>|</xhtml:code> operator. When you include this at the end of
your format string, any fields not included in the format are
zero'ed out:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php">$fromForm = DateTimeImmutable::createFromFormat(<xhtml:span class="hljs-string">'Y-m-d|'</xhtml:span>);
</xhtml:code></xhtml:pre>
<xhtml:hr/>
<xhtml:h4>Reference</xhtml:h4>
<xhtml:ul>
<xhtml:li><xhtml:a href="https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters">
DateTimeImmutable::createFromFormat() Parameters</xhtml:a></xhtml:li>
</xhtml:ul>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml: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</xhtml:a> was
originally published <xhtml:time class="dt-published" datetime="2026-04-16T10:30:00-05:00">16 April 2026</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Handling PHP-FPM using Caddy]]></title>
    <published>2025-03-21T11:59:06-05:00</published>
    <updated>2025-03-21T11:59:06-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2025-03-21-caddy-php-fpm.html"/>
    <id>https://mwop.net/blog/2025-03-21-caddy-php-fpm.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml: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, <xhtml:code>php_fastcgi</xhtml:code>, which seemed like it would
do the trick, but I found that no traffic was ever getting routed
to my FPM pool.</xhtml:p>
<xhtml:h2>What was happening</xhtml:h2>
<xhtml:p>There's a <xhtml:a href="https://stackoverflow.com/a/77024172">fantastic StackOverflow
answer describing what happens</xhtml:a>, and it's great reading.</xhtml:p>
<xhtml:p>The long and the short of it is that the
<xhtml:code>php_fastcgi</xhtml:code> directive looks for matching files on the
disk, and in the case of paths that look like directories, an
<xhtml:code>index.php</xhtml:code> under that directory. In other words, <xhtml:em>it
cannot match arbitrary paths (aka pretty URLs)</xhtml: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, <xhtml:em>without ever passing it on to PHP-FPM</xhtml:em>.</xhtml:p>
<xhtml:h2>The solution</xhtml:h2>
<xhtml:p>The solution is to use the standard <xhtml:code>reverse_proxy</xhtml:code>
directive, and have it use the <xhtml:code>fastcgi</xhtml:code> transport. You
wrap this in a <xhtml:code>handle</xhtml:code> directive so that you can also
specify a different filesystem root to pass to PHP-FPM.</xhtml:p>
<xhtml:p>As an example, let's assume:</xhtml:p>
<xhtml:ul>
<xhtml:li>I'm running PHP-FPM on port 9000 of the host "app".</xhtml:li>
<xhtml:li>The filesystem root for my PHP application is in
<xhtml:code>/var/www/app.example.org/public</xhtml:code></xhtml:li>
<xhtml:li>I want to route all requests to <xhtml:code>index.php</xhtml:code> in that
root so that they are handled by the application.</xhtml:li>
</xhtml:ul>
<xhtml:p>I can write the Caddyfile as follows:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua">app.example.org {
    # Note that this is the filesystem root <xhtml:span class="hljs-keyword">for</xhtml:span> the _server_
    root * /static/app.example.org

    file_server

    handle {
        # This is the filesystem root <xhtml:span class="hljs-keyword">for</xhtml:span> the FPM pool
        root * /var/www/app.example.org/public
        rewrite index.php
        reverse_proxy app:<xhtml:span class="hljs-number">9000</xhtml:span> {
            transport fastcgi {
                split .php
                capture_stderr
            }
        }
    }
}
</xhtml:code></xhtml:pre>
<xhtml: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
<xhtml:code>index.php</xhtml: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 <xhtml:code>PATH_INFO</xhtml:code>, and your framework
remains happy.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml: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</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2025-03-21T11:59:06-05:00">21 March
2025</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by
<xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew
Weier O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[SQL Nested Queries or Sub Queries with Doctrine DBAL]]></title>
    <published>2025-03-06T10:35:34-06:00</published>
    <updated>2025-03-10T08:24:34-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2025-03-06-dbal-sub-query.html"/>
    <id>https://mwop.net/blog/2025-03-06-dbal-sub-query.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml: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 <xhtml:a href="https://www.doctrine-project.org/projects/doctrine-dbal/en/4.2/index.html">
Doctrine DBAL</xhtml:a> for creating my dynamic queries, and there's no
documentation on how to do them.</xhtml:p>
<xhtml:h3>The problem</xhtml:h3>
<xhtml:p>For my art gallery, the raw SQL looks something like the
following:</xhtml:p>
<xhtml:pre><xhtml:code class="language-sql hljs sql" data-lang="sql"><xhtml:span class="hljs-keyword">SELECT</xhtml:span>
    p.filename,
    p.description,
    p.created,
    array_agg(<xhtml:span class="hljs-keyword">DISTINCT</xhtml:span> t.tag) <xhtml:span class="hljs-keyword">as</xhtml:span> tags
<xhtml:span class="hljs-keyword">FROM</xhtml:span>
    photos p
<xhtml:span class="hljs-keyword">LEFT</xhtml:span> <xhtml:span class="hljs-keyword">JOIN</xhtml:span> tags t <xhtml:span class="hljs-keyword">ON</xhtml:span> p.filename = t.content_id
<xhtml:span class="hljs-keyword">WHERE</xhtml:span>
    p.filename = :filename
    <xhtml:span class="hljs-keyword">AND</xhtml:span> t.content_type = :content_type
<xhtml:span class="hljs-keyword">GROUP</xhtml:span> <xhtml:span class="hljs-keyword">BY</xhtml:span>
    p.filename
</xhtml:code></xhtml:pre>
<xhtml: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 <xhtml:a href="https://www.zend.com/products/zendphp-enterprise/zendhq#tab-panel-16522">
Z-Ray from ZendHQ</xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>Hopefully you can see where this is leading.</xhtml:p>
<xhtml:p>A <xhtml:code>LEFT JOIN</xhtml: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 <xhtml:code>INNER JOIN</xhtml:code>. However, if you put
a condition that is based on a joined table outside the join
itself, it essentially acts like an <xhtml:code>INNER JOIN</xhtml:code>, as
this is now a condition of the <xhtml:code>SELECT</xhtml:code> query.</xhtml:p>
<xhtml:p>Sure enough, when I removed the <xhtml:code>LEFT JOIN</xhtml:code> and the
<xhtml:code>array_agg</xhtml:code> column, I got a hit.</xhtml:p>
<xhtml:h3>"Obvious" solution: move the condition</xhtml:h3>
<xhtml:p>As a commenter on this post noted, the immediate solution is to
move the <xhtml:code>AND t.content_type = :content_type</xhtml:code> clause to
the <xhtml:code>JOIN</xhtml:code>:</xhtml:p>
<xhtml:pre><xhtml:code class="language-sql hljs sql" data-lang="sql"><xhtml:span class="hljs-keyword">SELECT</xhtml:span>
    p.filename,
    p.description,
    p.created,
    array_agg(<xhtml:span class="hljs-keyword">DISTINCT</xhtml:span> t.tag) <xhtml:span class="hljs-keyword">as</xhtml:span> tags
<xhtml:span class="hljs-keyword">FROM</xhtml:span>
    photos p
<xhtml:span class="hljs-keyword">LEFT</xhtml:span> <xhtml:span class="hljs-keyword">JOIN</xhtml:span> tags t <xhtml:span class="hljs-keyword">ON</xhtml:span> p.filename = t.content_id <xhtml:span class="hljs-keyword">AND</xhtml:span> t.content_type = :content_type
<xhtml:span class="hljs-keyword">WHERE</xhtml:span>
    p.filename = :filename
<xhtml:span class="hljs-keyword">GROUP</xhtml:span> <xhtml:span class="hljs-keyword">BY</xhtml:span>
    p.filename
</xhtml:code></xhtml:pre>
<xhtml:p>This does work, and requires no huge changes to the DBAL query
builder; I just move the condition into the
<xhtml:code>joinLeft()</xhtml:code>, and carry on.</xhtml:p>
<xhtml:h3>Preferred solution: nested query</xhtml:h3>
<xhtml: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:</xhtml:p>
<xhtml:pre><xhtml:code class="language-sql hljs sql" data-lang="sql"><xhtml:span class="hljs-keyword">SELECT</xhtml:span>
    p.filename,
    p.description,
    p.created,
    (<xhtml:span class="hljs-keyword">SELECT</xhtml:span> <xhtml:span class="hljs-built_in">ARRAY</xhtml:span>(
        <xhtml:span class="hljs-keyword">SELECT</xhtml:span> <xhtml:span class="hljs-keyword">DISTINCT</xhtml:span> tag <xhtml:span class="hljs-keyword">FROM</xhtml:span> tags <xhtml:span class="hljs-keyword">WHERE</xhtml:span> p.filename = content_id <xhtml:span class="hljs-keyword">AND</xhtml:span> content_type = :content_type
        )) <xhtml:span class="hljs-keyword">as</xhtml:span> tags
<xhtml:span class="hljs-keyword">FROM</xhtml:span>
    photos p
<xhtml:span class="hljs-keyword">WHERE</xhtml:span>
    p.filename = :filename
<xhtml:span class="hljs-keyword">GROUP</xhtml:span> <xhtml:span class="hljs-keyword">BY</xhtml:span>
    p.filename
</xhtml:code></xhtml:pre>
<xhtml:p>With this approach, if no rows are returned from the
<xhtml:code>tags</xhtml:code> table, an empty array is created; otherwise an
array of the tag values that match is returned.</xhtml:p>
<xhtml:p>However, I didn't know how to create this query using the
Doctrine DBAL query builder.</xhtml:p>
<xhtml:h3>DBAL solution</xhtml:h3>
<xhtml:p>When creating a <xhtml:code>SELECT</xhtml:code> using the DBAL query
builder, you do something like this:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php">$select = $dbal-&gt;createQueryBuilder();
$select
    -&gt;select(
        <xhtml:span class="hljs-comment">// one argument per column to select</xhtml:span>
    )
    -&gt;from(<xhtml:span class="hljs-string">'table_name'</xhtml:span>, <xhtml:span class="hljs-string">'t'</xhtml:span>) <xhtml:span class="hljs-comment">// alias the table</xhtml:span>
</xhtml:code></xhtml:pre>
<xhtml:p>The arguments to <xhtml:code>select()</xhtml:code> are expected to be
strings, and any given string can be an arbitrary SQL
expression.</xhtml:p>
<xhtml:p>Creating the subselect is easy; you do it like any other
query:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php">$tags = $dbal-&gt;createQueryBuilder();
$tags
    -&gt;select(<xhtml:span class="hljs-string">'tag'</xhtml:span>)
    -&gt;distinct()
    -&gt;from(<xhtml:span class="hljs-string">'tags'</xhtml:span>)
    -&gt;where(<xhtml:span class="hljs-string">'content_id = p.filename'</xhtml:span>)
    -&gt;andWhere(<xhtml:span class="hljs-string">'content_type = :content_type'</xhtml:span>);
</xhtml:code></xhtml:pre>
<xhtml:p>Now, how do I get that into a column string for a select?</xhtml:p>
<xhtml:p>The <xhtml:code>getSQL()</xhtml:code> method of a query builder will spit
out the SQL sent. Moreover, it <xhtml:strong>does not</xhtml:strong> replace
placeholders, so even if you set a bound parameter, it won't be
injected into the generated SQL.</xhtml:p>
<xhtml:p>Knowing all this, I did the following:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php">$tags = $dbal-&gt;createQueryBuilder();
$tags
    -&gt;select(<xhtml:span class="hljs-string">'tag'</xhtml:span>)
    -&gt;distinct()
    -&gt;from(<xhtml:span class="hljs-string">'tags'</xhtml:span>)
    -&gt;where(<xhtml:span class="hljs-string">'content_id = p.filename'</xhtml:span>)
    -&gt;andWhere(<xhtml:span class="hljs-string">'content_type = :content_type'</xhtml:span>);

$select = $dbal-&gt;createQueryBuilder();
$select
    -&gt;select(
        <xhtml:span class="hljs-string">'p.filename'</xhtml:span>,
        <xhtml:span class="hljs-string">'p.description'</xhtml:span>,
        <xhtml:span class="hljs-string">'p.created'</xhtml:span>,
        sprintf(<xhtml:span class="hljs-string">'(SELECT ARRAY(%s)) as tags'</xhtml:span>, $tags-&gt;getSQL()),
    )
    -&gt;from(<xhtml:span class="hljs-string">'photos'</xhtml:span>, <xhtml:span class="hljs-string">'p'</xhtml:span>)
    -&gt;where(<xhtml:span class="hljs-string">'p.filename = :filename'</xhtml:span>)
    -&gt;groupBy(<xhtml:span class="hljs-string">'p.filename'</xhtml:span>)
    -&gt;setParameter(<xhtml:span class="hljs-string">'filename'</xhtml:span>, $filename, ParameterType::STRING)
    -&gt;setParameter(<xhtml:span class="hljs-string">'content_type'</xhtml:span>, <xhtml:span class="hljs-string">'photo'</xhtml:span>, ParameterType::STRING);
</xhtml:code></xhtml:pre>
<xhtml:p>(Where <xhtml:code>ParameterType</xhtml:code> is imported from the namespace
<xhtml:code>Doctrine\DBAL</xhtml:code>.)</xhtml:p>
<xhtml:p>This approach worked immediately, and generated exactly the same
result as the raw SQL I had tested.</xhtml:p>
<xhtml:h3>Changelog</xhtml:h3>
<xhtml:ul>
<xhtml: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.</xhtml:li>
<xhtml:li>2025-03-10: noted that a <xhtml:code>LEFT JOIN</xhtml:code> will still
work, as long as the <xhtml:code>t.content_type = :content_type</xhtml:code>
condition is moved from the <xhtml:code>SELECT</xhtml:code> to the <xhtml:code>LEFT
JOIN</xhtml:code>.</xhtml:li>
</xhtml:ul>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml: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</xhtml:a> was originally
published <xhtml:time class="dt-published" datetime="2025-03-06T10:35:34-06:00">6 March 2025</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Roundup of PHP 8.4 Posts]]></title>
    <published>2024-12-04T14:03:23-06:00</published>
    <updated>2024-12-13T11:00:23-06:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-12-04-php-8.4-roundup.html"/>
    <id>https://mwop.net/blog/2024-12-04-php-8.4-roundup.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>I've recently written several blog posts for <xhtml:a href="https://www.zend.com">Zend</xhtml:a> (one of the brands I help manage at
<xhtml:a href="https://www.perforce.com">Perforce</xhtml:a>) covering changes
in the recently released <xhtml:a href="https://www.php.net/releases/8.4/en.php">PHP 8.4</xhtml: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:</xhtml:p>
<xhtml:ul>
<xhtml:li><xhtml:a href="https://www.zend.com/blog/php-8-4">What's New in PHP
8.4: Features, Changes, and Deprecations</xhtml:a></xhtml:li>
<xhtml:li><xhtml:a href="https://www.zend.com/blog/php-8-4-property-hooks">A
Guide to PHP 8.4 Property Hooks</xhtml:a></xhtml:li>
<xhtml:li><xhtml:a href="https://www.zend.com/blog/php-asymmetric-visibility">Asymmetric
Visibility in PHP 8.4: What It Means for PHP Teams</xhtml:a></xhtml:li>
<xhtml:li><xhtml:a href="https://www.zend.com/blog/http-verbs-php-8-4">HTTP
Verbs Changes in PHP 8.4</xhtml:a></xhtml:li>
</xhtml:ul>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml: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</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2024-12-04T14:03:23-06:00">4 December
2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by
<xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew
Weier O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Configuring PHP.INI settings in a PHP-FPM pool]]></title>
    <published>2024-08-27T17:37:30-05:00</published>
    <updated>2024-08-27T17:37:30-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-08-27-til-php-fpm-admin-value.html"/>
    <id>https://mwop.net/blog/2024-08-27-til-php-fpm-admin-value.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml: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.</xhtml:p>
<xhtml:p>What was it? quite simply, the <xhtml:code>php_admin_value</xhtml:code>
struct can be used to configure <xhtml:code>php.ini</xhtml:code> settings for
the pool. This is a great alternative to <xhtml:em>also</xhtml:em> adding PHP
configuration settings via <xhtml:code>php.ini</xhtml:code> (or an include file
for <xhtml:code>php.ini</xhtml:code>), as it allows you to keep the settings
specific to that pool. That way, if you <xhtml:em>must</xhtml: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.</xhtml:p>
<xhtml:p>How does it work? In your pool configuration, add values to that
struct:</xhtml:p>
<xhtml:pre><xhtml:code class="language-ini hljs ini" data-lang="ini"><xhtml:span class="hljs-attr">php_admin_value[memory_limit]</xhtml:span> = <xhtml:span class="hljs-number">32</xhtml:span>M
<xhtml:span class="hljs-attr">php_admin_flag[error_reporting]</xhtml:span> = E_ALL &amp; ~E_NOTICE &amp; ~E_DEPRECATED
<xhtml:span class="hljs-attr">php_admin_flat[track_errors]</xhtml:span> = <xhtml:span class="hljs-literal">Off</xhtml:span>
<xhtml:span class="hljs-comment">; etc</xhtml:span>
</xhtml:code></xhtml:pre>
<xhtml:p>With <xhtml:a href="https://www.zend.com/products/zendphp-enterprise">ZendPHP</xhtml: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
<xhtml:code>php_admin_value</xhtml:code> settings to configure the pool for the
application you're deploying!</xhtml:p>
<xhtml:p>Looking forward to simplifying a few of my deployments with
this!</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml: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</xhtml:a> was originally published
<xhtml:time class="dt-published" datetime="2024-08-27T17:37:30-05:00">27
August 2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Initializing ZendHQ JobQueue During Application Deployment]]></title>
    <published>2024-05-07T15:19:07-05:00</published>
    <updated>2024-05-07T15:19:07-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-05-07-zendhq-jq-warmup.html"/>
    <id>https://mwop.net/blog/2024-05-07-zendhq-jq-warmup.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>In the past few years, I've transitioned from engineering into
product management at <xhtml:a href="https://www.zend.com">Zend</xhtml: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!</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:h3>What does it do?</xhtml:h3>
<xhtml: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.</xhtml:p>
<xhtml:p>Why would you use this over, say, a custom queue runner managed
by supervisord, or a tool like Beanstalk, or cronjobs?</xhtml:p>
<xhtml:p>There's a few reasons:</xhtml:p>
<xhtml:ul>
<xhtml: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 <xhtml:em>add</xhtml:em> those
features, but they're not built in.</xhtml:li>
<xhtml: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.</xhtml:li>
<xhtml: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.</xhtml:li>
<xhtml: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.</xhtml:li>
</xhtml:ul>
<xhtml:h3>So, what about queue warmup?</xhtml:h3>
<xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>As a quick example:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php"><xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">ZendHQ</xhtml:span>\<xhtml:span class="hljs-title">JobQueue</xhtml:span>\<xhtml:span class="hljs-title">HTTPJob</xhtml:span>;
<xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">ZendHQ</xhtml:span>\<xhtml:span class="hljs-title">JobQueue</xhtml:span>\<xhtml:span class="hljs-title">JobOptions</xhtml:span>;
<xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">ZendHQ</xhtml:span>\<xhtml:span class="hljs-title">JobQueue</xhtml:span>\<xhtml:span class="hljs-title">JobQueue</xhtml:span>;
<xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">ZendHQ</xhtml:span>\<xhtml:span class="hljs-title">JobQueue</xhtml:span>\<xhtml:span class="hljs-title">Queue</xhtml:span>;
<xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">ZendHQ</xhtml:span>\<xhtml:span class="hljs-title">JobQueue</xhtml:span>\<xhtml:span class="hljs-title">QueueDefinition</xhtml:span>;
<xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">ZendHQ</xhtml:span>\<xhtml:span class="hljs-title">JobQueue</xhtml:span>\<xhtml:span class="hljs-title">RecurringSchedule</xhtml:span>;

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

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

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

    <xhtml:span class="hljs-comment">// Schedule to run every 15 minutes</xhtml:span>
    $queue-&gt;scheduleJob($job, <xhtml:span class="hljs-keyword">new</xhtml:span> RecurringSchedule(<xhtml:span class="hljs-string">'* */15 * * * *'</xhtml:span>));
}
</xhtml:code></xhtml:pre>
<xhtml:p>That's literally it.</xhtml:p>
<xhtml:p>The takeaway points:</xhtml:p>
<xhtml:ul>
<xhtml: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.</xhtml:li>
<xhtml: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.</xhtml:li>
</xhtml:ul>
<xhtml:h3>Running one-off jobs on deployment</xhtml:h3>
<xhtml: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.</xhtml:p>
<xhtml:p>Because these scripts run <xhtml:em>before</xhtml: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 <xhtml:em>immediately</xhtml:em>, because there's nothing to answer
them.</xhtml:p>
<xhtml:p>The answer to this is to queue a job <xhtml:em>in the future</xhtml:em>:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php"><xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">DateTimeImmutable</xhtml:span>;
<xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">ZendHQ</xhtml:span>\<xhtml:span class="hljs-title">JobQueue</xhtml:span>\<xhtml:span class="hljs-title">ScheduledTime</xhtml:span>;

$queue-&gt;scheduleJob($job, <xhtml:span class="hljs-keyword">new</xhtml:span> ScheduledTime(<xhtml:span class="hljs-keyword">new</xhtml:span> DateTimeImmutable(<xhtml:span class="hljs-string">'+1 minute'</xhtml:span>)));
</xhtml:code></xhtml:pre>
<xhtml: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.)</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:h3>Closing notes</xhtml:h3>
<xhtml: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 <xhtml:em>just like the rest of my PHP
application</xhtml:em> is hugely powerful. I'm looking forward to seeing
what others build with it!</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml: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</xhtml:a> was originally
published <xhtml:time class="dt-published" datetime="2024-05-07T15:19:07-05:00">7 May 2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Advent 2023: PSR-15]]></title>
    <published>2023-12-14T17:21:00-06:00</published>
    <updated>2023-12-14T17:21:00-06:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2023-12-14-advent-psr-15.html"/>
    <id>https://mwop.net/blog/2023-12-14-advent-psr-15.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>I've mentioned a few times over the course of this <xhtml:a href="https://mwop.net/blog/tag/advent2023">2023 Advent series</xhtml:a> that
the longer I'm in the tech field, the more I appreciate and favor
<xhtml:em>simple</xhtml:em> solutions. I was reminded of this yesterday when I
<xhtml: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</xhtml:a> by
<xhtml:a href="https://joelclermont.com/">Joel Clermont</xhtml:a>.</xhtml:p>
<xhtml:blockquote>
<xhtml:h4>Request</xhtml:h4>
<xhtml:p>Please, <xhtml:em>please</xhtml:em>, <xhtml:strong><xhtml:em>please</xhtml:em></xhtml: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.</xhtml:p>
</xhtml:blockquote>
<xhtml:h3>Summarize the article, already...</xhtml:h3>
<xhtml: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.</xhtml:p>
<xhtml:p>In other words, it introduces brittleness.</xhtml:p>
<xhtml:h3>So what?</xhtml:h3>
<xhtml: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 <xhtml:em>prevents</xhtml: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.</xhtml:p>
<xhtml:h3>What are the alternatives?</xhtml:h3>
<xhtml:p>I worked for quite some time on <xhtml:a href="https://www.php-fig.org/psr/psr-7">PSR-7 HTTP Message
Interfaces</xhtml: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 <xhtml:a href="https://www.php-fig.org/psr/psr-15">PSR-15 HTTP Server Request
Handlers</xhtml:a> (which I sponsored and collaborated on, but was not
primary author of).</xhtml:p>
<xhtml: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 <xhtml:em>all</xhtml:em> you can return.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>Instead, your handler will <xhtml:em>create a response</xhtml: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 <xhtml:code>Location</xhtml:code> header. And so on and on.</xhtml:p>
<xhtml:p>Yes, this can lead to a little extra code at times, but:</xhtml:p>
<xhtml:ul>
<xhtml:li>You can see <xhtml:em>exactly</xhtml:em> what you intend to return to the
user, and <xhtml:em>why</xhtml:em>.</xhtml:li>
<xhtml:li>If you try and return anything <xhtml:em>but</xhtml:em> a response, it'll
result in a <xhtml:code>TypeError</xhtml:code>.</xhtml:li>
<xhtml: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.</xhtml:li>
</xhtml:ul>
<xhtml:p>But should you do <xhtml:em>everything</xhtml: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?</xhtml:p>
<xhtml:p>For those things, PSR-15 provides <xhtml:em>middleware</xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>Building complex behavior via piping one thing to another is
hugely powerful. There's a reason that the <xhtml:a href="https://en.wikipedia.org/wiki/Unix_philosophy">Unix Philosophy</xhtml:a>
has existed as long as it has, and I can appreciate an approach to
web development that builds on it.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2023-12-14-advent-psr-15.html">Advent 2023:
PSR-15</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2023-12-14T17:21:00-06:00">14 December 2023</xhtml:time> on
<xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Advent 2023: Doctrine DBAL]]></title>
    <published>2023-12-10T11:00:00-06:00</published>
    <updated>2023-12-11T09:21:00-06:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2023-12-10-advent-dbal.html"/>
    <id>https://mwop.net/blog/2023-12-10-advent-dbal.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>My alternatives have been straight PDO, or Doctrine DBAL, with
the latter being my preference.</xhtml:p>
<xhtml:h3>Doctrine <xhtml:em>what</xhtml:em>?</xhtml:h3>
<xhtml:p>When most folks who use PHP hear "Doctrine", they immediately
think "<xhtml:a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping">ORM</xhtml:a>";
it's how most folks use it, and what it's best known for.</xhtml:p>
<xhtml:p>Underlying the ORM is its database abstraction layer (hence
"DBAL"). 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 <xhtml:em>by itself</xhtml:em>, without the ORM.</xhtml:p>
<xhtml:h3>Why no ORM?</xhtml:h3>
<xhtml: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?</xhtml:p>
<xhtml:p>So, how does <xhtml:em>Matthew</xhtml:em> write code that interacts with the
database?</xhtml:p>
<xhtml: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 <xhtml:em><xhtml:a href="https://martinfowler.com/eaaCatalog/repository.html">repository</xhtml:a></xhtml: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.</xhtml:p>
<xhtml: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 <xhtml:a href="https://verraes.net/2014/06/named-constructors-in-php/">named
constructor</xhtml: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.</xhtml:p>
<xhtml:h3>Some gripes about the documentation, and some tips</xhtml:h3>
<xhtml:p>The <xhtml:a href="https://www.doctrine-project.org/projects/doctrine-dbal/en/current/index.html">
Doctrine DBAL docs</xhtml:a> are a bit sparse, particularly when it comes
to its <xhtml:a href="https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/query-builder.html">
SQL abstraction</xhtml:a>. And there's no "getting started" or "basic
usage" 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:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php"><xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">Doctrine</xhtml:span>\<xhtml:span class="hljs-title">DBAL</xhtml:span>\<xhtml:span class="hljs-title">DriverManager</xhtml:span>;

$connectionParams = [
    <xhtml:span class="hljs-string">'dbname'</xhtml:span>   =&gt; <xhtml:span class="hljs-string">'mydb'</xhtml:span>,
    <xhtml:span class="hljs-string">'user'</xhtml:span>     =&gt; <xhtml:span class="hljs-string">'user'</xhtml:span>,
    <xhtml:span class="hljs-string">'password'</xhtml:span> =&gt; <xhtml:span class="hljs-string">'secret'</xhtml:span>,
    <xhtml:span class="hljs-string">'host'</xhtml:span>     =&gt; <xhtml:span class="hljs-string">'localhost'</xhtml:span>,
    <xhtml:span class="hljs-string">'driver'</xhtml:span>   =&gt; <xhtml:span class="hljs-string">'pdo_mysql'</xhtml:span>,
];
$conn = DriverManager::getConnection($connectionParams);
</xhtml:code></xhtml:pre>
<xhtml: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 "data source name").</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>And that brings me to the SQL abstraction.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>But the documentation could be better.</xhtml:p>
<xhtml: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!)</xhtml:p>
<xhtml:blockquote>
<xhtml: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.</xhtml:p>
</xhtml:blockquote>
<xhtml: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
<xhtml:em>how to actually execute the query</xhtml:em>! So, for those
wondering:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php">$sql = $conn-&gt;createQueryBuilder();

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

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

<xhtml:span class="hljs-comment">// Execute a query that produces changes (INSERT, UPDATE, DELETE, etc.):</xhtml:span>
$count = $sql-&gt;executeStatement();
</xhtml:code></xhtml:pre>
<xhtml:p>Query results have a variety of <xhtml:code>fetch*()</xhtml:code> operations
on them, while executing a statement returns an integer indicating
the number of rows affected (assuming the database supports
this).</xhtml:p>
<xhtml: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:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php">$sql-&gt;innerJoin(
    $primaryTableOrItsAliasIfYouSpecifiedOne, <xhtml:span class="hljs-comment">// e.g. "user" or "u"</xhtml:span>
    $newTableToJoin,                          <xhtml:span class="hljs-comment">// e.g. "address"</xhtml:span>
    $aliasForNewTableToJoin,                  <xhtml:span class="hljs-comment">// e.g. "a"</xhtml:span>
    $conditionToJoinOn                        <xhtml:span class="hljs-comment">// e.g. "u.id = a.uid"</xhtml:span>
);
</xhtml:code></xhtml:pre>
<xhtml:p>Third, there's some odd differences in the API between INSERT
and UPDATE operations., When setting a value, one takes
<xhtml:code>setValue()</xhtml:code>, while the other takes <xhtml:code>set()</xhtml:code>,
and only one of these is valid for a given operation (it's
<xhtml:code>setValue()</xhtml:code> for INSERT operations, and
<xhtml:code>set()</xhtml:code> for UPDATE operations, in case you were
wondering). This is especially confusing when using bound
parameters, because <xhtml:em>both</xhtml:em> can use the
<xhtml:code>setParameter()</xhtml:code> method for binding positional
placeholder values.</xhtml:p>
<xhtml:p>Speaking of plaeholders, the docs don't do a great job of
detailing how to handle <xhtml:em>placeholders</xhtml:em> gracefully.</xhtml:p>
<xhtml:p>The documentation suggests patterns like this:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php">$queryBuilder
    -&gt;select(<xhtml:span class="hljs-string">'id'</xhtml:span>, <xhtml:span class="hljs-string">'name'</xhtml:span>)
    -&gt;from(<xhtml:span class="hljs-string">'users'</xhtml:span>)
    -&gt;where(<xhtml:span class="hljs-string">'email = ?'</xhtml:span>)
    -&gt;setParameter(<xhtml:span class="hljs-number">0</xhtml:span>, $userInputEmail);
</xhtml:code></xhtml:pre>
<xhtml: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?</xhtml:p>
<xhtml:p>The answer is in the docs, but the various <xhtml:em>examples</xhtml: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:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php">$queryBuilder
    -&gt;select(<xhtml:span class="hljs-string">'id'</xhtml:span>, <xhtml:span class="hljs-string">'name'</xhtml:span>)
    -&gt;from(<xhtml:span class="hljs-string">'users'</xhtml:span>)
    -&gt;where(<xhtml:span class="hljs-string">'email = '</xhtml:span> . $queryBuilder-&gt;createNamedParameter($userInputEmail));
</xhtml:code></xhtml:pre>
<xhtml:p>There's also a <xhtml:code>createPositionalParameter()</xhtml:code> method.
Both accept an optional second argument, where you can specify the
value <xhtml:em>type</xhtml:em>, which can help ensure that values are quoted
correctly for the SQL type they will map to. This also allows you
to do <xhtml:code>IN()</xhtml:code> operations, and each value will be quoted
correctly, with the appropriate list separator for the
database.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:blockquote>
<xhtml:h4>2023-12-11 Update</xhtml:h4>
<xhtml:p><xhtml:a href="https://mastodon.social/@nesl247">Alexander Kim</xhtml:a>
pointed out to me that you can use named parameters within the
query builder, along with the <xhtml:code>setParameter()</xhtml:code> method.
That usage looks like this:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php"><xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">Doctrine</xhtml:span>\<xhtml:span class="hljs-title">DBAL</xhtml:span>\<xhtml:span class="hljs-title">ParameterType</xhtml:span>;

$queryBuilder
    -&gt;select(<xhtml:span class="hljs-string">'id'</xhtml:span>, <xhtml:span class="hljs-string">'name'</xhtml:span>)
    -&gt;from(<xhtml:span class="hljs-string">'users'</xhtml:span>)
    -&gt;where(<xhtml:span class="hljs-string">'email = :email'</xhtml:span>)
    -&gt;setParameter(<xhtml:span class="hljs-string">'email'</xhtml:span>, $userInputEmail, ParameterType::STRING);
</xhtml:code></xhtml:pre>
<xhtml:p>You can also specify named parameters when using
<xhtml:code>set()</xhtml:code> and <xhtml:code>setValue()</xhtml:code>, though I'd argue
that using <xhtml:code>createNamedParameter()</xhtml:code> is easier in those
contexts.</xhtml:p>
</xhtml:blockquote>
<xhtml:p>But for all these issues, the fact is that the docs generally
give you <xhtml:em>enough</xhtml: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 <xhtml:em>very</xhtml:em> solid, <xhtml:em>very</xhtml:em> well written, and
absolutely something I can depend on.</xhtml:p>
<xhtml:h3>Final Thoughts</xhtml:h3>
<xhtml: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 "just work".</xhtml:p>
<xhtml:p>I also find the way it juggles <xhtml:em>types</xhtml: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
<xhtml:em>exactly</xhtml:em> the type of functionality I want from a DBAL.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2023-12-10-advent-dbal.html">Advent 2023:
Doctrine DBAL</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2023-12-10T11:00:00-06:00">10 December
2023</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by
<xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew
Weier O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Advent 2023: Forms]]></title>
    <published>2023-12-09T11:00:00-06:00</published>
    <updated>2023-12-09T11:00:00-06:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2023-12-09-advent-forms.html"/>
    <id>https://mwop.net/blog/2023-12-09-advent-forms.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml: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 <xhtml:em>lot</xhtml:em> of requests:</xhtml:p>
<xhtml:ul>
<xhtml:li>Ability to normalize values</xhtml:li>
<xhtml:li>Ability to validate values</xhtml:li>
<xhtml:li>Ability to get validation error messages</xhtml:li>
<xhtml:li>Ability to render HTML forms, and have customizable markup</xhtml:li>
<xhtml:li>Ability to do nested values</xhtml:li>
<xhtml:li>Ability to handle optional values</xhtml:li>
<xhtml:li>Ability to report missing values</xhtml:li>
</xhtml:ul>
<xhtml: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.</xhtml:p>
<xhtml:p><xhtml:a href="https://framework.zend.com/manual/1.12/en/zend.form.html">Zend_Form</xhtml:a>
was considered a big enough new feature that we actually bumped the
version from 1.0 to 1.5 to call it out.</xhtml:p>
<xhtml:p>And, honestly, in hindsight, it was a mistake.</xhtml:p>
<xhtml:h3>A mistake?</xhtml:h3>
<xhtml: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.</xhtml:p>
<xhtml:p>I first realized the issues when we started integrating the
<xhtml:a href="https://dojotoolkit.org/">Dojo Toolkit</xhtml: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
<xhtml:em>decorators</xhtml:em>, which were what we used to render form
elements. While the library gave us this flexibility, I saw a few
issues:</xhtml:p>
<xhtml:ul>
<xhtml:li><xhtml:strong>Duplication.</xhtml: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.</xhtml:li>
<xhtml:li><xhtml:strong>Javascript</xhtml: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 <xhtml:em>by
javascript</xhtml: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...</xhtml:li>
<xhtml:li><xhtml:strong>APIs.</xhtml: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 <xhtml:em>web forms</xhtml:em> becomes less
interesting.</xhtml:li>
</xhtml:ul>
<xhtml: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 <xhtml:code>Zend\InputFilter</xhtml:code> to address the first, and
<xhtml:code>Zend\Form</xhtml: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.</xhtml:p>
<xhtml:p>But I still feel we didn't get it right:</xhtml:p>
<xhtml:ul>
<xhtml:li>Our validation component and our input filter component were
each <xhtml:em>stateful</xhtml: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.</xhtml:li>
<xhtml: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 <xhtml:em>never</xhtml:em> fall into the
defaults the framework provides. Even if the framework provides
customization features, the end result is <xhtml:em>more</xhtml: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.</xhtml:li>
</xhtml:ul>
<xhtml: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.</xhtml:p>
<xhtml:h3>Uh oh, I did it again</xhtml:h3>
<xhtml:p>Until recently.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>I came up with a few goals:</xhtml:p>
<xhtml:ul>
<xhtml:li>Provide an idempotent way to validate individual items and/or
data sets.</xhtml:li>
<xhtml:li>Provide an extensible framework for developing validation
rules.</xhtml:li>
<xhtml:li>Allow handling optional data, with default values.</xhtml:li>
<xhtml:li>Allow reporting validation error messages.</xhtml:li>
<xhtml:li>Ensure missing required values are reported as validation
failures.</xhtml:li>
<xhtml:li>Use as few dependencies as possible.</xhtml:li>
</xhtml:ul>
<xhtml:p>I also came up with some explicit <xhtml:em>non-goals</xhtml:em>:</xhtml:p>
<xhtml:ul>
<xhtml:li>Creating an extensive set of validation rule classes.</xhtml:li>
<xhtml:li>Providing extensive mechanisms for validating and returning
nested data sets.</xhtml:li>
<xhtml:li>Providing a configuration-driven mechanism for creating rule
sets.</xhtml:li>
<xhtml:li>Providing HTML form input representations or all metadata
required to create HTML form input representations.</xhtml:li>
</xhtml:ul>
<xhtml: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 <xhtml:a href="https://www.rfc-editor.org/rfc/rfc7807">Problem Details for HTTP
APIs</xhtml:a> message. In the case of a web form, I'd be able to extract
values, validation status, and validation error messages.</xhtml:p>
<xhtml:p>One thing I realized early on was that it was also useful to be
able to represent a form's <xhtml:em>initial state</xhtml:em>. This would allow
using the same template for both the initial form, as well as
reporting form validation errors later.</xhtml:p>
<xhtml: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 <xhtml:em>type</xhtml: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.</xhtml:p>
<xhtml:p>The result is my <xhtml:a href="https://github.com/phly/phly-rule-validation">phly/phly-rule-validation</xhtml: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.</xhtml:p>
<xhtml:p>The basic idea is:</xhtml:p>
<xhtml:ul>
<xhtml:li>You create a <xhtml:em>rule set</xhtml:em>, consisting of
<xhtml:em>rules</xhtml:em>.</xhtml:li>
<xhtml:li>A <xhtml:em>rule</xhtml:em> defines:
<xhtml:ul>
<xhtml:li>The <xhtml:em>key</xhtml:em> it maps to in the data set being
validated.</xhtml:li>
<xhtml:li>A method for <xhtml:em>validating</xhtml:em> a value, which produces a
<xhtml:em>result</xhtml:em>.</xhtml:li>
<xhtml:li>A way to produce <xhtml:em>results</xhtml:em> for each of a
<xhtml:em>default</xhtml:em> value, and when the value is
<xhtml:em>missing</xhtml:em>.</xhtml:li>
</xhtml:ul>
</xhtml:li>
<xhtml:li>Rule validation produces a <xhtml:em>result</xhtml:em>, which composes:
<xhtml:ul>
<xhtml:li>The <xhtml:em>key</xhtml:em> associated with the result.</xhtml:li>
<xhtml:li>The <xhtml:em>value</xhtml:em> associated with the result. The validation
routine <xhtml:em>can</xhtml: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.</xhtml:li>
<xhtml:li>The <xhtml:em>validation state</xhtml:em>: is it valid, or not?</xhtml:li>
<xhtml:li>The validation <xhtml:em>message</xhtml:em>: this will generally only be
populated for <xhtml:em>invalid</xhtml:em> values, and representes a validation
<xhtml:em>error message</xhtml:em>.</xhtml:li>
</xhtml:ul>
</xhtml:li>
<xhtml:li>A <xhtml:em>rule set</xhtml:em> produces a <xhtml:em>result set</xhtml:em>, which is a
collection of <xhtml:em>results</xhtml:em>.</xhtml:li>
</xhtml:ul>
<xhtml:p>In all cases, there are static analysis templates provided to
allow defining the <xhtml:em>types</xhtml:em>. A validation result allows
defining the <xhtml:em>value type</xhtml:em>, and a result set allows mapping
keys to specific result types. Rules return result types. And so
on.</xhtml:p>
<xhtml:p>A rule set can produce a <xhtml:em>valid result set</xhtml:em>, and this can
be used to seed the initial state of a form. And I built support
for <xhtml:em>nested results</xhtml:em>, which allows having forms that have
groups of data.</xhtml:p>
<xhtml:p>The library provides usage examples, and I wrote <xhtml:a href="https://github.com/phly/phly-rule-validation/tree/0.2.x/docs">quite
a bit of documentation</xhtml:a>, if you want to see how it works.</xhtml:p>
<xhtml:h3>Some thoughts</xhtml:h3>
<xhtml: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.</xhtml:p>
<xhtml: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 <xhtml:em>how the library works</xhtml:em>; it's pretty
clear how it <xhtml:em>will</xhtml:em> work just from viewing the classes I've
created.</xhtml:p>
<xhtml:p>One benefit of creating the library is that it helped me better
understand <xhtml:a href="https://psalm.dev">Psalm</xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2023-12-09-advent-forms.html">Advent 2023:
Forms</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2023-12-09T11:00:00-06:00">9 December 2023</xhtml:time> on
<xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Goodbye Twitter]]></title>
    <published>2022-11-28T14:20:00-06:00</published>
    <updated>2022-11-28T14:20:00-06:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2022-11-28-goodbye-twitter.html"/>
    <id>https://mwop.net/blog/2022-11-28-goodbye-twitter.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>This is a long, personal post.</xhtml:p>
<xhtml:p><xhtml:strong>tl;dr</xhtml:strong>: I'm leaving Twitter. You can find me in
the <xhtml:a href="https://fediverse.info/">Fediverse</xhtml:a> as @<xhtml:a href="mailto:matthew@mwop.net">matthew@mwop.net</xhtml:a>.</xhtml:p>
<xhtml:h2>In the beginning</xhtml:h2>
<xhtml:p>I started using Twitter because of ZendCon 2007. <xhtml:a href="https://calevans.com">Cal Evans</xhtml: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.)</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>Over the next year or two, I found it became my "virtual
watercooler." 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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>When I realized folks couldn't spell my handle, I reached out
<xhtml:em>on Twitter</xhtml:em> to see if I knew somebody <xhtml:em>at Twitter</xhtml:em>,
or if somebody had a friend at Twitter, to see if I could change my
handle, as somebody was squatting on "mwop". A friend of a friend
made it happen — and I made a new friend in the process.</xhtml:p>
<xhtml:p>That was the honeymoon period, it seems.</xhtml:p>
<xhtml:h2>The start of the fall</xhtml:h2>
<xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>But I still had to see them at least once.</xhtml:p>
<xhtml:h2>Crumbling</xhtml:h2>
<xhtml:p>And then 2016 came along.</xhtml:p>
<xhtml: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
<xhtml:em>more</xhtml:em> liberal.</xhtml:p>
<xhtml:p>And the run-up to the 2016 US elections broke us.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>So following the election, I started pulling back.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>But here's the thing: we are <xhtml:em>all</xhtml: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.</xhtml:p>
<xhtml:p>I started checking Twitter less and less frequently.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml: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 <xhtml:a href="https://circle.so">circle.so</xhtml: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.</xhtml:p>
<xhtml:p>But I also kept checking my creator lists on Twitter; it was a
habit I couldn't quite shake.</xhtml:p>
<xhtml:h2>Chaos</xhtml:h2>
<xhtml:p>And then Musk came along, and threw his ego and money around,
announcing his intent to buy Twitter.</xhtml:p>
<xhtml:p>When he announced his intent in April of this year, I remembered
Mastodon, and remembered that the <xhtml:a href="https://phpc.social">PHP
community had an instance</xhtml: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.</xhtml:p>
<xhtml:p>But I still kept checking my creator lists on Twitter; I missed
these creators, and wanted to follow their work.</xhtml:p>
<xhtml:p>And then the Twitter deal closed, and now Musk was "in charge".
(The quotes are deliberate; his flailing hardly feels like somebody
in charge.)</xhtml:p>
<xhtml:p>And I just can't.</xhtml:p>
<xhtml: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</xhtml:p>
<xhtml:p>I just can't.</xhtml:p>
<xhtml: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
"world's town hall" if that means having to argue with people who
do not bring arguments to the table in good faith.</xhtml:p>
<xhtml:h2>Where I'll be</xhtml:h2>
<xhtml:p>And... I don't have to.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml: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 "spin up your own instance" is a valid
answer, the fact is that if your audience grows, you can DDoS your
instance really quickly any time you post.</xhtml:p>
<xhtml:p>But it's also important to note that Mastodon has been chugging
along for years <xhtml:em>already</xhtml: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.</xhtml:p>
<xhtml:p>It's good enough, at least for now.</xhtml:p>
<xhtml: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 "official" app). I modified my browser such
that the words "twitter" or "tweetdeck" now suggest the <xhtml:a href="https://pinafore.social">site I use with my Mastodon accounts</xhtml:a>.
I setup <xhtml:a href="https://webfinger.net/">WebFinger</xhtml:a> on my
website, such that "@<xhtml:a href="mailto:matthew@mwop.net">matthew@mwop.net</xhtml:a>" will resolve to
wherever I am microblogging. And I removed all references to
Twitter on my website; no more "Tweet this" links, no more embedded
tweet streams.</xhtml:p>
<xhtml:p>I'll continue occasionally posting to Twitter, <xhtml:em>via my
Mastodon account</xhtml: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.</xhtml:p>
<xhtml:p>Goodbye, Twitter. You were fun, until you weren't.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2022-11-28-goodbye-twitter.html">Goodbye
Twitter</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2022-11-28T14:20:00-06:00">28 November 2022</xhtml:time> on
<xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
</feed>
