<?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" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Swag Industries]]></title><description><![CDATA[Computing, except it works properly!]]></description><link>https://swag.industries/</link><image><url>https://swag.industries/favicon.png</url><title>Swag Industries</title><link>https://swag.industries/</link></image><generator>Ghost 5.26</generator><lastBuildDate>Sun, 22 Oct 2023 09:30:03 GMT</lastBuildDate><atom:link href="https://swag.industries/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[DNS over HTTPS on macOS]]></title><description><![CDATA[Improve your privacy by using DNS over HTTPS. Look up hostnames privately and avoid giving too much info to your ISP. This is a tutorial for dnscrypt proxy on macOS.]]></description><link>https://swag.industries/dns-over-https-natively-on-macos-and-ios/</link><guid isPermaLink="false">63aeed502d7103000121d980</guid><category><![CDATA[tutorial]]></category><category><![CDATA[macos]]></category><category><![CDATA[dns]]></category><dc:creator><![CDATA[Gaby]]></dc:creator><pubDate>Sun, 22 Oct 2023 09:29:15 GMT</pubDate><content:encoded><![CDATA[<p>Send your DNS requests over HTTPS, improve your privacy and protect yourself from potential dns poisoning.</p><p>You probably know what is a DNS, but you might not know about DoH (DNS over HTTPS) or DoT (DNS over TLS) yet. In this article, we will be focusing on DoH, what are the pros and cons, and how to set it up on macOS.</p><h3 id="whats-so-great-about-doh">What&apos;s so great about DoH?</h3><p>First, a quick recap: DNS runs on the port 53 in cleartext and over UDP. Therefore, your internet provider can see all the dns requests and thus, your browsing history. On top of that, your dns requests can get poisoned and you might encounter dns censorship.</p><p>Running DNS within https ensures your internet provider doesn&apos;t get to see your dns traffic anymore. No more cleartext DNS!</p><h3 id="lets-setup-doh-on-macos">Let&apos;s setup DoH on macOS</h3><p>In this tutorial, we are going to use dnscrypt on macOS. There are other alternatives out there such as cloudflared or warp, but <u>dnscrypt has multiple advantages over the alternatives</u>:</p><ul><li>Support for more protocols. Standard DoH is supported, but also its own dnscrypt protocol. <a href="https://dnscrypt.info/public-servers/">A list of supported servers can be found here</a>.</li><li>Better handling of network switches. Whenever you switch from WiFi to LAN, or when you turn on your VPN, other software like cloudflared will take a minute to adjust, whereas dnscrypt instantly switches networks.</li><li>Performance footprint is very minimal.</li></ul><p><strong>Step 1:</strong> using brew, install the dnscrypt-proxy package</p><pre><code>brew install dnscrypt-proxy</code></pre><p><strong>Step 2:</strong> review the configuration. I personally use the following configuration:</p><pre><code>nano /opt/homebrew/etc/dnscrypt-proxy.toml
##############################################
#        dnscrypt-proxy configuration        #
##############################################
server_names = [&apos;google&apos;, &apos;cloudflare&apos;]</code></pre><p>Yes, this is it. There are many examples provided in the .toml file but this is what I went with. Only one single line of configuration is needed!</p><p><strong>Step 3:</strong> run the dnscrypt-proxy in the background and install a permanent service.</p><pre><code>brew services start dnscrypt-proxy</code></pre><p>You might need to re-run the command using sudo depending your macOS version. This comand will start the dnscrypt-proxy service in the background and it will install a service to run dnscrypt-proxy every time your Mac boots. A popup might appear asking you to confirm this is what you want.</p><p><strong>step 4:</strong> tell your computer to use your local DNS server.</p><p><strong>Method A:</strong> set your DNS directly from the command line. <u>Only works with WiFi.</u></p><pre><code>networksetup -setdnsservers Wi-Fi 127.0.0.1</code></pre><p><strong>Method B:</strong> set your DNS from the System Settings, works with every interface.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://swag.industries/content/images/2023/10/image.png" class="kg-image" alt loading="lazy" width="1398" height="358" srcset="https://swag.industries/content/images/size/w600/2023/10/image.png 600w, https://swag.industries/content/images/size/w1000/2023/10/image.png 1000w, https://swag.industries/content/images/2023/10/image.png 1398w" sizes="(min-width: 720px) 720px"><figcaption>Go to System Settings -&gt; Network -&gt; Select your network -&gt; details</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://swag.industries/content/images/2023/10/image-1.png" class="kg-image" alt loading="lazy" width="720" height="224" srcset="https://swag.industries/content/images/size/w600/2023/10/image-1.png 600w, https://swag.industries/content/images/2023/10/image-1.png 720w" sizes="(min-width: 720px) 720px"><figcaption>Go to DNS, and save this IP: 127.0.0.1</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://swag.industries/content/images/2023/10/image-2.png" class="kg-image" alt loading="lazy" width="1072" height="284" srcset="https://swag.industries/content/images/size/w600/2023/10/image-2.png 600w, https://swag.industries/content/images/size/w1000/2023/10/image-2.png 1000w, https://swag.industries/content/images/2023/10/image-2.png 1072w" sizes="(min-width: 720px) 720px"><figcaption>Check whether the setting have updated using: cat /etc/resolv.conf</figcaption></figure><p><strong>Bravo!</strong> Every time an app needs to perform a dns lookup, your computer will either reach <a href="https://1.1.1.1">https://1.1.1.1</a> or <a href="https://8.8.8.8">https://8.8.8.8</a> using a local resolver running in the background of your computer.</p>]]></content:encoded></item><item><title><![CDATA[Install immich in three steps]]></title><description><![CDATA[<p>Quick tutorial to run immich.app, the free and open source google photos alternative. This article is intended to those who don&apos;t have much experience using docker. Follow these three steps to have a working immich.app server.</p><p>Requirements: Ubuntu with root access, internet access, open ports and</p>]]></description><link>https://swag.industries/install-immich-in-three-steps/</link><guid isPermaLink="false">64c7a75a635fce0001131169</guid><category><![CDATA[docker]]></category><category><![CDATA[immich]]></category><category><![CDATA[tutorial]]></category><dc:creator><![CDATA[Gaby]]></dc:creator><pubDate>Mon, 31 Jul 2023 13:00:13 GMT</pubDate><content:encoded><![CDATA[<p>Quick tutorial to run immich.app, the free and open source google photos alternative. This article is intended to those who don&apos;t have much experience using docker. Follow these three steps to have a working immich.app server.</p><p>Requirements: Ubuntu with root access, internet access, open ports and some storage.</p><p>Important note: immich.app is under heavy development, this article has been written in July 2023 and might need some update in the future, careful!</p><h3 id="first-step-install-docker-compose-on-ubuntu">First step: install docker compose on Ubuntu.</h3><p>Easy all-in-one command to add the docker repository and install the necessary packages: curl -s https://get.docker.com | bash</p><pre><code>root@swagindustries:~# curl -s https://get.docker.com | bash
# Executing docker install script, commit: c2de0811708b6d9015ed1a2c80f02c9b70c8ce7b
+ sh -c &apos;echo &quot;deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu focal stable&quot; &gt; /etc/apt/sources.list.d/docker.list&apos;
+ sh -c &apos;apt-get update -qq &gt;/dev/null&apos;
+ sh -c &apos;DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin &gt;/dev/null&apos;
+ sh -c &apos;docker version&apos;
Client: Docker Engine - Community
 Version:           24.0.5
Server: Docker Engine - Community
 Version:          24.0.5</code></pre><p>That was easy, wasn&apos;t it?</p><h3 id="second-step-edit-the-configuration-files-for-immichapp">Second step: edit the configuration files for immich.app</h3><p>You just installed docker, great! In order for immich to work, we still have some config to write before launching immich.</p><p>Create a new directory: /root/immich. Within this directory, you will have three components: docker-compose.yml, .env, and a data/ directory. Here&apos;s an example:</p><pre><code>/root/immich
&#x251C;&#x2500;&#x2500; docker-compose.yml
&#x251C;&#x2500;&#x2500; .env
&#x2514;&#x2500;&#x2500; data/</code></pre><p>First, you need the docker-compose.yml file. This file tells docker to download and run the software. <a href="https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml">Official link to the docker-compose.yml file here</a>. Save it as &quot;docker-compose.yml&quot;. You do not need to edit this file.</p><p>Once it&apos;s done, you will need the .env file. This file tells docker about the configuration you want immich to have. For example, you can tell docker that immich should save its files into &quot;/my/photos/directory&quot;. <a href="https://raw.githubusercontent.com/immich-app/immich/main/docker/example.env">Official link to the .env file here</a>. I suggest you edit this file. Here is the line that I altered in my .env:</p><pre><code>UPLOAD_LOCATION=/root/immich/data/
</code></pre><p>Create a directory /root/immich/data/. This is where your photos are going to be stored. You can point this to any other folder, this is just a preference.</p><h3 id="third-step-run-docker-compose">Third step: run docker-compose</h3><p>Run docker-compose to start immich: docker-compose up -d inside the directory where the docker-compose.yml &amp; .env files have been saved.</p><pre><code>root@swagindustries:~# docker-compose up -d
Creating immich_postgres         ... done
Creating immich_machine_learning ... done
Creating immich_typesense        ... done
Creating immich_redis            ... done
Creating immich_web              ... done
Creating immich_microservices    ... done
Creating immich_server           ... done
Creating immich_proxy            ... done</code></pre><p>And voil&#xE0;! Access your immich.app online through http://your.ip.address:2283/</p><p>tip: to find your LAN IP address, use hostname -I</p><pre><code>root@swagindustries:~# hostname -I
10.238.198.169 fd42:3873:3d5b:538b:216:3eff:fe9a:6fd5</code></pre><p>For example, my LAN IP is 10.238.198.169, I can access immich through http://10.238.198.169:2283/.</p><p>Feel free to add a frontend proxy with https and some firewalling in front of the port 2283.</p>]]></content:encoded></item><item><title><![CDATA[Simple webdav server with sftpgo, docker and traefik]]></title><description><![CDATA[<p>If you end up on this page, it&apos;s maybe because you asked yourself the same as me: how to make a simple webdav server running in a docker, routed by traefik. Maybe just like me you avoided sftpgo because it can do <em>too much.</em><br>And that&apos;s</p>]]></description><link>https://swag.industries/simple-webdav-server-with-sftpgo-docker-and/</link><guid isPermaLink="false">64ab3501635fce0001131117</guid><dc:creator><![CDATA[Maxime V]]></dc:creator><pubDate>Mon, 10 Jul 2023 08:04:08 GMT</pubDate><content:encoded><![CDATA[<p>If you end up on this page, it&apos;s maybe because you asked yourself the same as me: how to make a simple webdav server running in a docker, routed by traefik. Maybe just like me you avoided sftpgo because it can do <em>too much.</em><br>And that&apos;s true, but it&apos;s also super-simple to configure it for this use-case!</p><p>Here is the <code>docker-compose.yaml</code> configuration I use, it works, it&apos;s easy to understand.</p><pre><code class="language-yaml">services:
  webdav:
    image: drakkan/sftpgo:v2.5
    volumes:
      - type: bind
        source: /path/to/my/files
        target: /srv/sftpgo/data/my-folder
      - type: bind
        source: ./config # This folder must be `chown -R 1000:1000`
        target: /var/lib/sftpgo
    environment:
      # This env var enables webdav integration and specifies its port binding
      SFTPGO_WEBDAVD__BINDINGS__0__PORT: &apos;8090&apos;
    restart: always
    networks:
      - custom_traefik_network
    labels:
      # We need to route the port for webdav
      - &quot;traefik.enable=true&quot;
      - &quot;traefik.http.routers.webdav.entryPoints=websecure&quot;
      - &quot;traefik.http.routers.webdav.rule=Host(`webdav.you.com`)&quot;
      - &quot;traefik.http.routers.webdav.priority=2&quot;
      - &quot;traefik.http.routers.webdav.tls.certresolver=your_cert_resolver&quot;
      - &quot;traefik.http.routers.webdav.tls.domains[0].main=webdav.you.com&quot;
      - &quot;traefik.http.routers.webdav.service=webdav&quot;
      - &quot;traefik.http.services.webdav.loadbalancer.server.port=8090&quot;
      
      # And the port to access the UI
      - &quot;traefik.http.routers.ui_webdav.entryPoints=websecure&quot;
      - &quot;traefik.http.routers.ui_webdav.rule=Host(`ui-webdav.you.com`)&quot;
      - &quot;traefik.http.routers.ui_webdav.priority=2&quot;
      - &quot;traefik.http.routers.ui_webdav.tls.certresolver=your_cert_resolver&quot;
      - &quot;traefik.http.routers.ui_webdav.tls.domains[0].main=ui-webdav.you.com&quot;
      - &quot;traefik.http.routers.ui_webdav.service=ui_webdav&quot;
      - &quot;traefik.http.services.ui_webdav.loadbalancer.server.port=8080&quot;


