<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[AssoConnect Technical Blog]]></title><description><![CDATA[We help nonprofits change the world with an all-in-one software.
We like curious and product-oriented developers always eager to improve their coding skills.
[J]]></description><link>https://tech.assoconnect.com</link><generator>RSS for Node</generator><lastBuildDate>Sat, 11 Apr 2026 08:37:10 GMT</lastBuildDate><atom:link href="https://tech.assoconnect.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Our PHP TimeTraveler to address DateTime::modify() pitfalls]]></title><description><![CDATA[Everybody knows the PHP native method DateTime::modify()
The DateTime extension supports awesome modifiers like "2 days ago", "last day of this month", ... or basic ones like "+1 month".
But the last one is actually tricky: it increments the month va...]]></description><link>https://tech.assoconnect.com/our-php-timetraveler-to-address-datetimemodify-pitfalls</link><guid isPermaLink="true">https://tech.assoconnect.com/our-php-timetraveler-to-address-datetimemodify-pitfalls</guid><category><![CDATA[datetime]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Pitfalls]]></category><category><![CDATA[modify]]></category><dc:creator><![CDATA[Sylvain Fabre]]></dc:creator><pubDate>Thu, 03 Aug 2023 16:37:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/M8zCQTJZFbU/upload/9c63de2f17d368d2cfc1932de55ebd8c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Everybody knows the PHP native method <code>DateTime::modify()</code></p>
<p>The <code>DateTime</code> extension supports awesome modifiers like "2 days ago", "last day of this month", ... or basic ones like "+1 month".</p>
<p>But the last one is actually tricky: it increments the month value of the date.</p>
<p>For instance 2023-01-01 (January first) becomes 2023-(01+1)-01 =&gt; 2023-02-01</p>
<p>But as not all months last 31 days: incrementing one month on 2023-08-31 results to 2023-09-31 which doesn't exist and is thus corrected to 2023-10-01.</p>
<p>If you keep playing, you will notice that <code>modify('+1 year')</code> returns a different date than calling 12 times <code>modify('+1 month')</code> 🤯</p>
<pre><code class="lang-php">(<span class="hljs-keyword">new</span> DateTime(<span class="hljs-string">'2023-01-31'</span>))-&gt;modify(<span class="hljs-string">'+1 year'</span>); <span class="hljs-comment">// 2024-01-31</span>
(<span class="hljs-keyword">new</span> DateTime(<span class="hljs-string">'2023-01-31'</span>))
    -&gt;modify(<span class="hljs-string">'+1 month'</span>) <span class="hljs-comment">// 2023-03-03 which is the root cause</span>
    ...
    -&gt;modify(<span class="hljs-string">'+1 month'</span>); <span class="hljs-comment">// 2024-02-03</span>
