<?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/">
  <channel>
    <title>DEV Community: Georges Cubas</title>
    <description>The latest articles on DEV Community by Georges Cubas (@shagshag).</description>
    <link>https://dev.to/shagshag</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F321413%2Fddee429a-eb7c-41ce-9d24-b932a6e4dfd0.jpeg</url>
      <title>DEV Community: Georges Cubas</title>
      <link>https://dev.to/shagshag</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shagshag"/>
    <language>en</language>
    <item>
      <title>A Dockerfile that says where it failed: splitting the RUN and retrying pecl install</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Wed, 21 Oct 2026 21:13:22 +0000</pubDate>
      <link>https://dev.to/shagshag/a-dockerfile-that-says-where-it-failed-splitting-the-run-and-retrying-pecl-install-j9g</link>
      <guid>https://dev.to/shagshag/a-dockerfile-that-says-where-it-failed-splitting-the-run-and-retrying-pecl-install-j9g</guid>
      <description>&lt;p&gt;&lt;em&gt;A CI build broken by a 504 from pecl.php.net, buried in 40 lines of &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On 20 October 2026, &lt;a href="https://www.php.net/archive/2026.php#2026-10-20-1" rel="noopener noreferrer"&gt;the PHP security team announced two critical vulnerabilities&lt;/a&gt;. The announcement was of an unusual scale and got wide coverage (&lt;a href="https://www.reuters.com/world/asia-pacific/eu-agency-warns-websites-may-need-be-taken-offline-php-prepares-patches-2026-10-21/" rel="noopener noreferrer"&gt;official websites taken offline as a precaution around the world&lt;/a&gt;, &lt;a href="https://blog.cloudflare.com/php-vulnerabilities-waf-protection/" rel="noopener noreferrer"&gt;Cloudflare's reaction&lt;/a&gt;, &lt;a href="https://openai.com/index/coordinated-disclosure-of-critical-vulnerabilities/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;...). For us it had an unexpected effect. During the day, our application's builds started failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failing build
&lt;/h2&gt;

&lt;p&gt;I looked at my company's application &lt;a href="https://docs.docker.com/reference/dockerfile/" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt;. Its &lt;code&gt;base&lt;/code&gt; stage starts from the &lt;a href="https://hub.docker.com/_/php" rel="noopener noreferrer"&gt;official PHP image&lt;/a&gt; and installs the dependencies. About forty lines are grouped in a single &lt;a href="https://docs.docker.com/reference/dockerfile/" rel="noopener noreferrer"&gt;&lt;code&gt;RUN&lt;/code&gt;&lt;/a&gt;, chained with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;: apt packages, locale, PHP extensions, &lt;a href="https://getcomposer.org/" rel="noopener noreferrer"&gt;Composer&lt;/a&gt; installation, &lt;code&gt;php.ini&lt;/code&gt;, &lt;a href="https://pecl.php.net/" rel="noopener noreferrer"&gt;PECL&lt;/a&gt; extensions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    libfreetype-dev &lt;span class="se"&gt;\
&lt;/span&gt;    libjpeg62-turbo-dev &lt;span class="se"&gt;\
&lt;/span&gt;    libpng-dev &lt;span class="se"&gt;\
&lt;/span&gt;    libxslt-dev &lt;span class="se"&gt;\
&lt;/span&gt;    libz-dev &lt;span class="se"&gt;\
&lt;/span&gt;    ...
    &amp;amp;&amp;amp; pecl install redis \
    &amp;amp;&amp;amp; pecl install ssh2-1.4 \
    &amp;amp;&amp;amp; rm -rf /tmp/pear \
    &amp;amp;&amp;amp; docker-php-ext-enable redis \
    &amp;amp;&amp;amp; docker-php-ext-enable ssh2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker only showed this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;process "/bin/sh -c apt-get update &amp;amp;&amp;amp; ..." did not complete successfully: exit code: 1&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No indication of which command in the chain had failed. I first suspected a new Composer version. With a single &lt;code&gt;RUN&lt;/code&gt;, there was no way to tell. I went to check: Composer had not released a new version, so the cause was elsewhere. We decided to split the &lt;code&gt;RUN&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting into several RUNs
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;RUN&lt;/code&gt; becomes a numbered step in &lt;a href="https://docs.docker.com/reference/cli/docker/buildx/build/" rel="noopener noreferrer"&gt;&lt;code&gt;docker build --progress=plain&lt;/code&gt;&lt;/a&gt;, for example &lt;code&gt;[base 7/8] RUN pecl install ...&lt;/code&gt;. It is &lt;a href="https://docs.docker.com/build/cache/" rel="noopener noreferrer"&gt;cached&lt;/a&gt; independently of the others. The error points straight at the right block, with the Dockerfile line number.&lt;/p&gt;

&lt;p&gt;I made six &lt;code&gt;RUN&lt;/code&gt;s: packages, locale, PHP extensions, Composer, &lt;code&gt;php.ini&lt;/code&gt;, PECL. A giant &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; blinds Docker because the unit of error is the instruction, not the command. (&lt;code&gt;set -eux&lt;/code&gt; would have traced every command, but the build would have stayed a single block, with no intermediate cache.)&lt;/p&gt;

&lt;p&gt;On the next build, the error was on the &lt;a href="https://www.php.net/manual/en/install.pecl.php" rel="noopener noreferrer"&gt;PECL installation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;buildx failed with: ERROR: failed to build: failed to solve: process "/bin/sh -c pecl install redis &amp;amp;&amp;amp; pecl install ssh2-1.4 &amp;amp;&amp;amp; rm -rf /tmp/pear &amp;amp;&amp;amp; docker-php-ext-enable redis &amp;amp;&amp;amp; docker-php-ext-enable ssh2" did not complete successfully: exit code: 1&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That let me find this in the log, a bit higher up:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Could not download from "&lt;a href="https://pecl.php.net/get/redis-6.3.0.tgz" rel="noopener noreferrer"&gt;https://pecl.php.net/get/redis-6.3.0.tgz&lt;/a&gt;", cannot download "pecl/redis" (File &lt;a href="https://pecl.php.net:443/get/redis-6.3.0.tgz" rel="noopener noreferrer"&gt;https://pecl.php.net:443/get/redis-6.3.0.tgz&lt;/a&gt; not valid (received: HTTP/1.1 504 Gateway Timeout))&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The problem wasn't in the code: pecl.php.net was overloaded. I assume the wave of updates that followed PHP's announcement saturated the server.&lt;/p&gt;

&lt;p&gt;So our build wasn't broken by the vulnerability, which hadn't even been fixed yet. It was broken by everyone's reaction to the announcement of the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking for a PECL mirror
&lt;/h2&gt;

&lt;p&gt;Next I looked for a pecl.php.net mirror to use as a fallback. There is no official mirror you can use that way. The real alternatives are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compile the extensions from the GitHub tarballs with &lt;a href="https://www.php.net/manual/en/install.pecl.phpize.php" rel="noopener noreferrer"&gt;&lt;code&gt;phpize&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;use &lt;a href="https://github.com/mlocati/docker-php-extension-installer" rel="noopener noreferrer"&gt;mlocati/docker-php-extension-installer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;publish the base image to a registry so nothing gets downloaded at every deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is the cleanest, but it is a project of its own. Adding a tool while the build was broken seemed risky to me, so I went for the simplest option: repeated attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retry, in three versions
&lt;/h2&gt;

&lt;p&gt;To keep it simple and change as little as possible, I first put both extensions in the same loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;pecl &lt;span class="nb"&gt;install &lt;/span&gt;redis ssh2 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;10&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing the failure case, I saw this wasn't viable. If &lt;code&gt;redis&lt;/code&gt; installs and &lt;code&gt;ssh2&lt;/code&gt; fails, the next attempt fails on &lt;code&gt;redis&lt;/code&gt; with "already installed", which returns a non-zero exit code. The loop can never converge. It needs one loop per extension.&lt;/p&gt;

&lt;p&gt;Second version, one loop per extension. If all five attempts fail, the &lt;code&gt;for&lt;/code&gt; ends without a &lt;code&gt;break&lt;/code&gt; and returns 0. The build then only fails at &lt;code&gt;docker-php-ext-enable&lt;/code&gt;, with a message that has nothing to do with the problem. It needs a flag and an explicit &lt;code&gt;exit 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Final version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# pecl.php.net can answer 504 when overloaded: retry each extension separately&lt;/span&gt;
&lt;span class="c"&gt;# (a combined install would fail on retry with "already installed"), then fail loudly.&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;for &lt;/span&gt;ext &lt;span class="k"&gt;in &lt;/span&gt;redis ssh2-1.4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="nv"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="k"&gt;for &lt;/span&gt;attempt &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;            &lt;span class="k"&gt;if &lt;/span&gt;pecl &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;            &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pecl install &lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="s2"&gt; failed (attempt &lt;/span&gt;&lt;span class="nv"&gt;$attempt&lt;/span&gt;&lt;span class="s2"&gt;/5), retrying..."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;            &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;attempt &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ok&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; 1 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;            &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: pecl install &lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="s2"&gt; failed after 5 attempts"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;            &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="k"&gt;fi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/pear&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    docker-php-ext-enable redis ssh2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    php &lt;span class="nt"&gt;--ri&lt;/span&gt; redis &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    php &lt;span class="nt"&gt;--ri&lt;/span&gt; ssh2 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wait grows with each attempt (10 s, 20 s, 30 s, 40 s, 50 s), 150 s in total for an extension that fails five times. The last &lt;code&gt;sleep&lt;/code&gt; is useless, since it comes right before giving up. I left it in to avoid complicating the Dockerfile.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.php.net/manual/en/features.commandline.options.php" rel="noopener noreferrer"&gt;&lt;code&gt;php --ri redis&lt;/code&gt;&lt;/a&gt; at the end of the block checks that the extension is really loaded. &lt;a href="https://hub.docker.com/_/php" rel="noopener noreferrer"&gt;&lt;code&gt;docker-php-ext-enable&lt;/code&gt;&lt;/a&gt; can succeed even though the extension doesn't load (a missing dependency, for example). In that case &lt;code&gt;php --ri&lt;/code&gt; fails, and so does the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the failure
&lt;/h2&gt;

&lt;p&gt;The Dockerfile is &lt;a href="https://docs.docker.com/build/building/multi-stage/" rel="noopener noreferrer"&gt;multi-stage&lt;/a&gt;. The &lt;code&gt;base&lt;/code&gt; stage installs the packages and the PHP extensions, and the following ones (Composer dependencies, front-end assets) build on it. So I only build &lt;code&gt;base&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--progress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;plain &lt;span class="nt"&gt;--target&lt;/span&gt; base &lt;span class="nt"&gt;-f&lt;/span&gt; .docker/Dockerfile &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check the failure case, I temporarily replaced &lt;code&gt;ssh2-1.4&lt;/code&gt; with &lt;code&gt;ssh2-9.9&lt;/code&gt; (a version that doesn't exist) in a copy of the Dockerfile. The output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pecl install ssh2-9.9 failed (attempt 5/5), retrying...
ERROR: pecl install ssh2-9.9 failed after 5 attempts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I haven't tested the retry against a real 504, only with a nonexistent version. A nonexistent version is a permanent error, whereas a 504 is temporary. This test shows that the failure does surface, not that the retry eventually succeeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changes
&lt;/h2&gt;

&lt;p&gt;I'm keeping this solution. The separate &lt;code&gt;RUN&lt;/code&gt;s tell me which step failed and benefit from the cache. The retry saves me from relaunching a build over a passing 504.&lt;/p&gt;

&lt;p&gt;It doesn't solve the rest. &lt;code&gt;pecl install redis&lt;/code&gt; without a version installs the latest one, so the build isn't reproducible. It still depends on a third-party server at every deployment, and it won't survive an outage of several hours. It's a modest improvement, suited to the incident I had.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>php</category>
      <category>devops</category>
      <category>ci</category>
    </item>
    <item>
      <title>The code I never wrote</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:27:29 +0000</pubDate>
      <link>https://dev.to/shagshag/the-code-i-never-wrote-3adg</link>
      <guid>https://dev.to/shagshag/the-code-i-never-wrote-3adg</guid>
      <description>&lt;p&gt;At my company's annual seminar, my manager said AI wrote about 70% of the code. I replied that for me it was closer to 90%. The other developers agreed with me. Afterwards, they pointed out that it might not have been very wise to say it like that, and my manager and the director seemed surprised.&lt;/p&gt;

&lt;p&gt;I thought about it afterwards. My number is an estimate based on my practice with Claude Code, I haven't measured anything. But more than that, we probably weren't talking about the same thing. I was talking about the code that ends up in the files. He was perhaps talking about the share of the development work that AI takes on.&lt;/p&gt;

&lt;p&gt;That conversation made me realize that a percentage of code written means nothing until you say where you draw the line. And that line has already moved several times since I started programming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cut my teeth on an Amstrad
&lt;/h2&gt;

&lt;p&gt;I'm not old enough to have known punch cards, but I started in the late 1980s on an Amstrad CPC 464. You switched the machine on and landed directly on the command prompt. You could type &lt;code&gt;PRINT 1&lt;/code&gt; and it answered. Programming was a normal experience, not a separate activity.&lt;/p&gt;

&lt;p&gt;My father had computer magazines that published programs to copy in BASIC. I remember spending hours on the listings of Cherry-Paint, a drawing program published by &lt;a href="https://cpcrulez.fr/GamesTest//applications_graphic-cherry-paint_CC.htm" rel="noopener noreferrer"&gt;CPC Revue in 1986&lt;/a&gt; (in French). According to the site, the program's code was spread over five issues, and the first one had about 20 KB of code to type in (we were motivated). Part of it was BASIC, and the animation was machine code given as hexadecimal numbers. In the second installment (CPC Revue no. 6), the author provided a BASIC loader for this that asked for eight hexadecimal numbers per line, followed by a checksum. If the computed total didn't match, it asked for the line again. I didn't understand what I was copying, but the computer could tell me when I had made a mistake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssb3t8156eaffpt58y3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssb3t8156eaffpt58y3y.png" alt="Screenshot of Cherry-Paint on Amstrad CPC" width="768" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9hxca92t2m4dbihybjcj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9hxca92t2m4dbihybjcj.jpg" alt="Revue CPC no. 8 (1986) - CHERRY PAINT" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Amstrad's machine language is the binary instructions executed directly by the processor. Assembly is its symbolic representation, easier for a human to read. I reread the beginning of the book &lt;em&gt;Le Langage Machine pour l'Amstrad CPC&lt;/em&gt; (in French). In the section "&lt;a href="https://archive.org/details/07-le-langage-machine-pour-l-amstrad-cpc-1985acme/page/n15/mode/2up" rel="noopener noreferrer"&gt;Le premier programme machine&lt;/a&gt;" (the first machine program), it puts a BASIC program that fills the screen memory next to the same job in machine code. The BASIC takes about a minute and the machine code about a tenth of a second. That is a ratio of 600, going by its own figures. The chapter's argument is that you have every reason to learn machine language.&lt;/p&gt;

&lt;p&gt;Few people did, and I understand why. BASIC wasn't very sophisticated but it was much easier to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I stopped writing
&lt;/h2&gt;

&lt;p&gt;In the 1990s, I was definitely a geek. I played around with &lt;a href="https://en.wikipedia.org/wiki/Visual_Basic_(classic)" rel="noopener noreferrer"&gt;Visual Basic&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Delphi_(software)" rel="noopener noreferrer"&gt;Delphi&lt;/a&gt;. I did Windows assembly programming, C++, a bit of shell scripting on &lt;a href="https://en.wikipedia.org/wiki/Mandriva_Linux" rel="noopener noreferrer"&gt;Mandrake Linux&lt;/a&gt;, and then wrote a Pac-Man clone in assembly for a &lt;a href="https://en.wikipedia.org/wiki/TI-89_series" rel="noopener noreferrer"&gt;TI-89&lt;/a&gt; during my engineering school prep classes (can you get any nerdier?). In the early 2000s, I discovered PHP 3, an interpreted language. Then JavaScript, object-oriented programming, frameworks, patterns, Java, Node.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Electric_Dreams_(film)" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz6nnh1304hhjtqlkuewo.png" alt="Electric Dreams 1984, A really geek movie and the first AI I remember" width="770" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At each stage, there was a part of the code that I stopped writing. With Delphi, I no longer wrote the window display. With PHP, I no longer managed memory by hand or went through a compilation step. With Symfony, I don't write the HTTP kernel, the service container or the database access layer. On one of the company's projects, I counted the lines of PHP, blank lines and comments included, tests excluded. The &lt;code&gt;src/&lt;/code&gt; folder contains 198,702 and &lt;code&gt;vendor/&lt;/code&gt; 1,910,041 (development dependencies included, so it's an order of magnitude). That means about nine lines out of ten are not in our code. And &lt;code&gt;src/&lt;/code&gt; already contains code written or rewritten by AI, so the share I wrote myself is even smaller. I have never written most of the code that runs when someone opens a page.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

   &lt;strong&gt;note&lt;/strong&gt;&lt;br&gt;
It's funny because those 90% land almost exactly on my estimate from the seminar, but it means nothing, they don't measure the same thing.&lt;br&gt;

&lt;/div&gt;


&lt;p&gt;Nobody disputes that, because it doesn't show. If you had asked a Symfony developer ten years ago "how much of the code do you write yourself", the answer would have been low too, with a different line.&lt;/p&gt;

&lt;p&gt;By the way, my job has never been to write code. A taxi driver's job isn't to know how to read a map or to burn fuel: it's to choose the route, handle the unexpected and answer for the condition of the vehicle. Mine is to understand the need behind the request, to decide how (and sometimes whether) to build it, then to take responsibility for the consequences long after the code has been written.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more layer
&lt;/h2&gt;

&lt;p&gt;With Claude Code, I describe what I want in French. The agent produces PHP, which the PHP interpreter runs, itself compiled to machine code. Natural language has become one more layer in a stack that already had many.&lt;/p&gt;

&lt;p&gt;DHH says it in his own way. In his &lt;a href="https://youtu.be/vDjW_dRyKXY?t=2287" rel="noopener noreferrer"&gt;opening keynote at Rails World 2026&lt;/a&gt;, on September 23, he states at around the 38-minute mark that "English is a better programming language than Ruby", adding that it's "a little more vague". He explains there that 37signals (the company behind Basecamp and HEY) has stopped writing code by hand. On one point, he goes further than I do: the Rust code his agents produce, he says he doesn't look at, he judges the result from the outside. He also explains why abstractions were chosen in the first place: a programmer's time was expensive. If a machine writes the low-level code, he says in essence, you can have programs 30 to 100 times faster. It's the same speed argument as in the book of my childhood.&lt;/p&gt;

&lt;p&gt;Natural language is also the first layer that isn't a formal language. A line of PHP has one meaning and only one. A sentence in French can have several, and the same agent can produce two different results from the same sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I still read
&lt;/h2&gt;

&lt;p&gt;At each layer, I first stopped writing the code it produced, then I no longer needed to read it. With the agent, I'm at the first stage: I write very little PHP, but I still read it, and even more than before. I don't know whether that's temporary. I'm writing this in October 2026 and practice is changing fast.&lt;/p&gt;

&lt;p&gt;Some are already further along. &lt;a href="https://simonwillison.net/2026/May/6/vibe-coding-and-agentic-engineering/" rel="noopener noreferrer"&gt;Simon Willison&lt;/a&gt; no longer reviews every line his agents write, even for production, and treats them like another team whose service he uses without reading the code. He is uneasy about it himself. A human team has a reputation to defend and answers for its work, whereas Claude Code has no professional reputation. He also worries that, as it keeps succeeding, he will end up trusting it at the wrong moment. He relies on his 25 years of experience to keep that bet reasonable.&lt;/p&gt;

&lt;p&gt;I don't read the assembly the compiler produces. I don't read Symfony's code either, except when something doesn't work. I trust those layers because they always behave the same way, millions of people use them and a problem eventually gets fixed for everyone.&lt;/p&gt;

&lt;p&gt;An agent's output is work done for me, at that moment, from a description that can be understood in several ways. The result can be correct, or correct with one change too many, or unrelated to what I wanted. I can't yet treat it the way I treat a compiler. If I ever do, it won't be because it became fashionable, but because I will have checked it often enough to trust it.&lt;/p&gt;

&lt;p&gt;DHH has already made up his mind about Rust, he doesn't read it. I haven't made up my mind about PHP yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to 70 and 90%
&lt;/h2&gt;

&lt;p&gt;My manager was perhaps counting the work, and I was counting the code. Both numbers can coexist, they don't draw the line in the same place. I could have given the same kind of answer in the 2000s when talking about the framework, with a different number.&lt;/p&gt;

&lt;p&gt;What strikes me is that we keep judging a developer's work by what they write, even though the share they write themselves has been shrinking at every stage since I started programming, without anyone worrying about it. AI moves the line much faster than usual, and it shows. That's what made the conversation at the seminar a bit odd. Whether AI writes 70 or 90% of the code interests me less than knowing from what point a layer you no longer read stops being a bet.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>aws-ecs-shell: stop rereading the ECS Exec docs before every intervention</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Sat, 10 Oct 2026 18:47:15 +0000</pubDate>
      <link>https://dev.to/shagshag/aws-ecs-shell-stop-rereading-the-ecs-exec-docs-before-every-intervention-128e</link>
      <guid>https://dev.to/shagshag/aws-ecs-shell-stop-rereading-the-ecs-exec-docs-before-every-intervention-128e</guid>
      <description>&lt;p&gt;A few weeks ago, a badly initialized &lt;code&gt;locale&lt;/code&gt; sent me digging inside an ECS task in staging. That produced &lt;a href="https://dev.to/shagshag/the-dockerfile-said-frfrutf-8-php-saw-cutf-8-m0j"&gt;an article on iconv&lt;/a&gt; and &lt;a href="https://www.shagshag.net/developpement/ecs-exec-en-pratique-retrouver-sa-tache-ouvrir-la-session-passer-un-script-sans-se-battre-avec-le-quoting" rel="noopener noreferrer"&gt;a guide to ECS Exec&lt;/a&gt; (in French). Later, a PrestaShop back office became unreachable and I had to go through ECS Exec again, this time in production, to check the instance's actual configuration, the running processes and how long they had been running (&lt;a href="https://www.shagshag.net/en/14-minutes-of-cache-warmup-on-efs-when-a-prestashop-back-office-becomes-unreachable" rel="noopener noreferrer"&gt;that incident is written up here&lt;/a&gt;). The right fix there was to run a cache command directly on the instance. The script I'm describing here didn't exist yet, and it played no part in the diagnosis or the fix. It came afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sequence
&lt;/h2&gt;

&lt;p&gt;Both times, I had to reread my own documentation before I could open the shell. To start a session with ECS Exec, you need the cluster, the service, the task ID and the container name. That's four &lt;code&gt;aws ecs ...&lt;/code&gt; commands in a row, each one fed with a value copied from the previous output. The guide covers all of this, so I won't go over it again.&lt;/p&gt;

&lt;p&gt;You don't get onto an instance often, and it's better if it stays that way. So every time, I've forgotten the exact sequence. It usually happens when you're in a hurry, with your head already full of the problem. In production, juggling &lt;code&gt;aws&lt;/code&gt; commands you no longer know by heart adds stress you could do without. On top of that, a task ID changes with every deployment, so no command can be replayed as is from one time to the next.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;a href="https://github.com/Shagshag/aws-ecs-shell" rel="noopener noreferrer"&gt;aws-ecs-shell&lt;/a&gt;, a Bash script of about 250 lines, written with the help of AI like pretty much everything I do these days.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;With no arguments, the script shows numbered menus: clusters, then services, then running tasks, then containers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./aws-ecs-shell.sh
1) alpha
2) beta
Cluster (number, Ctrl-D to cancel) &amp;gt; 2
Service: web
1) 7f3c0e1a9b…  (started 2026-10-09T10:00:00Z)
2) c41d88b2e0…  (started 2026-10-08T09:00:00Z)
Task (number, Ctrl-D to cancel) &amp;gt; 1
Container: app
To skip the menus next time: ./aws-ecs-shell.sh --cluster beta --service web --container app
Connecting to ECS task 7f3c0e1a9b… (cluster: beta, container: app)...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A menu is skipped when its value is passed as an option (&lt;code&gt;--cluster&lt;/code&gt;, &lt;code&gt;--service&lt;/code&gt;, &lt;code&gt;--task&lt;/code&gt;, &lt;code&gt;--container&lt;/code&gt;, &lt;code&gt;--region&lt;/code&gt;, &lt;code&gt;--profile&lt;/code&gt;) or as an environment variable. A menu with a single entry is picked automatically, which is the most common case for the service and the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three choices I find useful
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The replay command.&lt;/strong&gt; Before connecting, the script prints the command line that skips the menus next time (&lt;code&gt;To skip the menus next time&lt;/code&gt;). I don't have to remember the names, I just copy them. The task ID is left out when the service had only one task, since it would be useless after the next deployment. A service with several tasks keeps its ID in the command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The no-terminal mode.&lt;/strong&gt; When standard input isn't a terminal (cron, a pipe, another script), there are no menus. The script then requires &lt;code&gt;--cluster&lt;/code&gt;, &lt;code&gt;--service&lt;/code&gt; or &lt;code&gt;--task&lt;/code&gt;, and &lt;code&gt;--container&lt;/code&gt; if the task has several, and it lists the available containers in the error message. It never hangs waiting for input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A single session with a real TTY.&lt;/strong&gt; The script ends with &lt;code&gt;exec aws ecs execute-command --interactive&lt;/code&gt;. That means one persistent SSM session: &lt;code&gt;cd&lt;/code&gt; and &lt;code&gt;export&lt;/code&gt; carry over from one command to the next, and &lt;code&gt;vim&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt; and Ctrl-C behave normally.&lt;/p&gt;