networks:
  custom_traefik_network:
    external: true</code></pre><p>Once you run this configuration, you can go to <code>ui-webdav.you.com</code> and setup your sftpgo installation. You now need one last thing: add a user and specify its folder (inside the sftpgo docker). In this example the path would have been <code>/srv/sftpgo/data/my-folder</code>.</p><p>I hope this helped you! &#x270C;&#xFE0F;</p>]]></content:encoded></item><item><title><![CDATA[Upgrade docker-compose installation of Gitlab]]></title><description><![CDATA[<p>If you are using a standard installation of Gitlab with Docker, it means that you are using the <em>omnibus</em> Gitlab installation. And it&apos;s excellent!</p><p>What is not that great however is that the upgrade way is <a href="https://docs.gitlab.com/ee/update/#installation-using-docker">not well documented</a>. But it&apos;s actually super-easy:</p><ol><li>Modify your docker-compose.</li></ol>]]></description><link>https://swag.industries/upgrade-docker-compose-installation-of-gitlab/</link><guid isPermaLink="false">63a6cbd9e803f6000165a69a</guid><category><![CDATA[docker]]></category><category><![CDATA[gitlab]]></category><dc:creator><![CDATA[Maxime V]]></dc:creator><pubDate>Sun, 02 Apr 2023 19:08:23 GMT</pubDate><content:encoded><![CDATA[<p>If you are using a standard installation of Gitlab with Docker, it means that you are using the <em>omnibus</em> Gitlab installation. And it&apos;s excellent!</p><p>What is not that great however is that the upgrade way is <a href="https://docs.gitlab.com/ee/update/#installation-using-docker">not well documented</a>. But it&apos;s actually super-easy:</p><ol><li>Modify your docker-compose.yaml file and specify the new version of Gitlab (and GitLab runner if required). You can find new versions on <a href="https://hub.docker.com/">Docker Hub</a>.</li><li>Use <code>docker-compose pull</code> to get new versions of GitLab</li><li>Use <code>docker-compose down</code> to stop GitLab</li><li>Use <code>docker-compose up -d</code> to start Gitlab again, <strong>it will run the upgrade scripts automatically</strong>! (it takes a while)</li></ol><p>&#x26A0; Warning &#xFE0F;&#x26A0;&#xFE0F; You need to follow the upgrade path of Gitlab, which means you cannot go to the last version in one step. You can find some documentation about the upgrade path <a href="https://docs.gitlab.com/ee/update/#upgrade-paths">here</a>. You may also want to use the <a href="https://gitlab-com.gitlab.io/support/toolbox/upgrade-path/">tool they designed</a> to generate your own upgrade path. <a href="https://gitlab-com.gitlab.io/support/toolbox/upgrade-path/">https://gitlab-com.gitlab.io/support/toolbox/upgrade-path/</a></p><p><strong>One more thing:</strong> you may experiment with issues while upgrading. You can find more information about it by using <code>docker-compose logs -f</code>. And depending on the issue you see, you can find information in <a href="https://gitlab.com/gitlab-org/gitlab/-/tree/master/doc/update">the update folder of Gitlab</a> (choose the version you&apos;re upgrading to in the tag list)</p>]]></content:encoded></item><item><title><![CDATA[Behat tutorial part 2: testing Symfony 6 application]]></title><description><![CDATA[This tutorial will help you add functional testing (end-to-end testing or E2E testing) to a Symfony web application or Symfony API with behat.]]></description><link>https://swag.industries/behat-tutorial-part-2-testing-symfony-6-application/</link><guid isPermaLink="false">62e8432be803f6000165a2aa</guid><category><![CDATA[behat]]></category><category><![CDATA[PHP]]></category><category><![CDATA[test]]></category><category><![CDATA[Symfony]]></category><dc:creator><![CDATA[Maxime V]]></dc:creator><pubDate>Thu, 18 Aug 2022 07:18:22 GMT</pubDate><media:content url="https://swag.industries/content/images/2022/08/wallhaven-4v8zp3.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://swag.industries/content/images/2022/08/wallhaven-4v8zp3.jpeg" alt="Behat tutorial part 2: testing Symfony 6 application"><p><a href="https://swag.industries/using-behat-part-1-simple-testing/">In part 1</a> we saw how to write a behat test. It&apos;s now time to use this knowledge to test our real-world application! Please notice that this tutorial will work with most versions of Symfony since behat is <a href="https://packagist.org/packages/behat/behat">compatible with Symfony 4, 5, and 6</a>.</p><h2 id="the-project-we-are-going-to-test">The project we are going to test</h2><p>The point is not to teach you how to build a Symfony project, so we are not going to do it. Instead, Symfony provides a <em>demo app</em> that does not contain any behat test. I suggest you take this as a base and add a functional behat test!</p><p>Let&apos;s take a new demo Symfony project:</p><pre><code class="language-bash">$ symfony new --demo AppToTest
$ cd AppToTest
$ symfony serve</code></pre><p>If you have SQLite installed, everything should be setup and running with those 3 commands. If you prefer to use MySQL or PostgreSQL, there&apos;s actually no problem, I will let you tweak the configuration the way you want.</p><p>Go to <a href="https://localhost:8000/fr">https://localhost:8000/</a> and you should see something like this:</p><figure class="kg-card kg-image-card"><img src="https://swag.industries/content/images/2022/08/image-1.png" class="kg-image" alt="Behat tutorial part 2: testing Symfony 6 application" loading="lazy" width="2000" height="616" srcset="https://swag.industries/content/images/size/w600/2022/08/image-1.png 600w, https://swag.industries/content/images/size/w1000/2022/08/image-1.png 1000w, https://swag.industries/content/images/size/w1600/2022/08/image-1.png 1600w, https://swag.industries/content/images/2022/08/image-1.png 2344w" sizes="(min-width: 720px) 720px"></figure><p>It&apos;s the homepage of the demo application of Symfony.<br><em>Something we can test with behat!</em><br>You can now shutdown the Symfony server, we don&apos;t need it to test our app for now!</p><h2 id="the-symfony-extension">The Symfony extension</h2><p>To work with behat and Symfony, some tooling exists: the organization <a href="https://github.com/FriendsOfBehat">friends of behat</a> got your back, they are maintaining <a href="https://github.com/FriendsOfBehat/SymfonyExtension">this nice extension</a> that helps to integrate behat with Symfony. Let&apos;s install it on our project:</p><pre><code>$ composer require --dev behat/behat friends-of-behat/symfony-extension</code></pre><p>If you have <a href="https://symfony.com/doc/current/setup/flex.html">Symfony Flex</a> installed, this command triggered the installation of behat and the Symfony plugin but also added a bunch of config files.</p><ul><li><code>behat.yml.dist</code> is the behat config file where you configure your test suite, we already had the first contact in part 1;</li><li><code>tests/Behat/</code> is the folder where you are going to put your behat contexts - classes that behat will execute to run your tests;</li><li><code>config/services_test.yaml</code> is loading the behat configuration in the test environment;</li><li><code>features</code> is the folder where you put your user stories, the actual tests.</li></ul><p>This configuration is almost ready to go, the only thing you need to do is to configure the bootstrap.php test file (already existing in the <code>tests</code> folder).</p><pre><code class="language-yaml"># behat.yaml.dist
default:
    suites:
        default:
            contexts:
                - App\Tests\Behat\DemoContext
    extensions:
        FriendsOfBehat\SymfonyExtension:
            # Add this configuration :
            bootstrap: tests/bootstrap.php</code></pre><p>Then you can run the command:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://swag.industries/content/images/2022/08/image-2.png" class="kg-image" alt="Behat tutorial part 2: testing Symfony 6 application" loading="lazy" width="1674" height="366" srcset="https://swag.industries/content/images/size/w600/2022/08/image-2.png 600w, https://swag.industries/content/images/size/w1000/2022/08/image-2.png 1000w, https://swag.industries/content/images/size/w1600/2022/08/image-2.png 1600w, https://swag.industries/content/images/2022/08/image-2.png 1674w" sizes="(min-width: 720px) 720px"><figcaption>Running behat for the first time in the demo app!</figcaption></figure><p>This does not test much. Open the <code>DemoContext.php</code> file to see how it works. This is the only actual &quot;test&quot; we have here:</p><pre><code class="language-php">if ($this-&gt;response === null) {
    throw new \RuntimeException(&apos;No response received&apos;);
}</code></pre><h2 id="testing-for-real">Testing for real</h2><p>Now that we have behat running, the point will be to really test something. We are going to improve the method <code>theResponseShouldBeReceived</code> and create a new one.</p><p>I suggest you to change our feature for something like this:</p><pre><code class="language-gherkin"># features/demo.feature

