<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Utc on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/utc/</link><description>Recent content in Utc on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Sun, 19 Feb 2017 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/tags/utc/index.xml" rel="self" type="application/rss+xml"/><item><title>Forcer l'UTC dans Doctrine sans toucher aux entités</title><link>https://guillaumedelre.github.io/fr/2017/02/19/forcer-lutc-dans-doctrine-sans-toucher-aux-entit%C3%A9s/</link><pubDate>Sun, 19 Feb 2017 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2017/02/19/forcer-lutc-dans-doctrine-sans-toucher-aux-entit%C3%A9s/</guid><description>Comment surcharger les types natifs de Doctrine pour imposer l&amp;#39;UTC partout, sans modifier une seule entité.</description><content:encoded><![CDATA[<p>Un timestamp qui revient de la base de données avec une heure de décalage. Pas à chaque fois. Uniquement quand le serveur de dev tourne en <code>Europe/Paris</code> et que la CI tourne en UTC. Le genre de bug qui disparaît quand on le cherche et qui revient en production un vendredi soir.</p>
<p>Le problème n&rsquo;est pas dans la logique métier. Il est dans ce que Doctrine fait discrètement avec les dates.</p>
<h2 id="ce-que-doctrine-fait-par-défaut">Ce que Doctrine fait par défaut</h2>
<p>Quand on déclare un champ <code>datetime</code> dans une entité Doctrine, la conversion entre PHP et la base de données passe par <code>DateTimeType</code>. Cette classe appelle <code>format()</code> sur l&rsquo;objet <code>DateTime</code> pour écrire en base, et <code>DateTime::createFromFormat()</code> pour le relire. Aucune mention de timezone nulle part.</p>
<p>Si l&rsquo;objet PHP est en <code>Europe/Paris</code>, Doctrine formate <code>2017-01-15 11:30:00</code> et l&rsquo;écrit tel quel. Si le serveur qui lit ce champ est en UTC, il obtient <code>2017-01-15 11:30:00</code> et l&rsquo;interprète comme UTC. Une heure s&rsquo;est évaporée dans l&rsquo;aller-retour, sans le moindre message d&rsquo;erreur.</p>
<p><a href="https://www.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/working-with-datetime.html" target="_blank" rel="noopener noreferrer">La doc Doctrine couvre ce sujet</a> et suggère des types personnalisés comme solution. Ce qu&rsquo;elle mentionne en passant, c&rsquo;est qu&rsquo;on peut donner à ces types personnalisés le même nom que les types natifs. Ce détail change tout.</p>
<h2 id="remplacer-pas-ajouter">Remplacer, pas ajouter</h2>
<p>La plupart des exemples de types Doctrine personnalisés introduisent un nouveau nom : <code>utc_datetime</code>, <code>app_date</code>, et ainsi de suite. On annote ensuite chaque champ avec <code>type: 'utc_datetime'</code> dans les entités. Ça fonctionne, mais c&rsquo;est fastidieux et ça ne protège pas contre un <code>type: 'datetime'</code> oublié.</p>
<p>L&rsquo;autre option : enregistrer le type personnalisé sous le nom <code>datetime</code>. Doctrine remplace sa propre implémentation par la nôtre, partout, sans exception. Chaque champ <code>datetime</code> de toutes les entités passe par notre logique, sans changer une seule annotation.</p>
<p>C&rsquo;est ce qu&rsquo;on vient de déployer sur notre plateforme de microservices PHP. Voici à quoi ça ressemble.</p>
<h2 id="le-trait-partagé">Le trait partagé</h2>
<p>Les deux types (<code>date</code> et <code>datetime</code>) partagent la même logique de conversion via un trait :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">trait</span> <span style="color:#a6e22e">UTCDate</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">\DateTimeZone</span> $utc;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">convertToPHPValue</span>($value, <span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">\DateTime</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">null</span> <span style="color:#f92672">===</span> $value <span style="color:#f92672">||</span> $value <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">\DateTime</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> $value;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        $format <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getFormat</span>($platform);
</span></span><span style="display:flex;"><span>        $converted <span style="color:#f92672">=</span> <span style="color:#a6e22e">\DateTime</span><span style="color:#f92672">::</span><span style="color:#a6e22e">createFromFormat</span>($format, $value, $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getUtc</span>());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>$converted) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\RuntimeException</span>(
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">sprintf</span>(<span style="color:#e6db74">&#39;Could not convert database value &#34;%s&#34; to DateTime using format &#34;%s&#34;.&#39;</span>, $value, $format)
</span></span><span style="display:flex;"><span>            );
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">postConvert</span>($converted);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $converted;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getFormat</span>(<span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getUtc</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">\DateTimeZone</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">empty</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">utc</span>)) {
</span></span><span style="display:flex;"><span>            $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">utc</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\DateTimeZone</span>(<span style="color:#e6db74">&#39;UTC&#39;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">utc</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Le point clé : <code>\DateTime::createFromFormat()</code> reçoit une timezone UTC explicite. La valeur brute issue de la base est interprétée comme UTC, peu importe la timezone configurée sur le serveur PHP.</p>
<h2 id="utcdatetimetype">UTCDateTimeType</h2>
<p>Pour les champs <code>datetime</code>, le chemin d&rsquo;écriture doit aussi imposer l&rsquo;UTC :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UTCDateTimeType</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">DateTimeType</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#a6e22e">UTCDate</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\Override]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">convertToPHPValue</span>($value, <span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">\DateTime</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">null</span> <span style="color:#f92672">===</span> $value <span style="color:#f92672">||</span> $value <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">\DateTimeInterface</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">parent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">convertToPHPValue</span>($value, $platform);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">parent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">convertToPHPValue</span>(<span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$value</span><span style="color:#e6db74">+0000&#34;</span>, $platform);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\Override]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">convertToDatabaseValue</span>($value, <span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> ($value <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">\DateTime</span>) {
</span></span><span style="display:flex;"><span>            $value<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setTimezone</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getUtc</span>());
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">parent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">convertToDatabaseValue</span>($value, $platform);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\Override]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getFormat</span>(<span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $platform<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getDateTimeFormatString</span>();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">postConvert</span>(<span style="color:#a6e22e">\DateTime</span> $converted)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>En lecture (<code>convertToPHPValue</code>), si la valeur est une chaîne brute, on y ajoute <code>+0000</code> avant de déléguer au parent. Le parent utilise ensuite ce suffixe de timezone pour créer correctement l&rsquo;objet PHP.</p>
<p>En écriture (<code>convertToDatabaseValue</code>), on force le <code>DateTime</code> en UTC avant de le formater. Ce qui va en base est toujours en UTC.</p>
<h2 id="utcdatetype">UTCDateType</h2>
<p>Pour les colonnes <code>date</code> (sans composante horaire), même approche avec une étape supplémentaire :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UTCDateType</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">DateType</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#a6e22e">UTCDate</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\Override]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getFormat</span>(<span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $platform<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getDateFormatString</span>();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">postConvert</span>(<span style="color:#a6e22e">\DateTime</span> $converted)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        $converted<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setTime</span>(<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La méthode <code>postConvert()</code> remet l&rsquo;heure à <code>00:00:00</code> après le parsing. Sans elle, un champ <code>date</code> pourrait revenir avec <code>23:59:59</code> ou <code>00:00:00+02:00</code> selon la timezone du serveur, ce qui casse les comparaisons et le tri.</p>
<h2 id="enregistrement-dans-symfony">Enregistrement dans Symfony</h2>
<p>La partie décisive : déclarer les types sous leurs noms natifs dans <code>config/packages/doctrine.yaml</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">doctrine</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dbal</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">types</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">date</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">class</span>: <span style="color:#ae81ff">App\Doctrine\DBAL\Types\UTCDateType</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">datetime</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">class</span>: <span style="color:#ae81ff">App\Doctrine\DBAL\Types\UTCDateTimeType</span>
</span></span></code></pre></div><p>C&rsquo;est tout. Doctrine échange ses propres implémentations contre les nôtres. Les entités existantes ne changent pas, les migrations ne bougent pas, les annotations restent <code>type: Types::DATETIME_MUTABLE</code>. Le comportement change globalement, sans friction.</p>
<h2 id="12-microservices-89-colonnes-un-bloc-de-config">12 microservices, 89 colonnes, un bloc de config</h2>
<p>Ces deux types tournent maintenant sur 12 microservices indépendants, chacun avec sa propre config Doctrine, couvrant 89 colonnes de production. Les serveurs CI tournent en UTC, les machines de dev en <code>Europe/Paris</code>, les données voyagent entre eux sans dériver. Ce n&rsquo;est pas spectaculaire. C&rsquo;est juste fiable.</p>
<p>La vraie leçon n&rsquo;est pas technique : un problème de timezone non résolu est un problème d&rsquo;intégrité des données. Les décalages s&rsquo;accumulent silencieusement, les comparaisons se trompent, les exports deviennent inexacts. Deux lignes de config et trois classes peuvent prévenir ça définitivement.</p>
]]></content:encoded></item></channel></rss>