<?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>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>
    <item>
      <title>Sauvegarde automatique sur FTP</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Wed, 20 Oct 2021 09:31:49 +0000</pubDate>
      <link>https://dev.to/shagshag/sauvegarde-automatique-sur-ftp-4f42</link>
      <guid>https://dev.to/shagshag/sauvegarde-automatique-sur-ftp-4f42</guid>
      <description>&lt;p&gt;A force je pense avoir un script de sauvegarde FTP efficace.&lt;/p&gt;

&lt;p&gt;Il sauvegarde les bases de données ainsi qu’un dossier du serveur et les envoi sur un serveur FTP. Si le serveur FTP est plein, il supprime les sauvegardes les plus anciennes jusqu’à avoir assez de place pour mettre la nouvelle.&lt;/p&gt;

&lt;p&gt;Si une erreur arrive, il envoi un mail d’alerte.&lt;/p&gt;

&lt;p&gt;Le script nécessite &lt;a href="https://doc.ubuntu-fr.org/curlftpfs" rel="noopener noreferrer"&gt;curlftpfs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Utilisation :
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Modifiez le script dans la partie configuration&lt;/li&gt;
&lt;li&gt;Enregistrez le sur le serveur, par exemple dans /opt/scripts/backup.sh&lt;/li&gt;
&lt;li&gt;Donnez les droits d’execution au fichier : &lt;code&gt;chmod +x /opt/scripts/backup.sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ajoutez une tâche cron : &lt;code&gt;02 00 * * * /bin/bash /opt/scripts/backup.sh &amp;gt; /var/log/backup.log 2&amp;gt;&amp;amp;1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Also on: &lt;a href="https://gist.github.com/Shagshag/acd208c48253b3b43ca941ceb4f3ad9a" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backup</category>
      <category>bash</category>
      <category>sysadmin</category>
      <category>french</category>
    </item>
    <item>
      <title>Autoriser les images avec des longs ID dans PrestaShop</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Wed, 09 Jun 2021 09:56:37 +0000</pubDate>
      <link>https://dev.to/shagshag/autoriser-les-images-avec-des-longs-id-dans-prestashop-631</link>
      <guid>https://dev.to/shagshag/autoriser-les-images-avec-des-longs-id-dans-prestashop-631</guid>
      <description>&lt;p&gt;Par construction PrestaShop ne peut pas gérer les images avec des ID supérieurs à 9 999 999 (7 chiffres). C’est déjà beaucoup et on a pas souvent besoin de plus mais j’ai eu affaire à un script d’import qui impose les ID d’images et donc ça arrive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pourquoi PrestaShop ne les gèrent pas ?
&lt;/h3&gt;

&lt;p&gt;En fait, PrestaShop n’a pas de soucis avec les ID de 8 chiffres ou plus, tant que la base de données l’accepte, lui aussi. Le problème vient de la façon d’accéder aux images.&lt;/p&gt;

&lt;p&gt;L’URL d’une image est &lt;code&gt;https://example.com/123-large/mon-image.jpg&lt;/code&gt; soit &lt;code&gt;[url de la boutique]/[ID de l'image]-[format à afficher]/[texte pour le référencement].jpg&lt;/code&gt; et &lt;a href="https://httpd.apache.org/docs/2.4/howto/htaccess.html" rel="noopener noreferrer"&gt;le fichier &lt;code&gt;.htaccess&lt;/code&gt;&lt;/a&gt; explique au serveur que quand on a une url de ce type, il faut aller chercher le fichier qu’il faut dans le dossier des images &lt;code&gt;/img/p/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Le fichier .htaccess de PrestaShop ressemble à ça&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$1$2$3.jpg [L]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$1$2$3$4.jpg [L]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$1$2$3$4$5.jpg [L]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^([0-9])([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$4/$1$2$3$4$5$6.jpg [L]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^([0-9])([0-9])([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$4/$5/$1$2$3$4$5$6$7.jpg [L]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$4/$5/$6/$1$2$3$4$5$6$7$8.jpg [L]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$4/$5/$6/$7/$1$2$3$4$5$6$7$8$9.jpg [L]Code language: JavaScript (javascript)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Donc quand le serveur reçoit l’URL &lt;code&gt;https://example.com/123-large/mon-image.jpg&lt;/code&gt; il affiche le fichier &lt;code&gt;/img/p/1/2/3/123-large.jpg&lt;/code&gt;. C’est rapide et ça ne demande pas de ressource, PrestaShop n’est même pas appelé quand on veut afficher une image produit.&lt;/p&gt;