Feature:
    In order to prove I can write a behat test
    As a user
    I want to test the homepage

    Scenario: I go to the homepage
        When a demo scenario sends a request to &quot;/&quot;
        Then the response should be received
        Then I should see the text &quot;Welcome to the Symfony Demo application&quot;
</code></pre><h3 id="tweaking-the-response-checker-to-actually-check">Tweaking the response checker to actually check</h3><p>Something not done by the previous test is verifying that the page is not an error, and we can do that by simply checking if we have a 200 error or not.</p><p>It&apos;s simple, this code will do the job:</p><pre><code class="language-php">/**
 * @Then the response should be received
 */
public function theResponseShouldBeReceived(): void
{
    if ($this-&gt;response === null) {
        throw new \RuntimeException(&apos;No response received&apos;);
    }

    assert($this-&gt;response-&gt;getStatusCode() === 200);
}</code></pre><p>I keep the previous code because why not? But it actually does not make a lot of sense and could be removed.</p><h3 id="testing-text-is-on-the-page">Testing text is on the page</h3><p>Before to start, we need a new method, don&apos;t try too hard, just run behat and let it generate the code for us:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://swag.industries/content/images/2022/08/image-3.png" class="kg-image" alt="Behat tutorial part 2: testing Symfony 6 application" loading="lazy" width="2000" height="1219" srcset="https://swag.industries/content/images/size/w600/2022/08/image-3.png 600w, https://swag.industries/content/images/size/w1000/2022/08/image-3.png 1000w, https://swag.industries/content/images/size/w1600/2022/08/image-3.png 1600w, https://swag.industries/content/images/2022/08/image-3.png 2172w" sizes="(min-width: 720px) 720px"><figcaption>Behat generates the code of missing methods &#x2728;</figcaption></figure><p>Let&apos;s make this simple:</p><pre><code class="language-php">/**
 * @Then I should see the text :text
 */
public function iShouldSeeTheText($text)
{
    // We are getting a whole HTML page and our text may have tags inside
    // strip_tags has issues, but for this example it will do the job
    $requestContentWithoutTags = strip_tags($this-&gt;response-&gt;getContent());

    if (!str_contains($requestContentWithoutTags, $text)) {
        throw new \RuntimeException(&quot;Cannot find expected text &apos;$text&apos;&quot;);
    }
}</code></pre><h2 id="using-a-browser">Using a browser</h2><p>This was definitely real-life testing. But you may want to do more, and furthermore, more easily. This is why it&apos;s possible to use a browser, I suggest you to use the Symfony default <a href="https://symfony.com/doc/current/components/browser_kit.html">browserkit</a>.</p><h3 id="rewrite-our-previous-test-with-browserkit">Rewrite our previous test with browserkit</h3><p>First of all, we need to create our browser... Luckily Symfony has our back! Modify the constructor of your class to inject the client:</p><pre><code class="language-php">use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Component\HttpKernel\KernelInterface;

final class DemoContext implements Context
{
    public function __construct(private KernelBrowser $client) {}
}</code></pre><p>You can now rewrite tests using browserkit (<a href="https://symfony.com/doc/current/components/browser_kit.html">use the documentation</a>!).</p><figure class="kg-card kg-code-card"><pre><code class="language-php">/**
 * @When a demo scenario sends a request to :path
 */
public function aDemoScenarioSendsARequestTo(string $path): void
{
    // Notice that response is now an instance
    // of \Symfony\Component\DomCrawler\Crawler
    $this-&gt;response = $this-&gt;client-&gt;request(&apos;GET&apos;, $path);
}

/**
 * @Then the response should be received
 */
public function theResponseShouldBeReceived(): void
{
    assert($this-&gt;client-&gt;getResponse()-&gt;getStatusCode() === 200);
}

/**
 * @Then I should see the text :text
 */
public function iShouldSeeTheText($text)
{
    if (!str_contains($this-&gt;response-&gt;text(), $text)) {
        throw new \RuntimeException(&quot;Cannot find expected text &apos;$text&apos;&quot;);
    }
}</code></pre><figcaption><em>And there you have it!</em></figcaption></figure><p>Some things to notice here:</p><ul><li>The response is now a Crawler;</li><li>The last actual response is stored inside the client;</li><li>We do not need to make any trick to retrieve the text, the crawler does it for us!</li></ul><p>Update your tests, run the test suite, and see how it works well. &#x1F642;</p><h3 id="use-the-browser-to-navigate">Use the browser to navigate</h3><p>We have a (fake) browser! We can now navigate throw pages! Let&apos;s start by clicking the first link. I suggest you the following scenario (which already exists in the Symfony demo project):</p><pre><code>    Scenario: I navigate to Symfony demo
        Given I navigate to &quot;/&quot;
        When I click on &quot;Browse application&quot;
        Then I should see the text &quot;Symfony Demo&quot;</code></pre><p>You may notice here that I reuse an old sentence (so nothing to do with the last one), and another one is really similar to another. Let&apos;s reuse our previous function:</p><pre><code class="language-php">/**
 * @Given I navigate to :path
 * @When a demo scenario sends a request to :path
 */
public function aDemoScenarioSendsARequestTo(string $path): void
{
    // Notice that response is now an instance
    // of \Symfony\Component\DomCrawler\Crawler
    $this-&gt;response = $this-&gt;client-&gt;request(&apos;GET&apos;, $path);
}</code></pre><p>After this modification:</p><ul><li>I run behat</li><li>I copy/paste the code behat generates to my behat context</li><li>I use the method <code>clickLink</code> of the crawler</li></ul><pre><code class="language-php">/**
 * @When I click on :link
 */
public function iClickOn($link)
{
    $this-&gt;response = $this-&gt;client-&gt;clickLink($link);
}</code></pre><p>This is it! You navigate through pages with a behat test!</p><h2 id="testing-forms-manage-data-with-behat">Testing forms: manage data with behat</h2><p>The crawler we saw previously fills forms easily and it&apos;s documented by Symfony. But this also means that we will start to mess with data... And in the Symfony demo project also with login!</p><p>&#x2139;&#xFE0F; <em>There are many ways to achieve a good database test. I want my test suite to be fast and reliable so I clear my database <strong>at each test</strong> and in <strong>the most efficient way</strong> regarding tools I can use. Here I will use directly the database file but with PostgreSQL or MariaDB I use the </em><code>ORMPurger</code><em> of Doctrine. Feel free to do it your way.</em></p><p>So let&apos;s consider that scenario:</p><pre><code class="language-gherkin">    @database
    Scenario: I comment a post
        Given I am logged in as a user
        And I navigate on a post
        When I fill a comment with &quot;Some random words&quot;
        Then the post should have a new comment
        And I should see the text &quot;Some random words&quot;</code></pre><p>We have never seen the tag syntax until now, it&apos;s used to add a special action on several tests.</p><h3 id="reset-and-restore-database">Reset and restore database</h3><p>Behat allows you to use special methods that <a href="https://docs.behat.org/en/latest/user_guide/context/hooks.html">hook before and after any scenario</a>. I will use this feature to:</p><ol><li>Save the database in a special location before the test suite;</li><li>Reset data on each scenario;</li><li>Drop the fake database after the test suite is done.</li></ol><p>For this, I will use a new behat context, let&apos;s call it <code>DataManagementContext</code>. Behat read annotations such as <code>BeforeSuite</code>, <code>AfterScenario</code> or <code>AfterSuite</code> to know when to run our methods. Beware some methods are <code>static</code> or not depending on the type of the event.</p><pre><code class="language-php">class DataManagementContext implements Context
{
    private static ?Filesystem $filesystem = null;
    private const DATABASE_LOCATION = __DIR__ . &apos;/../../data/database_test.sqlite&apos;;
    private const DATABASE_LOCATION_COPY = __DIR__ . &apos;/../../data/database_test.sqlite.original&apos;;

    public function __construct(private KernelInterface $kernel) {}

    /**
     * @BeforeSuite
     */
    public static function saveTestDatabase(BeforeSuiteScope $scope): void
    {
        $filesystem = self::getFilesystem();
        $filesystem-&gt;copy(
            self::DATABASE_LOCATION,
            self::DATABASE_LOCATION_COPY
        );
    }

    /**
     * @AfterSuite
     */
    public static function removeCopiedDatabase(AfterSuiteScope $scope): void
    {
        $filesystem = self::getFilesystem();
        $filesystem-&gt;copy(
            self::DATABASE_LOCATION,
            self::DATABASE_LOCATION_COPY
        );
    }

    /**
     * @AfterScenario @database
     */
    public function resetDatabase(): void
    {
        $filesystem = self::getFilesystem();
        $filesystem-&gt;remove(self::DATABASE_LOCATION);
        $filesystem-&gt;copy(
            self::DATABASE_LOCATION_COPY,
            self::DATABASE_LOCATION
        );
    }

    private static function getFilesystem(): Filesystem
    {
        if (self::$filesystem !== null) {
            return self::$filesystem;
        }

        return self::$filesystem = new Filesystem();
    }
}
</code></pre><p>In this code, you may see I specified that this hook will execute only on <code>@database</code> tagged scenarios.</p><p>Do not forget to add the context to the behat configuration:</p><pre><code class="language-yaml">default:
    suites:
        default:
            contexts:
                - App\Tests\Behat\DemoContext
                - App\Tests\Behat\DataManagementContext</code></pre><h3 id="faking-a-connection">Faking a connection</h3><p>Is it possible to log in manually via the login form? Yes.<br>But as I told you just before, I want my tests to be <strong>fast</strong>. Login manually on each scenario is not an option.</p><p>Luckily the client has a feature allowing us to log in easily. We just need to get the user, and for that, I will use the user repository.</p><pre><code class="language-php">public function __construct(
    private KernelBrowser $client,
    private UserRepository $userRepository
) {}

/**
 * @Given I am logged in as a user
 */
public function iAmLoggedInAsAUser()
{
    $user = $this-&gt;userRepository-&gt;findOneBy([&apos;username&apos; =&gt; &apos;john_user&apos;]);
    $this-&gt;client-&gt;loginUser($user);
}</code></pre><p>&#x2139;&#xFE0F;&#xFE0F; <em>You can use the UserRepository directly from the constructor because it&apos;s defined as a service in this project and because the</em> <code>service_test.yaml</code> <em>file generated at install time registers our contexts in the DIC, nothing is magic, it&apos;s just great stuff.</em></p><h3 id="getting-a-post-and-go-on-its-page">Getting a post and go on its page</h3><p>It&apos;s not the case here so I could just select an article and go on its page, but you may have fixture-generated data for your test. So the article slug may change on each test execution. The following code will work no matter the data are different on each new test or not:</p><pre><code class="language-php">/**
 * @Given I navigate on a post
 */