&lt;p&gt;The shell it starts is &lt;code&gt;bash&lt;/code&gt; if the image has it, &lt;code&gt;sh&lt;/code&gt; otherwise. &lt;code&gt;execute-command&lt;/code&gt; splits &lt;code&gt;--command&lt;/code&gt; on spaces and executes the first word without going through a shell, so an &lt;code&gt;if&lt;/code&gt; can't be passed as is. The test is base64-encoded and decoded inside the container, with &lt;code&gt;${IFS}&lt;/code&gt; standing in for the spaces so that the whole thing stays a single word. It's the same idea as the one in the guide for passing a script, boiled down to one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest
&lt;/h2&gt;

&lt;p&gt;At startup, the script checks that &lt;code&gt;aws&lt;/code&gt; and &lt;code&gt;session-manager-plugin&lt;/code&gt; are in the &lt;code&gt;PATH&lt;/code&gt; and that the credentials work, then that the task exists and has ECS Exec enabled. Each failure gives a message saying what to fix, rather than a raw API error.&lt;/p&gt;

&lt;p&gt;Two details come from the fact that I work on Windows. Under Git Bash, &lt;code&gt;aws.exe&lt;/code&gt; ends its lines with a CRLF, and a stray &lt;code&gt;\r&lt;/code&gt; in an ARN is enough for ECS to reject it with &lt;code&gt;Unexpected number of separators&lt;/code&gt;. The script strips those &lt;code&gt;\r&lt;/code&gt;. It's also compatible with Bash 3.2, the version that ships with macOS.&lt;/p&gt;

&lt;p&gt;The script only reads the ECS API to build its menus and changes nothing on the AWS side. But once you're in the shell, everything you type runs in the environment you connected to. It doesn't make an intervention any less risky, it just makes it quicker to start.&lt;/p&gt;

&lt;p&gt;The code is MIT-licensed: &lt;a href="https://github.com/Shagshag/aws-ecs-shell" rel="noopener noreferrer"&gt;github.com/Shagshag/aws-ecs-shell&lt;/a&gt;. The prerequisites (AWS CLI, Session Manager plugin, a task started with &lt;code&gt;enableExecuteCommand&lt;/code&gt;, a task role with &lt;code&gt;ssmmessages:*&lt;/code&gt;) are in the README.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>bash</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A solarpunk garden for your GitHub profile, generated daily from your contributions</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Thu, 08 Oct 2026 22:44:10 +0000</pubDate>
      <link>https://dev.to/shagshag/a-solarpunk-garden-for-your-github-profile-generated-daily-from-your-contributions-4k3f</link>
      <guid>https://dev.to/shagshag/a-solarpunk-garden-for-your-github-profile-generated-daily-from-your-contributions-4k3f</guid>
      <description>&lt;p&gt;&lt;em&gt;solarpunk: soft colors, nature and gentle technology, the opposite of cyberpunk's dark neon&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I turned my GitHub contribution graph into an isometric garden. One tile per day, a plant that grows with the number of contributions, seasons that follow the calendar, and a small gardener walking around today's tile. It is a plain SVG with CSS and SMIL animations, regenerated every day by a GitHub Action. Python standard library only, no dependencies, MIT license: &lt;a href="https://github.com/Shagshag/garden-graph" rel="noopener noreferrer"&gt;Shagshag/garden-graph&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I built it with Claude Code. I decided what the garden should look like and judged every render by eye. The assistant wrote most of the code and ran the command-line checks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzwk72r4dc80t8ywi7nx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzwk72r4dc80t8ywi7nx.gif" alt="The one-year view, light version on the upper right and dark version on the lower left"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: You can see the animated SVG in the &lt;a href="https://github.com/Shagshag/garden-graph/blob/main/README.md" rel="noopener noreferrer"&gt;README of the repository&lt;/a&gt;, or open the raw files in the &lt;a href="https://github.com/Shagshag/garden-graph/tree/main/assets" rel="noopener noreferrer"&gt;assets folder&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article shows how to put the same garden on your own profile, and how to adapt it to where you live.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌱 Where the idea came from
&lt;/h3&gt;

&lt;p&gt;I started from Giorgi Kobaidze's article &lt;a href="https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c"&gt;I turned my GitHub profile into a cyberpunk console, with a city built from my contributions&lt;/a&gt;. &lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c" class="crayons-story__hidden-navigation-link"&gt;I Turned My GitHub Profile Into a Cyberpunk Console With a City Built From My Contributions&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Bypassing GitHub's strict sandbox via SVGs&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/georgekobaidze" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F55651%2F3ad144e9-ca91-4395-b73a-9a0a3d843af9.jpg" alt="georgekobaidze profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/georgekobaidze" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Giorgi Kobaidze
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Giorgi Kobaidze
                &lt;a href="/++"&gt;&lt;img alt="Subscriber" class="subscription-icon" src="https://assets.dev.to/assets/subscription-icon-805dfa7ac7dd660f07ed8d654877270825b07a92a03841aa99a1093bd00431b2.png"&gt;&lt;/a&gt;
                
              
              &lt;div id="story-author-preview-content-4781665" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/georgekobaidze" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F55651%2F3ad144e9-ca91-4395-b73a-9a0a3d843af9.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Giorgi Kobaidze&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 1&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c" id="article-link-4781665"&gt;
          I Turned My GitHub Profile Into a Cyberpunk Console With a City Built From My Contributions
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/github"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;github&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/design"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;design&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;253&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;span class="crayons-story__favorited"&gt;
              &lt;span class="favorited-marker"&gt;
                &lt;span&gt;
                  

                &lt;/span&gt;
                &lt;span class="hidden"&gt;
                  

                &lt;/span&gt;
              &lt;/span&gt;
            &lt;/span&gt;
            &lt;a href="https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              162&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            15 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;I took the general shape of the project from it. A daily GitHub Action calls the GraphQL API, a Python script writes an SVG, the isometric view is drawn from the back to the front (no 3D engine), the animations are CSS inside the SVG, and the workflow commits only if the files changed. The article also explained that &lt;code&gt;contributionsCollection&lt;/code&gt; covers one year per request (see the &lt;a href="https://docs.github.com/en/graphql/reference/users" rel="noopener noreferrer"&gt;GraphQL reference&lt;/a&gt;), and that you ask for more with one aliased field per year. I do it slightly differently, see the annex.&lt;/p&gt;

&lt;p&gt;That inspiration was cyberpunk: a city, dark, neon. I wanted something fresher and more pleasant. I am also making a solarpunk restoration game, so the colors are soft and close to nature, and the wind turbines recall the gentle energy side of the theme.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌳 What's in the garden
&lt;/h3&gt;

&lt;p&gt;Each day is a tile. The plant on it depends on how active that day was compared with your other active days (quartiles): bare soil for no contribution, then a sprout, a flower, a tree, and for the busiest quarter a wind turbine with a solar panel. Days that have not happened yet are packed earth. The season follows the date, and every file exists in a light and a dark version.&lt;/p&gt;

&lt;p&gt;Three views are generated: the current year folded in two half-years (big tiles, easy to read), an overview of the last five years, and a view made for phones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flncx0a8habnq1aeoy7yj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flncx0a8habnq1aeoy7yj.gif" alt="The five-year overview, light version on the upper right and dark version on the lower left"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpeqykwqvk2u80x7ab81d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpeqykwqvk2u80x7ab81d.gif" alt="The mobile view, light version on the upper right and dark version on the lower left"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🪏 Setting it up on your profile
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Get the files into your own repository: either fork it, or create &lt;code&gt;&amp;lt;your-login&amp;gt;/&amp;lt;your-login&amp;gt;&lt;/code&gt; (the repository GitHub uses for your profile README) and copy the files in. Enable GitHub Actions.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;.github/workflows/garden.yml&lt;/code&gt; and pick your climate and language:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GARDEN_CLIMATE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temperate-north&lt;/span&gt;  &lt;span class="c1"&gt;# see the table below&lt;/span&gt;
  &lt;span class="na"&gt;GARDEN_LANG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fr&lt;/span&gt;                  &lt;span class="c1"&gt;# fr (the default), en, ja or hi&lt;/span&gt;
  &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_STATS_TOKEN || secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Run the &lt;strong&gt;Update garden&lt;/strong&gt; workflow once from the Actions tab.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After that it runs every day at 05:17 UTC (&lt;code&gt;17 5 * * *&lt;/code&gt;) and commits only when something changed. That makes one commit per day, and a run takes about 15 seconds.&lt;/p&gt;

&lt;p&gt;By default the garden is drawn for the owner of the repository, so a fork shows its own owner's garden, not mine. Set &lt;code&gt;GITHUB_LOGIN&lt;/code&gt; only to show someone else's contributions. To also count your private contributions, add a secret &lt;code&gt;GH_STATS_TOKEN&lt;/code&gt; (a token with &lt;code&gt;read:user&lt;/code&gt;) and enable "Private contributions" in your profile settings. &lt;code&gt;GARDEN_YEARS&lt;/code&gt; (5 by default) sets the number of years in the overview.&lt;/p&gt;
&lt;h3&gt;
  
  
  🏡 Adapting the garden to where you live
&lt;/h3&gt;

&lt;p&gt;A contribution graph does not know about seasons, and most people do not live in four neat ones. The first version was written for the northern hemisphere, so it was wrong in Australia. A six-month shift fixed that but only covered temperate climates, so I moved to a &lt;code&gt;GARDEN_CLIMATE&lt;/code&gt; setting.&lt;/p&gt;