</code></pre>
<p>At AssoConnect, we deal with monthly and yearly subscriptions, memberships, ... so we need reliable and predictable date operations.</p>
<p>The latest release of our <a target="_blank" href="https://github.com/assoconnect/php-date">https://github.com/assoconnect/php-date</a> library brings a month &amp; year traveler with 3 main features:</p>
<ol>
<li><p>The last day of the month is sticky</p>
<p> September, 30th + 1 month = October, 31st <em>instead of 30th</em></p>
</li>
<li><p>A month is never skipped</p>
<p> January, 30th + 1 month = February, 28th (or 29th) <em>instead of March, 1st</em></p>
<p> February, 29th + 1 year = February, 28th <em>instead of March, 1st</em></p>
</li>
<li><p>The year-over-year result is consistent with the initial reference</p>
<p> January, 30th + (1 month) x 12 = January, 30th <em>instead of February, 2nd</em></p>
</li>
</ol>
<p>We published this library under the MIT license so feel free to use it!</p>
]]></content:encoded></item><item><title><![CDATA[Adminer meets Azure MySQL Database]]></title><description><![CDATA[I recently deployed a new instance of Azure Database for MySQL Flexible Server.And I found out it uses a different SSL certificate than the legacy Single Server.
This obviously broke my current Adminer setup with the login-ssl.php plugin so I decided...]]></description><link>https://tech.assoconnect.com/adminer-meets-azure-mysql-database</link><guid isPermaLink="true">https://tech.assoconnect.com/adminer-meets-azure-mysql-database</guid><category><![CDATA[Azure]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Adminer]]></category><dc:creator><![CDATA[Sylvain Fabre]]></dc:creator><pubDate>Tue, 18 Jul 2023 20:35:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/HjBOmBPbi9k/upload/ed3005a579d49c423c3b7b153a816a18.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently deployed a new instance of Azure Database for MySQL Flexible Server.<br />And I found out it uses a different SSL certificate than the legacy Single Server.</p>
<p>This obviously broke my current Adminer setup with the <code>login-ssl.php</code> plugin so I decided to publish a dedicated Adminer Docker image dedicated to Azure-managed MySQL servers.</p>
<p>This image is based on the latest official <a target="_blank" href="https://hub.docker.com/_/adminer">Adminer Docker image</a> and ships with a combined certificate for both Flexible &amp; Single servers.</p>
<p>The code source is available at <a target="_blank" href="https://github.com/assoconnect/docker-adminer-azure">https://github.com/assoconnect/docker-adminer-azure</a> under the MIT license and is daily built &amp; published to Docker Hub at <a target="_blank" href="https://hub.docker.com/r/assoconnect/adminer-azure">https://hub.docker.com/r/assoconnect/adminer-azure</a></p>
<p>Happy admin!</p>
]]></content:encoded></item><item><title><![CDATA[A Migration Tale: from our custom PHP framework to Symfony]]></title><description><![CDATA[​​We decided to code our own PHP framework when founding AssoConnect many years ago. That was a great yet challenging way to learn how many critical parts of a framework work 🤓
But maintaining it was also too intense so we decided to migrate to the ...]]></description><link>https://tech.assoconnect.com/a-migration-tale-from-our-custom-php-framework-to-symfony</link><guid isPermaLink="true">https://tech.assoconnect.com/a-migration-tale-from-our-custom-php-framework-to-symfony</guid><category><![CDATA[PHP]]></category><category><![CDATA[Symfony]]></category><category><![CDATA[migration]]></category><dc:creator><![CDATA[Sylvain Fabre]]></dc:creator><pubDate>Wed, 01 Mar 2023 08:58:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/c0I4ahyGIkA/upload/7f14faadc132c2472193d9ec17bc16af.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>​​We decided to code our own PHP framework when founding AssoConnect many years ago. That was a great yet challenging way to learn how many critical parts of a framework work 🤓</p>
<p>But maintaining it was also too intense so we decided to migrate to the Symfony framework backed by a strong community.</p>
<hr />
<p>We aimed to get as many end-to-end legacy-free flows as possible to lay down strong foundations to build modern code ASAP.</p>
<p>This means Symfony must be exposed and visible to the world: it can't start as a small piece within our old framework.</p>
<hr />
<p>So we first changed the routing: Symfony will be from now on the entry point</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677660668726/ee4c896f-9b2e-4e53-b763-f87937466517.svg" alt class="image--center mx-auto" /></p>
<ul>
<li><p>If a modern controller supports the requested route, then it serves it</p>
</li>
<li><p>If not, then the request is routed to our legacy framework which handles it</p>
</li>
</ul>
<pre><code class="lang-yaml"><span class="hljs-comment"># routes.yaml</span>
<span class="hljs-attr">legacy_all:</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">/{path}</span>
    <span class="hljs-attr">controller:</span> <span class="hljs-string">App\Controller\LegacyController::index</span>