public function iNavigateOnAPost()
{
    $this-&gt;post = $this-&gt;postRepository-&gt;findAll()[0];
    $this-&gt;aDemoScenarioSendsARequestTo(
        &apos;/en/blog/posts/&apos;.$this-&gt;post-&gt;getSlug()
    );
}</code></pre><h3 id="filling-the-form">Filling the form</h3><p>It&apos;s actually <a href="https://symfony.com/doc/current/testing.html#submitting-forms">well documented</a> and super easy:</p><pre><code class="language-php">/**
 * @When I fill a comment with :content
 */
public function iFillACommentWith($content)
{
    $this-&gt;response = $this-&gt;client-&gt;submitForm(&apos;Publish comment&apos;, [
        &apos;comment[content]&apos; =&gt; $content,
    ]);
    $this-&gt;response = $this-&gt;client-&gt;followRedirect();
}</code></pre><p>There is however a redirection to follow after the comment is posted.</p><h3 id="count-the-difference-in-comments-beforeafter">Count the difference in comments before/after</h3><p>It&apos;s maybe the most tricky part. We need to store the number of comments before and verify after. But in the meantime, we need to refresh the data we gather from the database previously.</p><p>An update of the previous method is required :</p><pre><code>/**
 * @Given I navigate on a post
 */
public function iNavigateOnAPost()
{
    $this-&gt;post = $this-&gt;postRepository-&gt;findAll()[0];

    // Not storing it is fine, we just need to load the data
    $this-&gt;post-&gt;getComments()-&gt;count();

    $this-&gt;aDemoScenarioSendsARequestTo(&apos;/en/blog/posts/&apos;.$this-&gt;post-&gt;getSlug());
}</code></pre><p>Once again, there are many ways to do it, but I choose here to just load the result in the first step and then to compare:</p><pre><code>/**
 * @Then the post should have a new comment
 */
public function theArticleShouldHaveANewComment()
{
    $previousCount = $this-&gt;post-&gt;getComments()-&gt;count();
    
    // Re-loading the post with fresh data
    $this-&gt;post = $this-&gt;postRepository-&gt;findAll()[0];
    
    $currentCount = $this-&gt;post-&gt;getComments()-&gt;count();
    
    assert($previousCount+1 === $currentCount);
}</code></pre><h2 id="thats-all-for-now">That&apos;s all for now!</h2><p>You may now feel that the <code>DemoContext</code> contains too much code, and you are right! Feel free to add as many contexts as you need.</p><p>Do you want to write tests in your own language? Some of you probably think about sharing features with the rest of your team. And I can already tell you some of us are already using features directly from the product owner since the gherkin language is super-similar to <a href="https://en.wikipedia.org/wiki/User_story">user stories</a> format.<br>So yes, you can absolutely write features in your language if you want to. It&apos;s documented on <a href="https://docs.behat.org/en/latest/user_guide/gherkin.html#gherkin-in-many-languages">the website of behat</a>.</p><p><strong>One last thing</strong>: behat is often used to make documentation because it tests and explains the case at the same time. For example, you may want to read <a href="https://github.com/Sylius/Sylius/tree/1.12/features">features of Sylius</a> or simply features of the <a href="https://github.com/FriendsOfBehat/SymfonyExtension/tree/master/features">Symfony Extension of behat</a>. &#x1F609; </p><p>Follow us on Twitter! <a href="https://twitter.com/swagdotind" rel="noreferrer noopener">https://twitter.com/swagdotind</a></p>]]></content:encoded></item><item><title><![CDATA[Behat tutorial part 1: the basics]]></title><description><![CDATA[Learn how to use behat, starting from the ground: this article shows you how to write a super simple behat test (by testing the command ls).]]></description><link>https://swag.industries/using-behat-part-1-simple-testing/</link><guid isPermaLink="false">62cd427918cb64000194afea</guid><category><![CDATA[PHP]]></category><category><![CDATA[test]]></category><category><![CDATA[behat]]></category><dc:creator><![CDATA[Maxime V]]></dc:creator><pubDate>Thu, 11 Aug 2022 08:37:52 GMT</pubDate><media:content url="https://swag.industries/content/images/2022/07/pexels-pixabay-163016.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://swag.industries/content/images/2022/07/pexels-pixabay-163016.jpg" alt="Behat tutorial part 1: the basics"><p><em>TL;DR: we are going to functionally test the <code>ls</code> command, <a href="#the-actual-test">the code is here</a>.</em></p><p>Behat is a testing tool for PHP. But before understanding what behat is used for, you need to understand that different types of testing exist.</p><p>&#x26A0;&#xFE0F; I will try to explain here the differences between the types of tests, but let me <strong>warn you</strong>: each of us has its own vision of what is each type of test, don&apos;t be shocked to have another interpretation of the terms, here you will get mine only.</p><ol><li><strong>Unit testing</strong>: this is the most obvious, but what you should know is that a pure unit test is not using any other thing than the class it tests, any interaction with another object should be mocked.</li><li><strong>Integration testing</strong>: this is basically unit testing, but by using a set of classes. Your test may reflect better the real world but may miss some cases.</li><li><strong>Functional testing</strong>: this is it! That&apos;s what behat is about. It&apos;s about running your application, giving inputs, getting results, and verifying results. Please note that you can use PHPUnit or PestPHP to build a functional test, behat is better (to me) because it allows the functional test to be readable for people of the business.</li></ol><p>But before getting into our first test. You have to know one last thing: behat is special by the syntax it suggests to use. It&apos;s called <a href="https://en.wikipedia.org/wiki/Cucumber_(software)">Gherkin</a>, and it comes from... <a href="https://cucumber.io/docs/gherkin/">Cucumber</a>: the equivalent of behat for many other languages.</p><h2 id="testing-ls">Testing <code>ls</code></h2><p>In its documentation, behat shows an example of a test of the command <code>ls</code>. But no code associated with the test. I suggest you to start with testing the ls command of your terminal.</p><p>Here is how looks a behat test, it&apos;s formatted with gherkin syntax. This test is copy/pasted from the documentation of behat:</p><pre><code class="language-gherkin">Feature: Listing command
  In order to change the structure of the folder I am currently in
  As a UNIX user
  I need to be able see the currently available files and folders there

  Scenario: Listing two files in a directory
    Given I am in a directory &quot;test&quot;
    And I have a file named &quot;foo&quot;
    And I have a file named &quot;bar&quot;
    When I run &quot;ls&quot;
    Then I should get:
      &quot;&quot;&quot;
      bar
      foo
      &quot;&quot;&quot;</code></pre><p>Let&apos;s make this file actually test something!</p><h2 id="creating-a-project">Creating a project</h2><p>To test something with behat, it&apos;s (obviously) require to have a project up and running. Well, we are not going to code ls, so our project will only contain the test, but we need it anyway!<br>Start by running those commands in a new folder:</p><pre><code class="language-bash">$ composer init --name &quot;acme/test-ls&quot; --description &quot;testing ls with behat&quot; --author &quot;Nek &lt;nek@swag.industries&gt;&quot; --type=project -n
$ composer require --dev behat/behat</code></pre><p>At this stage, you have a new PHP project, with behat installed locally. Let&apos;s init the project with the behat cli:</p><pre><code class="language-bash">vendor/bin/behat --init</code></pre><p>Behat generates the following directory structure:</p><pre><code>.
&#x251C;&#x2500;&#x2500; (composer.json)
&#x251C;&#x2500;&#x2500; (composer.lock)
|
|
&#x251C;&#x2500;&#x2500; features
&#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; bootstrap
&#x2502;&#xA0;&#xA0;     &#x2514;&#x2500;&#x2500; FeatureContext.php
|
|
&#x2514;&#x2500;&#x2500; (vendor)</code></pre><p>Behat generates the following directory structure for its tests: you should write the tests in the <code>features</code> directory and the <code>bootstrap</code> directory should contain the code related to your tests.</p><p>You may want to run the following command to learn how to use the gherkin language:</p><pre><code class="language-bash">$ vendor/bin/behat --story-syntax</code></pre><h2 id="writing-the-gherkin">Writing the gherkin</h2><p>This is the easy part, you should describe what you want to test and how in a file that will have the extension <code>.feature</code>, and this file should be located in the <code>features</code> directory.</p><p>I suggest you to create this file by taking the example we saw previously:</p><pre><code class="language-gherkin"># features/ls.feature
Feature: Listing command
  In order to change the structure of the folder I am currently in
  As a UNIX user
  I need to be able see the currently available files and folders there

  Scenario: Listing two files in a directory
    Given I am in a directory &quot;test&quot;
    And I have a file named &quot;foo&quot;
    And I have a file named &quot;bar&quot;
    When I run &quot;ls&quot;
    Then I should get:
      &quot;&quot;&quot;
      bar
      foo
      &quot;&quot;&quot;</code></pre><p>The first lines (2-5) have no actual meaning for our test, it&apos;s just some information you&apos;d be glad to find if you have to read a behat test, you can see this as a comment, but here it&apos;s part of the language.<br>The <code>Scenario:</code> keyword starts an actual test. Each line will trigger a function that we will need to define.</p><p>Let&apos;s run the tests so behat help us with the functions! The option <code>--append-snippets</code> will fill our <code>Context</code> file.</p><pre><code class="language-bash">$ vendor/bin/behat --append-snippets</code></pre><p>The result should be something like this:</p><pre><code>[...]
1 scenario (1 undefined)
5 steps (5 undefined)
0m0.01s (8.37Mb)

 &gt;&gt; default suite has undefined steps. Please choose the context to generate snippets:

  [0] None
  [1] FeatureContext
 &gt;</code></pre><p>Just type <code>1</code> to tell behat to fill our Context file (we have only one!).</p><p>The result should look like this:</p><pre><code class="language-php">&lt;?php

use Behat\Behat\Tester\Exception\PendingException;
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;

/**
 * Defines application features from the specific context.
 */
class FeatureContext implements Context
{
    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    public function __construct()
    {
    }

    /**
     * @Given I am in a directory :arg1
     */
    public function iAmInADirectory($arg1)
    {
        throw new PendingException();
    }

    /**
     * @Given I have a file named :arg1
     */
    public function iHaveAFileNamed($arg1)
    {
        throw new PendingException();
    }

    /**
     * @When I run :arg1
     */
    public function iRun($arg1)
    {
        throw new PendingException();
    }