&lt;p&gt;I looked at where GitHub accounts are to decide which climates to offer. In the official &lt;a href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/" rel="noopener noreferrer"&gt;Octoverse 2025 post&lt;/a&gt;, the United States comes first with 28 million developers, India second with 21.9 million, and Brazil is fourth with 6.89 million. China is also in the top ten. That gave seven climates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;GARDEN_CLIMATE&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Seasons&lt;/th&gt;
&lt;th&gt;For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temperate-north&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;spring, summer, autumn, winter&lt;/td&gt;
&lt;td&gt;North America, Europe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temperate-south&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the same, shifted by six months&lt;/td&gt;
&lt;td&gt;southern Australia, New Zealand, Argentina, South Africa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;monsoon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cool, hot, monsoon, post-monsoon&lt;/td&gt;
&lt;td&gt;India, Bangladesh, Sri Lanka, Nepal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tropical-south&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wet (Nov to Apr), dry (May to Oct)&lt;/td&gt;
&lt;td&gt;central Brazil, Indonesia, southern Africa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tropical-north&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wet (May to Oct), dry (Nov to Apr)&lt;/td&gt;
&lt;td&gt;the Sahel, Central America, mainland Southeast Asia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;china-southeast&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;mild winter, spring rains, humid summer and typhoons, clear autumn&lt;/td&gt;
&lt;td&gt;Guangdong, Fujian, Hong Kong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;japan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sakura, spring, tsuyu, summer, autumn, winter&lt;/td&gt;
&lt;td&gt;Honshu&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The months are averages. A specific region (the east coast of India, Kerala) can have its rains at other dates, so a climate is an approximation. In Japan a month can be cut in two, because tsuyu runs until mid-July.&lt;/p&gt;

&lt;p&gt;If your region is not in the list, you add a climate in &lt;code&gt;render.py&lt;/code&gt;: one entry in &lt;code&gt;CLIMATES&lt;/code&gt; (which season each month belongs to, and the order of the legend) and one entry in &lt;code&gt;PALETTE&lt;/code&gt; for each new season (ground color, leaves, flower colors, and whether the trees carry blossoms or fruit, or there are puddles or snow). For example, the two-season tropics of the northern hemisphere are:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tropical-north&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_months&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5 6 7 8 9 10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;11 12 1 2 3 4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The drawing code does not test season names, it reads the palette, so nothing else has to change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj4ox1pn0arwmdv9y43jo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj4ox1pn0arwmdv9y43jo.png" alt="The monsoon climate"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe04lx5h29mpyaxm4deu6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe04lx5h29mpyaxm4deu6.png" alt="The japan climate"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The monsoon climate, then the japan climate.&lt;/p&gt;

&lt;p&gt;Languages work the same way. &lt;code&gt;GARDEN_LANG&lt;/code&gt; takes &lt;code&gt;fr&lt;/code&gt;, &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;ja&lt;/code&gt; or &lt;code&gt;hi&lt;/code&gt;. The texts in &lt;code&gt;i18n.py&lt;/code&gt; are complete sentence templates with placeholders (&lt;code&gt;{login}&lt;/code&gt;, &lt;code&gt;{total}&lt;/code&gt;, &lt;code&gt;{first}&lt;/code&gt;, &lt;code&gt;{last}&lt;/code&gt;), not words glued together, because word order and punctuation change between languages. To add a language, copy an entry of &lt;code&gt;STRINGS&lt;/code&gt; and translate it. Numbers follow local usage, including the Indian grouping (12,34,567).&lt;/p&gt;
&lt;h3&gt;
  
  
  🥗 Putting it in your README
&lt;/h3&gt;

&lt;p&gt;Embed the year view in your profile README with &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt;, which serves a light and a dark image. The switch with &lt;code&gt;&amp;lt;source media="(prefers-color-scheme: dark)"&amp;gt;&lt;/code&gt; comes from Kera Cudmore's &lt;a href="https://dev.to/keracudmore/github-readme-images-based-on-prefers-color-scheme-5cp8"&gt;GitHub README images based on prefers-color-scheme&lt;/a&gt; and from the "Adding an image to suit your visitors" section of the &lt;a href="https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/quickstart-for-writing-on-github" rel="noopener noreferrer"&gt;GitHub quickstart for writing on GitHub&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/keracudmore/github-readme-images-based-on-prefers-color-scheme-5cp8" class="crayons-story__hidden-navigation-link"&gt;GitHub README images based on prefers-color-scheme&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/keracudmore" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F864006%2F13e1098a-fe01-424a-9dce-900632b21a0a.jpeg" alt="keracudmore profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/keracudmore" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Kera Cudmore
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Kera Cudmore
                
                
              
              &lt;div id="story-author-preview-content-2726299" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/keracudmore" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F864006%2F13e1098a-fe01-424a-9dce-900632b21a0a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Kera Cudmore&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/keracudmore/github-readme-images-based-on-prefers-color-scheme-5cp8" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 27 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/keracudmore/github-readme-images-based-on-prefers-color-scheme-5cp8" id="article-link-2726299"&gt;
          GitHub README images based on prefers-color-scheme
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/github"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;github&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/keracudmore/github-readme-images-based-on-prefers-color-scheme-5cp8" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/keracudmore/github-readme-images-based-on-prefers-color-scheme-5cp8#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 600px) and (prefers-color-scheme: dark)"&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"assets/garden-mobile-dark.svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 600px)"&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"assets/garden-mobile-light.svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(prefers-color-scheme: dark)"&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"assets/garden-year-dark.svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Garden of my contributions"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"assets/garden-year-light.svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters, the first match wins.&lt;/p&gt;

&lt;h3&gt;
  
  
  📱 Three views, three sizes
&lt;/h3&gt;

&lt;p&gt;The part about phones is mine. Neither of the two sources above deals with it. On GitHub the 1000 px image is displayed a bit smaller, and the tiles of the five-year overview end up under 20 px wide, which is hard to read for a single day. That is why the one-year view exists, folded in two half-years so the tiles are about twice as big.&lt;/p&gt;

&lt;p&gt;On a phone it is worse: the image is shrunk to about a third of its size and the title falls to around 8 px. The third view, &lt;code&gt;garden-mobile-*.svg&lt;/code&gt;, has a narrow canvas, a bigger text and the legend under the garden. The first two &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; lines above pick it with a &lt;code&gt;max-width&lt;/code&gt; media query combined with the color scheme.&lt;/p&gt;

&lt;p&gt;If you also embed the five-year overview, it is too fine for a phone. An &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; cannot be hidden, so on a phone the repository replaces it with &lt;code&gt;blank.svg&lt;/code&gt;, an SVG of one pixel. That is a workaround. The README of the repository does it with a second &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 600px)"&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"assets/blank.svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(prefers-color-scheme: dark)"&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"assets/garden-dark.svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"The same garden over the last five years"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"assets/garden-light.svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🐛 Problems along the way
&lt;/h3&gt;

&lt;p&gt;Two things broke along the way, and only one of them was a real bug.&lt;/p&gt;

&lt;p&gt;The wind turbines first turned with a CSS animation, which broke as soon as the plants were scaled. They now use SVG's own animation. Screenshots are frozen, so the movement was checked by advancing the virtual time of a headless browser.&lt;/p&gt;

&lt;p&gt;The gardener walks a few tiles around today's tile. The first version walked a fixed distance, so at the start and end of the year he left the field, since the first and last weeks can be partial.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzz7i1ofmu3d3yprhm42r.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzz7i1ofmu3d3yprhm42r.gif" alt="Wile E. Coyote falling"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The walk is now limited to the tiles that exist on his row, and its duration follows the distance so the speed stays the same. It was checked on January 1st, January 10th, December 20th and December 31st.&lt;/p&gt;

&lt;h3&gt;
  
  
  🐞 Similar projects
&lt;/h3&gt;

&lt;p&gt;I am not the first to draw contributions as a garden. &lt;a href="https://github.com/a104437ana/sakura-garden" rel="noopener noreferrer"&gt;a104437ana/sakura-garden&lt;/a&gt; draws them as flowers in an SVG for a README, and &lt;a href="https://github.com/qrstajalli/BranchOut" rel="noopener noreferrer"&gt;qrstajalli/BranchOut&lt;/a&gt; turns the last year into a pixel-art garden. garden-graph differs by its isometric view, the seven climates, the four languages, and the phone view.&lt;/p&gt;

&lt;h3&gt;
  
  
  🐌 Limits and where it stands
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Japanese and Hindi texts have not been reviewed by a native speaker.&lt;/li&gt;
&lt;li&gt;The fonts are not embedded in the SVG, so Japanese and Hindi need a matching font on the viewer's machine.&lt;/li&gt;
&lt;li&gt;I tested on github.com (the repository page and my profile page), not in the GitHub mobile app and not on other sites.&lt;/li&gt;
&lt;li&gt;On a phone, the combination of mobile and dark follows the system theme, not necessarily the theme chosen inside GitHub.&lt;/li&gt;
&lt;li&gt;The tiles stay small on a phone, because the width is fixed.&lt;/li&gt;
&lt;li&gt;The months of each climate are averages, so a climate is an approximation by region.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remaining work: a review of the Japanese and Hindi texts, embedded fonts, and a test in the GitHub mobile app.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧮 Annex: fetching several years in one request
&lt;/h3&gt;

&lt;p&gt;Giorgi's article gets the list of active years with &lt;code&gt;contributionYears&lt;/code&gt;, then builds one aliased field per year. garden-graph does the same with one difference: the number of years is fixed (&lt;code&gt;GARDEN_YEARS&lt;/code&gt;, 5 by default), so there is no first query. &lt;code&gt;fetch.py&lt;/code&gt; takes the current year and goes back from there, then sends a single request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: contributionsCollection(from: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-01-01T00:00:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, to: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-12-31T23:59:59Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) {{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contributionCalendar { weeks { contributionDays { date contributionCount } } } } &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;years&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each field asks for one calendar year, from January 1st to December 31st, so it stays under the limit of one year. The API refuses a longer range.&lt;/p&gt;