&lt;p&gt;Mais &lt;a href="https://stackoverflow.com/a/5849017/2530962" rel="noopener noreferrer"&gt;on ne peut pas utiliser $10 dans un fichier .htaccess&lt;/a&gt;, il est interprété comme $1 puis le caractère 0. Comme dans la dernière ligne de l’extrait ci-dessus on utilise $9, on ne peut pas aller plus loin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ma solution
&lt;/h3&gt;

&lt;p&gt;Si on appelle l’URL &lt;code&gt;https://example.com/123456789-large/mon-image.jpg&lt;/code&gt;, le serveur ne reconnait pas ce format et essaye simplement d’afficher le fichier &lt;code&gt;/123456789-large/mon-image.jpg&lt;/code&gt; qui n’existe pas et renvoie donc une erreur 404. PrestaShop affiche donc une jolie page pour dire que le fichier n’a pas été trouvée.&lt;/p&gt;

&lt;p&gt;L’astuce consiste à utiliser cette page pour afficher l’image demandée en &lt;a href="https://devdocs.prestashop.com/1.7/modules/concepts/overrides/" rel="noopener noreferrer"&gt;complétant&lt;/a&gt; &lt;a href="https://github.com/PrestaShop/PrestaShop/blob/develop/controllers/front/PageNotFoundController.php" rel="noopener noreferrer"&gt;le controller PageNotFound&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="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PageNotFoundController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;PageNotFoundControllerCore&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;initContent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&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="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REQUEST_URI'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/([0-9]+)(\-[_a-zA-Z0-9-]*)?\/.*\.jpg/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;))&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="n"&gt;_PS_ROOT_DIR_&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/img/p/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;implode&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="nb"&gt;str_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$matches&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$matches&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="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'.jpg'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_exists&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="p"&gt;{&lt;/span&gt;
                &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: '&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;mime_content_type&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;'image/jpg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nb"&gt;readfile&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="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'HTTP/1.1 404 Not Found'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Status: 404 Not Found'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: image/gif'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nb"&gt;readfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_PS_ROOT_DIR_&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/img/404.gif'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;die&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;return&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;initContent&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;Code à placer dans &lt;code&gt;/override/controllers/front/PageNotFoundController.php&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Le principe est le suivant :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;quand le controller PageNotFound est appelé, on regarde si l’URL correspond à celle d’une image produit,&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;si oui on regarde si elle existe, &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;si oui on l’affiche et c’est terminé&lt;/li&gt;
&lt;li&gt;si non on affiche une image d’erreur&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;si non on laisse le controller gérer&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devdocs.prestashop.com/1.7/modules/concepts/overrides/" rel="noopener noreferrer"&gt;Overrides dans PrestaShop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://httpd.apache.org/docs/2.4/howto/htaccess.html" rel="noopener noreferrer"&gt;Format du fichier .htaccess&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also on: &lt;a href="https://gist.github.com/Shagshag/9b7ca766e6a855ba17d8af9c71412076" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>prestashop</category>
      <category>php</category>
      <category>french</category>
    </item>
    <item>
      <title>Rediriger les fiches produits dans WooCommerce</title>
      <dc:creator>Georges Cubas</dc:creator>
      <pubDate>Wed, 09 Jun 2021 09:00:39 +0000</pubDate>
      <link>https://dev.to/shagshag/rediriger-les-fiches-produits-dans-woocommerce-30f7</link>
      <guid>https://dev.to/shagshag/rediriger-les-fiches-produits-dans-woocommerce-30f7</guid>
      <description>&lt;p&gt;Dans WooCommerce chaque produit a &lt;a href="https://docs.woocommerce.com/document/marketing-product-pages-writing/#section-4" rel="noopener noreferrer"&gt;une page dédiée&lt;/a&gt;. Dans le panier, les produits renvoient à celles-ci.&lt;/p&gt;

&lt;p&gt;Si vous voulez désactiver cette fonctionnalité et afficher une page avec tous vos produits, 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="c1"&gt;// redirige les pages produits&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'prevent_access_to_product_page'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;prevent_access_to_product_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;is_product&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="nf"&gt;wp_redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;get_permalink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;269&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;Ainsi toutes les pages produits redirigeront vers la page 269.&lt;/p&gt;

&lt;p&gt;Source : &lt;a href="https://stackoverflow.com/q/44197739" rel="noopener noreferrer"&gt;How to disable/hide woocommerce single product page?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also on: &lt;a href="https://stackoverflow.com/a/67885098/2530962" rel="noopener noreferrer"&gt;Stack Overflow&lt;/a&gt;&lt;/p&gt;

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