    /**
     * @Then I should get:
     */
    public function iShouldGet(PyStringNode $string)
    {
        throw new PendingException();
    }
}
</code></pre><p>Notice how behat created annotations that will be read to match functions to sentences. You can see text in quote is transformed into an argument for the function. You can use many of them and change their name as well.</p><h2 id="the-actual-test">The actual test</h2><h3 id="sentence-1-i-m-in-a-directory-">Sentence 1: &#xAB; I&apos;m in a directory &#xBB;</h3><p>We just need to create a directory in a location of our choice.</p><pre><code class="language-php">    private string $directory;

    /**
     * @Given I am in a directory :directoryName
     */
    public function iAmInADirectory($directoryName)
    {
        $this-&gt;directory = __DIR__ . &apos;/../../tmp/&apos;.$directoryName;
        if (file_exists($this-&gt;directory)) {
            exec(&quot;rm -r {$this-&gt;directory}&quot;);
        }
        mkdir($this-&gt;directory, recursive: true);
        chdir($this-&gt;directory);
    }</code></pre><p>As you can see, this is straightforward. I&apos;m using basic PHP to create just what I need, this is fine, we&apos;re in a testing environment, locally or in a specific environment.</p><p>I take the time to add a property <code>directory</code> to my context so I can reuse it in other steps.</p><h3 id="sentence-2-i-have-a-file-named-">Sentence 2: &#xAB; I have a file named &#xBB; </h3><p>The code will be super-simple here, we do not need any specific file:</p><pre><code class="language-php">    /**
     * @Given I have a file named :filename
     */
    public function iHaveAFileNamed($filename)
    {
        touch($filename);
    }</code></pre><h3 id="sentence-3-i-run-">Sentence 3: &#xAB; I run &#xBB;</h3><p>This may be the hardest one!</p><pre><code class="language-php">    private string $output;

    /**
     * @When I run :commandName
     */
    public function iRun($commandName)
    {
        exec(&quot;$commandName&quot;, $output);
        // PHP store array lines in an array
        // we need to make it a string to compare
        $this-&gt;output = implode(&quot;\n&quot;, $output);
    }</code></pre><h3 id="sentence-4-i-should-get-">Sentence 4: &#xAB; I should get &#xBB;</h3><p>Finally, we are going to verify that the output of our <code>ls</code> execution returned the expected output.</p><pre><code>    /**
     * @Then I should get:
     */
    public function iShouldGet(PyStringNode $expectedOutput)
    {
        assert($this-&gt;output === $expectedOutput-&gt;getRaw());
    }</code></pre><p>I use here the standard assertion of PHP, but you can also use <a href="https://packagist.org/packages/phpunit/phpunit">PHPUnit assertions</a> or <a href="https://packagist.org/packages/webmozart/assert">the package from webmozart</a> that helps you write assertions. Feel free to use what you used to!</p><h2 id="this-is-it-">This is it!</h2><p>If you run the test now, you should see something like this :</p><figure class="kg-card kg-image-card"><img src="https://swag.industries/content/images/2022/07/image-1.png" class="kg-image" alt="Behat tutorial part 1: the basics" loading="lazy" width="1312" height="880" srcset="https://swag.industries/content/images/size/w600/2022/07/image-1.png 600w, https://swag.industries/content/images/size/w1000/2022/07/image-1.png 1000w, https://swag.industries/content/images/2022/07/image-1.png 1312w" sizes="(min-width: 720px) 720px"></figure><p>And congratulation for your first behat test &#x1F64C; !</p><p>This is obviously not testing your PHP application, but you really have all the basics here. You need an HTTP server to test your app? Just run one! You need a WebSocket server to run your app? Just start it! It&apos;s really just how you should do.</p><p>Of course, some tasks may become repetitive, this is why we are going to cover some tooling you can use with behat in <strong>2 more articles</strong> focused on real-world PHP application testing. </p><p>See you next week for the next article! &#x1F918;</p><p>Follow us on Twitter! <a href="https://twitter.com/swagdotind">https://twitter.com/swagdotind</a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Deploy Ghost 5 using docker compose and traefik]]></title><description><![CDATA[<p>Have your ghost blog engine running in only a few minutes! Here&apos;s our configuration if you want some inspiration. We are using docker compose, traefik, and an SQLite database.</p><h2 id="our-configuration">Our configuration</h2><p><em>docker-compose.yml</em></p><pre><code class="language-yml">version: &apos;3.1&apos;

services:

  ghost:
    image: ghost:5.x.x-alpine
    restart: always
    volumes:</code></pre>]]></description><link>https://swag.industries/deploy-ghost-5-using-docker-compose-traefik/</link><guid isPermaLink="false">62ce864be803f6000165a07b</guid><dc:creator><![CDATA[Gaby]]></dc:creator><pubDate>Wed, 13 Jul 2022 13:14:50 GMT</pubDate><content:encoded><![CDATA[<p>Have your ghost blog engine running in only a few minutes! Here&apos;s our configuration if you want some inspiration. We are using docker compose, traefik, and an SQLite database.</p><h2 id="our-configuration">Our configuration</h2><p><em>docker-compose.yml</em></p><pre><code class="language-yml">version: &apos;3.1&apos;

services:

  ghost:
    image: ghost:5.x.x-alpine
    restart: always
    volumes:
      - /data/swag-ghost/content:/var/lib/ghost/versions/5.x.x/content
      - /data/swag-ghost/content:/var/lib/ghost/content
      - /data/swag-ghost/config.json:/var/lib/ghost/config.production.json
    environment:
      url: https://swag.industries
    labels:
      traefik.enable: &quot;true&quot;
      traefik.http.routers.swag-ghost.rule: &quot;Host(`swag.industries`)&quot;
      traefik.http.routers.swag-ghost.entrypoints: &quot;https&quot;
      traefik.http.routers.swag-ghost.service: &quot;swag-ghost&quot;
      traefik.http.routers.swag-ghost.tls.certresolver: &quot;default&quot;
      traefik.http.services.swag-ghost.loadbalancer.server.port: &quot;2368&quot;
networks:
  default:
    external:
      name: traefik</code></pre><p>In our configuration, traefik acts as the proxy and this is the reason why the port 2368 isn&apos;t exposed.</p><p>There seems to be a bug in ghost where the &quot;current&quot; version isn&apos;t mounted with the content&apos;s volume. Our workaround was to mount it twice as the ghost/content and as the &quot;current&quot; version (for example, 5.1.1).</p><p><em>config.json</em></p><pre><code class="language-json">{
    &quot;url&quot;: &quot;https://swag.industries&quot;,
    &quot;server&quot;: {
        &quot;port&quot;: 2368,
        &quot;host&quot;: &quot;0.0.0.0&quot;
    },
    &quot;database&quot;: {
        &quot;client&quot;: &quot;sqlite3&quot;,
        &quot;connection&quot;: {
            &quot;filename&quot;: &quot;content/data/threadlet.db&quot;
        },
        &quot;debug&quot;: false
    },
    &quot;paths&quot;: {
        &quot;contentPath&quot;: &quot;content/&quot;
    },
    &quot;privacy&quot;: {
        &quot;useRpcPing&quot;: false,
        &quot;useUpdateCheck&quot;: false
    },
    &quot;useMinFiles&quot;: false,
    &quot;caching&quot;: {
        &quot;theme&quot;: {
            &quot;maxAge&quot;: 0
        },
        &quot;admin&quot;: {
            &quot;maxAge&quot;: 0
        }
    }
}</code></pre><p>The config.json (config.production.json inside the docker container) is very basic, and we did not change anything in particular. Do note we expose ghost on the port 2368, make sure to use this port as traefik (or your own public port) won&apos;t know how to reach your blog.</p><h2 id="quick-and-easy-import">Quick and easy import</h2><p>We used to have a manual installation of ghost. As we migrated our blog to docker, we needed to migrate our articles as well. We did so in two steps:</p><p>We exported and imported our articles and accounts using the ghost admin interface.</p><figure class="kg-card kg-image-card"><img src="https://swag.industries/content/images/2022/07/image.png" class="kg-image" alt loading="lazy" width="896" height="280" srcset="https://swag.industries/content/images/size/w600/2022/07/image.png 600w, https://swag.industries/content/images/2022/07/image.png 896w" sizes="(min-width: 720px) 720px"></figure><p>Now that we exported the articles and accounts, we manually imported the related files. We only had images to import from &quot;content/images&quot;.</p><p>Do not hesitate to reach us out if you have any questions.</p><p>Follow us on Twitter! <a href="https://twitter.com/swagdotind" rel="noreferrer noopener">https://twitter.com/swagdotind</a></p>]]></content:encoded></item><item><title><![CDATA[Creating a PHP archive (phar)]]></title><description><![CDATA[How to create PHP archives (phar files) and use them, what they really are, all the answers are here.]]></description><link>https://swag.industries/creating-a-php-archive-phar/</link><guid isPermaLink="false">62cd427918cb64000194afe3</guid><category><![CDATA[PHP]]></category><dc:creator><![CDATA[Maxime V]]></dc:creator><pubDate>Wed, 24 Feb 2021 11:00:00 GMT</pubDate><media:content url="https://swag.industries/content/images/2021/02/Huge-pile-of-cardboard-boxes-forming-a-wall-ideal-for-backgrou.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://swag.industries/content/images/2021/02/Huge-pile-of-cardboard-boxes-forming-a-wall-ideal-for-backgrou.jpg" alt="Creating a PHP archive (phar)"><p>Java has jar files. PHP has phar files.</p><p>Those files are PHP projects packaged. You may know them through usages like composer or PHPUnit. But phar files were designed by taking into consideration the nature of PHP: the web.<br>Therefore, we&apos;re going to try to show how to build a phar either &quot;classic&quot; or &quot;web&quot; oriented in this tutorial.</p><h2 id="what-is-a-phar-exactly">What is a phar exactly?</h2><p>A phar is basically many files assembled in only one. But it is composed of the following content:</p><ul><li>A stub: this is the very first piece of code that will be executed when you run your phar. It can be either automatically generated or entirely customized.<br>This part in the file usually starts with <code>&lt;?php</code> and ends with <code>__HALT_COMPILER();</code>.<br>The stub is some kind of <em>pre-front-controller</em>. It will drive PHP to get the correct thing depending on the &quot;entry&quot;. For that to work, it uses the Phar API.</li><li>Some binary containing metadata of the package: filenames, their start/end in the current file, some information about the entry point...</li><li>The concatenation of all your files (potentially compressed)</li></ul><p>To generate the phar, we will need to make a builder script (in PHP) and we will use the <a href="https://www.php.net/manual/en/book.phar.php">Phar API</a>.</p><h2 id="making-a-phar-for-the-cli">Making a phar for the cli</h2><p>Before you begin, you may need to enable a special capability in your <code>php.ini</code> file. The following configuration may be set on your installation:</p><pre><code class="language-ini">[Phar]
; http://php.net/phar.readonly
phar.readonly = On
; Set it to Off</code></pre><p>It is required to set the option <code>readonly</code> to <code>Off</code>.</p><hr><p>The goal will be to generate a file <code>app.phar</code> that is executable from the cli directly. On my side I create a folder named <code>app</code> that contains the two following files:</p><pre><code class="language-php">&lt;?php
// app/index.php