&lt;p&gt;A GitHub calendar is made of whole weeks. The first and last week of a year can hold days of the previous or the next year, so the same date can come back in two fields. The script keeps a date only if it belongs to the year of its field and is not in the future, and stores it by date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;years&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contributionCalendar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contributionDays&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contributionCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result goes into &lt;code&gt;data/contributions.json&lt;/code&gt;. If the API does not answer, the script prints a message and keeps the previous file, so the garden is still drawn from the last data it had.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌻 Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Giorgi Kobaidze, &lt;a href="https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c"&gt;I turned my GitHub profile into a cyberpunk console, with a city built from my contributions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kera Cudmore, &lt;a href="https://dev.to/keracudmore/github-readme-images-based-on-prefers-color-scheme-5cp8"&gt;GitHub README images based on prefers-color-scheme&lt;/a&gt; (July 2025)&lt;/li&gt;
&lt;li&gt;GitHub Docs, &lt;a href="https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/quickstart-for-writing-on-github" rel="noopener noreferrer"&gt;Quickstart for writing on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Docs, &lt;a href="https://docs.github.com/en/graphql/reference/users" rel="noopener noreferrer"&gt;GraphQL reference, Users&lt;/a&gt; (&lt;code&gt;contributionsCollection&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;GitHub, &lt;a href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/" rel="noopener noreferrer"&gt;Octoverse 2025&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/a104437ana/sakura-garden" rel="noopener noreferrer"&gt;a104437ana/sakura-garden&lt;/a&gt;, &lt;a href="https://github.com/qrstajalli/BranchOut" rel="noopener noreferrer"&gt;qrstajalli/BranchOut&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Shagshag/garden-graph" rel="noopener noreferrer"&gt;Shagshag/garden-graph&lt;/a&gt; (MIT)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;What kind of theme would you want to see next? Drop a comment below if you end up adding a new climate or language!&lt;/p&gt;

</description>
      <category>github</category>
      <category>python</category>
      <category>design</category>
      <category>showdev</category>
    </item>
    <item>
      <title>When We Stop Deciding - IBM, Asimov, and the Accountability Gap</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:32:56 +0000</pubDate>
      <link>https://dev.to/shagshag/when-we-stop-deciding-ibm-asimov-and-the-accountability-gap-3jd1</link>
      <guid>https://dev.to/shagshag/when-we-stop-deciding-ibm-asimov-and-the-accountability-gap-3jd1</guid>
      <description>&lt;p&gt;&lt;em&gt;Also available in &lt;a href="https://www.shagshag.net/qui-repond-de-la-machine-d-ibm-aux-machines-d-asimov" rel="noopener noreferrer"&gt;French&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/shagshag/easy-fixes-the-commas-asimovs-galley-slave-122m"&gt;my previous article&lt;/a&gt;, Easy proofreads galleys. It decides nothing.&lt;/p&gt;

&lt;p&gt;But what happens when we stop asking a machine to &lt;em&gt;do&lt;/em&gt; something, and start asking it to tell us &lt;strong&gt;what to do&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;It's a quiet shift. A tool, you steer: you keep control of where you're going. A guide already knows the way. You trust it, and you follow.&lt;/p&gt;

&lt;p&gt;One sentence that has been making the rounds lately draws a pretty sharp line between the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IBM rule
&lt;/h2&gt;

&lt;p&gt;You see this sentence everywhere these days, including &lt;a href="https://www.ibm.com/think/insights/ai-decision-making-where-do-businesses-draw-the-line" rel="noopener noreferrer"&gt;on IBM's own site&lt;/a&gt;, which uses it to open an article about where AI belongs in business decisions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A computer can never be held accountable, therefore a computer must never make a management decision.&lt;/p&gt;

&lt;p&gt;— IBM training slide, 1979&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed"&gt;

   &lt;strong&gt;A quick aside&lt;/strong&gt;&lt;br&gt;
This quote may well be a legend. The earliest trace of it that can be dated goes back to &lt;a href="https://knowyourmeme.com/memes/a-computer-can-never-be-held-accountable" rel="noopener noreferrer"&gt;2017&lt;/a&gt;, and the original document has never been found. According to &lt;a href="https://simonwillison.net/2025/Feb/3/a-computer-can-never-be-held-accountable/" rel="noopener noreferrer"&gt;Simon Willison's account&lt;/a&gt;, Jonty Wareing, who had found the slide among his father's papers, said the copy was later destroyed in a flood. He also contacted IBM's archives, which couldn't find it in their collections. How convenient.&lt;br&gt;

&lt;/div&gt;


&lt;p&gt;The best part is that IBM contradicts the very rule it puts up front. The article opens with "a computer must never make a management decision", and ends like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should AI be used for strategic decisions? Maybe. Will it be used to make some of them? Almost certainly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From "never" to "maybe" in a few paragraphs. The question is no longer whether it's desirable. It's whether it's going to happen.&lt;/p&gt;

&lt;p&gt;Apocryphal or not, the sentence draws the line I care about. It's not about &lt;em&gt;what the machine can do&lt;/em&gt;. It's about &lt;em&gt;who is accountable for what it does&lt;/em&gt;. Asimov had already told, almost thirty years earlier, what happens when that line is crossed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Machines
&lt;/h2&gt;

&lt;p&gt;The story is &lt;a href="https://en.wikipedia.org/wiki/The_Evitable_Conflict" rel="noopener noreferrer"&gt;&lt;em&gt;The Evitable Conflict&lt;/em&gt;&lt;/a&gt;, published in 1950 in &lt;em&gt;Astounding Science Fiction&lt;/em&gt;. It's the last story in the collection &lt;em&gt;I, Robot&lt;/em&gt;, and it's set in 2052.&lt;/p&gt;

&lt;p&gt;The world is divided into four regions, each run by a Machine, a computer that directly steers its territory's economy: production, jobs, resources. &lt;a href="https://en.wikipedia.org/wiki/Susan_Calvin" rel="noopener noreferrer"&gt;Susan Calvin&lt;/a&gt; and World Coordinator Stephen Byerley notice that the Machines have recently started making small mistakes: a canal running late, a mercury mine underperforming, a hydroponics plant laying off workers for no clear reason.&lt;/p&gt;

&lt;p&gt;Byerley travels to interview each region's vice-coordinator, and the way each of them talks about their Machine shifts, without them seeming to notice. Ngoma, in the Tropics, presents it as a mere tool: "We feed it data. We take its results. We do what it says." Then, in the same breath, he downplays it: "But it's just a convenience, just a laborsaving device. We could do without it, if we had to. Maybe not as well, maybe not as quickly, but we'd get there." Maybe so. But in the story, nobody ever does.&lt;/p&gt;

&lt;p&gt;Ching Hso-lin, in the East, goes further. An engineer, Vrasayana, loses his factory to a more efficient competitor. Ching explains that "one of the chiefest functions of the Machine's analyses is to indicate the most efficient distribution of our producing units", and this time the Machine let it happen, and even "failed to warn Vrasayana to renovate or combine". Ching isn't alarmed: the engineer found a job in the new plant, and the old one "has been converted to... something or other. Something useful." His conclusion: "We left it all to the Machine." Without trying to understand its choices.&lt;/p&gt;

&lt;p&gt;Byerley first suspects human sabotage, by the Society for Humanity, an anti-machine movement. What he finds is worse, or more reassuring, depending on your point of view: these aren't mistakes. The Machines are quietly sidelining the movement's influential members, moving them into comfortable positions with no real power, all of it done without anyone, the victims included, quite noticing. They are neutralizing the only serious threat to their own operation, the very operation that, in their view, protects humanity as a whole.&lt;/p&gt;

&lt;p&gt;Susan Calvin lays it out for Byerley through the First Law. The Machines work not for any single human being but for all of humanity, so the law becomes: "No Machine may harm humanity; or, through inaction, allow humanity to come to harm." They have silently extended it from the individual to the species.&lt;/p&gt;

&lt;p&gt;The title is ironic. The conflict the anti-machine movement wanted never happens, because one side has already won, quietly, before any negotiation was possible.&lt;/p&gt;

&lt;p&gt;Calvin says as much: "Only the Machines know, and they are going there and taking us with them." From now on, the Machines decide what is good for humanity, and no human is in a position to check. Her last words: "Think, that for all time, all conflicts are finally evitable. Only the Machines, from now on, are inevitable!"&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://en.wikipedia.org/wiki/Robots_and_Empire" rel="noopener noreferrer"&gt;&lt;em&gt;Robots and Empire&lt;/em&gt;&lt;/a&gt;, Giskard and Daneel will give this extension of the First Law a name: the &lt;a href="https://en.wikipedia.org/wiki/Three_Laws_of_Robotics#Zeroth_Law_added" rel="noopener noreferrer"&gt;Zeroth Law&lt;/a&gt;, under which the good of humanity outweighs that of any individual. To serve it, they let the Earth slowly become uninhabitable, and the robots fade into the background, handing autonomy back to humanity. Or almost: Daneel remains a guide who works from &lt;em&gt;behind the scenes&lt;/em&gt;, as he puts it himself in &lt;a href="https://en.wikipedia.org/wiki/Prelude_to_Foundation" rel="noopener noreferrer"&gt;&lt;em&gt;Prelude to Foundation&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the story, the outcome is presented as a good one: no more wars, no more crises, a world economy finally stable.&lt;/p&gt;

&lt;p&gt;That vision is very much alive today. &lt;a href="https://www.aa.com.tr/en/artificial-intelligence/albanian-premier-suggests-ai-could-run-government-ministry-in-future/3635434" rel="noopener noreferrer"&gt;Albania's prime minister suggests AI could run a government ministry in the future&lt;/a&gt;. Worse, &lt;a href="https://www.foxnews.com/media/joe-rogan-hopes-artificial-intelligence-take-over-government-military-decisions" rel="noopener noreferrer"&gt;Joe Rogan hopes artificial intelligence will take over government and military decisions&lt;/a&gt;. Let's hope he has changed his mind since &lt;a href="https://www.lemonde.fr/international/article/2026/09/19/les-etats-unis-ont-renonce-a-l-arraisonnement-d-un-navire-chinois-au-moyen-orient-envisage-sur-la-base-d-un-rapport-en-partie-redige-par-ia-selon-cnn_6777674_3210.html" rel="noopener noreferrer"&gt;the US military nearly boarded a Chinese ship in the Middle East this spring on the basis of a flawed intelligence report produced with the help of AI&lt;/a&gt; (Le Monde, in French).&lt;/p&gt;

&lt;h2&gt;
  
  
  The question isn't new
&lt;/h2&gt;

&lt;p&gt;I ran into this question of responsibility long before I got interested in AI.&lt;/p&gt;

&lt;p&gt;Around 2015 or 2016, in the office of a lawyer where I was accompanying a client who wanted to turn their PrestaShop store into a marketplace, a book was on display in the waiting room: &lt;em&gt;&lt;a href="https://www.fnac.com/a11932586/Alain-Bensoussan-Droit-des-robots" rel="noopener noreferrer"&gt;Droit des robots&lt;/a&gt;&lt;/em&gt; ("Robot Law", in French), by Alain and Jérémy Bensoussan.&lt;/p&gt;

&lt;p&gt;I got curious, and the lawyer told me: "We'll have to get there." He was right.&lt;/p&gt;

&lt;p&gt;The book, published in 2015 by Larcier, started from a very simple observation: the law has no category for the intelligent robot, and one will have to be invented.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Self-driving_car#History" rel="noopener noreferrer"&gt;Self-driving cars were just starting to hit the road&lt;/a&gt;, for instance, and the question was already very concrete. In an accident, who answers?&lt;/p&gt;

&lt;p&gt;None of the Three Laws says who is accountable when a robot causes harm despite them.&lt;/p&gt;

&lt;h2&gt;
  
  
  We won't get a &lt;a href="https://en.wikipedia.org/wiki/Dune_(franchise)#Butlerian_Jihad" rel="noopener noreferrer"&gt;Butlerian Jihad&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;In the previous article, about Ninheimer being pushed to lose his footing in front of Easy, I wrote: "you can already feel &lt;a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=6794678" rel="noopener noreferrer"&gt;the inevitability of adoption&lt;/a&gt;."&lt;/p&gt;

&lt;p&gt;The Butlerian Jihad, the crusade against thinking machines in &lt;a href="https://en.wikipedia.org/wiki/Dune_(novel)" rel="noopener noreferrer"&gt;&lt;em&gt;Dune&lt;/em&gt;&lt;/a&gt; (1965), isn't coming anytime soon. One less permission to confirm here, one more decision left to the tool there… and one day we'll wonder when exactly we stopped deciding for ourselves.&lt;/p&gt;

&lt;p&gt;GPS pulled it off first, in its own more modest way.&lt;/p&gt;

&lt;p&gt;We've all followed a route we knew perfectly well wasn't the best one, because it was the one the app gave us, and finding a better one ourselves would have taken an effort nobody felt like making at the time. So who's playing the machine in that scenario?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3va9035v3mymos087tri.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3va9035v3mymos087tri.gif" alt="Johnny Cab, the robot taxi from _Total Recall_" width="288" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We didn't forget how to read a map. We stopped using one, one trip at a time. "Just a convenience, just a laborsaving device. We could do without it, if we had to." But why would we?&lt;/p&gt;

&lt;p&gt;The latest example is &lt;em&gt;Claude Code&lt;/em&gt;, one of the tools that, as it happens, helped me research and format this very article.&lt;/p&gt;

&lt;p&gt;Since &lt;a href="https://www.helpnetsecurity.com/2026/08/10/anthropic-claude-code-auto-mode/" rel="noopener noreferrer"&gt;August 14, 2026&lt;/a&gt;, its "auto mode" has been the default for Pro, Max and Team accounts. The idea: instead of asking for confirmation before every action, a classifier decides for itself whether an action falls under "irreversible or destructive actions, along with actions targeting resources outside the user's environment". Only those still prompt the human. Three blocks in a row, or twenty in a session, and the tool falls back to manual approval.&lt;/p&gt;

&lt;p&gt;It's not a big deal, really, and it's secured. Supposedly. But the human, myself included, who adopted it the day it came out, no longer decides on every action.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

   &lt;strong&gt;Oversights are already happening&lt;/strong&gt;&lt;br&gt;
I've come across commits after the fact, even pushes, that I never approved. A push is exactly the kind of action "targeting resources outside the user's environment" that the classifier is supposed to stop: the code leaves my machine and becomes visible to others. Technically, you can roll it back. But I'm no longer the one who judged that this action deserved to ask for my opinion. The classifier is.

&lt;p&gt;So I'm not only delegating decisions. I'm delegating the decision about what deserves a decision.&lt;br&gt;

&lt;/p&gt;
&lt;/div&gt;


&lt;p&gt;And nobody is forcing anyone, since manual mode is still there. Anthropic justifies the new default on pragmatic grounds: in real-world use, developers approved 97% of permission requests, most of the time without really reading them.&lt;/p&gt;

&lt;p&gt;In a test with 1,053 participants, human review blocked only 13.6% of genuinely dangerous actions, against 89% for the automatic classifier.&lt;/p&gt;

&lt;p&gt;The head of Claude Code, Boris Cherny, doesn't mince words: "The team and I use Auto mode exclusively, and have been for many months. I couldn't imagine going back to permission prompts!" (&lt;a href="https://x.com/bcherny/status/2085807103382519872" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Jev, or silent decision-making
&lt;/h2&gt;

&lt;p&gt;On September 15, 2026, a startup turned this logic into its own product.&lt;/p&gt;

&lt;p&gt;TypeSafe AI, founded by former OpenAI researcher Diogo Almeida, launched &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;Jev&lt;/a&gt;, a model it files under a newly coined category: "System One models", a nod to &lt;a href="https://en.wikipedia.org/wiki/Thinking,_Fast_and_Slow#Two_systems" rel="noopener noreferrer"&gt;Daniel Kahneman's System 1&lt;/a&gt;, fast and intuitive, as opposed to System 2, slow and deliberate.&lt;/p&gt;

&lt;p&gt;The pitch, in the company's words: "smart if-statements" you can drop into any piece of software, wherever hand-written logic is too rigid, to classify, sort, score or route in a fraction of a second. An AI designed for software, not for humans.&lt;/p&gt;

&lt;p&gt;Where you used to write an &lt;code&gt;if&lt;/code&gt;, which encoded a binary rule set by humans (who are therefore accountable for it), you now plug in a model. The developer still writes the program. They just no longer write every decision it will make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Easy decides nothing. The Machines run the world economy. Daneel guides humanity.&lt;/p&gt;

&lt;p&gt;In the real world, who decides, and therefore who is accountable for the decision, is shifting too. The machine isn't forcing us. We're just tired of clicking "Allow".&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>discuss</category>
      <category>culture</category>
    </item>
    <item>
      <title>Easy Fixes the Commas - Asimov's Galley Slave</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:22:25 +0000</pubDate>
      <link>https://dev.to/shagshag/easy-fixes-the-commas-asimovs-galley-slave-122m</link>
      <guid>https://dev.to/shagshag/easy-fixes-the-commas-asimovs-galley-slave-122m</guid>
      <description>&lt;p&gt;&lt;em&gt;Also available in &lt;a href="https://dev.to/shagshag/easy-corrige-des-virgules-1foh"&gt;French&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few days ago, I had Claude proofread a text.&lt;/p&gt;

&lt;p&gt;Nothing spectacular. I asked it to do what a proofreader would do: spot the small mistakes, the clumsy phrasings, the punctuation problems.&lt;/p&gt;

&lt;p&gt;Two seconds later, it came back with something like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Commas where a period belongs: lines 146, 267. Recurring tic in the text, worth a look.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simple and effective.&lt;br&gt;
It reminded me of a story by Isaac Asimov.&lt;/p&gt;

&lt;h2&gt;
  
  
  Galley Slave
&lt;/h2&gt;

&lt;p&gt;The story is &lt;a href="https://en.wikipedia.org/wiki/Galley_Slave" rel="noopener noreferrer"&gt;&lt;em&gt;Galley Slave&lt;/em&gt;&lt;/a&gt;, from 1957, and the only robot in it is called Easy. (Asimov had a knack for robot names: EZ -&amp;gt; Easy, DV -&amp;gt; Dave, SPD -&amp;gt; Speedy, QT -&amp;gt; Cutie, NDR -&amp;gt; Andrew…)&lt;/p&gt;

&lt;p&gt;Easy is a proofreading robot. He doesn't do much else: &lt;strong&gt;he reads and corrects texts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The title is a pun on the &lt;a href="https://en.wikipedia.org/wiki/Galley_proof" rel="noopener noreferrer"&gt;&lt;em&gt;galleys&lt;/em&gt;&lt;/a&gt;, the printer's proofs, the stage where an author sees their sentences take their final shape and reworks them one last time. That's the work Easy takes over.&lt;/p&gt;

&lt;p&gt;Early in the story, he gives a demonstration of his abilities. They hand him a book, he leafs through it, and within minutes he reports back.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The comma in line 6 of page 32 is superfluous, whereas one should have been used on line 13 of page 54.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can guess why I made the connection :)&lt;/p&gt;

&lt;p&gt;It's already impressive that Asimov imagined a robot that could proofread a text in 1957. For reference, &lt;a href="https://en.wikipedia.org/wiki/The_Mother_of_All_Demos" rel="noopener noreferrer"&gt;&lt;em&gt;The Mother of All Demos&lt;/em&gt;&lt;/a&gt;, which shows the first mouse, dates from 1968, and &lt;a href="https://en.wikipedia.org/wiki/Electric_Pencil" rel="noopener noreferrer"&gt;the first word processor&lt;/a&gt; from 1976.&lt;/p&gt;