</code></pre>
<p>Running <code>php bin/console debug:router</code> shows that the routing rule comes last ✅</p>
<hr />
<p>Then we established a base rule for dependency injection:</p>
<ul>
<li><p>✅ Old code can depend on modern code</p>
</li>
<li><p>❌ Modern code cannot depend on old code</p>
</li>
</ul>
<p><em>The container is passed along the Request object so old code can access modern dependencies through public aliases.</em></p>
<p>This makes it possible to have an end-to-end flow without a line of old code 😎</p>
<p>And old code doesn't stain the new code ✅</p>
<p>It also designs a strategy to migrate the codebase:</p>
<ul>
<li><p>Draw the dependency tree</p>
</li>
<li><p>Start with the leaves until there is no more old code</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677267178011/311cda5f-7e53-4d81-b570-aec185df6fed.svg" alt class="image--center mx-auto" /></p>
<hr />
<p>Our proprietary ORM uses entities close to Doctrine 1 ones (they persist themselves) which is very different from the Doctrine 2 logic (the ORM is in charge of persisting entities).</p>
<p>Our entities also have built-in lifecycle events and validation.</p>
<p>So migrating overnight wasn't an option 🟥</p>
<p>We set a second rule for migrating entities in line with the main plan:</p>
<ul>
<li><p>✅ Old tables can sync (create, update, delete) modern tables</p>
</li>
<li><p>❌ Modern tables cannot sync back to old tables</p>
</li>
</ul>
<p>⚠️ This creates a strong constraint: a modern table (and the related entity) is read-only as long as the old table exists.</p>
<p>It wasn't obvious at first sight but it actually makes sense when migrating data</p>
<ul>
<li><p>You first expose a new interface (modern tables and entities).</p>
</li>
<li><p>Then you migrate read services.</p>
</li>
<li><p>Then you migrate write services (you should only have a few ones anyway or you have duplicate code or bad responsibilities segregation).</p>
</li>
</ul>
<hr />
<p>We are still migrating and our strategy is sometimes frustrating: rules may require us to migrate a part that is not a business priority.</p>
<p>But it helps to keep a clean modern codebase 💫</p>
<p>And it has a rewarding upside: the more code you migrate, the more code turns migratable 🤩</p>
]]></content:encoded></item><item><title><![CDATA[Troubleshoot a Symfony ClockMock test]]></title><description><![CDATA[The Symfony ClockMock is an amazing yet tricky feature to mock date & time in your PHPUnit test suite.
You followed the documentation and yet date and time don't get mocked in your tests?
Let's review how it works and the common pitfalls we know of a...]]></description><link>https://tech.assoconnect.com/troubleshoot-a-symfony-clockmock-test</link><guid isPermaLink="true">https://tech.assoconnect.com/troubleshoot-a-symfony-clockmock-test</guid><category><![CDATA[Symfony]]></category><category><![CDATA[PHPUnit]]></category><dc:creator><![CDATA[Sylvain Fabre]]></dc:creator><pubDate>Sun, 27 Mar 2022 17:00:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/ft0-Xu4nTvA/upload/v1648397668818/qBUEL1YxN.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The Symfony ClockMock is an amazing yet tricky feature to mock date &amp; time in your PHPUnit test suite.</p>
<p>You followed the documentation and yet date and time don't get mocked in your tests?</p>
<p>Let's review how it works and the common pitfalls we know of at <a target="_blank" href="https://www.springly.org">Springly</a>.</p>
<h1 id="heading-how-it-works">How it works</h1>
<p>The ClockMock relies on <a target="_blank" href="https://www.php.net/manual/en/language.namespaces.rules.php">PHP name resolution rules</a> to define date &amp; time functions in your app namespace when you run tests annotated <code>@time-sensitive</code></p>
<pre><code>namespace App\Tests\Something

use PHPUnit\Framework\TestCase;
use Symfony\Component\Stopwatch\Stopwatch;

<span class="hljs-comment">/**
 * @group time-sensitive
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyTest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">TestCase</span>
</span>{
    public <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testSomething</span>(<span class="hljs-params"></span>)
    </span>{
        $stopwatch = <span class="hljs-keyword">new</span> Stopwatch();

        $stopwatch-&gt;start(<span class="hljs-string">'event_name'</span>);
        sleep(<span class="hljs-number">10</span>);
        $duration = $stopwatch-&gt;stop(<span class="hljs-string">'event_name'</span>)-&gt;getDuration();

        self::assertEquals(<span class="hljs-number">10000</span>, $duration);
    }
}
</code></pre><p>The following functions are defined with the previous code block:</p>
<ul>
<li><code>App\Tests\Something\time()</code> &amp; <code>App\Something\time()</code></li>
<li><code>App\Tests\Something\microtime()</code> &amp; <code>App\Something\microtime()</code></li>
<li><code>App\Tests\Something\sleep()</code> &amp; <code>App\Something\sleep()</code></li>
<li><code>App\Tests\Something\usleep()</code> &amp; <code>App\Something\usleep()</code></li>
<li><code>App\Tests\Something\date()</code> &amp; <code>App\Something\date()</code></li>
<li><code>App\Tests\Something\gmdate()</code> &amp; <code>App\Something\gmdate()</code></li>
</ul>
<p><em>For the most curious, this is done in <a target="_blank" href="https://github.com/symfony/phpunit-bridge/blob/6.1/ClockMock.php"><code>Symfony\Bridge\PhpUnit\ClockMock</code></a> with an <code>eval()</code> call.</em></p>
<h1 id="heading-common-pitfalls">Common pitfalls</h1>
<h2 id="heading-use-the-right-binary">Use the right binary</h2>
<p>ClockMock only works if you run your test suite with the  <code>vendor/bin/simple-phpunit</code> binary.</p>
<p>Don't use Composer <code>vendor/autoload.php</code> or the base <code>vendor/bin/phpunit</code> binary when running tests. For instance, the right setup with Docker &amp; PHPStorm is
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648399044079/4GMrejEyV.png" alt="image.png" /></p>
<p>Alternatively, you can also use this configuration to explicitly set up the right listener:</p>
<pre><code>&lt;!-- phpunit.xml.dist --&gt;
&lt;!-- ... --&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">listeners</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">listener</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"\Symfony\Bridge\PhpUnit\SymfonyTestsListener"</span>/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">listeners</span>&gt;</span></span>
</code></pre><h2 id="heading-not-every-time-classes-and-functions-are-mocked">Not every time classes and functions are mocked</h2>
<p>Only the following functions are mockable:</p>
<ul>
<li><code>time()</code></li>
<li><code>microtime()</code></li>
<li><code>sleep()</code></li>
<li><code>usleep()</code></li>
<li><code>date()</code></li>
<li><code>gmdate()</code></li>
</ul>
<p>The <code>DateTime</code> class is not mockable nor other PHP classes.</p>
<p>However, you can use this as a workaround:</p>
<pre><code>$real = <span class="hljs-keyword">new</span> DateTime(<span class="hljs-string">'now'</span>);
$mocked = <span class="hljs-keyword">new</span> DateTime(<span class="hljs-string">'@'</span> . time());
</code></pre><h2 id="heading-fully-qualified-function-name-are-not-mocked">Fully qualified function name are not mocked</h2>
<p>Time-based function mocking follows the <a target="_blank" href="https://www.php.net/manual/en/language.namespaces.rules.php">PHP namespace resolutions rules</a> so "fully qualified function calls" (e.g <code>\time()</code>) cannot be mocked.</p>
<h2 id="heading-functions-are-mocked-in-a-limited-set-of-namespaces">Functions are mocked in a limited set of namespaces</h2>
<p>Following the first example, the PHPUnit bridge will mock time functions in these namespaces:</p>
<ul>
<li><code>App\Something</code></li>
<li><code>App\Tests\Something</code></li>
</ul>
<p>Check that your time functions calls are actually done within one of these mocked namespace or call <code>ClockMock::register(MyClass::class)</code> beforehand.</p>
<h2 id="heading-workaround-php-internal-cache-system">Workaround PHP internal cache system</h2>
<p>Newest versions of PHP cache name resolution results to improve performance as detailed in the following bug reports:</p>
<ul>
<li><a target="_blank" href="https://bugs.php.net/bug.php?id=76788">#76788</a></li>
<li><a target="_blank" href="https://bugs.php.net/bug.php?id=64346">#64346</a></li>
</ul>
<p>It means that the following test suite won't work as you expect.</p>
<pre><code>namespace App\Tests\Something

use PHPUnit\Framework\TestCase;
use Symfony\Component\Stopwatch\Stopwatch;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyTest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">TestCase</span>
</span>{
    public <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testSomething</span>(<span class="hljs-params"></span>)
    </span>{
        $now = time();
        self::assertTrue($now &gt; <span class="hljs-number">0</span>);

        ClockMock::register(MyTest::<span class="hljs-class"><span class="hljs-keyword">class</span>)</span>;
        <span class="hljs-attr">ClockMock</span>::withClockMock(<span class="hljs-number">0</span>);

        <span class="hljs-comment">// This will fail as the namespace resolution result was cached</span>
        <span class="hljs-comment">// by the previous call to time() </span>
        self::assertEquals(<span class="hljs-number">0</span>, time());
    }
}
</code></pre><p>You can either use a <code>bootstrap.php</code> or a custom PHPUnit configuration to work around it for common used classes in your codebase (like a base Doctrine entity trait to set <code>createdAt</code> &amp; <code>updatedAt</code> properties).</p>
<pre><code>&lt;!-- phpunit.xml.dist --&gt;
&lt;!-- ... --&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">listeners</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">listener</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"Symfony\Bridge\PhpUnit\SymfonyTestsListener"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">arguments</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">array</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">element</span> <span class="hljs-attr">key</span>=<span class="hljs-string">"time-sensitive"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>App\Tests\Something<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">element</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">array</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">arguments</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">listener</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">listeners</span>&gt;</span></span>
</code></pre><p>or a bootstrap file:</p>
<pre><code># bootstrap.php to setup <span class="hljs-keyword">in</span> phpunit.xml
<span class="hljs-attr">ClockMock</span>::register(<span class="hljs-string">'App\\Something\\'</span>);
</code></pre><p>⚠️ Use the namespace and not the fully qualified class name.
If your class is <code>App\Something\MyClass</code> then use <code>App\Something</code> in the example above and not <code>App\Something\MyClass</code>.</p>
<p>Happy testing!</p>
]]></content:encoded></item></channel></rss>