// This will be the entry point

echo &quot;&lt;h1&gt;Hello from PHP&lt;/h1&gt;&quot;;</code></pre><pre><code class="language-php">&lt;?php
// app/vendor.php

function hello() {
    echo &quot;Im a function part of whatever vendor you may think.&quot;;
}</code></pre><p>I will then create my builder that uses the <a href="https://www.php.net/manual/en/class.phar.php">class Phar</a>. Here is how to process, I let you go to the documentation to learn more.</p><pre><code class="language-php">&lt;?php
// builder.php

$pharFile = &apos;app.phar&apos;;

// clean up
if (file_exists($pharFile)) 
{
    unlink($pharFile);
}

$phar = new Phar($pharFile);

// First thing to do is to start the buffering
// otherwise no other action will be possible.
$phar-&gt;startBuffering();

// Here I get get the default stub for .phar files
// it&apos;s allowed to customize it entirely, but it&apos;s
// recommended only for advanced usage.
// I also specify my entry point (the front controller)
$defaultStub = $phar-&gt;createDefaultStub(&apos;index.php&apos;);

// Let&apos;s all the rest of the files
$phar-&gt;buildFromDirectory(__DIR__ . &apos;\\app&apos;);

// Customizing the stub
// What we add here allow to execute the file
// without a call to PHP with an unix OS
// *it will not work on windows* (but will not be a problem either)
$stub = &quot;#!/usr/bin/env php \n&quot; . $defaultStub;

// Add the stub
$phar-&gt;setStub($stub);

// Generating the file
$phar-&gt;stopBuffering();

// Some compression option are available.
// Most common are GZ and ZIP. (for many obvious reason)
// And yes, PHP do it afterwards so it must be at the end.
$phar-&gt;compressFiles(Phar::GZ);

# Make the file executable
chmod(__DIR__ . &apos;/app.phar&apos;, 0770);</code></pre><p>If you run this script, you should see a file <code>app.phar</code> generated. Success!</p><hr><p>Let&apos;s use this app.phar. Under Linux, as stated in the comments you should be able to use it this way:</p><pre><code>./app.phar</code></pre><p>If you use Windows, you need to call directly PHP this way:</p><pre><code>php app.phar</code></pre><p>And finally, if you want to use this phar as a library, you can use the following code:</p><pre><code class="language-php">&lt;?php

require_once &apos;phar://app.phar/vendor.php&apos;;

hello();</code></pre><h2 id="making-a-phar-file-for-the-web">Making a phar file for the web</h2><p>That was easy, isn&apos;t it? Let&apos;s take this to the next level. If you want to make a phar file ready for the web it&apos;s possible.</p><p>If you take the last example, just remove the custom stub and execute the phar file with the following command:</p><pre><code>php -S localhost:8000 app.phar</code></pre><p>Then go to <a href="https://localhost:8000">https://localhost:8000</a>, the magic should happen. But there&apos;s more!</p><p>What is a website without assets? Yes, you can add them into your archive, but everything will pass through your front controller (in my case, <code>index.php</code>). So your code must require the assets when PHP asks for them. Here is an example of support for PNG.</p><pre><code>&lt;?php

// current file is index.php, the entry point

function index() {
	echo &quot;&lt;h1&gt;Hello from PHP&lt;/h1&gt;&quot;;
	echo &apos;&lt;img src=&quot;/images/some-image.png&quot; /&gt;&apos;;
}


// index.php is now mostly a router (a real front controller if you prefer)
switch ($_SERVER[&apos;REQUEST_URI&apos;] ?? &apos;/&apos;) {
	case &apos;/&apos;:
	case &apos;/index.php&apos;:
		index();
		break;
	default:
		// PHP will look inside the phar!
		if (file_exists(__DIR__ . $_SERVER[&apos;REQUEST_URI&apos;])) {
			// This simple example is crap, but you got the idea.
			if (str_ends_with($_SERVER[&apos;REQUEST_URI&apos;], &apos;png&apos;)) {
				header(&apos;Content-Type: image/png&apos;);
			}
			require __DIR__ . $_SERVER[&apos;REQUEST_URI&apos;];
			exit;
		}
		http_response_code(404);
		echo &quot;&lt;h1&gt;The content does not exists&quot;;
}</code></pre><p>If you really want to do something like this, I suggest you to use the component HttpFoundation of Symfony with its router or some vendor that will do the job for you. (yes, you can add vendors to your folder, it&apos;s PHP, it will just work fine)</p><p>Finally, I wanted to introduce you to something you <strong>can&apos;t</strong> do: edit a file inside the phar. For example, the following code will actually work:</p><pre><code class="language-php">file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . &apos;tmp.txt&apos;, &apos;yolo&apos;);</code></pre><p>But it modifies the phar file itself and <strong>corrupts</strong> it definitely. Thus you simply can&apos;t write a file in the current directory.</p><h2 id="other-ways-to-generate-a-phar">Other ways to generate a phar</h2><p><em>Let me add one last thing. In every test until now, I suggested you to create a folder and add this one to a file. But there are actually 2 other methods to make a phar.</em></p><h3 id="a-phar-based-on-an-archive">A phar based on an archive</h3><p>If you have a zip (or gz or whatever) file, no problem. PHP can create a phar from it. You just need to tell PHP at the instantiation of the phar:</p><pre><code class="language-php">&lt;?php

@unlink(&apos;app.phar&apos;);
copy(&apos;app.tar.gz&apos;, &apos;app.phar&apos;);
$phar = new Phar(&apos;app.phar&apos;);</code></pre><h3 id="a-phar-based-on-nothing">A phar based on nothing</h3><p>You can generate the phar file entirely from your script, here is an example:</p><pre><code class="language-php">&lt;?php

$phar = new Phar(&apos;app.phar&apos;);
$phar-&gt;startBuffering();
$phar[&apos;index.php&apos;] = &apos;&lt;?php include &quot;config.php&quot;; echo $username;&apos;;
$phar[&apos;config.php&apos;] = &apos;&lt;?php $username = &quot;Nek&quot;;&apos;;
// ... You already know the end</code></pre><p>You can also combine all the methods and finally override some files with this last one.</p><p>Hope you liked it! Find out something missing? Feel free to comment!</p><p>Follow us on Twitter! <a href="https://twitter.com/swagdotind" rel="noreferrer noopener">https://twitter.com/swagdotind</a></p>]]></content:encoded></item><item><title><![CDATA[Test emails with Symfony]]></title><description><![CDATA[Let's see together how to test Symfony emails with PHPUnit, but also with Behat.]]></description><link>https://swag.industries/test-emails-with-symfony/</link><guid isPermaLink="false">62cd427918cb64000194afdc</guid><category><![CDATA[Symfony]]></category><category><![CDATA[PHP]]></category><dc:creator><![CDATA[Maxime V]]></dc:creator><pubDate>Sun, 29 Nov 2020 14:09:47 GMT</pubDate><media:content url="https://swag.industries/content/images/2020/11/ezgif.com-gif-maker.png" medium="image"/><content:encoded><![CDATA[<img src="https://swag.industries/content/images/2020/11/ezgif.com-gif-maker.png" alt="Test emails with Symfony"><p>Symfony 4 brought us the awesome Mailer Component. Something that you may not know about it, is how to test if an email has been sent. This is something not well known, but actually easy!</p><p>Let&apos;s see together how to do it with PHPUnit, but also with Behat - since it&apos;s about functional testing more than unit testing, doing it with Behat makes more sense to me.</p><p>Here is the controller that we are going to test in the following sections:</p><pre><code class="language-php">/**
 * @Route(path=&quot;/sendmail&quot;, name=&quot;sendmail&quot;)
 */
public function index(MailerInterface $mailer): Response
{
    $email = (new Email())
        -&gt;from(&apos;hello@example.com&apos;)
        -&gt;to(&apos;you@example.com&apos;)
        -&gt;subject(&apos;I use Symfony mailer woohoooooo&apos;)
        -&gt;text(&apos;This is the content of the email&apos;)
        -&gt;html(&apos;&lt;p&gt;Simple HTML will be ok for this example, but you can obviously use twig for it.&lt;/p&gt;&apos;);

    $mailer-&gt;send($email);

    return $this-&gt;json([&apos;message&apos; =&gt; &apos;Simple response that we dont really care here&apos;]);
}</code></pre><h2 id="testing-symfony-emails-with-phpunit">Testing Symfony emails with PHPUnit</h2><p>Even if it&apos;s not documented; testing emails is really super easy. But before getting into the actual test, you may be interested about how to not send emails for real. And here again, it is simple: you just have to change the value of the environment variable <code>MAILER_DSN</code>, and here is how:</p><pre><code># file .env.test
MAILER_DSN=null://null</code></pre><p>And here is the code that allows us to test it:</p><pre><code class="language-php">public function testSomething()
{
    $client = static::createClient();
    $client-&gt;request(&apos;GET&apos;, &apos;/sendmail&apos;);
    $this-&gt;assertEmailCount(1);
}</code></pre><!--kg-card-begin: markdown--><p>This test only verifies that an email has been sent, but you have many possibilities of assertions thanks to <a href="https://github.com/symfony/symfony/blob/c31fc9dbda00fa86ae7a2c489d9b813674b73405/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php#L21">the <code>MailerAssertionsTrait</code> trait</a>.</p>
<!--kg-card-end: markdown--><h2 id="testing-with-behat-tests-using-the-kernel-">Testing with Behat tests (using the kernel)</h2><p><em>&#x26A0; This approach assumes that you are testing by using the kernel directly, you can do it very easily by using the Symfony extension. It means that it cannot work with selenium, in this case you may be interested by the last section of this article.</em></p><p>In the previous section I told you about the <code>MailerAssertionsTrait</code>, and you can use it on your behat context under 2 conditions:</p><ol><li>You must extend <code>PHPUnit\Framework\Assert</code> from PHPUnit</li><li>You must have a static variable with your container</li></ol><p>I personally think those 2 conditions are extremely restrictive so I decided to use another approach: I copied/pasted the code of this trait, and modified it. The most important method to redefine is <code>getMessageMailerEvents</code>, and here my version of it (beware, it is static in the original trait):</p><pre><code class="language-php">private function getMessageMailerEvents(): MessageEvents
{
    if (!$this-&gt;container()-&gt;has(&apos;mailer.logger_message_listener&apos;)) {
    	Assert::fail(&apos;A client must have Mailer enabled to make email assertions. Did you forget to require symfony/mailer?&apos;);
    }

    return $this-&gt;container()-&gt;get(&apos;mailer.logger_message_listener&apos;)-&gt;getEvents();
}</code></pre><p>This approach is very helpful if you want to add your own methods. Here is one I use in my test (you may like it or not, here it is), this method returns a link contained in the HTML version of your mail:</p><pre><code class="language-php">public function getLinkInEmail(string $containing = &apos;&apos;, int $index = 0, string $transport = null): string
{
    $message = $this-&gt;getMailerMessage($index, $transport);

    if (null === $message) {
        Assert::fail(&apos;There is no email&apos;);
    }
    if (RawMessage::class === \get_class($message)) {
        throw new \LogicException(&apos;Unable to test a message HTML body on a RawMessage.&apos;);
    }

    preg_match_all(&apos;/&quot;(https?:\/\/[a-zA-Z0-9\-.]+\.[a-zA-Z]{2,3}(\/\S*)?)&quot;/&apos;, $message-&gt;getHtmlBody(), $urls);

    foreach ($urls[1] as $url) {
        if (StringTools::contains($url, $containing)) {
            return $url;
        }
    }

    Assert::fail(&apos;No URL found for the pattern &apos;.$containing);
}</code></pre><p>Now by using your trait inside your behat contexts, you can easily test emails.</p><h2 id="testing-emails-with-behat-mink-and-selenium-or-external-approach-">Testing emails with Behat/Mink and Selenium (or external approach)</h2><p>Here you can&apos;t use the tooling that Symfony provides, so you will need to be a little bit more creative. The following example is something that works and that you can customize to fit your needs (or for better code quality, this is super-raw here).</p><p>I suggest you to use a redis instance. For this test to work I used a redis docker with no special configuration: it just works.</p><h3 id="first-step-a-custom-symfony-mailer-transport">First step: a custom Symfony Mailer transport</h3><p>Defining a custom transport for Symfony Mailer is really simple: you need a factory that instantiate a transport, and this factory should be tagged as transport factory in the dependency injection, it&apos;s basically 3 files.</p><p>In the transport we will just store inside redis that we sent a mail.</p><pre><code class="language-php">&lt;?php