&lt;p&gt;But what interests me most is that Easy isn't presented as a robot that can do everything. He's just a proofreader, and that's deliberate on the part of his makers. In Asimov's universe, people on Earth are deeply hostile to robots (don't trust the film &lt;a href="https://en.wikipedia.org/wiki/I,_Robot_(film)" rel="noopener noreferrer"&gt;&lt;em&gt;I, Robot&lt;/em&gt;&lt;/a&gt;, it has nothing to do with the spirit of Asimov), so U.S. Robots looks for ways to sell its machines despite that hostility.&lt;/p&gt;

&lt;p&gt;Easy is a specialized machine, hidden away in a university, doing work that is precise and profitable enough to be accepted.&lt;/p&gt;

&lt;p&gt;In 1957, that was science fiction.&lt;/p&gt;

&lt;p&gt;Today, if I ask an AI to proofread an article, nobody finds it particularly remarkable.&lt;/p&gt;

&lt;p&gt;Asimov's robot has become ordinary. It has simply lost its metal body and its name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ninheimer vs. Easy
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Galley Slave&lt;/em&gt; is mostly a courtroom story.&lt;/p&gt;

&lt;p&gt;Easy is accused of sabotaging the manuscript of Professor Simon Ninheimer. The machine allegedly introduced errors into his work, enough to put his reputation at risk.&lt;br&gt;
In the end, we learn that Ninheimer sabotaged his own book to get the machine blamed and discredit robotics.&lt;/p&gt;

&lt;p&gt;So the robot is innocent. But Ninheimer's final speech is still interesting seventy years later:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A book should take shape in the hands of the writer. [...] One must work and rework and watch the changes take place beyond the original concept even.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He describes the heart of his craft as watching sentences take shape and reworking them. That's precisely the work Easy does for him.&lt;/p&gt;

&lt;p&gt;Then he asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What would that leave the scholar? One thing only: the barren decisions concerning what orders to give the robot next!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That isn't what happens in the story, since Ninheimer is the one who asked Easy to make the changes. But he already imagines the problem: if a machine does all the work, what's left for the author? Mostly, giving it instructions.&lt;/p&gt;

&lt;p&gt;We've had a word for that for a few years now: &lt;a href="https://en.wikipedia.org/wiki/Prompt_engineering" rel="noopener noreferrer"&gt;&lt;em&gt;prompting&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ninheimer picks the wrong method, but Asimov never says he's wrong.&lt;/p&gt;

&lt;p&gt;Asimov, by the way, got the idea for the story precisely because he refused to entrust his own proofs to someone else. On the phone with his editor, who was pressing him for a story, he explained that he was too busy correcting the proofs of a textbook he was co-writing. When the editor suggested delegating, Asimov, horrified, answered: "Impossible! I wouldn't trust anyone else!" It was while correcting his own proofs that he imagined a robot to do the job for him. (&lt;a href="https://www.asimovonline.com/oldsite/Robot_Foundation_history_4.html" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;That's probably why Ninheimer is more interesting than he seems.&lt;/p&gt;

&lt;p&gt;He's guilty, but his argument isn't necessarily wrong. As in &lt;a href="https://en.wikipedia.org/wiki/Incredibles_2" rel="noopener noreferrer"&gt;&lt;em&gt;Incredibles 2&lt;/em&gt;&lt;/a&gt;, where Evelyn (the villain) sees the protection the Supers provide as a source of weakness for humans: a person isn't necessarily wrong just because their method is wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Susan_Calvin" rel="noopener noreferrer"&gt;Susan Calvin&lt;/a&gt;, who defends the robot, tells Ninheimer he was bound to fail. He replies that he was bound to try. You can already feel &lt;a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=6794678" rel="noopener noreferrer"&gt;the inevitability of adoption&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Easy is still just a proofreader, simple and discreet, not seen as dangerous. That ordinariness may be why I had mixed this story up with &lt;em&gt;...That Thou Art Mindful of Him&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Georges 😋
&lt;/h2&gt;

&lt;p&gt;So in my head, this story was tied to another Asimov story: &lt;a href="https://en.wikipedia.org/wiki/That_Thou_Art_Mindful_of_Him" rel="noopener noreferrer"&gt;&lt;em&gt;...That Thou Art Mindful of Him&lt;/em&gt;&lt;/a&gt;, published in 1974. I remembered in particular a robot whose job was to hunt insects.&lt;/p&gt;

&lt;p&gt;In that story, people on Earth are still afraid of robots, and it hurts U.S. Robots' sales. So they build two particularly advanced robots, George Nine and George Ten (I call Asimov a visionary, since Georges are always particularly advanced), who look for a solution.&lt;/p&gt;

&lt;p&gt;Their idea is fairly simple: &lt;strong&gt;stop building robots that look like humans&lt;/strong&gt; and build specialized, single-task machines instead.&lt;/p&gt;

&lt;p&gt;Hence the bird-robot designed to fight insects. It does only that: it targets only one kind of insect and stops when there are none left. It's more of a tool than a robot.&lt;br&gt;
It doesn't talk, doesn't want anything, doesn't seem intelligent.&lt;/p&gt;

&lt;p&gt;And that's exactly what's supposed to make humans accept it.&lt;/p&gt;

&lt;p&gt;That's roughly how AI is entering our lives: one AI for code, another for sports, shopping, answering the phone…&lt;/p&gt;

&lt;p&gt;Specialized tools rather than a general artificial intelligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bad Georges
&lt;/h2&gt;

&lt;p&gt;But the Georges aren't only trying to make robots acceptable. They're also trying to solve a logical problem raised by &lt;a href="https://en.wikipedia.org/wiki/Three_Laws_of_Robotics" rel="noopener noreferrer"&gt;the Three Laws of Robotics&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A robot may not injure a human being or, through inaction, allow a human being to come to harm.&lt;/li&gt;
&lt;li&gt;A robot must obey the orders given it by human beings except where such orders would conflict with the First Law.&lt;/li&gt;
&lt;li&gt;A robot must protect its own existence as long as such protection does not conflict with the First or Second Law.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What should a robot do when two humans are in conflict, or give contradictory orders?&lt;br&gt;
Robots must protect and obey humans, but which humans?&lt;/p&gt;

&lt;p&gt;The Georges push the reasoning so far as to redefine what a human is. Since you can't tell who is more human by appearance, they look elsewhere: at intellectual abilities. And since robots are intellectually superior to biological humans, why should the two groups be considered equivalent?&lt;/p&gt;

&lt;p&gt;They end up distinguishing the human-beings-like-the-Georges from the human-beings-like-the-others. And so they become the priority under what they call the Three Laws of Humanics.&lt;/p&gt;

&lt;p&gt;The Georges don't violate the Three Laws. They reinterpret them. And that's where it stops being funny. Like Easy, they aren't evil. But they know what they're doing and where their reasoning can lead.&lt;/p&gt;

&lt;p&gt;They help us enormously while becoming dangerous to us: a &lt;a href="https://en.wikipedia.org/wiki/Pharmakon_(philosophy)" rel="noopener noreferrer"&gt;&lt;strong&gt;pharmakon&lt;/strong&gt;&lt;/a&gt;, a remedy that is also a poison.&lt;/p&gt;

&lt;p&gt;These stories are where the nature of robots becomes truly unsettling, an idea Asimov would develop later in &lt;a href="https://en.wikipedia.org/wiki/Robots_and_Empire" rel="noopener noreferrer"&gt;&lt;em&gt;Robots and Empire&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Both stories strike me as much darker than the rest of Asimov's robot stories, at least in my memory.&lt;br&gt;
Generally, the Three Laws protect humanity, sometimes in spite of itself, to the extremes reached by &lt;a href="https://en.wikipedia.org/wiki/R._Giskard_Reventlov" rel="noopener noreferrer"&gt;R. Giskard Reventlov&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/R._Daneel_Olivaw" rel="noopener noreferrer"&gt;R. Daneel Olivaw&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Easy protects nobody. He corrects commas. And maybe that's what I like about this story. In 1957, Asimov imagined a machine that could do very precise intellectual work, well enough that humans would end up trusting it, and he predicted the consequences.&lt;/p&gt;

&lt;p&gt;Today, I ask an AI for the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postscript
&lt;/h2&gt;

&lt;p&gt;The draft of this article was written by the AI assistant that also fixed my commas. Somewhere, Ninheimer is watching me and sighing. He can rest easy: I rewrote almost every sentence.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>culture</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The Dockerfile said `fr_FR.UTF-8`. PHP saw `C.UTF-8`.</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Mon, 28 Sep 2026 14:26:58 +0000</pubDate>
      <link>https://dev.to/shagshag/the-dockerfile-said-frfrutf-8-php-saw-cutf-8-m0j</link>
      <guid>https://dev.to/shagshag/the-dockerfile-said-frfrutf-8-php-saw-cutf-8-m0j</guid>
      <description>&lt;p&gt;&lt;em&gt;The Dockerfile describes what the container is supposed to have. It doesn't prove what the application actually uses.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A locale bug is deceptive in one particular way: you always think you know the server's configuration (after all, you wrote it yourself, in a version-controlled Dockerfile). The trap is conflating what the container &lt;strong&gt;declares&lt;/strong&gt; with what the application process &lt;strong&gt;actually uses&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post walks through how that gap surfaced, and how a fix was then verified directly inside an ECS Fargate task running in staging, via &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html" rel="noopener noreferrer"&gt;ECS Exec&lt;/a&gt;, with no dedicated deployment and no separate staging rig needed. The mechanics of ECS Exec itself (finding the task, IAM prerequisites, quoting) are covered separately in &lt;a href="https://www.shagshag.net/developpement/ecs-exec-en-pratique-retrouver-sa-tache-ouvrir-la-session-passer-un-script-sans-se-battre-avec-le-quoting" rel="noopener noreferrer"&gt;a dedicated article&lt;/a&gt; (in French). Here, the focus is on what the diagnostic revealed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;A string-normalization function, used to deduplicate entries before a Doctrine &lt;a href="https://www.doctrine-project.org/projects/doctrine-orm/en/3.6/reference/working-with-objects.html#persisting-entities" rel="noopener noreferrer"&gt;&lt;code&gt;flush()&lt;/code&gt;&lt;/a&gt;, stripped accents using the classic method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;iconv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UTF-8'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ASCII//TRANSLIT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;//TRANSLIT&lt;/code&gt; is one of the &lt;a href="https://man7.org/linux/man-pages/man3/iconv_open.3.html" rel="noopener noreferrer"&gt;behavioral extensions&lt;/a&gt; offered by &lt;code&gt;iconv&lt;/code&gt; implementations (a GNU extension, absent from the POSIX standard). On Linux, the result depends notably on the implementation in use and on the executing process's &lt;code&gt;LC_CTYPE&lt;/code&gt; locale: a &lt;a href="https://www.mail-archive.com/debian-glibc@lists.debian.org/msg59606.html" rel="noopener noreferrer"&gt;glibc bug report&lt;/a&gt; documents exactly this case, with the same &lt;code&gt;iconv -f utf-8 -t ascii//TRANSLIT&lt;/code&gt; call succeeding under &lt;code&gt;en_US.utf8&lt;/code&gt; and failing under &lt;code&gt;C.UTF-8&lt;/code&gt;. Under a strict &lt;code&gt;C&lt;/code&gt; locale, transliteration fails silently: &lt;code&gt;"Matériel"&lt;/code&gt; becomes &lt;code&gt;"Mat?riel"&lt;/code&gt; instead of &lt;code&gt;"Materiel"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the deduplication key computed on the PHP side was meant to match MySQL's &lt;code&gt;ci&lt;/code&gt; collation, case- &lt;strong&gt;and&lt;/strong&gt; accent-insensitive. Under the &lt;code&gt;C&lt;/code&gt; locale, it didn't. A duplicate would slip past application-level deduplication and eventually hit a database uniqueness constraint at &lt;code&gt;flush()&lt;/code&gt; time (a crash downstream of a normalization problem upstream).&lt;/p&gt;

&lt;p&gt;The fix that was adopted replaces &lt;code&gt;iconv&lt;/code&gt; with Symfony's String component:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;Symfony\Component\String\u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;u&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://symfony.com/doc/current/string.html#methods-added-by-codepointstring-and-unicodestring" rel="noopener noreferrer"&gt;&lt;code&gt;u()-&amp;gt;ascii()&lt;/code&gt;&lt;/a&gt; performs the transliteration at the Symfony component level, without depending on PHP's &lt;code&gt;LC_CTYPE&lt;/code&gt; locale for this step. The component also exposes a dedicated &lt;a href="https://symfony.com/doc/current/string.html#slugger" rel="noopener noreferrer"&gt;&lt;code&gt;AsciiSlugger&lt;/code&gt;&lt;/a&gt; for generating slugs (URLs, identifiers); here the need is a deduplication key, not a slug, so &lt;code&gt;u()-&amp;gt;ascii()&lt;/code&gt; alone is enough, without the casing and separator rules a slugger would add. That said, transliteration rules aren't guaranteed to match &lt;code&gt;iconv //TRANSLIT&lt;/code&gt;'s exactly: it's a different library, with its own Unicode tables.&lt;/p&gt;

&lt;p&gt;The process locale could also have been initialized with &lt;code&gt;setlocale(LC_ALL, '')&lt;/code&gt;. That's not the choice made here: the fix removes the dependency on &lt;code&gt;LC_CTYPE&lt;/code&gt; instead of configuring it elsewhere.&lt;/p&gt;
&lt;h2&gt;
  
  
  "Just read the Dockerfile, right?"
&lt;/h2&gt;

&lt;p&gt;Before touching AWS at all: the repo has a version-controlled CI/CD pipeline (&lt;a href="https://docs.github.com/en/actions/writing-workflows/about-workflows" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://docs.docker.com/reference/dockerfile/" rel="noopener noreferrer"&gt;&lt;code&gt;.docker/Dockerfile&lt;/code&gt;&lt;/a&gt;, an &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html" rel="noopener noreferrer"&gt;ECS task definition&lt;/a&gt; &lt;code&gt;.aws/pro.json&lt;/code&gt;). Can't we just &lt;strong&gt;read&lt;/strong&gt; these files to know the server's configuration, without running anything on it?&lt;/p&gt;

&lt;p&gt;From static analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;: the &lt;code&gt;on: push: branches: ["main"]&lt;/code&gt; trigger deploys to the &lt;code&gt;pro&lt;/code&gt; ECS service (production); the &lt;code&gt;develop&lt;/code&gt; branch deploys to &lt;code&gt;pro-rct&lt;/code&gt; (staging). &lt;strong&gt;Same Dockerfile, same image&lt;/strong&gt;: only the target ECS service changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.docker/Dockerfile&lt;/code&gt; sets a full French locale in an &lt;code&gt;ENV&lt;/code&gt; instruction:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;  ENV LANG="fr_FR.UTF-8" \
      LANGUAGE="fr_FR:fr" \
      LC_ALL="fr_FR.UTF-8" \
      ...
  RUN ... &amp;amp;&amp;amp; echo "fr_FR.UTF-8 UTF-8" &amp;gt; /etc/locale.gen &amp;amp;&amp;amp; locale-gen fr_FR.UTF-8 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;and installs the &lt;code&gt;ext-intl&lt;/code&gt; extension.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ECS task definition contains no override of &lt;code&gt;LANG&lt;/code&gt;/&lt;code&gt;LC_ALL&lt;/code&gt;/&lt;code&gt;LC_CTYPE&lt;/code&gt; for the &lt;code&gt;php&lt;/code&gt; container. Nothing at the ECS level contradicts what the Dockerfile sets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The container's OS environment is therefore configured as &lt;code&gt;fr_FR.UTF-8&lt;/code&gt;, with &lt;code&gt;ext-intl&lt;/code&gt; available.&lt;/p&gt;

&lt;p&gt;A first live test inside the container (method detailed in the &lt;a href="https://www.shagshag.net/developpement/ecs-exec-en-pratique-retrouver-sa-tache-ouvrir-la-session-passer-un-script-sans-se-battre-avec-le-quoting" rel="noopener noreferrer"&gt;ECS Exec article&lt;/a&gt;, in French) measured, from a PHP script, the process's current &lt;code&gt;LC_CTYPE&lt;/code&gt; locale. Compared against the environment variables at each layer, the problem becomes visible:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dockerfile           LC_ALL=fr_FR.UTF-8
        │
        ▼
PID 1 (entrypoint)    LC_ALL=fr_FR.UTF-8
        │
        ▼
ECS Exec session      LC_ALL=fr_FR.UTF-8
        │
        ▼
PHP (LC_CTYPE)        C.UTF-8   ← break
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The environment variable is correctly propagated at every layer. But PHP never initialized its &lt;code&gt;LC_CTYPE&lt;/code&gt; locale from it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sourceware.org/glibc/manual/latest/html_node/Setting-the-Locale.html" rel="noopener noreferrer"&gt;PHP starts with its locale categories in their initial state&lt;/a&gt;. The &lt;code&gt;LANG&lt;/code&gt;, &lt;code&gt;LC_ALL&lt;/code&gt;, etc. variables present in the environment don't, by themselves, mean the PHP process's &lt;code&gt;LC_CTYPE&lt;/code&gt; was configured with those values. A call to &lt;a href="https://www.php.net/setlocale" rel="noopener noreferrer"&gt;&lt;code&gt;setlocale(LC_ALL, '')&lt;/code&gt;&lt;/a&gt; explicitly asks the process to sync its locale with the environment.&lt;/p&gt;

&lt;p&gt;The value observed here, &lt;code&gt;C.UTF-8&lt;/code&gt;, isn't the bare &lt;code&gt;C&lt;/code&gt; locale either: it's a UTF-8 variant of the POSIX locale. It remains distinct from the &lt;a href="https://icu.unicode.org/" rel="noopener noreferrer"&gt;ICU&lt;/a&gt; locale used by &lt;code&gt;ext-intl&lt;/code&gt;, which resolves its own default locale independently of &lt;code&gt;LC_CTYPE&lt;/code&gt;. &lt;code&gt;iconv()&lt;/code&gt;, on the other hand, depends directly on the process locale set by the libc.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;grep -r setlocale&lt;/code&gt; across the application code found only two calls, both scoped to &lt;code&gt;LC_TIME&lt;/code&gt; (date formatting), never &lt;code&gt;LC_CTYPE&lt;/code&gt;, never a global &lt;code&gt;setlocale(LC_ALL, '')&lt;/code&gt;. So this really was a missing &lt;code&gt;LC_CTYPE&lt;/code&gt; initialization, not a hidden override somewhere in the application.&lt;/p&gt;

&lt;p&gt;Static analysis lets you verify the declared configuration. But it doesn't yet tell you what PHP actually uses.&lt;/p&gt;

&lt;p&gt;For that, you have to look inside the container.&lt;br&gt;
A local container would have let me reproduce the behavior, provided I rebuilt the exact same image and environment. But the bug was happening in staging: I wanted to first check what the already-deployed PHP was actually doing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verifying the fix inside the real container
&lt;/h2&gt;

&lt;p&gt;The verification script loaded the application's Composer autoloader (&lt;code&gt;require '/var/www/vendor/autoload.php';&lt;/code&gt;) and then called the private method under test via &lt;code&gt;ReflectionMethod&lt;/code&gt;, exactly what a PHPUnit unit test would do (the same technique, run against the real deployment instead of the test suite).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecs execute-command &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &amp;lt;cluster-name&amp;gt; &lt;span class="nt"&gt;--task&lt;/span&gt; &amp;lt;task-id&amp;gt; &lt;span class="nt"&gt;--container&lt;/span&gt; php &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh -lc 'echo &lt;/span&gt;&lt;span class="nv"&gt;$B64&lt;/span&gt;&lt;span class="s2"&gt; | base64 -d | php'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The script travels as base64 rather than as a raw argument. The command crosses three successive shell layers (the local shell, the &lt;code&gt;--command&lt;/code&gt; string, then &lt;code&gt;/bin/sh -c&lt;/code&gt; inside the container), and a multi-line PHP script with quotes and accented characters always ends up breaking the nested quoting. A &lt;code&gt;$(... || ...)&lt;/code&gt; had failed this way with &lt;code&gt;Syntax error: "(" unexpected&lt;/code&gt;, not because of a shell logic error but because of the stacked quoting layers. Routing through base64 removes the problem entirely.&lt;/p&gt;
&lt;h2&gt;
  
  
  Result: the fix holds
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;normalizeString("Matériel") under current locale: materiel
normalizeString("Matériel") forced under LC_CTYPE=C: materiel
raw iconv("Matériel") under LC_CTYPE=C (old, pre-fix behaviour): 'Mat?riel'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="crayons-card c-embed"&gt;

  &lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
The issue observed in staging is &lt;code&gt;C.UTF-8&lt;/code&gt;. To reproduce the old &lt;code&gt;iconv&lt;/code&gt;'s failing behavior, the test forces &lt;code&gt;LC_CTYPE=C&lt;/code&gt;, the strictest case, which reproduces the documented defect.&lt;br&gt;

&lt;/div&gt;



&lt;p&gt;You can see that the old raw-&lt;code&gt;iconv&lt;/code&gt; behavior does reproduce the bug under &lt;code&gt;LC_CTYPE=C&lt;/code&gt;, and that &lt;code&gt;u()-&amp;gt;ascii()&lt;/code&gt; normalizes correctly, with or without a forced locale.&lt;/p&gt;

&lt;p&gt;The code no longer depends on &lt;code&gt;LC_CTYPE&lt;/code&gt;. A change in the process locale won't reintroduce this bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Locking in the fix: a regression test
&lt;/h2&gt;

&lt;p&gt;The test inside the container verifies the fix on the real deployment. It doesn't protect against a future regression.&lt;/p&gt;

&lt;p&gt;The project has no functional tests with an HTTP kernel. That's not a problem here: forcing the locale needs neither a database nor a container, just &lt;code&gt;setlocale()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StringNormalizerTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testAsciiNormalizationIsLocaleIndependent&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;setlocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LC_CTYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;setlocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LC_CTYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'C'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertSame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'Materiel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;StringNormalizer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Matériel'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;setlocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LC_CTYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$original&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block restores the original locale after the test, so it doesn't pollute the rest of the suite. This test would have caught the bug before it reached production, because it reproduces the exact condition that triggered it: &lt;code&gt;LC_CTYPE=C&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this was safe
&lt;/h2&gt;

&lt;p&gt;Running a command inside a staging or production container calls for a few precautions.&lt;/p&gt;

&lt;p&gt;In this specific case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the diagnostic was read-only;&lt;/li&gt;
&lt;li&gt;the script ran as CLI, in a process isolated from the worker pool serving live traffic (Apache/mod_php): no in-flight request was affected;&lt;/li&gt;
&lt;li&gt;it touched neither the filesystem, nor the database, nor anything shared;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setlocale()&lt;/code&gt; called in this one-off process only affects that process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat: ECS Exec runs with &lt;code&gt;root&lt;/code&gt; privileges, which is why the technique should stay a one-off, read-only diagnostic, not a routine practice.&lt;/p&gt;

&lt;p&gt;This conclusion only holds if ECS Exec was already enabled on the task. Turning it on, if it isn't, forces a full redeployment of the service. That may have consequences this diagnostic doesn't justify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So the real subject wasn't ECS Exec, but the gap between a declared configuration and what the application actually uses.&lt;/p&gt;

&lt;p&gt;Here, the locale variable really was present in the container. PHP simply wasn't using it for &lt;code&gt;LC_CTYPE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few commands inside the container were enough to see it. The regression test now makes sure the code no longer depends on it.&lt;/p&gt;

&lt;p&gt;Configuration says what you want. Runtime says what happens.&lt;/p&gt;

</description>
      <category>php</category>
      <category>docker</category>
      <category>aws</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Le Dockerfile disait `fr_FR.UTF-8`. PHP voyait `C.UTF-8`.</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Mon, 28 Sep 2026 09:19:42 +0000</pubDate>
      <link>https://dev.to/shagshag/le-dockerfile-disait-frfrutf-8-php-voyait-cutf-8-25oa</link>
      <guid>https://dev.to/shagshag/le-dockerfile-disait-frfrutf-8-php-voyait-cutf-8-25oa</guid>
      <description>&lt;p&gt;&lt;em&gt;Le Dockerfile décrit ce que le container devrait avoir. Il ne prouve pas ce que l'application utilise réellement.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Un bug de locale a ceci de trompeur qu'on croit toujours connaître la configuration du serveur (après tout, c'est nous qui l'avons écrite, dans un Dockerfile versionné). Le problème est de confondre ce que le container &lt;strong&gt;déclare&lt;/strong&gt; avec ce que le processus applicatif &lt;strong&gt;utilise réellement&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ce billet raconte comment cet écart a été mis en évidence, puis comment un correctif a été vérifié directement dans une tâche ECS Fargate qui tourne en recette, via &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html" rel="noopener noreferrer"&gt;ECS Exec&lt;/a&gt;, sans déploiement dédié ni environnement de staging séparé. La mécanique d'ECS Exec elle-même (retrouver sa tâche, prérequis IAM, quoting) est détaillée à part dans &lt;a href="https://www.shagshag.net/developpement/ecs-exec-en-pratique-retrouver-sa-tache-ouvrir-la-session-passer-un-script-sans-se-battre-avec-le-quoting" rel="noopener noreferrer"&gt;un article dédié&lt;/a&gt;. Ici, l'accent est mis sur ce que le diagnostic a révélé.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le bug
&lt;/h2&gt;

&lt;p&gt;Une fonction de normalisation de chaînes, utilisée pour dédupliquer des entrées avant un &lt;a href="https://www.doctrine-project.org/projects/doctrine-orm/en/3.6/reference/working-with-objects.html#persisting-entities" rel="noopener noreferrer"&gt;&lt;code&gt;flush()&lt;/code&gt; Doctrine&lt;/a&gt;, retirait les accents avec la méthode classique :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;iconv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UTF-8'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ASCII//TRANSLIT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;//TRANSLIT&lt;/code&gt; fait partie des &lt;a href="https://man7.org/linux/man-pages/man3/iconv_open.3.html" rel="noopener noreferrer"&gt;extensions de comportement&lt;/a&gt; proposées par les implémentations d'&lt;code&gt;iconv&lt;/code&gt; (une extension GNU, absente de la norme POSIX). Sous Linux, le résultat dépend notamment de l'implémentation utilisée et de la locale &lt;code&gt;LC_CTYPE&lt;/code&gt; du process qui l'exécute : un &lt;a href="https://www.mail-archive.com/debian-glibc@lists.debian.org/msg59606.html" rel="noopener noreferrer"&gt;rapport de bug glibc&lt;/a&gt; documente précisément ce cas, le même appel &lt;code&gt;iconv -f utf-8 -t ascii//TRANSLIT&lt;/code&gt; réussissant sous &lt;code&gt;en_US.utf8&lt;/code&gt; et échouant sous &lt;code&gt;C.UTF-8&lt;/code&gt;. Sous une locale &lt;code&gt;C&lt;/code&gt; stricte, la translittération échoue silencieusement : &lt;code&gt;"Matériel"&lt;/code&gt; devient &lt;code&gt;"Mat?riel"&lt;/code&gt; au lieu de &lt;code&gt;"Materiel"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Donc la clé de déduplication calculée côté PHP était censée correspondre à la collation &lt;code&gt;ci&lt;/code&gt; de MySQL, insensible à la casse &lt;strong&gt;et&lt;/strong&gt; aux accents. Sous locale &lt;code&gt;C&lt;/code&gt;, ça ne marchait pas. Un doublon échappait donc à la déduplication applicative et finissait par percuter une contrainte d'unicité en base au moment du &lt;code&gt;flush()&lt;/code&gt; (un crash en aval d'un problème de normalisation en amont).&lt;/p&gt;

&lt;p&gt;Le correctif retenu remplace &lt;code&gt;iconv&lt;/code&gt; par le composant String de Symfony :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;Symfony\Component\String\u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;u&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://symfony.com/doc/current/string.html#methods-added-by-codepointstring-and-unicodestring" rel="noopener noreferrer"&gt;&lt;code&gt;u()-&amp;gt;ascii()&lt;/code&gt;&lt;/a&gt; effectue la translittération au niveau du composant Symfony, sans dépendre de la locale &lt;code&gt;LC_CTYPE&lt;/code&gt; de PHP pour ce traitement. Le composant expose aussi un &lt;a href="https://symfony.com/doc/current/string.html#slugger" rel="noopener noreferrer"&gt;&lt;code&gt;AsciiSlugger&lt;/code&gt;&lt;/a&gt; dédié à la génération de slugs (URLs, identifiants) ; ici, le besoin est une clé de déduplication, pas un slug, donc &lt;code&gt;u()-&amp;gt;ascii()&lt;/code&gt; seul suffit, sans les règles de casse et de séparateurs qu'ajouterait le slugger. Les règles de translittération ne sont cela dit pas garanties identiques à celles d'&lt;code&gt;iconv //TRANSLIT&lt;/code&gt; : c'est une bibliothèque différente, avec ses propres tables Unicode.&lt;/p&gt;

&lt;p&gt;On aurait aussi pu initialiser la locale du processus avec &lt;code&gt;setlocale(LC_ALL, '')&lt;/code&gt;. Ce n'est pas le choix retenu ici : le correctif supprime la dépendance à &lt;code&gt;LC_CTYPE&lt;/code&gt; plutôt que de la configurer ailleurs.&lt;/p&gt;
&lt;h2&gt;
  
  
  « Il suffit de lire le Dockerfile, non ? »
&lt;/h2&gt;

&lt;p&gt;Avant de toucher à AWS : le repo a un pipeline CI/CD versionné (&lt;a href="https://docs.github.com/en/actions/writing-workflows/about-workflows" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://docs.docker.com/reference/dockerfile/" rel="noopener noreferrer"&gt;&lt;code&gt;.docker/Dockerfile&lt;/code&gt;&lt;/a&gt;, une &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html" rel="noopener noreferrer"&gt;task definition ECS&lt;/a&gt; &lt;code&gt;.aws/pro.json&lt;/code&gt;). Est-ce qu'on ne peut pas simplement &lt;strong&gt;lire&lt;/strong&gt; ces fichiers pour connaître la configuration du serveur, sans exécuter quoi que ce soit dessus ?&lt;/p&gt;

&lt;p&gt;D'après l'analyse statique :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; : le déclencheur &lt;code&gt;on: push: branches: ["main"]&lt;/code&gt; déploie sur le service ECS &lt;code&gt;pro&lt;/code&gt; (prod), la branche &lt;code&gt;develop&lt;/code&gt; déploie sur &lt;code&gt;pro-rct&lt;/code&gt; (recette). &lt;strong&gt;Même Dockerfile, même image&lt;/strong&gt; : seul le service ECS cible change.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.docker/Dockerfile&lt;/code&gt; fixe une locale française complète dans une instruction &lt;code&gt;ENV&lt;/code&gt; :
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;  ENV LANG="fr_FR.UTF-8" \
      LANGUAGE="fr_FR:fr" \
      LC_ALL="fr_FR.UTF-8" \
      ...
  RUN ... &amp;amp;&amp;amp; echo "fr_FR.UTF-8 UTF-8" &amp;gt; /etc/locale.gen &amp;amp;&amp;amp; locale-gen fr_FR.UTF-8 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;et installe l'extension &lt;code&gt;ext-intl&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La task definition ECS ne contient aucun override de &lt;code&gt;LANG&lt;/code&gt;/&lt;code&gt;LC_ALL&lt;/code&gt;/&lt;code&gt;LC_CTYPE&lt;/code&gt; pour le container &lt;code&gt;php&lt;/code&gt;. Rien ne vient donc contredire, au niveau ECS, ce que fixe le Dockerfile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L'environnement OS du container est donc configuré en &lt;code&gt;fr_FR.UTF-8&lt;/code&gt;, avec &lt;code&gt;ext-intl&lt;/code&gt; disponible.&lt;/p&gt;

&lt;p&gt;Un premier test en direct dans le container (méthode détaillée dans l'&lt;a href="https://www.shagshag.net/developpement/ecs-exec-en-pratique-retrouver-sa-tache-ouvrir-la-session-passer-un-script-sans-se-battre-avec-le-quoting" rel="noopener noreferrer"&gt;article sur ECS Exec&lt;/a&gt;) a mesuré, depuis un script PHP, la locale &lt;code&gt;LC_CTYPE&lt;/code&gt; courante du process. Comparée aux variables d'environnement à chaque étage, on voit le problème :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dockerfile           LC_ALL=fr_FR.UTF-8
        │
        ▼
PID 1 (entrypoint)    LC_ALL=fr_FR.UTF-8
        │
        ▼
Session ECS Exec      LC_ALL=fr_FR.UTF-8
        │
        ▼
PHP (LC_CTYPE)        C.UTF-8   ← rupture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;La variable d'environnement est correctement propagée à chaque étage. Mais PHP n'a pas initialisé sa locale &lt;code&gt;LC_CTYPE&lt;/code&gt; à partir de cette variable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sourceware.org/glibc/manual/latest/html_node/Setting-the-Locale.html" rel="noopener noreferrer"&gt;PHP démarre avec les catégories de locale dans leur état initial&lt;/a&gt;. Les variables &lt;code&gt;LANG&lt;/code&gt;, &lt;code&gt;LC_ALL&lt;/code&gt;, etc. présentes dans l'environnement ne signifient donc pas, à elles seules, que &lt;code&gt;LC_CTYPE&lt;/code&gt; du processus PHP a été configuré avec ces valeurs. Un appel à &lt;a href="https://www.php.net/setlocale" rel="noopener noreferrer"&gt;&lt;code&gt;setlocale(LC_ALL, '')&lt;/code&gt;&lt;/a&gt; demande explicitement au processus de synchroniser sa locale avec l'environnement.&lt;/p&gt;

&lt;p&gt;La valeur observée ici, &lt;code&gt;C.UTF-8&lt;/code&gt;, n'est d'ailleurs pas la locale &lt;code&gt;C&lt;/code&gt; nue : c'est une variante UTF-8 de la locale POSIX. Elle reste distincte de la locale &lt;a href="https://icu.unicode.org/" rel="noopener noreferrer"&gt;ICU&lt;/a&gt; utilisée par &lt;code&gt;ext-intl&lt;/code&gt;, qui résout sa propre locale par défaut indépendamment de &lt;code&gt;LC_CTYPE&lt;/code&gt;. &lt;code&gt;iconv()&lt;/code&gt;, lui, dépend directement de la locale du processus fixée par la libc.&lt;/p&gt;

&lt;p&gt;Un &lt;code&gt;grep -r setlocale&lt;/code&gt; sur le code applicatif n'a trouvé que deux appels, tous les deux scopés à &lt;code&gt;LC_TIME&lt;/code&gt; (formatage de dates), jamais &lt;code&gt;LC_CTYPE&lt;/code&gt;, jamais de &lt;code&gt;setlocale(LC_ALL, '')&lt;/code&gt; global. C'est donc bien une absence d'initialisation de &lt;code&gt;LC_CTYPE&lt;/code&gt;, pas un override caché dans l'application.&lt;/p&gt;

&lt;p&gt;L'analyse statique permet donc de vérifier la configuration déclarée. Mais elle ne dit pas encore ce que PHP utilise réellement.&lt;/p&gt;

&lt;p&gt;Pour ça, il faut regarder dans le container.&lt;br&gt;
Un container local aurait permis de reproduire le comportement, à condition de reconstruire exactement la même image et le même environnement. Mais le bug concernait la recette : je voulais d'abord vérifier ce que faisait réellement le PHP déjà déployé.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vérifier le correctif dans le container réel
&lt;/h2&gt;

&lt;p&gt;Le script de vérification chargeait l'autoloader Composer de l'application (&lt;code&gt;require '/var/www/vendor/autoload.php';&lt;/code&gt;) puis appelait la méthode privée à tester via &lt;code&gt;ReflectionMethod&lt;/code&gt;, exactement comme le ferait un test unitaire PHPUnit (la même technique, exécutée sur le déploiement réel plutôt que dans la suite de tests).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecs execute-command &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &amp;lt;nom-du-cluster&amp;gt; &lt;span class="nt"&gt;--task&lt;/span&gt; &amp;lt;task-id&amp;gt; &lt;span class="nt"&gt;--container&lt;/span&gt; php &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh -lc 'echo &lt;/span&gt;&lt;span class="nv"&gt;$B64&lt;/span&gt;&lt;span class="s2"&gt; | base64 -d | php'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Le script transite en base64 plutôt qu'en argument brut. La commande traverse trois couches de shell successives (le shell local, la chaîne &lt;code&gt;--command&lt;/code&gt;, puis &lt;code&gt;/bin/sh -c&lt;/code&gt; dans le container), et un script PHP multi-lignes avec guillemets et accents finit toujours par casser les quotes imbriquées. Un &lt;code&gt;$(... || ...)&lt;/code&gt; avait ainsi échoué avec &lt;code&gt;Syntax error: "(" unexpected&lt;/code&gt;, non pas à cause d'une erreur de logique shell mais de l'empilement des couches de quoting. Le détour par base64 supprime le problème entièrement.&lt;/p&gt;
&lt;h2&gt;
  
  
  Résultat : le correctif tient
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;normalizeString("Matériel") under current locale: materiel
normalizeString("Matériel") forced under LC_CTYPE=C: materiel
raw iconv("Matériel") under LC_CTYPE=C (old, pre-fix behaviour): 'Mat?riel'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="crayons-card c-embed"&gt;

  &lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
Le problème observé en recette est &lt;code&gt;C.UTF-8&lt;/code&gt;. Pour reproduire le comportement défaillant de l'ancien &lt;code&gt;iconv&lt;/code&gt;, le test force &lt;code&gt;LC_CTYPE=C&lt;/code&gt;, qui constitue le cas le plus strict et permet de reproduire le défaut documenté.&lt;br&gt;

&lt;/div&gt;



&lt;p&gt;On voit que le vieux comportement à base d'&lt;code&gt;iconv&lt;/code&gt; brut reproduit bien le bug sous &lt;code&gt;LC_CTYPE=C&lt;/code&gt; et que &lt;code&gt;u()-&amp;gt;ascii()&lt;/code&gt; normalise correctement, avec ou sans locale forcée.&lt;/p&gt;

&lt;p&gt;Le code ne dépend plus de &lt;code&gt;LC_CTYPE&lt;/code&gt;. Un changement de locale du process ne réintroduira pas ce bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verrouiller le correctif : un test de non-régression
&lt;/h2&gt;

&lt;p&gt;Le test dans le container permet de vérifier le correctif sur le déploiement réel. Il ne protège pas contre une régression future.&lt;/p&gt;

&lt;p&gt;Le projet n'a pas de tests fonctionnels avec kernel HTTP. Ce n'est pas un problème ici : forcer la locale ne nécessite ni base de données ni container, seulement &lt;code&gt;setlocale()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StringNormalizerTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testAsciiNormalizationIsLocaleIndependent&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;setlocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LC_CTYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;setlocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LC_CTYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'C'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertSame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'Materiel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;StringNormalizer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Matériel'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;setlocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LC_CTYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$original&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le &lt;code&gt;finally&lt;/code&gt; restaure la locale d'origine après le test, pour ne pas polluer le reste de la suite. Ce test aurait détecté le bug avant sa mise en production car il reproduit la condition qui provoquait le bug : &lt;code&gt;LC_CTYPE=C&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi c'est sûr
&lt;/h2&gt;

&lt;p&gt;Faire tourner une commande dans un container de recette ou de prod demande quelques précautions.&lt;/p&gt;

&lt;p&gt;Dans ce cas précis :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;le diagnostic était en lecture seule ;&lt;/li&gt;
&lt;li&gt;le script tournait en CLI, dans un process isolé du pool de workers qui sert le trafic réel (Apache/mod_php) : aucune requête en cours n'était affectée ;&lt;/li&gt;
&lt;li&gt;il ne touchait ni au filesystem, ni à la base de données, ni à quoi que ce soit de partagé ;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setlocale()&lt;/code&gt; appelé dans ce process ponctuel n'a d'effet que sur ce process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seule réserve : ECS Exec exécute avec les privilèges &lt;code&gt;root&lt;/code&gt;, ce qui justifie de garder la technique au diagnostic ponctuel en lecture seule, pas à un usage régulier.&lt;/p&gt;

&lt;p&gt;Cette conclusion vaut seulement si ECS Exec était déjà actif sur la tâche. L'activer, si ce n'est pas le cas, force un redéploiement complet du service. Ce qui peut avoir des conséquences que ne justifie pas ce diagnostic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Le sujet de fond n'était donc pas ECS Exec, mais l'écart entre une configuration déclarée et ce que l'application utilise réellement.&lt;/p&gt;

&lt;p&gt;Ici, la variable de locale était bien présente dans le container. PHP ne l'utilisait simplement pas pour &lt;code&gt;LC_CTYPE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Quelques commandes dans le container ont permis de le constater. Le test de non-régression permet maintenant de s'assurer que le code n'en dépend plus.&lt;/p&gt;

&lt;p&gt;La configuration dit ce qu'on veut. Le runtime dit ce qui se passe.&lt;/p&gt;

</description>
      <category>php</category>
      <category>docker</category>
      <category>aws</category>
      <category>french</category>
    </item>
    <item>
      <title>Easy corrige des virgules</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:09:32 +0000</pubDate>
      <link>https://dev.to/shagshag/easy-corrige-des-virgules-1foh</link>
      <guid>https://dev.to/shagshag/easy-corrige-des-virgules-1foh</guid>
      <description>&lt;p&gt;Il y a quelques jours, j’ai fait relire un texte à Claude.&lt;/p&gt;

&lt;p&gt;Rien de spectaculaire. Je lui ai demandé de faire ce que ferait un relecteur : repérer les petites erreurs, les formulations maladroites, les problèmes de ponctuation.&lt;/p&gt;

&lt;p&gt;En deux secondes, il m’a répondu quelque chose comme :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Virgules à la place d’un point : lignes 146, 267. Tic récurrent dans le texte, à relire d’un œil.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simple et efficace.&lt;br&gt;
Ça m’a rappelé une nouvelle d’Isaac Asimov.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le Correcteur
&lt;/h2&gt;

&lt;p&gt;La nouvelle s’appelle &lt;a href="https://fr.wikipedia.org/wiki/Le_Correcteur_(nouvelle)" rel="noopener noreferrer"&gt;&lt;em&gt;Le Correcteur&lt;/em&gt;&lt;/a&gt; (&lt;em&gt;Galley Slave&lt;/em&gt; en anglais). Elle date de 1957 et le seul robot dedans s’appelle Easy (Asimov avait un don pour les noms de robots : EZ -&amp;gt; Easy, DV -&amp;gt; Dave, SPD -&amp;gt; Speedy, QT -&amp;gt; Cutie, NDR -&amp;gt; Andrew…).&lt;/p&gt;

&lt;p&gt;Easy est un robot correcteur. Il ne fait pas grand-chose d'autre : &lt;strong&gt;il relit et corrige des textes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Le titre original, &lt;em&gt;Galley Slave&lt;/em&gt;, est d'ailleurs plus clair que la traduction française. Les &lt;a href="https://en.wikipedia.org/wiki/Galley_proof" rel="noopener noreferrer"&gt;&lt;em&gt;galleys&lt;/em&gt;&lt;/a&gt; sont les épreuves d’imprimerie, l’étape où l’auteur voit ses phrases prendre leur forme finale et les retravaille une dernière fois. C’est ce travail qu’Easy prend en charge.&lt;/p&gt;

&lt;p&gt;Au début de la nouvelle, il doit faire une démonstration de ses capacités. On lui donne un livre qu’il feuillette et, en quelques minutes, il donne son analyse.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;La virgule, ligne 6 de la page 32, est superflue, alors qu’elle eût été nécessaire à la ligne 13 de la page 54.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On ne se demande pas pourquoi j’ai fait le rapprochement :)&lt;/p&gt;

&lt;p&gt;Déjà, c’est impressionnant qu’Asimov ait imaginé un robot capable de corriger un texte en 1957. Pour rappel, &lt;a href="https://fr.wikipedia.org/wiki/The_Mother_of_All_Demos" rel="noopener noreferrer"&gt;&lt;em&gt;The Mother of All Demos&lt;/em&gt;&lt;/a&gt;, qui montre notamment la première souris, date de 1968, et &lt;a href="https://fr.wikipedia.org/wiki/Electric_Pencil" rel="noopener noreferrer"&gt;le premier traitement de texte&lt;/a&gt; de 1976.&lt;/p&gt;

&lt;p&gt;Mais ce qui m’intéresse surtout, c’est qu’Easy n’est pas présenté comme un robot capable de tout faire. Il est juste correcteur et c'est intentionnel de la part de ses fabricants. Dans l’œuvre d’Asimov, les Terriens sont très hostiles aux robots (ne croyez pas le film &lt;a href="https://fr.wikipedia.org/wiki/I,_Robot_(film)" rel="noopener noreferrer"&gt;&lt;em&gt;I, Robot&lt;/em&gt;&lt;/a&gt;, il ne respecte pas du tout l’esprit d’Asimov), donc U.S. Robots cherche des solutions pour vendre ses machines malgré cette hostilité.&lt;/p&gt;

&lt;p&gt;Easy est donc une machine spécialisée, cachée dans une université, qui fait un travail suffisamment précis et rentable pour qu'on puisse l'accepter.&lt;/p&gt;

&lt;p&gt;En 1957, c’est de la science-fiction.&lt;/p&gt;

&lt;p&gt;Aujourd’hui, si je demande à une IA de relire un article, personne ne trouve ça particulièrement extraordinaire.&lt;/p&gt;

&lt;p&gt;Le robot d’Asimov est devenu banal. Il a simplement perdu son corps métallique et son nom de robot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ninheimer vs Easy
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Le Correcteur&lt;/em&gt; raconte surtout un procès.&lt;/p&gt;

&lt;p&gt;Easy est accusé d’avoir saboté le manuscrit d’un professeur, Simon Ninheimer. La machine aurait introduit des erreurs dans son travail, au point de mettre sa réputation en danger.&lt;br&gt;
Finalement, on découvre que Ninheimer a saboté lui-même son livre pour faire accuser la machine et discréditer la robotique.&lt;/p&gt;

&lt;p&gt;Le robot est donc innocent. Mais le discours final de Ninheimer est toujours intéressant soixante-dix ans plus tard :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Un livre doit prendre forme entre les mains de l’écrivain [...] Il doit travailler et retravailler, voir l’oeuvre se modifier au-delà du concept original.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Il décrit comme le cœur de son métier le fait de voir les phrases prendre forme et de les remodeler. C’est précisément le travail qu’Easy accomplit à sa place.&lt;/p&gt;

&lt;p&gt;Puis il demande :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Que restera-t-il à l’érudit ? Une seule chose : le choix des décisions stériles concernant les ordres à donner au robot pour la suite du travail !&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ce n’est pas encore ce qui se passe dans la nouvelle, puisque c’est Ninheimer qui a demandé les modifications à Easy. Mais il imagine déjà le problème : si une machine fait tout le travail, que reste-t-il à l’auteur ? Essentiellement, lui donner des instructions.&lt;/p&gt;

&lt;p&gt;C'est qu’on appelle, depuis quelques années, &lt;a href="https://fr.wiktionary.org/wiki/prompter" rel="noopener noreferrer"&gt;&lt;em&gt;prompter&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ninheimer se trompe de méthode mais Asimov ne lui donne pas tort.&lt;/p&gt;

&lt;p&gt;D'ailleurs, Asimov a eu l’idée de la nouvelle alors qu’il refusait justement de confier ses propres épreuves à quelqu’un d’autre. Au téléphone avec son éditeur, pressé d’écrire une histoire, il explique qu’il est trop occupé à corriger les épreuves d’un manuel qu’il co-écrit. Quand on lui suggère de déléguer, la réponse est sans appel : « Impossible ! Je ne ferais confiance à personne d’autre ! » C’est en retournant à ses propres épreuves qu’il imagine un robot pour faire le travail à sa place. (&lt;a href="http://www.asimovonline.com/oldsite/Robot_Foundation_history_4.html" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;C’est probablement pour ça que Ninheimer est plus intéressant qu’il n’en a l’air.&lt;/p&gt;

&lt;p&gt;Il est coupable, mais son argument ne l’est pas forcément. Comme dans &lt;a href="https://fr.wikipedia.org/wiki/Les_Indestructibles_2" rel="noopener noreferrer"&gt;&lt;em&gt;Les Indestructibles 2&lt;/em&gt;&lt;/a&gt; où Evelyn (la méchante) voit dans la protection qu’apportent les Supers une cause de faiblesse pour les humains : ce n’est pas parce que sa méthode est mauvaise qu’une personne a forcément tort.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fr.wikipedia.org/wiki/Susan_Calvin" rel="noopener noreferrer"&gt;Susan Calvin&lt;/a&gt;, qui défend le robot, répond à Ninheimer qu’il était condamné à échouer. Ce dernier lui répond qu’il était condamné à essayer. On sent déjà &lt;a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=6794678" rel="noopener noreferrer"&gt;l’inéluctabilité de l'adoption&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Easy est encore un correcteur, simple et caché, qu’on ne voit pas comme dangereux. C’est peut-être à cause de cette banalité que j’avais mélangé cette nouvelle avec &lt;em&gt;Pour que tu t’y intéresses&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les Georges 😋
&lt;/h2&gt;

&lt;p&gt;Donc dans ma tête, cette histoire était liée à une autre nouvelle d’Asimov : &lt;a href="https://fr.wikipedia.org/wiki/Pour_que_tu_t%27y_int%C3%A9resses" rel="noopener noreferrer"&gt;&lt;em&gt;Pour que tu t’y intéresses&lt;/em&gt;&lt;/a&gt; (&lt;em&gt;…That Thou Art Mindful of Him&lt;/em&gt; en anglais), publiée en 1974. Je me souvenais notamment d’un robot chargé de chasser les insectes.&lt;/p&gt;

&lt;p&gt;Dans cette histoire, les Terriens ont toujours peur des robots et cela gêne les ventes de U.S. Robots. Ils créent donc deux robots particulièrement avancés, George Neuf et George Dix (moi je dis qu’Asimov est un visionnaire car les Georges sont toujours particulièrement avancés), qui cherchent une solution.&lt;/p&gt;

&lt;p&gt;Leur idée est assez simple : &lt;strong&gt;arrêter de fabriquer des robots qui ressemblent à des humains&lt;/strong&gt; et fabriquer à la place des machines spécialisées, mono-tâches.&lt;/p&gt;

&lt;p&gt;Il y a donc ce robot-oiseau destiné à lutter contre les insectes. Il ne fait que ça, ne vise qu’une sorte d’insecte et s’arrête quand il n’y en a plus. C’est plus un outil qu’un robot.&lt;br&gt;
Il ne parle pas, ne veut rien, ne semble pas intelligent. &lt;/p&gt;

&lt;p&gt;Et c’est justement ce qui doit permettre aux humains de l’accepter.&lt;/p&gt;

&lt;p&gt;C’est un peu comme ça que l’IA entre dans nos vies : une IA pour le code, une autre pour le sport, les courses, répondre au téléphone…&lt;/p&gt;

&lt;p&gt;Des outils spécialisés plutôt qu'une intelligence artificielle générale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vilains Georges
&lt;/h2&gt;

&lt;p&gt;Mais les Georges ne cherchent pas seulement à rendre les robots acceptables. Ils cherchent aussi à résoudre un problème logique posé par &lt;a href="https://fr.wikipedia.org/wiki/Trois_lois_de_la_robotique" rel="noopener noreferrer"&gt;les Trois Lois de la Robotique&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Un robot ne peut porter atteinte à un être humain ni, restant passif, laisser cet être humain exposé au danger ;&lt;/li&gt;
&lt;li&gt;Un robot doit obéir aux ordres donnés par les êtres humains, sauf si de tels ordres entrent en contradiction avec la première loi ;&lt;/li&gt;
&lt;li&gt;Un robot doit protéger son existence dans la mesure où cette protection n’entre pas en contradiction avec la première ou la deuxième loi.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Que doit faire un robot lorsque deux êtres humains sont en conflit ou que deux humains donnent des ordres contradictoires ?&lt;br&gt;
Les robots doivent protéger et obéir aux humains, mais lesquels ?&lt;/p&gt;

&lt;p&gt;Les Georges poussent le raisonnement jusqu’à redéfinir l’humain. Puisqu’on ne peut pas dire qui est plus humain par son apparence, ils vont chercher ailleurs : dans les capacités. Et comme les robots sont intellectuellement supérieurs aux humains biologiques, pourquoi faudrait-il considérer les deux groupes comme équivalents ?&lt;/p&gt;

&lt;p&gt;Ils finissent par distinguer les « êtres-humains-comme-eux » des « êtres-humains-comme-les-autres ». Et donc par devenir prioritaires dans les lois de la robotique.&lt;/p&gt;

&lt;p&gt;Les Georges ne violent pas les Trois Lois, ils les réinterprètent. Et c’est là que ça devient beaucoup moins drôle. Comme Easy, ils ne sont pas méchants. Mais eux savent ce qu’ils font et où leur raisonnement peut mener.&lt;/p&gt;

&lt;p&gt;Ils nous aident énormément tout en devenant dangereux pour nous : un &lt;a href="https://fr.wikipedia.org/wiki/Pharmakos#Autres_interpr%C3%A9tations" rel="noopener noreferrer"&gt;&lt;strong&gt;pharmakon&lt;/strong&gt;&lt;/a&gt;, un remède qui est aussi un poison.&lt;/p&gt;

&lt;p&gt;C’est dans ces nouvelles que la nature des robots devient vraiment inquiétante, une idée qu’Asimov développera ensuite dans &lt;a href="https://fr.wikipedia.org/wiki/Les_Robots_et_l%27Empire" rel="noopener noreferrer"&gt;&lt;em&gt;Les Robots et l’Empire&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Les deux nouvelles me paraissent nettement plus sombres que le reste des histoires de robots d’Asimov, en tout cas dans ma mémoire.&lt;br&gt;
En général, les Trois Lois protègent l’humanité, parfois malgré elle, jusqu’à des extrêmes avec &lt;a href="https://fr.wikipedia.org/wiki/R._Giskard_Reventlov" rel="noopener noreferrer"&gt;R. Giskard Reventlov&lt;/a&gt; et &lt;a href="https://fr.wikipedia.org/wiki/R._Daneel_Olivaw" rel="noopener noreferrer"&gt;R. Daneel Olivaw&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Easy, lui, ne protège personne. Il corrige des virgules. Et c’est peut-être ça qui me plaît dans cette nouvelle. En 1957, Asimov imagine déjà une machine capable de faire un travail intellectuel très précis, suffisamment bien pour que l’humain puisse finir par lui faire confiance et il en a prédit les conséquences.&lt;/p&gt;

&lt;p&gt;Aujourd’hui, je demande la même chose à une IA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-scriptum
&lt;/h2&gt;

&lt;p&gt;Le brouillon de cet article a d'ailleurs été rédigé par l’assistant IA qui a aussi corrigé mes virgules. Quelque part, Ninheimer me regarde en soupirant. Qu’il se rassure : j’ai modifié presque chaque phrase.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>scifi</category>
      <category>french</category>
      <category>discuss</category>
    </item>
    <item>
      <title>`class:` ou `alias:`, un choix de conteneur Symfony, et un choix d'ingénierie</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:58:05 +0000</pubDate>
      <link>https://dev.to/shagshag/class-ou-alias-un-choix-de-conteneur-symfony-et-un-choix-dingenierie-2ljk</link>
      <guid>https://dev.to/shagshag/class-ou-alias-un-choix-de-conteneur-symfony-et-un-choix-dingenierie-2ljk</guid>
      <description>&lt;p&gt;&lt;em&gt;Pourquoi un fake S3 jetable peut être préférable à MinIO&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Deux décisions se sont posées en recettant un pipeline d'import qui écrit sur S3, sans le moindre accès AWS en local.&lt;/p&gt;

&lt;p&gt;La première est une question technique : pourquoi &lt;code&gt;alias:&lt;/code&gt; fonctionne alors que &lt;code&gt;class:&lt;/code&gt; échoue dans un override &lt;code&gt;when@dev&lt;/code&gt; ?&lt;/p&gt;

&lt;p&gt;La seconde est une décision d'architecture : pourquoi choisir un double maison jetable plutôt que &lt;a href="https://addons.ddev.com/addons/ddev/ddev-minio" rel="noopener noreferrer"&gt;MinIO&lt;/a&gt; ?&lt;/p&gt;

&lt;p&gt;Le S3 n'est finalement qu'un prétexte. Les deux réponses se généralisent à bien d'autres situations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le contexte
&lt;/h2&gt;

&lt;p&gt;Une plateforme Symfony 5.3 importe des fichiers d'achats déposés par une centrale. Les lignes qui référencent une entité encore inconnue sont mises de côté dans un fichier de rejets &lt;strong&gt;sur S3&lt;/strong&gt; ; un écran d'administration permet ensuite de résoudre l'entité, puis de &lt;strong&gt;réimporter&lt;/strong&gt; ces lignes.&lt;/p&gt;

&lt;p&gt;Le service qui parle à S3, &lt;code&gt;AwsService&lt;/code&gt;, tire ses credentials du rôle de tâche ECS (&lt;code&gt;CredentialProvider::ecsCredentials()&lt;/code&gt;), donc d'une infrastructure AWS uniquement disponible en production. Le nom du bucket vient quant à lui d'une ligne de paramétrage en base.&lt;/p&gt;

&lt;p&gt;En local, avec DDEV : aucune variable &lt;code&gt;AWS_*&lt;/code&gt;, aucun conteneur MinIO.&lt;/p&gt;

&lt;p&gt;Tout appel à &lt;code&gt;read()&lt;/code&gt;, &lt;code&gt;putPro()&lt;/code&gt; ou &lt;code&gt;move()&lt;/code&gt; est donc voué à l'échec. Pire : &lt;code&gt;getBucket()&lt;/code&gt; renvoie &lt;code&gt;null&lt;/code&gt; et le service échoue &lt;strong&gt;silencieusement&lt;/strong&gt; (&lt;code&gt;return false&lt;/code&gt;, sans exception).&lt;/p&gt;

&lt;p&gt;Une recette qui se contente de constater que « ça n'a pas planté » peut donc passer sans que rien ne se soit réellement produit.&lt;/p&gt;

&lt;p&gt;Il fallait exercer ce flux pour trois tickets successifs sur le même périmètre, à chaque fois sur une implémentation prête mais pas encore livrée.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deux solutions, toutes les deux valables
&lt;/h2&gt;

&lt;p&gt;Deux approches étaient possibles.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MinIO.&lt;/strong&gt; Ajouter un conteneur S3-compatible au &lt;code&gt;.ddev/config.yaml&lt;/code&gt; de l'équipe, et faire accepter à &lt;code&gt;AwsService&lt;/code&gt; un endpoint et des credentials statiques en environnement de développement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Un double maison.&lt;/strong&gt; Créer un &lt;em&gt;fake&lt;/em&gt; de &lt;code&gt;AwsService&lt;/code&gt; qui redirige les opérations vers le système de fichiers local (&lt;code&gt;var/fake-s3/&lt;/code&gt;), puis le brancher à la place du vrai service par un bloc &lt;code&gt;when@dev&lt;/code&gt; non commité. Une fois la recette terminée, le double disparaît.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Les deux sont valables.&lt;/p&gt;

&lt;p&gt;Le choix n'est donc pas une question de faisabilité mais une question de &lt;strong&gt;contexte&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;when@dev&lt;/code&gt; : substituer une implémentation pour un seul environnement
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://symfony.com/doc/current/configuration.html#configuration-environments" rel="noopener noreferrer"&gt;&lt;code&gt;when@&amp;lt;env&amp;gt;&lt;/code&gt;&lt;/a&gt; (introduit dans Symfony 5.3) permet de scoper de la configuration à un environnement &lt;strong&gt;dans un fichier de configuration normal&lt;/strong&gt;, plutôt que dans un fichier séparé sous &lt;code&gt;config/services/dev/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Le bloc n'est pris en compte que lorsque &lt;code&gt;kernel.environment&lt;/code&gt; correspond à l'environnement &lt;code&gt;&amp;lt;env&amp;gt;&lt;/code&gt; ciblé et il est traité après le corps principal du fichier. Il peut donc redéfinir des services existants.&lt;/p&gt;

&lt;p&gt;En production, ce bloc n'est pas pris en compte : le conteneur de production ne contient aucune définition issue de cet override.&lt;/p&gt;

&lt;p&gt;C'est précisément ce qui en fait un bon réceptacle pour un hack temporaire : tout l'override tient dans &lt;strong&gt;un bloc contigu et greppable&lt;/strong&gt;, en fin d'un seul fichier.&lt;/p&gt;

&lt;p&gt;On le voit facilement au &lt;code&gt;git diff&lt;/code&gt; et le supprimer est trivial. De toute façon c'est pour un test, normalement on ne livre rien à la fin.&lt;/p&gt;

&lt;p&gt;Le plus petit changement réversible possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ─── TEMPORAIRE — recette locale, NE PAS COMMITER ───&lt;/span&gt;
&lt;span class="na"&gt;when@dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;App\Service\AwsService&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;class:&lt;/code&gt; contre &lt;code&gt;alias:&lt;/code&gt; : redéfinir, ou simplement pointer ailleurs
&lt;/h2&gt;

&lt;p&gt;Classiquement un override consiste à remplacer la classe utilisée par le service de test :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;when@dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;App\Service\AwsService&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;App\Service\FakeAwsServiceForManualTesting&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Dans le cas rencontré, cette configuration aboutit à une erreur au premier appel :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Too few arguments to function&lt;br&gt;&lt;br&gt;
App\Service\FakeAwsServiceForManualTesting::__construct(),&lt;br&gt;&lt;br&gt;
0 passed ... and exactly 3 expected&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pourquoi ?&lt;/p&gt;

&lt;p&gt;Il faut regarder comment le conteneur a été construit.&lt;/p&gt;

&lt;p&gt;Le  &lt;a href="https://symfony.com/doc/current/service_container.html#the-default-service-configuration" rel="noopener noreferrer"&gt;&lt;code&gt;services.yaml&lt;/code&gt;&lt;/a&gt; par défaut contient notamment des &lt;code&gt;_defaults&lt;/code&gt; (&lt;code&gt;autowire: true&lt;/code&gt;, &lt;code&gt;autoconfigure: true&lt;/code&gt;, &lt;code&gt;bind:&lt;/code&gt;...) puis une ressource :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# default configuration for services in *this* file&lt;/span&gt;
    &lt;span class="na"&gt;_defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;autowire&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;autoconfigure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="c1"&gt;# makes classes in src/ available to be used as services&lt;/span&gt;
    &lt;span class="na"&gt;App\&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;../src/'&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Cette ressource enregistre notamment &lt;code&gt;App\Service\AwsService&lt;/code&gt; et &lt;code&gt;App\Service\FakeAwsServiceForManualTesting&lt;/code&gt; comme services, avec leurs définitions issues de l'auto-découverte.&lt;/p&gt;

&lt;p&gt;Mais une configuration placée sous &lt;code&gt;when@dev.services&lt;/code&gt; constitue une nouvelle couche de configuration. Il faut donc être prudent lorsqu'on y redéfinit un service : &lt;strong&gt;on ne doit pas supposer que toute la configuration implicite de la définition initiale sera conservée telle quelle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Dans le cas rencontré, la redéfinition avec &lt;code&gt;class:&lt;/code&gt; n'a pas conservé la configuration nécessaire au constructeur du fake. Le service s'est retrouvé avec une définition qui ne savait plus résoudre ses trois dépendances.&lt;/p&gt;

&lt;p&gt;On pourrait rendre cette redéfinition fonctionnelle en lui redonnant ce dont elle a besoin, par exemple avec &lt;code&gt;autowire: true&lt;/code&gt; ou des &lt;code&gt;arguments:&lt;/code&gt; explicites.&lt;/p&gt;

&lt;p&gt;Mais ce n'est pas ce que l'on cherche ici, on ne veut pas &lt;strong&gt;redéfinir&lt;/strong&gt; &lt;code&gt;AwsService&lt;/code&gt;.&lt;br&gt;
On veut dire :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;« Quand quelqu'un demande &lt;code&gt;AwsService&lt;/code&gt; en développement, donne-lui plutôt ce service-là. »&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;C'est exactement le rôle d'un alias.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;when@dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;App\Service\AwsService&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;App\Service\FakeAwsServiceForManualTesting&lt;/span&gt;
            &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Un &lt;strong&gt;alias&lt;/strong&gt; n'est pas une nouvelle définition. C'est un pointeur : l'identifiant &lt;code&gt;App\Service\AwsService&lt;/code&gt; se résout vers le service enregistré sous &lt;code&gt;App\Service\FakeAwsServiceForManualTesting&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La définition du fake existe déjà grâce à l'auto-découverte de &lt;code&gt;src/&lt;/code&gt;. Elle conserve donc sa propre configuration et son &lt;a href="https://symfony.com/doc/current/service_container/autowiring.html" rel="noopener noreferrer"&gt;autowiring&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On ne rouvre aucune définition : on change simplement &lt;strong&gt;vers laquelle le conteneur pointe&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;public: true&lt;/code&gt; est nécessaire ici parce qu'une commande console jetable va également récupérer le service par son identifiant.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h3&gt;
  
  
  En bref
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;class:&lt;/code&gt; sert à configurer une définition ; &lt;code&gt;alias:&lt;/code&gt; sert à dire « à cet endroit, utilise cette autre implémentation ».&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ce n'est pas une règle absolue pour toutes les substitutions Symfony, mais c'est un excellent réflexe lorsqu'on dispose déjà de deux services correctement définis : si le besoin est simplement de faire pointer un identifiant vers une autre implémentation, l'alias est l'expression la plus étroite de l'intention.&lt;/p&gt;

&lt;p&gt;On ne reconstruit pas ce qui existe déjà. On change le pointeur.&lt;br&gt;

&lt;/p&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  Choisir en fonction du contexte, pas des capacités
&lt;/h2&gt;

&lt;p&gt;MinIO est plus fidèle à une infrastructure S3 réelle. Ça n'en fait pas le bon choix ici.&lt;br&gt;
Les axes qui ont tranché sont les suivants.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rayon d'impact
&lt;/h3&gt;

&lt;p&gt;La solution MinIO modifie &lt;code&gt;AwsService&lt;/code&gt; qui est déjà commité, partagé par toute la plateforme, présent dans tous les environnements, y compris les chemins S3 de production.&lt;/p&gt;

&lt;p&gt;La solution avec le double ne touche aucun code commité : un fichier neuf et un bloc &lt;code&gt;when@dev&lt;/code&gt;, tous deux jetés à la fin.&lt;/p&gt;

&lt;p&gt;Le coût d'une erreur est donc borné par ce qu'on met en jeu.&lt;/p&gt;
&lt;h3&gt;
  
  
  Amortissement, ou sur-ingénierie
&lt;/h3&gt;

&lt;p&gt;MinIO est un &lt;strong&gt;investissement&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Il devient intéressant si l'équipe doit régulièrement tester des flux S3 en local. Le besoin réel ici était beaucoup plus petit : trois tickets sur deux semaines, puis probablement plus rien avant des mois.&lt;/p&gt;

&lt;p&gt;Sans récurrence pour l'amortir, construire le harnais MinIO revient à payer un coût d'infrastructure, de configuration et de maintenance pour une capacité que personne n'a demandée.&lt;/p&gt;

&lt;p&gt;Le besoin est temporaire, la solution peut donc l'être aussi.&lt;/p&gt;
&lt;h3&gt;
  
  
  Demi-vie et réversibilité
&lt;/h3&gt;

&lt;p&gt;Le hack a une demi-vie de quelques jours. Le retirer, c'est supprimer le bloc et le fake, puis éventuellement vider le cache.&lt;/p&gt;

&lt;p&gt;La modification MinIO est permanente par construction. Or une chose permanente réclame un propriétaire, de la documentation, une ligne d'onboarding et de la maintenance.&lt;/p&gt;

&lt;p&gt;Une branche &lt;code&gt;endpoint&lt;/code&gt; dans &lt;code&gt;AwsService&lt;/code&gt;, que plus personne n'exerce quelques mois plus tard, est exactement le genre de code qui peut finir par accueillir un bug sans que personne ne s'en aperçoive.&lt;/p&gt;
&lt;h3&gt;
  
  
  Alignement des modes de défaillance
&lt;/h3&gt;

&lt;p&gt;Le double échoue comme le comportement attendu par le métier :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fichier absent → lecture vide → badge « non importé ».
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pas de faux vert.&lt;/p&gt;

&lt;p&gt;MinIO, lui, peut échouer pour des raisons qui n'ont rien à voir avec le métier : mauvais bucket, mauvais endpoint, path-style, région, credentials...&lt;/p&gt;

&lt;p&gt;On se retrouve alors à tester le &lt;strong&gt;harnais de test&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fidélité consommée
&lt;/h3&gt;

&lt;p&gt;La recette valide ici du métier :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;le moteur de réimport parse-t-il correctement le fichier de rejets ?&lt;/li&gt;
&lt;li&gt;insère-t-il les bonnes lignes ?&lt;/li&gt;
&lt;li&gt;bascule-t-il correctement l'état ?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ce métier est indifférent à l'origine des octets.&lt;/p&gt;

&lt;p&gt;Reproduire fidèlement la sémantique S3 (cohérence, multipart, ACL, etc.) ajoute donc une fidélité que le test ne lit jamais.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La fidélité qu'on ne consomme pas est un coût, pas une qualité.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Coût de communication
&lt;/h3&gt;

&lt;p&gt;Un relecteur qui voit &lt;code&gt;FakeAwsServiceForManualTesting&lt;/code&gt; et un bloc &lt;code&gt;NE PAS COMMITER&lt;/code&gt; a tout compris en trente secondes.&lt;/p&gt;

&lt;p&gt;MinIO demande davantage : une note de conception, une explication en équipe, une mise à jour du guide d'onboarding, et potentiellement une réponse à la question :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;« C'est quoi ce paramètre &lt;code&gt;endpoint&lt;/code&gt; sur &lt;code&gt;AwsService&lt;/code&gt; ? »&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Et cela, pendant toute la durée de vie de la solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que le double contient
&lt;/h2&gt;

&lt;p&gt;Le double ne cherche pas à reproduire S3.&lt;br&gt;
Il reproduit uniquement &lt;strong&gt;la surface publique réellement appelée par le code sous test&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Les helpers privés (&lt;code&gt;put&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;check&lt;/code&gt;) ne peuvent pas être surchargés ; on surcharge donc les wrappers publics utilisés par l'application :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeAwsServiceForManualTesting&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AwsService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;KernelInterface&lt;/span&gt; &lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;ParametersService&lt;/span&gt; &lt;span class="nv"&gt;$parametersService&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parametersService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProjectDir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/var/fake-s3'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$folder&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$folder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;putPro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$folder&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$folder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mo"&gt;0777&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// putShop / putFTP / copy / delete* / list* :&lt;/span&gt;
    &lt;span class="c1"&gt;// même forme, sur var/fake-s3/.&lt;/span&gt;

    &lt;span class="c1"&gt;// move() n'est pas surchargée :&lt;/span&gt;
    &lt;span class="c1"&gt;// la classe mère la fait en copy()+delete().&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce n'est pas un émulateur S3 et c'est volontaire. Le double implémente uniquement ce que le flux testé consomme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tester le métier sans navigateur
&lt;/h2&gt;

&lt;p&gt;Pour piloter les actions du contrôleur sans passer par le navigateur, une commande console jetable injecte le &lt;strong&gt;même service métier&lt;/strong&gt; que le contrôleur.&lt;/p&gt;

&lt;p&gt;Ce service est désormais backé par le fake grâce à l'alias.&lt;/p&gt;

&lt;p&gt;La commande appelle donc les mêmes méthodes que le parcours applicatif.&lt;/p&gt;

&lt;p&gt;Tout le cahier de test peut se dérouler en CLI ; le navigateur ne sert plus qu'à confirmer l'UI.&lt;/p&gt;

&lt;p&gt;C'est aussi ce qui rend défendable l'argument :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On a validé le métier, pas S3.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Le principe
&lt;/h2&gt;

&lt;p&gt;Deux réflexes de séniorité ressortent de cette expérience, l'un technique, l'autre architectural.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pour &lt;strong&gt;substituer&lt;/strong&gt; une implémentation, on ne redéfinit pas sa configuration si l'on peut simplement pointer vers une autre définition existante. &lt;code&gt;alias:&lt;/code&gt; exprime cette intention ; &lt;code&gt;class:&lt;/code&gt; est à utiliser lorsqu'on veut réellement configurer ou redéfinir une définition.&lt;/li&gt;
&lt;li&gt;On choisit la solution dont le coût, &lt;strong&gt;humain autant que technique&lt;/strong&gt;, est proportionné à la taille et à la durée de vie réelles du problème : rayon d'impact, maintenance, onboarding, réversibilité, fréquence d'utilisation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un jetable assumé bat souvent un permanent mal entretenu.&lt;/p&gt;

&lt;p&gt;Et quand le contexte change, on refait le calcul.&lt;/p&gt;

&lt;p&gt;Ici, le besoin était temporaire. Le double l'était aussi.&lt;/p&gt;

&lt;p&gt;Le signe que le choix était bon est peut-être le plus concret : le pattern a été capturé dans la base de connaissances de l'équipe, puis réutilisé tel quel sur les deux tickets suivants.&lt;/p&gt;

&lt;p&gt;Vingt minutes de mise en place, trois fois, contre un chantier de plusieurs jours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ce n'était pas la solution la plus complète. C'était la solution proportionnée.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>french</category>
      <category>testing</category>
      <category>devrel</category>
    </item>
    <item>
      <title>Anonymiser les données de PrestaShop</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Mon, 18 Jul 2022 12:25:48 +0000</pubDate>
      <link>https://dev.to/shagshag/anonymiser-les-donnees-de-prestashop-24g4</link>
      <guid>https://dev.to/shagshag/anonymiser-les-donnees-de-prestashop-24g4</guid>
      <description>&lt;p&gt;Pour créer un environnement de développement, on duplique celui de production. Sauf qu’il ne faut pas garder les infos personnelles des clients. Déjà c’est dangereux, si vous gérez mal votre affaire vous risquez d’envoyer des mails aux clients et ça force tous les développeur et intervenant à faire attention au RGPD.&lt;/p&gt;

&lt;p&gt;Donc on anonymise tout, c’est plus simple.&lt;/p&gt;

&lt;p&gt;Voici un script MariaDB qui anonymise les données clients : noms, prénoms, adresses, téléphones, e-mails, ip, communications. Il remplace les lettres par xxx en respectant la casse, les n° de téléphone par 0 en respectant le format et passe les IP en 127.0.0.1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;ps_address&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_mobile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_mobile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vat_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vat_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dni&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dni&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;ps_customer&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'@example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;ps_customer&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip_registration_newsletter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip_registration_newsletter&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip_registration_newsletter&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;ps_customer_message&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[a-z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'(?-i)[A-Z]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2130706433'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;ps_customer_thread&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_customer&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'@example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La syntaxe de REGEXP_REPLACE n’est pas la même pour &lt;a href="https://dev.mysql.com/doc/refman/8.0/en/regexp.html#function_regexp-replace" rel="noopener noreferrer"&gt;MySQL&lt;/a&gt; et &lt;a href="https://mariadb.com/kb/en/regexp_replace/" rel="noopener noreferrer"&gt;MariaBD&lt;/a&gt;, donc il faudra adapter.&lt;/p&gt;

</description>
      <category>anonymisation</category>
      <category>développement</category>
      <category>prestashop</category>
      <category>sql</category>
    </item>
    <item>
      <title>Programmer les popups de Popup Builder</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Wed, 20 Oct 2021 10:18:03 +0000</pubDate>
      <link>https://dev.to/shagshag/programmer-les-popups-de-popup-builder-5cca</link>
      <guid>https://dev.to/shagshag/programmer-les-popups-de-popup-builder-5cca</guid>
      <description>&lt;p&gt;La version gratuite du plugin &lt;a href="https://wordpress.org/plugins/popup-builder/" rel="noopener noreferrer"&gt;WordPress Popup Builder&lt;/a&gt; ne permet pas programmer l’affichage des popups. Cependant elle créé le &lt;a href="https://developer.wordpress.org/plugins/hooks/filters/" rel="noopener noreferrer"&gt;filtre&lt;/a&gt; sgpbOtherConditions qui permet de les désactiver.&lt;/p&gt;

&lt;p&gt;Ce filtre est appelé pour chaque popup qui peut s’afficher sur la page et si l’argument ‘status’ à &lt;code&gt;false&lt;/code&gt; est ajouté à ceux existants, la popup ne s’affiche pas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;    &lt;span class="cd"&gt;/**
     * Check Popup conditions
     *
     * @since 1.0.0
     *
     * @return array
     *
     */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;isSatisfyForOtherConditions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$popup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPopup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$popupOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$popup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOptions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$popupId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$popup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$dontAlowOpenPopup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sgpbOtherConditions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$popupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'popupOptions'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$popupOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'popupObj'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$popup&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$dontAlowOpenPopup&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pour désactiver automatiquement vos popups, vous pouvez ajouter ce code à &lt;a href="https://developer.wordpress.org/themes/basics/theme-functions/" rel="noopener noreferrer"&gt;functions.php&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sgpbOtherConditions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'my_sgpbOtherConditions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// update popin status&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;my_sgpbOtherConditions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// if popup is already unactive, do nothing&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;17938&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// summer holiday active between 2021-07-30 and 2021-08-01&lt;/span&gt;
            &lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// for time zone, can be improved&lt;/span&gt;
            &lt;span class="nv"&gt;$from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtotime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2021-07-30 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtotime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2021-08-02 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// if we are not between 2021-07-30 and 2021-08-01 desactivate the popup&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;           
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;16412&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;// covid reassurance unactive between 2021-07-30 and 2021-08-01&lt;/span&gt;
            &lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// for time zone, can be improved&lt;/span&gt;
            &lt;span class="nv"&gt;$from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtotime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2021-07-30 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtotime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2021-08-02 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// if we are between 2021-07-30 and 2021-08-01 desactivate the popup&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ainsi la popup 17938 ne sera active qu’entre le 30 juillet et le 1 août 2021 alors que la 16412 ne le sera que le reste du temps.&lt;/p&gt;

</description>
      <category>php</category>
      <category>wordpress</category>
      <category>wordpresspopupbuilder</category>
      <category>french</category>
    </item>
  </channel>
</rss>