namespace App\Test;

use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\TransportInterface;

class BehatMailerFactory extends AbstractTransportFactory
{
    public function create(Dsn $dsn): TransportInterface
    {
        if (!\in_array($dsn-&gt;getScheme(), $this-&gt;getSupportedSchemes(), true)) {
            throw new UnsupportedSchemeException($dsn, &apos;behat&apos;, $this-&gt;getSupportedSchemes());
        }

        return new FakeRedisMailerTransport();
    }

    protected function getSupportedSchemes(): array
    {
    	// This is important for the Symfony to find the related
        // transport for the DSN we provide
        return [&apos;behat&apos;];
    }
}</code></pre><pre><code class="language-php">&lt;?php

namespace App\Test;

use Predis\Client;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\RawMessage;

class FakeRedisMailerTransport implements TransportInterface
{
    /**
     * @param Email $message
     */
    public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage
    {
        // Instantiating predis here is not awesome, in real usage
        // I suggest you to use the redis bundle
        $client = new Client();
        
        // If you get the original value, you can just increment it
        $client-&gt;set(&apos;email_count&apos;, 1);

        // Oh yes I know. This part really sucks.
        // I just wanted to have the simplest example
        // But you have all you want inside the $message object
        // which is an Email instance, not just a RawMessage.
        return new SentMessage(
            $message,
            new Envelope(
                new Address(&apos;foo@bar.com&apos;),
                [new Address(&apos;bar@foo.com&apos;)]
            )
        );
    }

    public function __toString(): string
    {
        return &apos;behat&apos;;
    }
}
</code></pre><pre><code class="language-yaml"># services.yaml
services:
    App\Test\BehatMailerFactory:
        tags:
            - {name: &apos;mailer.transport_factory&apos;}</code></pre><p>And we can now use the following DSN for our tests: <code>behat://null</code></p><h3 id="second-step-the-behat-context">Second step: the behat context</h3><p>Let&apos;s start this part with the actual feature we want to test:</p><pre><code class="language-gherkin"># my-feature.feature
Feature:
  As a user
  blah blah blah (you should say a lot here)

  @email
  Scenario: It sends email
    Given I send an email
    Then an email has been sent
</code></pre><p>So we need to have 2 step, here is an example of how you can do it:</p><pre><code class="language-php">/**
 * @Given I send an email
 */
public function iSendAnEmail()
{
    $client = \Symfony\Component\HttpClient\HttpClient::create();
    $response = $client-&gt;request(&apos;GET&apos;, &apos;http://localhost:8001/sendmail&apos;);

    Assert::eq($response-&gt;getStatusCode(), 200);
}

/**
 * @Then an email has been sent
 */
public function anEmailHasBeenSent()
{
    $client = new Predis\Client();
    $value = $client-&gt;get(&apos;email_count&apos;);
    Assert::greaterThan($value, 0);
}</code></pre><p>And here we are, you have a test that passes on the CI!</p><h3 id="bonus-part-cleaning-redis">Bonus part: cleaning redis</h3><p>After the test is executed, if the mail is no more sent, the test will pass: that&apos;s because the first test added data to redis and didn&apos;t clean it.</p><p>You may have notice that I used the tag <code>@email</code> in the feature file, nothing innocent here. It&apos;s because I use a behat hook to clean my email data in redis. Here is how to do it for our current example:</p><pre><code class="language-php">/**
 * @AfterScenario email
 */
public function cleanEmail()
{
    $client = new Predis\Client();
    $client-&gt;set(&apos;email_count&apos;, 0);
}</code></pre><p><em>This concludes this little tutorial. I hope you liked it.</em></p><p>Follow us on Twitter! <a href="https://twitter.com/swagdotind" rel="noreferrer noopener">https://twitter.com/swagdotind</a></p>]]></content:encoded></item><item><title><![CDATA[How to get the real PHP memory usage on macOS]]></title><description><![CDATA[<p>PHP provides the function <a href="https://www.php.net/manual/fr/function.memory-get-usage.php">memory_get_usage()</a>, this is very helpful whenever you find your code using a lot of RAM. But this method has a significant limitation: it only counts memory used by your code directly <strong>and nothing out of the zend engine</strong>.</p><p>The problem is that you may</p>]]></description><link>https://swag.industries/get-real-memory-usage-of-php-under-macos/</link><guid isPermaLink="false">62cd427918cb64000194afe2</guid><category><![CDATA[PHP]]></category><category><![CDATA[protip]]></category><dc:creator><![CDATA[Maxime V]]></dc:creator><pubDate>Thu, 29 Oct 2020 11:02:25 GMT</pubDate><media:content url="https://swag.industries/content/images/2020/10/ram2b.png" medium="image"/><content:encoded><![CDATA[<img src="https://swag.industries/content/images/2020/10/ram2b.png" alt="How to get the real PHP memory usage on macOS"><p>PHP provides the function <a href="https://www.php.net/manual/fr/function.memory-get-usage.php">memory_get_usage()</a>, this is very helpful whenever you find your code using a lot of RAM. But this method has a significant limitation: it only counts memory used by your code directly <strong>and nothing out of the zend engine</strong>.</p><p>The problem is that you may use external libraries, a typical example is using libxml to parse any XML file. And you may want to know how much memory it uses. On linux it&apos;s pretty simple since you can parse the file <code>/proc/{pid}/status</code> but on macOS it&apos;s more complicated as this file does not exist.</p><p>And it looks like there is no easy solution. In my use case, as I wanted to monitor a single process, I decided to use top directly. So here is my solution:</p><pre><code class="language-php">$pid = getmypid();

echo exec(&quot;top -pid $pid -l 1 | grep $pid | awk &apos;{print $8}&apos;&quot;) . &quot;\n&quot;;</code></pre><p>Here are the details for what it does:</p><ul><li>It runs top in log mode with only 1 iteration (<code>-l 1</code>) and only for your pid</li><li>It grep the line with your pid since top outputs a lot more</li><li>It finally get the 8th column using awk</li></ul><p>This did the trick for me. It even shows human format for the memory size used. If you have any better/easiest/more reliable idea, feel free to comment!</p><p>Follow us on Twitter! <a href="https://twitter.com/swagdotind" rel="noreferrer noopener">https://twitter.com/swagdotind</a></p>]]></content:encoded></item><item><title><![CDATA[Debunking btrfs]]></title><description><![CDATA[<p><em>btrfs means no evil! btrfs is awesome!</em></p><p>A few weeks ago I listened to <a href="https://linuxunplugged.com/358">one of the Linux Unplugged podcasts</a> about btrfs. It was a reminder that btrfs is great. I have been using btrfs for a few months now, so here is my comment.</p><p>For those who aren&apos;</p>]]></description><link>https://swag.industries/debunking-btrfs/</link><guid isPermaLink="false">62cd427918cb64000194afdd</guid><dc:creator><![CDATA[Gaby]]></dc:creator><pubDate>Mon, 17 Aug 2020 15:55:39 GMT</pubDate><content:encoded><![CDATA[<p><em>btrfs means no evil! btrfs is awesome!</em></p><p>A few weeks ago I listened to <a href="https://linuxunplugged.com/358">one of the Linux Unplugged podcasts</a> about btrfs. It was a reminder that btrfs is great. I have been using btrfs for a few months now, so here is my comment.</p><p>For those who aren&apos;t familiar with btrfs, it is an <em>advanced</em> filesystem. Its main features are snapshots, compression, deduplication / CoW and volume management.</p><h3 id="the-bad-publicity">The bad publicity</h3><p><em>I installed btrfs and I lost all of my data! Now that I&apos;m on btrfs, my computer has been super slow! btrfs said my drive was faulty!</em></p><p>We all came across one of these statements on the internet. It is important to remember that what people experienced can be outdated: btrfs has changed a lot over the past few years. </p><p>Every kernel ships with a different version of btrfs, it gets more and more stable with time, tests, and feedback. Therefore, a valid concern from a few years ago wouldn&apos;t stand today. It doesn&apos;t mean these problems never happened, but they shouldn&apos;t happen anymore. If you want to give btrfs a try, make sure to use the latest kernel possible.</p><h3 id="avoid-raid-5-6">Avoid raid 5/6</h3><blockquote>I used to think that until I witnessed a RAID 6 fail. Man was I pissed that day, I remember waking up and checking my phone out of habit and seeing a shitload of idrac&apos;s alerts from one of our client&apos;s server... 3 disks died in one night... (<a href="https://www.reddit.com/r/sysadmin/comments/6pw36x/til_why_we_should_stop_using_raid_5/dkslmqz/">source</a>)</blockquote><p>This is a reminder that raid 5 and raid 6 are more fragile than you may think. On average, a raid 5 with three disks means the three disks are going to wear out in about the same time, meaning when you get a faulty drive, odds are another drive is gonna die soon, soon enough that you will not be able to recover the data. This applies for <em>every</em> file system, not only btrfs.</p><p>There are some workarounds, like having a mix of different HDD/SSD brands, bigger &amp; smaller sizes, but you are better off using something else.</p><h3 id="the-problem-with-df">The &quot;problem&quot; with df</h3><p>As you use df, you will notice that what df returns is sometimes inaccurate. &#xA0;My first thought was to blame btrfs as the documentation recommends using <code>btrfs df /</code> instead of <code>df</code>.</p><p>Turns out the same problem happens when using zfs: the snapshots are not taken into consideration and therefore the df output tells you there&apos;s still &#xA0;some space left when in reality your drive is full. Bear in mind that it is reliable most of the time but if you want a better output, <code>btrfs df /</code> is for you.</p><h3 id="btrbk">btrbk</h3><p>If you want to use btrfs (or any other fs) you must have a good backup strategy. This post is focusing on btrfs, therefore, you won&apos;t find anything better than <a href="https://github.com/digint/btrbk">btrbk</a>!</p><p>btrbk relies on btrfs&apos; snapshots and deduplication. You can have hundreds of gigabytes stored and backed up in seconds, that&apos;s right, seconds. Your backups will not be incremented, so you can have a window of 14 days for example and prune the old snapshots/backups. </p><p>This is a great software for your volumes, it can handle anything on btrfs, even your virtual machines, or LXD containers. I wish such a tool could exist for the other filesystems.</p><h3 id="conclusion">Conclusion</h3><p>My personal experience with running btrfs on servers is very positive. Btrbk made things easier for me as the data on my servers is backed up on a remote server every day. Being able to sync data easily, as well as shrinking and expanding etc, with all of these features, managing my data became so much easier!</p><p>Btrfs keeps on improving and if you&apos;re eyeing at btrfs, don&apos;t hesitate, give it a try.</p><p>Follow us on Twitter! <a href="https://twitter.com/swagdotind" rel="noreferrer noopener">https://twitter.com/swagdotind</a></p>]]></content:encoded></item><item><title><![CDATA[Simple strategy pattern with Symfony]]></title><description><![CDATA[Use the dependency injection of Symfony to configure super-easily a strategy pattern with your services.]]></description><link>https://swag.industries/simple-strategy-with-symfony/</link><guid isPermaLink="false">62cd427918cb64000194afde</guid><category><![CDATA[PHP]]></category><category><![CDATA[Symfony]]></category><dc:creator><![CDATA[Maxime V]]></dc:creator><pubDate>Sat, 15 Aug 2020 19:56:01 GMT</pubDate><media:content url="https://swag.industries/content/images/2020/08/signs.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://swag.industries/content/images/2020/08/signs.jpg" alt="Simple strategy pattern with Symfony"><p>The strategy pattern is a very easy implementation with the DIC from Symfony. Here is how: </p><p>First let&apos;s define an interface for our strategies:</p><pre><code class="language-php">namespace App\Domain\CustomProcess;

interface MyStrategyInterface
{
    public function execute(Whatever $input);
    public function supports(Whatever $input);
}</code></pre><!--kg-card-begin: markdown--><p>Then, we will add the tag <code>app.my_strategy</code> in our services.yaml file, so every service which implements the previous interface will automatically receive the tag.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><pre><code class="language-yaml">services:
    _instanceof:
        App\Domain\CustomProcess\MyStrategyInterface:
            tags: [&apos;app.my_strategy&apos;]
</code></pre>
<!--kg-card-end: markdown--><p>Still in our services.yaml file you can then use the following syntax:</p><pre><code class="language-yaml">services:
    App\Domain\Doer:
        arguments:
            $strategies: !tagged_iterator app.my_strategy</code></pre><!--kg-card-begin: markdown--><p>An example for a <code>Doer</code> class could be the following:</p>
<pre><code class="language-php">class Doer
{
    private iterable $strategies;
    
    public function __construct(iterable $strategies)
    {
        $this-&gt;strategies = $strategies;
    }
    
    public function doSomeWorkOnInput(Whatever $input)
    {
        // In here, you could imagine an algorithm that will try to guess
        // what is the best strategy, but I added a support method
        // on the interface do we will use it.
        foreach ($this-&gt;strategies as $strategy) {
            if ($strategy-&gt;support($input)) {
                return $strategy-&gt;execute($input);
            }
        }
        
        throw new ApplicationLogicException(&apos;The given input is not supported.&apos;);
    }
}
</code></pre>
<!--kg-card-end: markdown--><p>Hope you&apos;ll like it!</p><h2 id="bonus-">Bonus!</h2><!--kg-card-begin: markdown--><p>If you do not want to manually define your service for the Doer class or if you want to inject the strategy in many places, you can use the section <code>bind</code> of the service.yaml file to enable autowiring for your strategy!</p>
<pre><code class="language-yaml">services:
    _defaults:
        bind:
            # I would recommend a better name, but at least you get it
            iterable $strategies: !tagged_iterator app.my_strategy
    
</code></pre>
<!--kg-card-end: markdown--><p>Follow us on Twitter! <a href="https://twitter.com/swagdotind" rel="noreferrer noopener">https://twitter.com/swagdotind</a></p>]]></content:encoded></item><item><title><![CDATA[Easy locks with Symfony]]></title><description><![CDATA[Learn how to configure an lock with Symfony in minutes.]]></description><link>https://swag.industries/easy-locks-with-symfony/</link><guid isPermaLink="false">62cd427918cb64000194afd9</guid><category><![CDATA[PHP]]></category><category><![CDATA[Symfony]]></category><dc:creator><![CDATA[Maxime V]]></dc:creator><pubDate>Mon, 13 Jul 2020 11:26:14 GMT</pubDate><media:content url="https://swag.industries/content/images/2020/07/testlock1-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://swag.industries/content/images/2020/07/testlock1-1.png" alt="Easy locks with Symfony"><p>The <a href="https://symfony.com/doc/current/components/lock.html">Symfony documentation</a> describes quite well how locks actually work, that&apos;s why this article will not describe why you should use a lock and how it actually works. But it says nothing about the short time installation it requires to set it up and run!</p><p>Let&apos;s see that together.</p><p>Install the lock component with composer just like any other Symfony component:</p><!--kg-card-begin: markdown--><pre><code>composer require lock
</code></pre>
<!--kg-card-end: markdown--><p>This component is one of the rare components that will not come with a recipe, you need to add the configuration yourself, and it&apos;s quite easy.</p><pre><code class="language-yaml"># In the file packages/lock.yaml
framework:
    lock: &apos;redis://localhost&apos;</code></pre><!--kg-card-begin: markdown--><p>This is an example to configure a redis, but it could work with your database directly: <code>lock: &apos;%env(DATABASE_URL)%&apos;</code></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Here is a command that uses our lock, it&apos;s that simple!</p>
<pre><code class="language-php">class LockTestCommand extends Command
{
    protected static $defaultName = &apos;app:lock-test&apos;;

    // In Symfony 5.2 you can inject directly the lock factory
    // because the RetryTillSaveStore (see bellow) is no more required
    private PersistingStoreInterface $lockStore;

    public function __construct(PersistingStoreInterface $lockStore)
    {
        $this-&gt;lockStore = $lockStore;
        parent::__construct();
    }

    protected function configure()
    {
        $this
            -&gt;setDescription(&apos;This is a simple lock test&apos;)
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        // Depending on what store you uses, you may need to decorate the store
        // to be able to use the `BlockingStoreInterface` feature
        // and it&apos;s required for the redis store.
        $store = new RetryTillSaveStore($this-&gt;lockStore, retrySleep: 100, retryCount: 1000); // 100ms 1000 times try
        
        // In the case we decorate the store we need to create by hand the factory
        $lockFactory = new LockFactory($store);
        $lock = $this-&gt;lockFactory-&gt;createLock(&apos;test-lock&apos;);

        // Passing &quot;true&quot; will enforce the waiting for the lock to be available
        if (!$lock-&gt;acquire(blocking: true)) {
            $io-&gt;success(&quot;Cannot acquire the lock&quot;);
            return 1;
        }

        $io-&gt;success(&quot;Lock acquired&quot;);
        $this-&gt;task();

        $lock-&gt;release();

        return 0;
    }

    private function task()
    {
        sleep(10);
    }
}
</code></pre>
<!--kg-card-end: markdown--><p>The previous configuration only registers the default lock (and its store and factory) that will be automatically injected to your services. But you can actually define many locks (and keeping the default one!). Here is how:</p><pre><code class="language-yaml">framework:
    lock:
        default: &apos;%env(DATABASE_URL)%&apos;
        redis_high_availability: [&apos;redis://r1.docker&apos;, &apos;redis://r2.docker&apos;]
</code></pre><!--kg-card-begin: markdown--><p>This will generate a lock factory as a service named <code>lock.redis_high_availability.factory</code>, and you will need to specify it explicitely in your services configuration to use it.</p>
<!--kg-card-end: markdown--><p>I hope it helps! Please let us know in the comments if there&apos;s any mistake or a better way to do it.</p><p>Follow us on Twitter! <a href="https://twitter.com/swagdotind" rel="noreferrer noopener">https://twitter.com/swagdotind</a></p>]]></content:encoded></item><item><title><![CDATA[Welcome to the Swag Industries]]></title><description><![CDATA[<h2 id="we-made-a-blog-">We made a blog!</h2><p>Maxime V. and Gaby O. have teamed up to found the swag industries, an IT-oriented blog.</p><p>As we wanted to write our thoughts about computing, help people and make a few short tutorials, we decided to have a blog. So here you are! This is our</p>]]></description><link>https://swag.industries/welcome/</link><guid isPermaLink="false">62cd427918cb64000194afda</guid><dc:creator><![CDATA[Gaby]]></dc:creator><pubDate>Tue, 07 Jul 2020 19:05:55 GMT</pubDate><content:encoded><![CDATA[<h2 id="we-made-a-blog-">We made a blog!</h2><p>Maxime V. and Gaby O. have teamed up to found the swag industries, an IT-oriented blog.</p><p>As we wanted to write our thoughts about computing, help people and make a few short tutorials, we decided to have a blog. So here you are! This is our tech blog, but also a tech blog for Wemint and Gangbowl.</p><p>You can learn more about us and our parent organizations on the <a href="https://swag.industries/about-us">&quot;About Us&quot; page</a>.</p>]]></content:encoded></item></channel></rss>