<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yannick Chenot</title>
    <description>The latest articles on DEV Community by Yannick Chenot (@osteel).</description>
    <link>https://dev.to/osteel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F810450%2F21c74969-54d7-4a4f-9648-e6acb5eef1b5.jpeg</url>
      <title>DEV Community: Yannick Chenot</title>
      <link>https://dev.to/osteel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/osteel"/>
    <language>en</language>
    <item>
      <title>Upgrade your project to PHP 8.2 with Rector</title>
      <dc:creator>Yannick Chenot</dc:creator>
      <pubDate>Sat, 18 Mar 2023 15:07:23 +0000</pubDate>
      <link>https://dev.to/osteel/upgrade-your-project-to-php-82-with-rector-3ihk</link>
      <guid>https://dev.to/osteel/upgrade-your-project-to-php-82-with-rector-3ihk</guid>
      <description>&lt;p&gt;I wanted to upgrade a project to PHP 8.2 and figured I could use &lt;a href="https://getrector.com/"&gt;Rector&lt;/a&gt; for this.&lt;/p&gt;

&lt;p&gt;Not only was that the case, but I also completely underestimated how easy it was going to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Rector?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://getrector.com/"&gt;Rector&lt;/a&gt; is a free and open-source tool written in PHP allowing you to automate various refactoring tasks. It analyses your code and applies whatever &lt;a href="https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md"&gt;rules&lt;/a&gt; you've specified in its &lt;a href="https://getrector.com/documentation"&gt;configuration&lt;/a&gt;. It is also possible to &lt;a href="https://getrector.com/documentation/custom-rule"&gt;create your own rules&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to upgrade your project to PHP 8.2
&lt;/h2&gt;

&lt;p&gt;First, install Rector as a development dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer require --dev rector/rector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, generate the &lt;code&gt;rector.php&lt;/code&gt; configuration file at the root of your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/rector init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open that file and replace its content with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Rector\Config\RectorConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Rector\Set\ValueObject\LevelSetList&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;RectorConfig&lt;/span&gt; &lt;span class="nv"&gt;$rectorConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$rectorConfig&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;LevelSetList&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UP_TO_PHP_82&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perform a dry run to check what changes Rector is about to make (the below analyses the &lt;code&gt;app&lt;/code&gt; folder as an example):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/rector process app --dry-run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the changes if you're happy with them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/rector process app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also specify the paths to analyse in the configuration file directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Rector\Config\RectorConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Rector\Set\ValueObject\LevelSetList&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;RectorConfig&lt;/span&gt; &lt;span class="nv"&gt;$rectorConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$rectorConfig&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/app'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$rectorConfig&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;LevelSetList&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UP_TO_PHP_82&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/rector process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's happening under the hood?
&lt;/h2&gt;

&lt;p&gt;In the above, the &lt;code&gt;UP_TO_PHP_82&lt;/code&gt; constant means that not only the rules to upgrade from PHP 8.1 to PHP 8.2 were applied, but so did those from PHP 8.0 to PHP 8.1, as well as all the other versions' down to PHP 5.2.&lt;/p&gt;

&lt;p&gt;In other words, you can bring an entire code base from PHP 5.2 to the latest version with a single command, using predefined rulesets provided by Rector.&lt;/p&gt;

&lt;p&gt;But there's more – you can check the available rules and their description &lt;a href="https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md"&gt;here&lt;/a&gt;, which go way beyond version-related changes. And, as mentioned earlier, you can also &lt;a href="https://getrector.com/documentation/custom-rule"&gt;create your own rules&lt;/a&gt; if need be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Rector has been on my radar for a while and I even used it a few times before, yet I'm still surprised by how powerful it is.&lt;/p&gt;

&lt;p&gt;While it seems to be getting a little more traction recently, along with other static analysis tools like &lt;a href="https://phpstan.org/"&gt;PHPStan&lt;/a&gt;, it is still very much underused. One of the reasons is probably that developers are unaware of its potential, of which the above is just a taste.&lt;/p&gt;

&lt;p&gt;There is a &lt;a href="https://getrector.com/book"&gt;book&lt;/a&gt; covering Rector in depth, which I'm planning on reading soon.&lt;/p&gt;

</description>
      <category>php</category>
      <category>automation</category>
      <category>refactoring</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A GitHub Workflow to Check the Compatibility of Your PHP Package with a Range of Dependency Versions</title>
      <dc:creator>Yannick Chenot</dc:creator>
      <pubDate>Tue, 05 Jul 2022 08:05:28 +0000</pubDate>
      <link>https://dev.to/osteel/a-github-workflow-to-check-the-compatibility-of-your-php-package-with-a-range-of-dependency-versions-11ab</link>
      <guid>https://dev.to/osteel/a-github-workflow-to-check-the-compatibility-of-your-php-package-with-a-range-of-dependency-versions-11ab</guid>
      <description>&lt;p&gt;A common aspect of a PHP developer's job is to deal with &lt;a href="https://getcomposer.org/"&gt;Composer&lt;/a&gt; dependencies. We use the work of others as Lego bricks to build our own projects, making the most of the beautiful thing that is the open-source movement.&lt;/p&gt;

&lt;p&gt;As typical end-users of these community efforts, we maintain a list of dependencies whose versions are &lt;em&gt;pinned&lt;/em&gt; through the &lt;code&gt;composer.lock&lt;/code&gt; file. We don't need to think about supporting other versions of these dependencies, just as we don't need to think about accommodating a range of PHP versions – we work within the constraints of our application's target environments, which are known and expected to be stable over time.&lt;/p&gt;

&lt;p&gt;Things are different for open-source software maintainers. They are largely agnostic to the constraints of their users' environments, so the best they can do is make sure their libraries are compatible with as many PHP and dependency versions as possible.&lt;/p&gt;

&lt;p&gt;This is where compatibility testing is crucial. Open-source maintainers need to ensure that any change made to the code will work with all the versions listed in &lt;code&gt;composer.json&lt;/code&gt;. Doing so manually is unpractical, so they resort to test coverage and automated scripts.&lt;/p&gt;

&lt;p&gt;This post explores one such way of automating compatibility testing, using a combination of test coverage and a &lt;a href="https://docs.github.com/en/actions/using-workflows"&gt;GitHub Actions workflow&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  In this post
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Complete workflow&lt;/li&gt;
&lt;li&gt;Breakdown&lt;/li&gt;
&lt;li&gt;Closing thoughts&lt;/li&gt;
&lt;li&gt;Resources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Complete workflow
&lt;/h2&gt;

&lt;p&gt;Here is the final version of the workflow. If you're just here for the code, feel free to copy and reuse the below as much as you please. Otherwise keep reading for a progressive breakdown of the file, supported by an example project.&lt;/p&gt;

&lt;p&gt;You can also check out this article's &lt;a href="https://github.com/osteel/php-compatibility-demo"&gt;repository&lt;/a&gt; for reference.&lt;/p&gt;


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


&lt;h2&gt;
  
  
  Breakdown
&lt;/h2&gt;

&lt;p&gt;The following is a full breakdown of the workflow. We will build it up incrementally, using an example project that we will update as we expand the range of supported dependency versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial setup
&lt;/h3&gt;

&lt;p&gt;This step is optional, but if you're a "learn through practice" kind of person, you may want to build this example project on your machine and update it as we go. If you don't, you may want to read this section anyway, as it contains some relevant information.&lt;/p&gt;

&lt;p&gt;If you do, go ahead and &lt;a href="https://github.com/new"&gt;create&lt;/a&gt; a new empty GitHub repository now. Call it &lt;code&gt;php-compatibility-demo&lt;/code&gt; (or anything else if you feel inspired) and make sure it's public so you can enjoy the free GitHub Actions &lt;a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions"&gt;allowance&lt;/a&gt; (unless you've got a paid account, in which case feel free to keep the repository private).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The below assumes that your development setup supports PHP 8.0+ and that you've got access to a terminal with Composer and Git.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Clone the repository on your development machine and initialise a new Composer project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git clone php-compatibility-demo
$ cd php-compatibility-demo
$ composer init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The last command will ask you a series of questions and generate a &lt;code&gt;composer.json&lt;/code&gt; file at the end. Most questions don't matter in the context of this article, but make sure to select &lt;a href="https://getcomposer.org/doc/04-schema.md#type"&gt;&lt;em&gt;library&lt;/em&gt;&lt;/a&gt; for the type of project, &lt;a href="https://getcomposer.org/doc/04-schema.md#minimum-stability"&gt;&lt;em&gt;stable&lt;/em&gt;&lt;/a&gt; for the minimum stability, and to leave the default values for the PSR-4 autoload bit.&lt;/p&gt;

&lt;p&gt;Once that's done, add &lt;code&gt;/vendor/&lt;/code&gt; and &lt;code&gt;composer.lock&lt;/code&gt; to the &lt;code&gt;.gitignore&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/vendor/
composer.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the first major difference from regular company projects, where developers will usually commit the &lt;code&gt;composer.lock&lt;/code&gt; file so the application is consistent across environments. Here we embrace inconsistency instead, and let Composer figure out what dependency versions to use based on the constraints of client environments.&lt;/p&gt;

&lt;p&gt;In this context, the GitHub workflow is yet another client environment, and we don't want it to pick up a &lt;code&gt;composer.lock&lt;/code&gt; file that will force the installation of specific versions while our goal is to test a whole range of them.&lt;/p&gt;
&lt;h3&gt;
  
  
  Library and test coverage
&lt;/h3&gt;

&lt;p&gt;With that in place, let's talk about our test project for a minute. We are going to build a small library on top of &lt;a href="https://github.com/moneyphp/money"&gt;Money&lt;/a&gt;, a popular PHP package facilitating the manipulation of monetary values and currencies.&lt;/p&gt;

&lt;p&gt;Our library will consist of a single helper class – &lt;code&gt;CurrencyHelper.php&lt;/code&gt; – that will simplify working with currencies. It will expose two methods – one to make sure that a bunch of &lt;code&gt;Money&lt;/code&gt; objects have the same currency, and another one returning the &lt;a href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"&gt;numeric ISO 4217 code&lt;/a&gt; of a &lt;code&gt;Money&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;You will soon realise that this library is pretty pointless, but it makes up a good example to support this article, so I trust you will turn a blind eye.&lt;/p&gt;

&lt;p&gt;Here's the file structure you should get at the end of this section (remember that you can also refer to this article's &lt;a href="https://github.com/osteel/php-compatibility-demo"&gt;repository&lt;/a&gt; at any time for comparison):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php-compatibility-demo/
├── src/
│   └── CurrencyHelper.php
├── tests/
│   └── CurrencyHelperTest.php
├── vendor/
├── .gitignore
├── composer.json
└── composer.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let's instal the Money library:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer require moneyphp/money:^4.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note that we intentionally import v4 here, even though it may not be the latest version when you're reading this.&lt;/p&gt;

&lt;p&gt;Let's also instal &lt;a href="https://phpunit.de/"&gt;PHPUnit&lt;/a&gt; as a development dependency:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer require --dev phpunit/phpunit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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



&lt;p&gt;If you're also building this example library on your machine, don't forget to update the vendor name in the namespace at the top.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;isSame&lt;/code&gt; method takes any number of &lt;code&gt;Money&lt;/code&gt; objects and returns a boolean indicating whether they share the same currency. The way it works is that it removes the first element from the array (using &lt;a href="https://www.php.net/manual/en/function.array-shift"&gt;&lt;code&gt;array_shift&lt;/code&gt;&lt;/a&gt;) and then compares this element's currency with that of the other ones, using the &lt;code&gt;isSameCurrency&lt;/code&gt; method from the &lt;code&gt;Money&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;The second method – &lt;code&gt;numericCode&lt;/code&gt; – takes a &lt;code&gt;Money&lt;/code&gt; object as its only parameter and returns its currency's numeric ISO code, using the &lt;code&gt;numericCodeFor&lt;/code&gt; method from the &lt;code&gt;ISOCurrencies&lt;/code&gt; helper class (which is part of the Money library).&lt;/p&gt;

&lt;p&gt;And here is the content for &lt;code&gt;tests/CurrencyHelperTest.php&lt;/code&gt;, the class testing the behaviour of our "library":&lt;/p&gt;


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


&lt;p&gt;These tests will allow us to confirm whether our library works with various PHP and dependency versions later on. Again, don't forget to update the vendor's name at the top.&lt;/p&gt;

&lt;p&gt;Let's check that the tests are passing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/phpunit tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If they do, we can move on to the workflow.&lt;/p&gt;
&lt;h3&gt;
  
  
  Basic workflow
&lt;/h3&gt;

&lt;p&gt;Let's start with a simple GitHub Actions workflow whose only job will be to run the test suite. Basically what we just did, but in an automated way.&lt;/p&gt;

&lt;p&gt;We need to create a new &lt;code&gt;.github&lt;/code&gt; folder at the root of the project, itself containing a &lt;code&gt;workflows&lt;/code&gt; folder in which we add a file named &lt;code&gt;ci.yml&lt;/code&gt;. These directories are GitHub conventions – that's where the platform expects to find our workflows, &lt;code&gt;ci.yml&lt;/code&gt; being one of them.&lt;/p&gt;

&lt;p&gt;This is the updated file structure:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php-compatibility-demo/
├── .github/
│   └── workflows/
│       └── ci.yml
├── src/
│   └── CurrencyHelper.php
├── tests/
│   └── CurrencyHelperTest.php
├── vendor/
├── .gitignore
├── composer.json
└── composer.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And here's the content of &lt;code&gt;ci.yml&lt;/code&gt;:&lt;/p&gt;


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



&lt;p&gt;Let's break it down quickly – we first give the workflow a name ("CI" stands for "Continuous Integration"), and then define the conditions on which it should trigger (here, any changes pushed to the &lt;code&gt;main&lt;/code&gt; branch, or any pull request opened towards it).&lt;/p&gt;

&lt;p&gt;We then list the jobs to be executed. There's only one in our case – running the test suite. We call this job "PHPUnit" and specify that it will run on the latest Ubuntu version.&lt;/p&gt;

&lt;p&gt;Then we define the steps making up the job – checking out the code, setting up PHP, installing the Composer dependencies and, finally, running the test suite. The first three steps are using popular GitHub Actions (see the &lt;code&gt;uses&lt;/code&gt; keys):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/actions/checkout"&gt;Checkout&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shivammathur/setup-php"&gt;Setup PHP&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ramsey/composer-install"&gt;Composer Install&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We don't need a GitHub Action for the last step as it is a simple command.&lt;/p&gt;

&lt;p&gt;Save the file, commit and push. After a few seconds, you should see a screen similar to this under the &lt;em&gt;Actions&lt;/em&gt; tab of your GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HE81qEuB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6ne8wzi9q2o4pnk8u0z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HE81qEuB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6ne8wzi9q2o4pnk8u0z.png" alt="Successful basic workflow" width="880" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can click on the workflow run to see the logs.&lt;/p&gt;

&lt;p&gt;And that's it for the first version of our workflow. It's a decent first step already, as the workflow will trigger any time some new code is pushed to/a pull request is opened towards &lt;code&gt;main&lt;/code&gt;, automatically reporting any breaking changes.&lt;/p&gt;

&lt;p&gt;But so far we're only testing a single set of dependency versions – whatever Composer decides is best when the workflow reaches the &lt;em&gt;Install composer dependencies&lt;/em&gt; step.&lt;/p&gt;

&lt;p&gt;Let's go one step further.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency ranges
&lt;/h3&gt;

&lt;p&gt;Say we want to support both PHP 7.4+ and PHP 8.0+. Instead of letting Composer do its best based on what software version is available on the client environment, we are going to tip it off by explicitly listing the PHP versions our library is compatible with.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;composer.json&lt;/code&gt; and update the &lt;code&gt;require&lt;/code&gt; section as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"require": {
    "php": "^7.4|^8.0",
    "moneyphp/money": "^4.0"
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We're now supporting both PHP 7.4+ and PHP 8.0+. But wait, that's not going to work – the Money library &lt;a href="https://github.com/moneyphp/money/blob/v4.0.0/composer.json#L27"&gt;requires PHP 8.0+&lt;/a&gt; for its v4, so if we want to add support for PHP 7.4+, we also need to include an older version of Money.&lt;/p&gt;

&lt;p&gt;Let's update &lt;code&gt;composer.json&lt;/code&gt; again:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"require": {
    "php": "^7.4|^8.0",
    "moneyphp/money": "^3.0|^4.0"
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We're now also supporting both Money v3 and v4. As Money v3 &lt;a href="https://github.com/moneyphp/money/blob/3.x/composer.json#L27"&gt;needs PHP 5.6+&lt;/a&gt;, we should be compatible with PHP 7.4 as well.&lt;/p&gt;

&lt;p&gt;As we've updated &lt;code&gt;composer.json&lt;/code&gt;, let's make sure our local &lt;code&gt;composer.lock&lt;/code&gt; is up to date with the following command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer update --lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We now need to update our workflow to ensure the test suite will run for both versions of PHP:&lt;/p&gt;


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



&lt;p&gt;The main change here is the addition of the &lt;code&gt;strategy&lt;/code&gt; section. A &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategy"&gt;strategy&lt;/a&gt; allows us to define a &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix"&gt;matrix&lt;/a&gt; for our jobs – a way to run the steps using different configurations.&lt;/p&gt;

&lt;p&gt;But let's have a quick look at &lt;code&gt;fail-fast&lt;/code&gt; first – by setting this &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast"&gt;property&lt;/a&gt; to &lt;code&gt;false&lt;/code&gt;, we make sure the steps will run for all configurations and won't be interrupted even if one of them fails. We want comprehensive feedback for all PHP versions here, so we can fix any issue that comes up.&lt;/p&gt;

&lt;p&gt;Then comes the &lt;code&gt;matrix&lt;/code&gt; section, composed of a single &lt;code&gt;php-version&lt;/code&gt; key, which is an array of values. What that does is that for each successive run of the steps, the &lt;code&gt;php-version&lt;/code&gt; key will be assigned a new value from the array, until we reach the end of the array.&lt;/p&gt;

&lt;p&gt;Note that we've also listed PHP 8.1 in there, as there are some &lt;a href="https://www.php.net/releases/8.1/en.php"&gt;notable differences&lt;/a&gt; with PHP 8.0.&lt;/p&gt;

&lt;p&gt;We can see the &lt;code&gt;php-version&lt;/code&gt; key being referenced further down, in the &lt;em&gt;Setup PHP&lt;/em&gt; step. We added a new &lt;code&gt;php-version&lt;/code&gt; key under the &lt;code&gt;with&lt;/code&gt; section – that's where we assign the value of the current run (&lt;code&gt;${{ matrix.php-version }}&lt;/code&gt;), using a &lt;a href="https://github.com/shivammathur/setup-php#php-version-required"&gt;configuration option&lt;/a&gt; from the Setup PHP GitHub Action.&lt;/p&gt;

&lt;p&gt;Let's try this out – commit and push the changes and head over to the repository's &lt;em&gt;Actions&lt;/em&gt; tab again. Give it a few seconds – you should see that this time, the run has failed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lwtCw_ST--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8g87qjjjvj5szwqq3paq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lwtCw_ST--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8g87qjjjvj5szwqq3paq.png" alt="Failed intermediate workflow" width="880" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the workflow run – there was an issue with the PHP 7.4 job:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Olobry1w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xwi0xluysj52fu13fjm6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Olobry1w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xwi0xluysj52fu13fjm6.png" alt="Failed intermediate workflow – PHP 7.4" width="326" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on it and expand the &lt;em&gt;Run PHPUnit&lt;/em&gt; logs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--L5pJV1o0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0q9ve3kfhglwm6z6qgk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L5pJV1o0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0q9ve3kfhglwm6z6qgk5.png" alt="Failed intermediate workflow – PHP 7.4 logs" width="880" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Interesting. One of our test assertions is failing on line 23:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$this-&amp;gt;assertFalse($checker-&amp;gt;isSame($amount1, $amount2, $amount4));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Looking at the Money library's &lt;a href="https://github.com/moneyphp/money/blob/master/CHANGELOG.md"&gt;changelog&lt;/a&gt;, the reason appears to be because the &lt;code&gt;Money@isSameCurrency&lt;/code&gt; method only added support for multiple arguments &lt;a href="https://github.com/moneyphp/money/blob/v4.0.0/CHANGELOG.md#changed"&gt;from v4&lt;/a&gt;. We know that this version can only be used with PHP 8.0+, which explains why Composer instals Money v3 when it detects PHP 7.4.&lt;/p&gt;

&lt;p&gt;To fix the issue, we need to change the way the &lt;code&gt;isSame&lt;/code&gt; method works in &lt;code&gt;CurrencyHelper.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function isSame(Money ...$amounts): bool
{
    if (count($amounts) === 1) {
        return true;
    }

    $first = array_shift($amounts);

    foreach ($amounts as $amount) {
        if (! $first-&amp;gt;isSameCurrency($amount)) {
            return false;
        }
    }

    return true;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Instead of passing all the remaining elements of the &lt;code&gt;$amounts&lt;/code&gt; array to &lt;code&gt;isSameCurrency&lt;/code&gt; at once, we now loop through them and compare their currency to that of the first element, one by one.&lt;/p&gt;

&lt;p&gt;Let's make sure we haven't broken our library before pushing our changes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/phpunit tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Commit and push and check the &lt;em&gt;Actions&lt;/em&gt; tab again:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FY8M61aT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6zi0zgotvpv13zd5uh5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FY8M61aT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6zi0zgotvpv13zd5uh5.png" alt="Successful intermediate workflow" width="880" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The error is gone!&lt;/p&gt;

&lt;p&gt;So it appears our library is now compatible with both PHP 7.4+ and PHP 8.0+.&lt;/p&gt;

&lt;p&gt;Or is it?&lt;/p&gt;
&lt;h3&gt;
  
  
  Testing lower dependency versions
&lt;/h3&gt;

&lt;p&gt;Let's take a look at the logs again. Click on the latest successful workflow run and enter the &lt;em&gt;PHPUnit (7.4)&lt;/em&gt; job from the left-hand side. Expand the &lt;em&gt;Install composer dependencies&lt;/em&gt; logs and look for the line referencing the Money library.&lt;/p&gt;

&lt;p&gt;It looks like Composer is picking the latest minor version by default (v3.3.1 at the time of writing):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sajhxeAp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0mgpm3iax3sg5x7s2m4v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sajhxeAp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0mgpm3iax3sg5x7s2m4v.png" alt="Composer picks the latest version" width="880" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The issue is we want our library to be compatible with previous versions as well – how can we force Composer to instal the lowest version – v3.0.0?&lt;/p&gt;

&lt;p&gt;We need to update &lt;code&gt;ci.yml&lt;/code&gt; again:&lt;/p&gt;


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



&lt;p&gt;Note that we've added a new key to our matrix – &lt;code&gt;dependency-versions&lt;/code&gt;. It is also an array, with two values – &lt;code&gt;lowest&lt;/code&gt; and &lt;code&gt;highest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This time we're taking advantage of a &lt;a href="https://github.com/ramsey/composer-install#dependency-versions"&gt;configuration option&lt;/a&gt; from the Composer Install GitHub Action, allowing us to force Composer to either use the lowest or highest versions of the dependencies.&lt;/p&gt;

&lt;p&gt;We can see the &lt;code&gt;dependency-versions&lt;/code&gt; key being used further down, in the &lt;em&gt;Install composer dependencies&lt;/em&gt; step. We added a new &lt;code&gt;with&lt;/code&gt; section to it, with a single &lt;code&gt;dependency-versions&lt;/code&gt; key to which we assign the current value from the matrix (&lt;code&gt;${{ matrix.dependency-versions }}&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When that value is &lt;code&gt;lowest&lt;/code&gt;, the Composer Install GitHub Action will run this command behind the scenes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer update --prefer-lowest --prefer-stable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's the consequence of adding a new array of values to our matrix? The answer is that it will trigger as many step runs as there are combinations from both arrays.&lt;/p&gt;

&lt;p&gt;Let's see it in action – commit and push the new workflow, and check the &lt;em&gt;Actions&lt;/em&gt; tab again:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--v1yMesvJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q979alkuiyqpbcin5txv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--v1yMesvJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q979alkuiyqpbcin5txv.png" alt="Failed advanced workflow" width="880" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looks like there was an issue with the new run. If we click on it, we'll see that the test suite was run for each PHP and dependency version, as well as for each value from the &lt;code&gt;dependency-versions&lt;/code&gt; array.&lt;/p&gt;

&lt;p&gt;But all the runs using the &lt;code&gt;lowest&lt;/code&gt; value have failed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ld2zaX7H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/po27fwfgwe8gt6t4u28d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ld2zaX7H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/po27fwfgwe8gt6t4u28d.png" alt="Failed advanced workflow – lowest dependencies" width="414" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's find out why. Click on the &lt;em&gt;PHPUnit (7.4, lowest)&lt;/em&gt; job and expand the &lt;em&gt;Run PHPUnit&lt;/em&gt; logs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IOshJPsR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7v7umjjsnm9j3pp694j1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IOshJPsR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7v7umjjsnm9j3pp694j1.png" alt="Failed advanced workflow – lowest dependencies logs" width="880" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The error is the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Call to undefined method Money\Currencies\ISOCurrencies::numericCodeFor()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we take a look at the Money library's changelog again, we can see that this method was &lt;a href="https://github.com/moneyphp/money/blob/v4.0.0/CHANGELOG.md#305---2017-04-26"&gt;added with v3.0.5&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We now have two ways to fix this – either we update the &lt;code&gt;CurrencyHelper@numericCode&lt;/code&gt; method and manually implement &lt;a href="https://github.com/moneyphp/money/compare/v3.0.4...v3.0.5#diff-0631517d385a0d04aa9dbc68ee7e92163b406e084208483ab1837c3ace4bb7f1"&gt;the change&lt;/a&gt;, or we bump the minimum supported version to 3.0.5.&lt;/p&gt;

&lt;p&gt;Since this version is a small &lt;a href="https://semver.org/#summary"&gt;patch&lt;/a&gt;, it feels reasonable to do the latter.&lt;/p&gt;

&lt;p&gt;Let's update &lt;code&gt;composer.json&lt;/code&gt; one last time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"require": {
    "php": "^7.4|^8.0",
    "moneyphp/money": "^3.0.5|^4.0"
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit and push, and wait for the workflow run to finish:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5mCEtQEC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/im0y0cf90b4t00m3ql2g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5mCEtQEC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/im0y0cf90b4t00m3ql2g.png" alt="Successful advanced workflow" width="880" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No more errors!&lt;/p&gt;

&lt;p&gt;For good measure, let's make sure our local &lt;code&gt;composer.lock&lt;/code&gt; is in sync with &lt;code&gt;composer.json&lt;/code&gt; again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer update --lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Compatibility testing highlights once again the importance of having good test coverage for our applications. Even if we don't need to support a range of dependency versions, whenever we find ourselves in need to update one of them, having a strong test suite gives us the confidence that it won't break some parts of our code. That also goes for upgrading to a newer PHP version.&lt;/p&gt;

&lt;p&gt;But then again, most PHP developers don't have to think about this too much. I personally started to look into compatibility testing more seriously when I created &lt;a href="https://github.com/osteel/openapi-httpfoundation-testing"&gt;my first open-source library&lt;/a&gt; and, later on, when I started exploring &lt;a href="https://tech.osteel.me/posts/how-to-build-and-distribute-beautiful-command-line-applications-with-php-and-composer"&gt;building for the console with PHP&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Yet you may eventually find yourself in a situation where you need to build and provide a PHP package with limited knowledge of target environments. Be it in the context of a private company or as a way to contribute back to the community, when that day comes it may be useful to have this kind of guide handy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/osteel/php-compatibility-demo"&gt;Article repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions"&gt;GitHub Actions documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows"&gt;GitHub Actions workflows documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/checkout"&gt;Checkout (GitHub Action)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/shivammathur/setup-php"&gt;Setup PHP (GitHub Action)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ramsey/composer-install"&gt;Composer Install (GitHub Action)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/moneyphp/money"&gt;PHP Money library&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>githubactions</category>
      <category>php</category>
      <category>compatibilitytesting</category>
      <category>composer</category>
    </item>
    <item>
      <title>Validate Your PHP API Tests Against OpenAPI Definitions – a Laravel Example [2023 update]</title>
      <dc:creator>Yannick Chenot</dc:creator>
      <pubDate>Tue, 22 Mar 2022 12:03:37 +0000</pubDate>
      <link>https://dev.to/osteel/validate-your-php-api-tests-against-openapi-definitions-a-laravel-example-kca</link>
      <guid>https://dev.to/osteel/validate-your-php-api-tests-against-openapi-definitions-a-laravel-example-kca</guid>
      <description>&lt;p&gt;&lt;a href="https://swagger.io/specification/"&gt;OpenAPI&lt;/a&gt; is a specification for describing RESTful APIs in JSON and YAML format, aiming at being understandable by both humans and machines.&lt;/p&gt;

&lt;p&gt;OpenAPI definitions are language-agnostic and can be used in several ways:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;An OpenAPI definition can be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;– &lt;a href="https://swagger.io/specification/"&gt;The OpenAPI Specification&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article demonstrates how to write integration tests that compare API responses to OpenAPI 3+ definitions to validate that the former conform to the latter.&lt;/p&gt;

&lt;p&gt;We will use the &lt;a href="https://github.com/osteel/openapi-httpfoundation-testing"&gt;OpenAPI HttpFoundation Testing&lt;/a&gt; package in a fresh &lt;a href="https://laravel.com/"&gt;Laravel&lt;/a&gt; installation for which we'll also generate a &lt;a href="https://swagger.io/tools/swagger-ui/"&gt;Swagger UI&lt;/a&gt; documentation using the &lt;a href="https://github.com/DarkaOnLine/L5-Swagger"&gt;L5 Swagger&lt;/a&gt; package.&lt;/p&gt;

&lt;p&gt;The first part of the post will describe the issue and provide further context – if you're just here for the code, you're welcome to skip ahead and go to the &lt;em&gt;Laravel example&lt;/em&gt; section straight away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The issue
&lt;/h2&gt;

&lt;p&gt;APIs are commonplace nowadays and the decent ones come with documentation describing the endpoints and how to use them. These documents come in various shapes and forms, but a fact they can't escape is that they need to be updated every time the API changes.&lt;/p&gt;

&lt;p&gt;To many developers, API documentation is an afterthought – it's boring, tedious, and often unrewarding. Using annotations to keep the code and the documentation in one place helps to some extent, but these are often painful to write and even the most willing developer is likely to introduce errors that won't necessarily be caught by reviewers.&lt;/p&gt;

&lt;p&gt;The result is documentation and API getting out of sync, leading to frustrated consumers.&lt;/p&gt;

&lt;p&gt;Another goal of API maintenance is to ensure that existing endpoints keep functioning properly – regressions can and will appear over time, and may go unnoticed if there isn't a proper testing strategy in place.&lt;/p&gt;

&lt;p&gt;One approach is to write integration tests controlling the API's behaviour and automatically detecting breaking changes as soon as they are introduced. While this is a good strategy, it isn't foolproof – there's no guarantee that the expectations set in the tests will always 100% match the documentation.&lt;/p&gt;

&lt;p&gt;How can we make sure they remain in sync?&lt;/p&gt;

&lt;h2&gt;
  
  
  A solution
&lt;/h2&gt;

&lt;p&gt;Let's assume that we have an API documented with OpenAPI, some integration tests, and that we want to align the expectations of the latter with the operations described in the former.&lt;/p&gt;

&lt;p&gt;OpenAPI comes with a growing number of &lt;a href="https://openapi.tools"&gt;tools&lt;/a&gt; built on top of it, pushing its utility far beyond the documenting aspect.&lt;/p&gt;

&lt;p&gt;One tool destined for the PHP community and maintained by &lt;a href="https://thephpleague.com"&gt;The PHP League&lt;/a&gt; is &lt;a href="https://github.com/thephpleague/openapi-psr7-validator"&gt;OpenAPI PSR-7 Message Validator&lt;/a&gt;, a package validating &lt;a href="https://www.php-fig.org/psr/psr-7"&gt;PSR-7&lt;/a&gt; HTTP messages against OpenAPI definitions.&lt;/p&gt;

&lt;p&gt;The idea is to compare HTTP requests and responses with OpenAPI definitions and return errors when they don't match.&lt;/p&gt;

&lt;p&gt;In other words, we can use this package to add an extra layer on top of our integration tests that will compare API requests and responses with the OpenAPI definitions describing our API and fail the test if they don't match.&lt;/p&gt;

&lt;p&gt;Here is a fancy diagram to illustrate the relationship between the API, the integration tests and the OpenAPI definition:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eKKjki5q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6h1moahl5vf6olvl7vsy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eKKjki5q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6h1moahl5vf6olvl7vsy.png" alt="Relationship between OpenAPI, API and tests" width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The definition describes the API, and the tests use the definition to make sure the API behaves the way the definition says it does.&lt;/p&gt;

&lt;p&gt;All of a sudden, our OpenAPI definition becomes a reference for both our code and our tests – it acts as the API's single source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  PSR-7
&lt;/h2&gt;

&lt;p&gt;One caveat of the OpenAPI PSR-7 Message Validator package is that, as its name indicates, it only works with &lt;a href="https://www.php-fig.org/psr/psr-7"&gt;PSR-7&lt;/a&gt; messages.&lt;/p&gt;

&lt;p&gt;The issue is that not all frameworks support this standard by default – in fact, a lot of them use Symfony's &lt;a href="https://symfony.com/components/HttpFoundation"&gt;HttpFoundation component&lt;/a&gt; under the hood, whose requests and responses do not implement that standard out of the box.&lt;/p&gt;

&lt;p&gt;The Symfony folks thought of this, however, and provided &lt;a href="https://symfony.com/doc/current/components/psr7.html"&gt;a bridge&lt;/a&gt; that converts HttpFoundation objects to PSR-7 ones. The bridge simply needs a PSR-7 and &lt;a href="https://www.php-fig.org/psr/psr-17"&gt;PSR-17&lt;/a&gt; factory, for which they suggest using &lt;a href="https://github.com/Nyholm"&gt;Tobias Nyholm&lt;/a&gt;'s &lt;a href="https://github.com/Nyholm/psr7"&gt;PSR-7 implementation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All these pieces are brought together by &lt;a href="https://github.com/osteel/openapi-httpfoundation-testing"&gt;OpenAPI HttpFoundation Testing&lt;/a&gt;, a package allowing developers to back their integration tests with OpenAPI definitions in applications relying on the HttpFoundation component.&lt;/p&gt;

&lt;p&gt;Let's see how to use it in a Laravel project, which falls into this category.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Laravel example
&lt;/h2&gt;

&lt;p&gt;The code featured in this section is also available as a &lt;a href="https://github.com/osteel/openapi-httpfoundation-testing-laravel-example/"&gt;GitHub repository&lt;/a&gt; for reference.&lt;/p&gt;

&lt;p&gt;First, let's create a new Laravel 10 project, using Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;composer create-project &lt;span class="nt"&gt;--prefer-dist&lt;/span&gt; laravel/laravel openapi-example &lt;span class="s2"&gt;"10.*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter the project's root folder and instal a couple of dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;openapi-example
&lt;span class="nv"&gt;$ &lt;/span&gt;composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; osteel/openapi-httpfoundation-testing
&lt;span class="nv"&gt;$ &lt;/span&gt;composer require darkaonline/l5-swagger &lt;span class="nt"&gt;-W&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one is the &lt;a href="https://github.com/osteel/openapi-httpfoundation-testing"&gt;OpenAPI HttpFoundation Testing&lt;/a&gt; package mentioned earlier, which we instal as a development dependency as it's intended to be used as part of our test suite.&lt;/p&gt;

&lt;p&gt;The second one is &lt;a href="https://github.com/DarkaOnLine/L5-Swagger"&gt;L5 Swagger&lt;/a&gt;, a popular package bringing &lt;a href="https://github.com/zircote/swagger-php"&gt;Swagger PHP&lt;/a&gt; and &lt;a href="https://swagger.io/tools/swagger-ui/"&gt;Swagger UI&lt;/a&gt; to Laravel. We actually don't need Swagger PHP here, as it uses Doctrine annotations to generate OpenAPI definitions and we're going to manually write our own instead. We do need Swagger UI, however, and L5 Swagger conveniently adapts it to work with Laravel.&lt;/p&gt;

&lt;p&gt;If you're wondering, the &lt;code&gt;-W&lt;/code&gt; option is simply here to also update related dependencies and thus avoid conflicts.&lt;/p&gt;

&lt;p&gt;To make sure Swagger PHP doesn't overwrite the OpenAPI definition, let's set the following environment variable in the &lt;code&gt;.env&lt;/code&gt; file, at the root of the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;L5_SWAGGER_GENERATE_ALWAYS&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's also make YAML the default format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;L5_FORMAT_TO_USE_FOR_DOCS&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file named &lt;code&gt;api-docs.yaml&lt;/code&gt; in the &lt;code&gt;storage/api-docs&lt;/code&gt; folder (which you need to create), and add the following content to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.1.0&lt;/span&gt;

&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OpenAPI HttpFoundation Testing Laravel Example&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0&lt;/span&gt;

&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8000/api&lt;/span&gt;

&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/test'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;200'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ok&lt;/span&gt;
            &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
                &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;foo&lt;/span&gt;
                &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;foo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                    &lt;span class="na"&gt;example&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simple OpenAPI definition describing a single operation – a &lt;code&gt;GET&lt;/code&gt; request to the &lt;code&gt;/api/test&lt;/code&gt; endpoint, that should return a JSON object containing a required &lt;code&gt;foo&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;Let's check whether Swagger UI displays our OpenAPI definition correctly. Start the PHP development server (from the project's root):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php artisan serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:8000/api/documentation"&gt;localhost:8000/api/documentation&lt;/a&gt; – you should see the OpenAPI definition rendered as a Swagger UI documentation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4pY9Rbqt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tp9tw7gznj4tlqk8i1pz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4pY9Rbqt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tp9tw7gznj4tlqk8i1pz.png" alt="Swagger UI" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Expand the &lt;code&gt;/test&lt;/code&gt; endpoint and try it out – it should fail with a &lt;code&gt;404 Not Found&lt;/code&gt; error because we haven't implemented it yet.&lt;/p&gt;

&lt;p&gt;Let's fix that now. Open the &lt;code&gt;routes/api.php&lt;/code&gt; file and replace the example route with this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/test'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'foo'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'bar'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go back to the Swagger UI tab and try the endpoint again – it should now return a successful response.&lt;/p&gt;

&lt;p&gt;Time to write a test! Open &lt;code&gt;tests/Feature/ExampleTest.php&lt;/code&gt; and replace its content with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Tests\Feature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Osteel\OpenApi\Testing\ValidatorBuilder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Tests\TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExampleTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * A basic test example.
     *
     * @return void
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testBasicTest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/test'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$validator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ValidatorBuilder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromYamlFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api-docs/api-docs.yaml'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValidator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;baseResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/test'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'get'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's unpack the above. If you're unfamiliar with Laravel, &lt;code&gt;$this-&amp;gt;get()&lt;/code&gt; is a test method provided by the &lt;code&gt;MakesHttpRequests&lt;/code&gt; trait to perform a &lt;code&gt;GET&lt;/code&gt; request to the provided endpoint, executing the request's lifecycle without leaving the application. It returns a response that is identical to one you would obtain if you'd perform the same request from the outside.&lt;/p&gt;

&lt;p&gt;We then create a validator using the &lt;code&gt;Osteel\OpenApi\Testing\ValidatorBuilder&lt;/code&gt; class, to which we feed the YAML definition we wrote earlier via the &lt;code&gt;fromYamlFile&lt;/code&gt; static method (the &lt;code&gt;storage_path&lt;/code&gt; function is a helper returning the path to the &lt;code&gt;storage&lt;/code&gt; folder, where we placed the definition).&lt;/p&gt;

&lt;p&gt;Had we had a JSON definition instead, we could have used the &lt;code&gt;fromJsonFile&lt;/code&gt; method (note that other methods are available and listed in the &lt;a href="https://github.com/osteel/openapi-httpfoundation-testing#usage"&gt;documentation&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The builder returns an instance of &lt;code&gt;Osteel\OpenApi\Testing\Validator&lt;/code&gt;, on which we call the &lt;code&gt;get&lt;/code&gt; method, passing the path and the response as parameters (&lt;code&gt;$response&lt;/code&gt; is an instance of &lt;code&gt;Illuminate\Testing\TestResponse&lt;/code&gt; here – a wrapper for the underlying HttpFoundation object, which can be retrieved through the &lt;code&gt;baseResponse&lt;/code&gt; public property).&lt;/p&gt;

&lt;p&gt;The above is basically the equivalent of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I want to validate that this response conforms to the OpenAPI definition of a &lt;code&gt;GET&lt;/code&gt; request to the &lt;code&gt;/test&lt;/code&gt; path.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It could also be written this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;baseResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/test'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's because the validator exposes a shortcut method for each of the HTTP methods supported by OpenAPI (&lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;HEAD&lt;/code&gt;, &lt;code&gt;OPTIONS&lt;/code&gt; and &lt;code&gt;TRACE&lt;/code&gt;), to make it simpler to test responses for the corresponding operations.&lt;/p&gt;

&lt;p&gt;Note that the specified path must exactly match one of the OpenAPI definition's &lt;a href="https://swagger.io/specification/#paths-object"&gt;paths&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can now run the test, which should be successful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php artisan &lt;span class="nb"&gt;test &lt;/span&gt;tests/Feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;routes/api.php&lt;/code&gt; again, and change the route for this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/test'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'baz'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'bar'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the test again – it should now fail because the response contains &lt;code&gt;baz&lt;/code&gt; instead of &lt;code&gt;foo&lt;/code&gt;, and the OpenAPI definition says the latter is expected.&lt;/p&gt;

&lt;p&gt;Our test is officially backed by our OpenAPI definition!&lt;/p&gt;

&lt;p&gt;The above is an oversimplified example for demonstration purposes, but in real-life situations, a good practice would be to overwrite the &lt;code&gt;MakesHttpRequests&lt;/code&gt; trait's &lt;code&gt;call&lt;/code&gt; method, so it performs both the request and the OpenAPI check.&lt;/p&gt;

&lt;p&gt;This way, our test would boil down to this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/test'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This could be implemented as a new &lt;code&gt;MakesOpenApiRequests&lt;/code&gt; trait that would "extend" the &lt;code&gt;MakesHttpRequests&lt;/code&gt; one, and that would first call the parent &lt;code&gt;call&lt;/code&gt; method to get the response. It would then work out the path from the URI, and validate the response against the OpenAPI definition before returning it, for the calling test to perform any further assertions, as needed.&lt;/p&gt;

&lt;p&gt;The OpenAPI HttpFoundation Testing package has more to offer, like &lt;a href="https://github.com/osteel/openapi-httpfoundation-testing#extending-the-package"&gt;extension points&lt;/a&gt; and &lt;a href="https://github.com/osteel/openapi-httpfoundation-testing#caching"&gt;caching&lt;/a&gt; to speed up the execution of your tests (Laravel's caching API is supported out of the box) – please refer to the &lt;a href="https://github.com/osteel/openapi-httpfoundation-testing"&gt;documentation&lt;/a&gt; for details.&lt;/p&gt;

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

&lt;p&gt;While this setup is a great step towards improving an API's robustness, it is no silver bullet – it requires that every single endpoint is covered with integration tests, which is not easily enforceable in an automated way and ultimately still requires some discipline and vigilance on the part of developers.&lt;/p&gt;

&lt;p&gt;It may even feel a bit coercive at first since, as a result, maintainers are basically &lt;em&gt;forced&lt;/em&gt; to keep the documentation up to date to write successful tests.&lt;/p&gt;

&lt;p&gt;The added value, however, is that said documentation is now guaranteed to be much more accurate, leading to happy consumers which, in turn, should lead to less frustrated developers, who will spend less time hunting down pesky discrepancies.&lt;/p&gt;

&lt;p&gt;All in all, making OpenAPI definitions the single source of truth for both the API documentation and integration tests is in itself a strong incentive to keep them up to date. They naturally become a priority, where they used to be an afterthought.&lt;/p&gt;

&lt;p&gt;As for maintaining the OpenAPI definition itself, doing so manually can feel a bit daunting. Annotations are a solution, but I personally don't like them and prefer to maintain a YAML file directly. IDE extensions like &lt;a href="https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi"&gt;this one for VSCode&lt;/a&gt; make it much easier, but if you can't bear the sight of a YAML or JSON file, you can also use &lt;a href="https://openapi.tools/#gui-editors"&gt;tools&lt;/a&gt; like &lt;a href="https://stoplight.io/studio"&gt;Stoplight Studio&lt;/a&gt; to do it through a more user-friendly interface.&lt;/p&gt;

&lt;p&gt;And since we're talking about Stoplight, &lt;a href="https://stoplight.io/blog/api-design-first-vs-code-first/"&gt;this article about API Design-First vs Code First&lt;/a&gt; by &lt;a href="https://twitter.com/philsturgeon"&gt;Phil Sturgeon&lt;/a&gt; is a good starting point for API documentation in general and might help you choose an approach to documenting that works for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://swagger.io/specification"&gt;The OpenAPI Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/osteel/openapi-httpfoundation-testing"&gt;The OpenAPI HttpFoundation Testing package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/osteel/openapi-httpfoundation-testing-laravel-example"&gt;Laravel example repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://swagger.io/tools/swagger-ui"&gt;Swagger UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/DarkaOnLine/L5-Swagger"&gt;The L5 Swagger Laravel package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openapi.tools"&gt;OpenAPI.Tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/thephpleague/openapi-psr7-validator"&gt;The OpenAPI PSR-7 Message Validator package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://symfony.com/components/HttpFoundation"&gt;The HttpFoundation component&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php-fig.org/psr/psr-7"&gt;PSR-7: HTTP message interfaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://symfony.com/doc/current/components/psr7.html"&gt;The PSR-7 Bridge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Nyholm/psr7"&gt;PSR-7 implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stoplight.io/blog/api-design-first-vs-code-first"&gt;API Design-First vs Code First&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>openapi</category>
      <category>testing</category>
      <category>laravel</category>
    </item>
    <item>
      <title>A Complete Guide to Laravel Sail</title>
      <dc:creator>Yannick Chenot</dc:creator>
      <pubDate>Wed, 09 Mar 2022 09:43:02 +0000</pubDate>
      <link>https://dev.to/osteel/a-complete-guide-to-laravel-sail-ibc</link>
      <guid>https://dev.to/osteel/a-complete-guide-to-laravel-sail-ibc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Truman continues to steer his wrecked sailboat towards the infinitely receding horizon. All is calm until we see the bow of the boat suddenly strike a huge, blue wall, knocking Truman off his feet. Truman recovers and clambers across the deck to the bow of the boat. Looming above him out of the sea is a cyclorama of colossal dimensions. The sky he has been sailing towards is nothing but a painted backdrop.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;– Andrew M. Niccol, &lt;a href="http://www.dailyscript.com/scripts/the-truman-show_shooting.html" rel="noopener noreferrer"&gt;The Truman Show&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;On December 8 2020, Taylor Otwell announced the launch of &lt;a href="https://laravel.com/docs/sail" rel="noopener noreferrer"&gt;Laravel Sail&lt;/a&gt;, a development environment based on Docker, along with a large overhaul of Laravel's documentation:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1336357588791947264-751" src="https://platform.twitter.com/embed/Tweet.html?id=1336357588791947264"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1336357588791947264-751');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1336357588791947264&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;The announcement caused a wave of excitement across the community, as a lot of people identified the new environment as an opportunity to finally get into Docker. But it also left some confusion in its wake, as Sail isn't exactly a guide to becoming a Docker expert, and it introduces an approach to development that is quite different from its predecessors.&lt;/p&gt;

&lt;p&gt;This post is about what to expect from Laravel Sail, how it works and how to make the most of it. It is also a plea to developers to break away from it, in favour of their own, tailored solution.&lt;/p&gt;

&lt;p&gt;But before we get there, we need to take a look under the deck, starting with a high-level explanation of what Sail is.&lt;/p&gt;

&lt;h2&gt;
  
  
  In this post
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What is Laravel Sail?&lt;/li&gt;
&lt;li&gt;How does it compare to its predecessors?&lt;/li&gt;
&lt;li&gt;How does it work?&lt;/li&gt;
&lt;li&gt;Extending Laravel Sail&lt;/li&gt;
&lt;li&gt;What's wrong with Laravel Sail anyway?&lt;/li&gt;
&lt;li&gt;The whale in the cabin&lt;/li&gt;
&lt;li&gt;You don't need Laravel Sail&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;Resources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is Laravel Sail?
&lt;/h2&gt;

&lt;p&gt;Sail is Laravel's latest development environment. It is the most recent addition to an already long list featuring official solutions like &lt;a href="https://laravel.com/docs/homestead" rel="noopener noreferrer"&gt;Homestead&lt;/a&gt; and &lt;a href="https://laravel.com/docs/valet" rel="noopener noreferrer"&gt;Valet&lt;/a&gt; on the one hand, and community efforts like &lt;a href="https://laragon.org" rel="noopener noreferrer"&gt;Laragon&lt;/a&gt;, &lt;a href="http://laradock.io" rel="noopener noreferrer"&gt;Laradock&lt;/a&gt;, &lt;a href="https://github.com/tighten/takeout" rel="noopener noreferrer"&gt;Takeout&lt;/a&gt; and &lt;a href="https://vessel.shippingdocker.com/" rel="noopener noreferrer"&gt;Vessel&lt;/a&gt; on the other (according to the &lt;a href="https://github.com/laravel/sail#inspiration" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;, Sail is largely inspired by the latter).&lt;/p&gt;

&lt;p&gt;Laravel Sail is based on &lt;a href="https://www.docker.com" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;, a technology leveraging &lt;a href="https://www.docker.com/resources/what-container" rel="noopener noreferrer"&gt;containers&lt;/a&gt; to essentially package up applications so they can run quickly and easily on any operating system.&lt;/p&gt;

&lt;p&gt;The future of Sail appears to be bright, as the Laravel documentation immediately featured it as the &lt;a href="https://laravel.com/docs/installation#your-first-laravel-project" rel="noopener noreferrer"&gt;preferred way&lt;/a&gt; to instal and run Laravel projects locally, a spot that Homestead and Valet had occupied for a long time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it compare to its predecessors?
&lt;/h2&gt;

&lt;p&gt;As a refresher, &lt;a href="https://laravel.com/docs/homestead" rel="noopener noreferrer"&gt;Homestead&lt;/a&gt; is a &lt;a href="https://www.vagrantup.com" rel="noopener noreferrer"&gt;Vagrant&lt;/a&gt; box (a virtual machine) featuring everything most Laravel applications need. That includes essential components like PHP, MySQL and a web server (Nginx), but also other technologies like PostgreSQL, Redis or Memcached.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://laravel.com/docs/valet" rel="noopener noreferrer"&gt;Valet&lt;/a&gt;, on the other hand, is a lightweight environment for macOS focussed on performance. It relies on a local installation of PHP instead of a virtual machine, and is intended to be used alongside other services like &lt;a href="https://dbngin.com" rel="noopener noreferrer"&gt;DBngin&lt;/a&gt; or &lt;a href="https://github.com/tighten/takeout" rel="noopener noreferrer"&gt;Takeout&lt;/a&gt; to manage other dependencies like databases.&lt;/p&gt;

&lt;p&gt;While Homestead and Valet look quite different on paper, they promote the same general approach to local development, which is also shared by most of the aforementioned solutions: they try to be one-size-fits-all environments for Laravel projects and aim at managing them all under one roof.&lt;/p&gt;

&lt;p&gt;Sail's approach is different, in that the instructions to build the development environment come with the project's source code. Instead of relying on the presence of a third-party solution like Homestead on the developer's machine, Docker will read the set of instructions and build the corresponding environment from scratch.&lt;/p&gt;

&lt;p&gt;In other words, the application comes with batteries included and a single command will spin up and configure its dependencies. It will work regardless of the developer's operating system, so long as Docker is installed on it.&lt;/p&gt;

&lt;p&gt;Laravel Sail introduces the notion of a bespoke development environment for the application, which, in my opinion, is the technology's real kicker.&lt;/p&gt;

&lt;p&gt;While this approach is a major departure from traditional solutions, Sail still bears some resemblance to them around the tools it comes with, some of which are essential, others not.&lt;/p&gt;

&lt;p&gt;Let's review the most important ones and how they are implemented.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;From here on, it will be easier to follow along with a fresh installation of Laravel, although the files I refer to always point to the &lt;a href="https://github.com/laravel/sail" rel="noopener noreferrer"&gt;official GitHub repository&lt;/a&gt;. If you've got a little bit of time, go follow the &lt;a href="https://laravel.com/docs/installation#your-first-laravel-project" rel="noopener noreferrer"&gt;instructions for your operating system&lt;/a&gt; now and come back once you've successfully run &lt;code&gt;./vendor/bin/sail up&lt;/code&gt; – I'll take it from there.&lt;/p&gt;

&lt;p&gt;While Sail allows us to &lt;a href="https://laravel.com/docs/#choosing-your-sail-services" rel="noopener noreferrer"&gt;pick the services&lt;/a&gt; we're interested in when creating a new Laravel application, by default it is composed of three main components – PHP, MySQL and Redis. As per the &lt;a href="https://laravel.com/docs/sail#introduction" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, the whole setup gravitates around two files – &lt;a href="https://github.com/laravel/sail/blob/1.x/stubs/docker-compose.stub" rel="noopener noreferrer"&gt;docker-compose.yml&lt;/a&gt; (located at the root of the project after a fresh installation) and the &lt;a href="https://github.com/laravel/sail/blob/1.x/bin/sail" rel="noopener noreferrer"&gt;sail script&lt;/a&gt; (under &lt;code&gt;vendor/bin&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;docker-compose.yml&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;As mentioned already, Laravel Sail is based on Docker, which is a technology leveraging containers. The rule of thumb is that each container should only run one process, which roughly translates to each container running a single piece of software. Applying this rule to the above setup means we'll need a container for PHP, one for MySQL, and a third one for Redis.&lt;/p&gt;

&lt;p&gt;These containers make up your application, and for it to function properly, they need to be &lt;em&gt;orchestrated&lt;/em&gt;. There are several ways to do this, but Laravel Sail relies on &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; for the job, which is the preferred solution for local setups.&lt;/p&gt;

&lt;p&gt;Docker Compose expects us to describe the various components of our application in a &lt;code&gt;docker-compose.yml&lt;/code&gt; file, in YAML format. If you open the one at the root of the project, you will see a &lt;code&gt;version&lt;/code&gt; parameter at the top, followed by a &lt;code&gt;services&lt;/code&gt; section containing a list of components comprising the ones I've just mentioned – &lt;code&gt;laravel.test&lt;/code&gt;, &lt;code&gt;mysql&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt; (and some other ones I'll briefly cover later).&lt;/p&gt;

&lt;p&gt;I'll describe the &lt;code&gt;mysql&lt;/code&gt;, &lt;code&gt;redis&lt;/code&gt; and &lt;code&gt;laravel.test&lt;/code&gt; services in that order, as the two first ones are simpler than the last.&lt;/p&gt;

&lt;h4&gt;
  
  
  The &lt;code&gt;mysql&lt;/code&gt; service
&lt;/h4&gt;

&lt;p&gt;As the name suggests, the &lt;code&gt;mysql&lt;/code&gt; service handles the MySQL database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mysql:
    image: 'mysql/mysql-server:8.0'
    ports:
        - '${FORWARD_DB_PORT:-3306}:3306'
    environment:
        MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
        MYSQL_ROOT_HOST: "%"
        MYSQL_DATABASE: '${DB_DATABASE}'
        MYSQL_USER: '${DB_USERNAME}'
        MYSQL_PASSWORD: '${DB_PASSWORD}'
        MYSQL_ALLOW_EMPTY_PASSWORD: 1
    volumes:
        - 'sailmysql:/var/lib/mysql'
    networks:
        - sail
    healthcheck:
        test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"]
        retries: 3
        timeout: 5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;image&lt;/code&gt; parameter indicates which &lt;em&gt;image&lt;/em&gt; should be used for this container. An easy way to understand the difference between images and containers is to borrow from Object-Oriented Programming concepts – an image is akin to a class and a container to an instance of that class.&lt;/p&gt;

&lt;p&gt;Here, we specify that we want to use the tag &lt;code&gt;8.0&lt;/code&gt; of the &lt;code&gt;mysql/mysql-server&lt;/code&gt; image, corresponding to MySQL Server version 8.0. By default, images are downloaded from &lt;a href="https://hub.docker.com" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;, which is the largest Docker image registry. Have a look at &lt;a href="https://hub.docker.com/r/mysql/mysql-server" rel="noopener noreferrer"&gt;the page for MySQL Server&lt;/a&gt; – most images come with simple documentation explaining how to use them.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ports&lt;/code&gt; section allows us to map local ports to container ports, following the &lt;code&gt;local:container&lt;/code&gt; notation. In the code snippet above, the value of the &lt;code&gt;FORWARD_DB_PORT&lt;/code&gt; environment variable (or &lt;code&gt;3306&lt;/code&gt; if that value is empty) is mapped to the container's &lt;code&gt;3306&lt;/code&gt; port. This is mostly useful to connect third-party software to the database, like &lt;a href="https://www.mysql.com/products/workbench/" rel="noopener noreferrer"&gt;MySQL Workbench&lt;/a&gt; or &lt;a href="https://sequel-ace.com/" rel="noopener noreferrer"&gt;Sequel Ace&lt;/a&gt; (the setup would also work without it).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;environments&lt;/code&gt; is where we define environment variables for the container. Here, most of them receive the value of existing environment variables, which are loaded from the &lt;code&gt;.env&lt;/code&gt; file at the root of the project – &lt;code&gt;docker-compose.yml&lt;/code&gt; automatically detects and imports the content of this file. For instance, in the &lt;code&gt;MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'&lt;/code&gt; line, the container's &lt;code&gt;MYSQL_ROOT_PASSWORD&lt;/code&gt; environment variable will receive the value of &lt;code&gt;DB_PASSWORD&lt;/code&gt; from the &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;volumes&lt;/code&gt; is to declare some of the container's files or folders as &lt;em&gt;volumes&lt;/em&gt;, either by mapping specific local files or folders to them or by letting Docker deal with it.&lt;/p&gt;

&lt;p&gt;Here, a single Docker-managed volume is defined: &lt;code&gt;sailmysql&lt;/code&gt;. This type of volume must be declared in a separate &lt;code&gt;volumes&lt;/code&gt; section, at the same level as &lt;code&gt;services&lt;/code&gt;. We can find it at the bottom of the &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;volumes:
    sailmysql:
        driver: local
    sailredis:
        driver: local
    sailmeilisearch:
        driver: local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sailmysql&lt;/code&gt; volume is mapped to the container's &lt;code&gt;/var/lib/mysql&lt;/code&gt; folder, which is where the MySQL data is stored. This volume ensures that the data is persisted even when the container is destroyed, which is the case when we run the &lt;code&gt;./vendor/bin/sail down&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;networks&lt;/code&gt; section allows us to specify which internal networks the container should be available on. Here, all services are connected to the same &lt;code&gt;sail&lt;/code&gt; network, which is also defined at the bottom of &lt;code&gt;docker-compose.yml&lt;/code&gt;, in the &lt;code&gt;networks&lt;/code&gt; section above the &lt;code&gt;volumes&lt;/code&gt; one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;networks:
    sail:
        driver: bridge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, &lt;code&gt;healthcheck&lt;/code&gt; is a way to indicate which conditions need to be true for the service to be &lt;em&gt;ready&lt;/em&gt;, as opposed to just be &lt;em&gt;started&lt;/em&gt;. I'll get back to this soon.&lt;/p&gt;

&lt;h4&gt;
  
  
  The &lt;code&gt;redis&lt;/code&gt; service
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;redis&lt;/code&gt; service is very similar to the &lt;code&gt;mysql&lt;/code&gt; one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;redis:
    image: 'redis:alpine'
    ports:
        - '${FORWARD_REDIS_PORT:-6379}:6379'
    volumes:
        - 'sailredis:/data'
    networks:
        - sail
    healthcheck:
        test: ["CMD", "redis-cli", "ping"]
        retries: 3
        timeout: 5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We pull the &lt;code&gt;alpine&lt;/code&gt; tag of the &lt;a href="https://hub.docker.com/_/redis" rel="noopener noreferrer"&gt;official image&lt;/a&gt; for Redis (&lt;a href="https://alpinelinux.org/" rel="noopener noreferrer"&gt;Alpine&lt;/a&gt; is a lightweight Linux distribution) and we define which port to forward. We then declare a volume to persist the data, connect the container to the &lt;code&gt;sail&lt;/code&gt; network, and define the health check to perform to ensure the service is ready.&lt;/p&gt;

&lt;h4&gt;
  
  
  The &lt;code&gt;laravel.test&lt;/code&gt; service
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;laravel.test&lt;/code&gt; service is slightly more complex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;laravel.test:
    build:
        context: ./vendor/laravel/sail/runtimes/8.0
        dockerfile: Dockerfile
        args:
            WWWGROUP: '${WWWGROUP}'
    image: sail-8.0/app
    extra_hosts:
        - 'host.docker.internal:host-gateway'
    ports:
        - '${APP_PORT:-80}:80'
    environment:
        WWWUSER: '${WWWUSER}'
        LARAVEL_SAIL: 1
        XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
        XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
    volumes:
        - '.:/var/www/html'
    networks:
        - sail
    depends_on:
        - mysql
        - redis
        - meilisearch
        - selenium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For starters, the name is a bit confusing, but this service is the one handling PHP (i.e. the one serving the Laravel application).&lt;/p&gt;

&lt;p&gt;Next, it has a &lt;code&gt;build&lt;/code&gt; section that we haven't seen before, which points to the &lt;a href="https://github.com/laravel/sail/blob/1.x/runtimes/8.0/Dockerfile" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt; that is present under the &lt;code&gt;vendor/laravel/sail/runtimes/8.0&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.docker.com/engine/reference/builder/" rel="noopener noreferrer"&gt;Dockerfiles&lt;/a&gt; are text documents containing instructions to build images. Instead of using an existing image &lt;em&gt;as-is&lt;/em&gt; from Docker Hub, the Laravel team described their own in a Dockerfile. The first time we run the &lt;code&gt;./vendor/bin/sail up&lt;/code&gt; command, we build that image and create a container based on it.&lt;/p&gt;

&lt;p&gt;Open the Dockerfile and take a look at the first line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:21.04
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that the tag &lt;code&gt;21.04&lt;/code&gt; of &lt;a href="https://hub.docker.com/_/ubuntu" rel="noopener noreferrer"&gt;Ubuntu's image&lt;/a&gt; is used as a starting point for the custom image; the rest of the file is essentially a list of instructions to build upon it, installing everything a standard Laravel application needs. That includes PHP, various extensions, and other packages like Git or Supervisor, as well as Composer.&lt;/p&gt;

&lt;p&gt;The end of the file also deserves a quick explanation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.0/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container

EXPOSE 8000

ENTRYPOINT ["start-container"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see that a bunch of local files are copied over to the container:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;a href="https://github.com/laravel/sail/blob/1.x/runtimes/8.0/php.ini" rel="noopener noreferrer"&gt;php.ini file&lt;/a&gt; is some custom configuration for PHP;&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://github.com/laravel/sail/blob/1.x/runtimes/8.0/supervisord.conf" rel="noopener noreferrer"&gt;supervisord.conf file&lt;/a&gt; is a configuration file for &lt;a href="http://supervisord.org/" rel="noopener noreferrer"&gt;Supervisor&lt;/a&gt;, a process manager here responsible for starting the PHP process;&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://github.com/laravel/sail/blob/1.x/runtimes/8.0/start-container" rel="noopener noreferrer"&gt;start-container file&lt;/a&gt; is a Bash script that will do a few things every time the container starts, because it is defined as the container's &lt;a href="https://docs.docker.com/engine/reference/builder/#entrypoint" rel="noopener noreferrer"&gt;ENTRYPOINT&lt;/a&gt;. We can see that it's made executable by the &lt;code&gt;RUN chmod +x&lt;/code&gt; instruction;&lt;/li&gt;
&lt;li&gt;finally, &lt;code&gt;EXPOSE 8000&lt;/code&gt; doesn't do anything, apart from informing the reader that this container listens on the specified port at runtime (which actually seems wrong here, since the application is served on port 80, not 8000).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other things are happening in this Dockerfile, but the above is the gist of it. Note that this one pertains to PHP 8.0, but Laravel Sail also supports &lt;a href="https://github.com/laravel/sail/blob/1.x/runtimes" rel="noopener noreferrer"&gt;other versions&lt;/a&gt; whose Dockerfiles you can point to from the &lt;code&gt;laravel.test&lt;/code&gt; service in &lt;code&gt;docker-compose.yml&lt;/code&gt;, instead of that of PHP 8.0.&lt;/p&gt;

&lt;p&gt;Bringing our attention back to &lt;code&gt;docker-compose.yml&lt;/code&gt;, we can see that there is an &lt;code&gt;extra_hosts&lt;/code&gt; section we haven't encountered before. I won't get into too much detail, but it's here to ensure &lt;a href="https://xdebug.org/" rel="noopener noreferrer"&gt;Xdebug&lt;/a&gt; runs smoothly &lt;a href="https://github.com/laravel/sail/pull/222" rel="noopener noreferrer"&gt;across operating systems&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The service also has a &lt;code&gt;depends_on&lt;/code&gt; section containing the list of services that should be available before the Laravel application starts. Here, we need MySQL, Redis, Meilisearch and Selenium to be up and running first to avoid connection errors.&lt;/p&gt;

&lt;p&gt;This is where the health checks described earlier are useful. By default, &lt;code&gt;depends_on&lt;/code&gt; will wait for the specified services to be &lt;em&gt;started&lt;/em&gt;, which doesn't necessarily mean they are &lt;em&gt;ready&lt;/em&gt;. By specifying on which conditions these services are deemed ready, we ensure they are in the required state prior to starting the Laravel application.&lt;/p&gt;

&lt;p&gt;The rest of the settings should be familiar by now, so I'll skip them.&lt;/p&gt;

&lt;h4&gt;
  
  
  The &lt;code&gt;meilisearch&lt;/code&gt;, &lt;code&gt;mailhog&lt;/code&gt; and &lt;code&gt;selenium&lt;/code&gt; services
&lt;/h4&gt;

&lt;p&gt;These are the smaller services I referred to earlier; they are already documented &lt;a href="https://laravel.com/docs/sail#meilisearch" rel="noopener noreferrer"&gt;here&lt;/a&gt;, &lt;a href="https://laravel.com/docs/sail#previewing-emails" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://laravel.com/docs/sail#laravel-dusk" rel="noopener noreferrer"&gt;here&lt;/a&gt;. The point is they work the same way as the other ones: they pull existing images from Docker Hub and use them as-is, with minimal configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;sail&lt;/code&gt; script
&lt;/h3&gt;

&lt;p&gt;If you followed &lt;a href="https://laravel.com/docs/installation#your-first-laravel-project" rel="noopener noreferrer"&gt;Laravel's installation instructions&lt;/a&gt; earlier, you must have run the following command at some point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/laravel/sail/blob/1.x/bin/sail" rel="noopener noreferrer"&gt;sail file&lt;/a&gt; that we call here is a Bash script whose function is to present a user-friendly interface to interact with Sail, saving us the pain to run and remember sometimes long-winded Docker commands.&lt;/p&gt;

&lt;p&gt;Let's open it now for closer inspection (don't worry if you're not familiar with Bash – it's pretty straightforward).&lt;/p&gt;

&lt;p&gt;We can ignore the whole first part of the file and focus on the big &lt;code&gt;if&lt;/code&gt; statement which starts like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if [ $# -gt 0 ]; then
    # Proxy PHP commands to the "php" binary on the application container...
    if [ "$1" == "php" ]; then
        # ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain English, the &lt;code&gt;$# -gt 0&lt;/code&gt; bit translates to "if the number of arguments is greater than 0", meaning whenever we call the &lt;code&gt;sail&lt;/code&gt; script with arguments, the execution will enter that &lt;code&gt;if&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;In other words, when we run the &lt;code&gt;./vendor/bin/sail up&lt;/code&gt; command, we call the &lt;code&gt;sail&lt;/code&gt; script with the &lt;code&gt;up&lt;/code&gt; argument, and the execution gets inside the big &lt;code&gt;if&lt;/code&gt; statement where it looks for a condition matching the &lt;code&gt;up&lt;/code&gt; argument. Since there is none, the script goes all the way down to the end of the big &lt;code&gt;if&lt;/code&gt;, in the sort of catch-all &lt;code&gt;else&lt;/code&gt; we can find there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Pass unknown commands to the "docker-compose" binary...
else
    docker compose "$@"
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment already tells us what's going on – the script passes the &lt;code&gt;up&lt;/code&gt; argument on to the &lt;code&gt;docker-compose&lt;/code&gt; binary. In other words, when we run &lt;code&gt;./vendor/bin/sail up&lt;/code&gt; we actually run &lt;code&gt;docker-compose up&lt;/code&gt;, which is the &lt;a href="https://docs.docker.com/compose/reference/up/" rel="noopener noreferrer"&gt;standard Docker Compose command&lt;/a&gt; to start the containers for the services listed in &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This command downloads the corresponding images once if necessary, and builds the Laravel image based on the Dockerfile we talked about earlier.&lt;/p&gt;

&lt;p&gt;Give it a try! Run &lt;code&gt;./vendor/bin/sail up&lt;/code&gt; first, then &lt;code&gt;docker compose up&lt;/code&gt; – they do the same thing.&lt;/p&gt;

&lt;p&gt;Let's now look at a more complicated example, one involving Composer, which is among the packages installed by the application's Dockerfile. But before we do that, let's start Sail in &lt;a href="https://laravel.com/docs/sail#starting-and-stopping-sail" rel="noopener noreferrer"&gt;detached mode&lt;/a&gt; to run the containers in the background:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sail&lt;/code&gt; script allows us to run Composer commands, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail composer --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above calls the &lt;code&gt;sail&lt;/code&gt; script with &lt;code&gt;composer&lt;/code&gt; and &lt;code&gt;--version&lt;/code&gt; as arguments, meaning the execution will enter that big &lt;code&gt;if&lt;/code&gt; statement again.&lt;/p&gt;

&lt;p&gt;Let's look for the condition dealing with Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ...
# Proxy Composer commands to the "composer" binary on the application container...
elif [ "$1" == "composer" ]; then
    shift 1

    if [ "$EXEC" == "yes" ]; then
        docker-compose exec \
            -u sail \
            "$APP_SERVICE" \
            composer "$@"
    else
        sail_is_not_running
    fi
    # ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line of the condition starts with &lt;code&gt;shift&lt;/code&gt;, which is a Bash &lt;em&gt;built-in&lt;/em&gt; that skips as many arguments as the number it is followed by. In this case, &lt;code&gt;shift 1&lt;/code&gt; skips the &lt;code&gt;composer&lt;/code&gt; argument, making &lt;code&gt;--version&lt;/code&gt; the new first argument. The program then makes sure that Sail is running, before executing a weird command split over four lines, which I break down below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose exec \
    -u sail \
    "$APP_SERVICE" \
    composer "$@"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;exec&lt;/code&gt; is the way Docker Compose allows us to execute commands on a container which is already running. &lt;code&gt;-u&lt;/code&gt; is an option indicating which user we want to execute the command as, and &lt;code&gt;$APP_SERVICE&lt;/code&gt; is the container on which we want to run it all. Here, its value is &lt;code&gt;laravel.test&lt;/code&gt;, which is the service's name in &lt;code&gt;docker-compose.yml&lt;/code&gt; as explained in a previous section. It is followed by the command we want to run once we're in the container, namely &lt;code&gt;composer&lt;/code&gt; followed by all the script's arguments. These now only comprise &lt;code&gt;--version&lt;/code&gt;, since we've skipped the first argument.&lt;/p&gt;

&lt;p&gt;In other words, when we run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail composer --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command that is executed behind the scenes is the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker compose exec -u sail "laravel.test" composer "--version"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It would be quite cumbersome to type this kind of command every single time; that's why the &lt;code&gt;sail&lt;/code&gt; script provides shortcuts for them, making the user experience much smoother.&lt;/p&gt;

&lt;p&gt;Have a look at the rest of the smaller &lt;code&gt;if&lt;/code&gt; statements inside the big one to see what else is covered – you'll see that the same principle is always more or less applied.&lt;/p&gt;




&lt;p&gt;There are a few other features available out of the box (like &lt;a href="https://laravel.com/docs/sail#sharing-your-site" rel="noopener noreferrer"&gt;making local containers public&lt;/a&gt;), but we've now covered the substance of what Laravel Sail currently offers. While this is a pretty good start already, it is somewhat limited, even for a basic application.&lt;/p&gt;

&lt;p&gt;The good news is that the Laravel team is aware of this, and built the environment with extension in mind:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Since Sail is just Docker, you are free to customize nearly everything about it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;– &lt;a href="https://laravel.com/docs/sail#sail-customization" rel="noopener noreferrer"&gt;The Laravel documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's see what that means in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending Laravel Sail
&lt;/h2&gt;

&lt;p&gt;The code covered in this section is also available as a &lt;a href="https://github.com/osteel/laravel-sail-extended" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; you can refer to at any moment.&lt;/p&gt;

&lt;p&gt;We're going to explore three ways to extend Laravel Sail, using &lt;a href="https://www.mongodb.com" rel="noopener noreferrer"&gt;MongoDB&lt;/a&gt; as a test subject. But before we proceed, let's make sure we get our hands on as many configuration files as possible.&lt;/p&gt;

&lt;p&gt;The only thing we've got access to initially is the &lt;a href="https://github.com/laravel/sail/blob/1.x/stubs/docker-compose.stub" rel="noopener noreferrer"&gt;docker-compose.yml file&lt;/a&gt;, but we can publish more assets with the following command, which will create a new &lt;code&gt;docker&lt;/code&gt; folder at the root of the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail artisan sail:publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll get back to those in a minute; for the time being, let's try and instal the &lt;a href="https://github.com/jenssegers/laravel-mongodb" rel="noopener noreferrer"&gt;Laravel MongoDB&lt;/a&gt; package, which will make it easy to use MongoDB with our favourite framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail composer require jenssegers/mongodb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, Composer is complaining about some missing extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
- mongodb/mongodb[dev-master, 1.10.0, ..., v1.10.x-dev] require ext-mongodb ^1.11.0 -&amp;gt; it is missing from your system. Install or enable PHP's mongodb extension.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's fix this!&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing extra extensions
&lt;/h3&gt;

&lt;p&gt;Earlier in this post, we talked about the way Sail uses Dockerfiles to build images matching Laravel's requirements for various PHP versions. These files were published with the command we ran at the beginning of this section – all we need to do is edit them to add the extensions we need, and to rebuild the corresponding images.&lt;/p&gt;

&lt;p&gt;Many extensions are available out of the box and we can list them with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail php -m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MongoDB is not part of them; to add it, open the &lt;code&gt;docker/8.0/Dockerfile&lt;/code&gt; file and spot the &lt;code&gt;RUN&lt;/code&gt; instruction installing the various packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN apt-get update \
    &amp;amp;&amp;amp; apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \
    &amp;amp;&amp;amp; mkdir -p ~/.gnupg \
    &amp;amp;&amp;amp; chmod 600 ~/.gnupg \
    &amp;amp;&amp;amp; echo "disable-ipv6" &amp;gt;&amp;gt; ~/.gnupg/dirmngr.conf \
    &amp;amp;&amp;amp; apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys E5267A6C \
    &amp;amp;&amp;amp; apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C300EE8C \
    &amp;amp;&amp;amp; echo "deb http://ppa.launchpad.net/ondrej/php/ubuntu hirsute main" &amp;gt; /etc/apt/sources.list.d/ppa_ondrej_php.list \
    &amp;amp;&amp;amp; apt-get update \
    &amp;amp;&amp;amp; apt-get install -y php8.0-cli php8.0-dev \
        php8.0-pgsql php8.0-sqlite3 php8.0-gd \
        php8.0-curl php8.0-memcached \
        php8.0-imap php8.0-mysql php8.0-mbstring \
        php8.0-xml php8.0-zip php8.0-bcmath php8.0-soap \
        php8.0-intl php8.0-readline php8.0-pcov \
        php8.0-msgpack php8.0-igbinary php8.0-ldap \
        php8.0-redis php8.0-swoole php8.0-xdebug \
    &amp;amp;&amp;amp; php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \
    &amp;amp;&amp;amp; curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \
    &amp;amp;&amp;amp; apt-get install -y nodejs \
    &amp;amp;&amp;amp; npm install -g npm \
    &amp;amp;&amp;amp; curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    &amp;amp;&amp;amp; echo "deb https://dl.yarnpkg.com/debian/ stable main" &amp;gt; /etc/apt/sources.list.d/yarn.list \
    &amp;amp;&amp;amp; apt-get update \
    &amp;amp;&amp;amp; apt-get install -y yarn \
    &amp;amp;&amp;amp; apt-get install -y mysql-client \
    &amp;amp;&amp;amp; apt-get install -y postgresql-client \
    &amp;amp;&amp;amp; apt-get -y autoremove \
    &amp;amp;&amp;amp; apt-get clean \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's easy to identify the block related to PHP extensions since they all start with &lt;code&gt;php8.0&lt;/code&gt;. Add the MongoDB extension at the end of the list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php8.0-redis php8.0-swoole php8.0-xdebug php8.0-mongodb \
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see the detail of available PHP extensions for Ubuntu 21.04 &lt;a href="https://packages.ubuntu.com/hirsute/php/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Save the file and run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will go through all the services listed in &lt;code&gt;docker-compose.yml&lt;/code&gt; and build the corresponding images if they have changed, including the &lt;code&gt;laravel.test&lt;/code&gt; service's, whose Dockerfile we've just updated.&lt;/p&gt;

&lt;p&gt;Once it's done, start the containers again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command will detect that the image corresponding to the &lt;code&gt;laravel.test&lt;/code&gt; service has changed, and recreate the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
laravel-sail-extended_mailhog_1 is up-to-date
laravel-sail-extended_meilisearch_1 is up-to-date
Recreating laravel-sail-extended_laravel.test_1 ... done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! The MongoDB extension for PHP is now installed and enabled. We've only done it for the PHP 8.0 image, but you can apply the same process to &lt;a href="https://github.com/laravel/sail/tree/1.x/runtimes" rel="noopener noreferrer"&gt;other PHP versions&lt;/a&gt; by updating their own Dockerfile, with the right extension name (e.g. &lt;code&gt;php7.4-mongodb&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;We can now safely import the Laravel MongoDB package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail composer require jenssegers/mongodb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next up: adding a Docker service for MongoDB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding new services
&lt;/h3&gt;

&lt;p&gt;MongoDB is essentially another database, meaning the corresponding service will be very similar to that of MySQL and Redis. A quick search on &lt;a href="https://hub.docker.com" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt; reveals that there is &lt;a href="https://hub.docker.com/_/mongo" rel="noopener noreferrer"&gt;an official image&lt;/a&gt; for it, which we are going to use.&lt;/p&gt;

&lt;p&gt;Its documentation contains an example configuration for Docker Compose, which we can copy and adjust to our needs. Open &lt;code&gt;docker-compose.yml&lt;/code&gt; and add the following service at the bottom, after the other ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mongo:
    image: 'mongo:4.4'
    restart: always
    environment:
        MONGO_INITDB_ROOT_USERNAME: '${DB_USERNAME}'
        MONGO_INITDB_ROOT_PASSWORD: '${DB_PASSWORD}'
        MONGO_INITDB_DATABASE: '${DB_DATABASE}'
    volumes:
        - 'sailmongo:/data/db'
    networks:
        - sail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The changes I've made are the following: first, I specified the tag &lt;code&gt;4.4&lt;/code&gt; of the &lt;code&gt;mongo&lt;/code&gt; image. If you don't specify one, Docker Compose will pull the &lt;code&gt;latest&lt;/code&gt; tag by default, which is not a good practice since it will refer to different versions of MongoDB over time, as new releases are available. The introduction of breaking changes could create instability in your Docker setup, so it's better to target a specific version, ideally matching the production one.&lt;/p&gt;

&lt;p&gt;Then, I declared a &lt;code&gt;MONGO_INITDB_DATABASE&lt;/code&gt; environment variable for the container to create a database with the corresponding name at start-up, and I matched the value of each environment variable to one coming from the &lt;code&gt;.env&lt;/code&gt; file (we'll come back to those in a minute).&lt;/p&gt;

&lt;p&gt;I also added a &lt;code&gt;volumes&lt;/code&gt; section, mounting a Docker-managed volume in the container's &lt;code&gt;/data/db&lt;/code&gt; folder. The same principle as MySQL and Redis here applies – if you don't persist the data on your local machine, it will be lost every time the MongoDB container is destroyed. In other words, as the MongoDB data is stored in the container's &lt;code&gt;/data/db&lt;/code&gt; folder, we persist that folder locally using a volume.&lt;/p&gt;

&lt;p&gt;As this volume doesn't exist yet, we need to declare it at the bottom of &lt;code&gt;docker-compose.yml&lt;/code&gt;, after the other ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;volumes:
    sailmysql:
        driver: local
    sailredis:
        driver: local
    sailmeilisearch:
        driver: local
    sailmongo:
        driver: local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, I added the &lt;code&gt;networks&lt;/code&gt; section to ensure the service is on the same network as the others.&lt;/p&gt;

&lt;p&gt;We can now configure Laravel MongoDB as per the package's &lt;a href="https://github.com/jenssegers/laravel-mongodb#configuration" rel="noopener noreferrer"&gt;instructions&lt;/a&gt;. Open &lt;code&gt;config/database.php&lt;/code&gt; and add the following database connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'mongodb' =&amp;gt; [
    'driver' =&amp;gt; 'mongodb',
    'host' =&amp;gt; env('DB_HOST'),
    'port' =&amp;gt; env('DB_PORT'),
    'database' =&amp;gt; env('DB_DATABASE'),
    'username' =&amp;gt; env('DB_USERNAME'),
    'password' =&amp;gt; env('DB_PASSWORD'),
    'options' =&amp;gt; [
        'database' =&amp;gt; env('DB_AUTHENTICATION_DATABASE', 'admin'),
    ],
],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the &lt;code&gt;.env&lt;/code&gt; file at the root of the project and change the database values as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_CONNECTION=mongodb
DB_HOST=mongo
DB_PORT=27017
DB_DATABASE=laravel_sail
DB_USERNAME=root
DB_PASSWORD=root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above makes MongoDB the main database connection. In a real case scenario, you might want to make it a secondary database like Redis, but for demonstration purposes, this will do.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DB_HOST&lt;/code&gt; is the name of the MongoDB service from &lt;code&gt;docker-compose.yml&lt;/code&gt;. Behind the scenes, Docker Compose resolves the service's name to the container's IP on the networks it manages (in our case, that's the single &lt;code&gt;sail&lt;/code&gt; network defined at the end of &lt;code&gt;docker-compose.yml&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DB_PORT&lt;/code&gt; is the port MongoDB is available on, which is &lt;code&gt;27017&lt;/code&gt; by default, as per the &lt;a href="https://hub.docker.com/_/mongo" rel="noopener noreferrer"&gt;image's description&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We're ready for a test! Run the following command again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will download MongoDB's image, create the new volume and start the new container, which will also create the &lt;code&gt;laravel_sail&lt;/code&gt; database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜  ./vendor/bin/sail up -d
Pulling mongo (mongo:4.4)...
4.4: Pulling from library/mongo
7b1a6ab2e44d: Pull complete
90eb44ebc60b: Pull complete
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's make sure of that by running Laravel's default migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can push the test further by updating the &lt;code&gt;User&lt;/code&gt; model so it &lt;a href="https://github.com/jenssegers/laravel-mongodb#extending-the-authenticable-base-model" rel="noopener noreferrer"&gt;extends&lt;/a&gt; Laravel MongoDB's &lt;code&gt;Authenticable&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;
use Jenssegers\Mongodb\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    // ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Tinker to try and create a model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail tinker

Psy Shell v0.10.5 (PHP 8.0.0 — cli) by Justin Hileman
&amp;gt;&amp;gt;&amp;gt; \App\Models\User::factory()-&amp;gt;create();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great! Our MongoDB integration is functional.&lt;/p&gt;

&lt;p&gt;We can keep interacting with it using Tinker and Eloquent, but oftentimes it is useful to have direct access to the database, through third-party software or via a command-line interface such as the &lt;a href="https://docs.mongodb.com/manual/mongo/" rel="noopener noreferrer"&gt;Mongo shell&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's add the latter to our setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom &lt;code&gt;sail&lt;/code&gt; commands
&lt;/h3&gt;

&lt;p&gt;The good news is the Mongo shell is already available, as long as we know the right formula to summon it. Here it is, along with some extra commands to log into the database and list the users (run the first command from the project's root):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker-compose exec mongo mongo

MongoDB shell version v4.4.10
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&amp;amp;gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("be711959-a359-4d1f-8fd4-9af7bad42c23") }
MongoDB server version: 4.4.10
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
    https://docs.mongodb.com/
Questions? Try the MongoDB Developer Community Forums
    https://community.mongodb.com
&amp;gt; use admin
switched to db admin
&amp;gt; db.auth("root", "root")
1
&amp;gt; use laravel_sail
switched to db laravel_sail
&amp;gt; db.users.find()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;docker-compose exec mongo mongo&lt;/code&gt; command should look familiar – earlier in the article, we looked at what the &lt;code&gt;sail&lt;/code&gt; script does behind the scenes, which mostly consists of translating simple &lt;code&gt;sail&lt;/code&gt; commands into more complex &lt;code&gt;docker-compose&lt;/code&gt; ones. Here, we're telling the &lt;code&gt;docker-compose&lt;/code&gt; binary to execute the &lt;code&gt;mongo&lt;/code&gt; command on the &lt;code&gt;mongo&lt;/code&gt; container.&lt;/p&gt;

&lt;p&gt;To be fair, this command isn't too bad and we could easily remember it; but for consistency, it would be nice to have a simpler &lt;code&gt;sail&lt;/code&gt; equivalent, like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/sail mongo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To achieve this we'd need to complete the &lt;code&gt;sail&lt;/code&gt; script somehow, but as it is located inside the &lt;code&gt;vendor&lt;/code&gt; folder – which is created by Composer – we cannot update it directly. We need a way to build upon it without modifying it, which I've summarised below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;make a copy of the &lt;code&gt;sail&lt;/code&gt; script at the root of the project;&lt;/li&gt;
&lt;li&gt;replace the content of its big &lt;code&gt;if&lt;/code&gt; statement with custom conditions;&lt;/li&gt;
&lt;li&gt;if none of the custom conditions matches the current arguments, pass them on to the original &lt;code&gt;sail&lt;/code&gt; script.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If we take a closer look at the &lt;code&gt;sail&lt;/code&gt; file with &lt;code&gt;ls -al&lt;/code&gt;, we can see that it's a symbolic link to the &lt;code&gt;vendor/laravel/sail/bin/sail&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜  ls -al vendor/bin
total 0
drwxr-xr-x   8 yannick  staff   256 26 Nov 10:37 .
drwxr-xr-x  48 yannick  staff  1536 26 Nov 12:24 ..
...
lrwxr-xr-x   1 yannick  staff    24 26 Nov 10:36 sail -&amp;gt; ../laravel/sail/bin/sail
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy that file and paste it at the root of our project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cp vendor/laravel/sail/bin/sail .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the new copy and replace the content of its big &lt;code&gt;if&lt;/code&gt; with the following, leaving the rest as-is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if [ $# -gt 0 ]; then
    # Initiate a Mongo shell terminal session within the "mongo" container...
    if [ "$1" == "mongo" ]; then

        if [ "$EXEC" == "yes" ]; then
            docker-compose exec mongo mongo
        else
            sail_is_not_running
        fi

    # Pass unknown commands to the original "sail" script...
    else
        ./vendor/bin/sail "$@"
    fi
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above code, we removed all the &lt;code&gt;if...else&lt;/code&gt; conditions inside the big &lt;code&gt;if&lt;/code&gt; and added one of our own, which runs the command we used earlier to access the Mongo shell if the value of the script's first argument is &lt;code&gt;mongo&lt;/code&gt;. If it's not, the execution will hit the last &lt;code&gt;else&lt;/code&gt; statement and call the original &lt;code&gt;sail&lt;/code&gt; script with all the arguments.&lt;/p&gt;

&lt;p&gt;You can try this out now – save the file and run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./sail mongo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should open a Mongo shell session in your terminal.&lt;/p&gt;

&lt;p&gt;Try another command, to make sure the original &lt;code&gt;sail&lt;/code&gt; script is taking over when it's supposed to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./sail artisan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Artisan menu should display.&lt;/p&gt;

&lt;p&gt;That's it! If you need more commands, you can add them as new &lt;code&gt;if...else&lt;/code&gt; conditions inside the big &lt;code&gt;if&lt;/code&gt; of the copy of the &lt;code&gt;sail&lt;/code&gt; script at the root of the project.&lt;/p&gt;

&lt;p&gt;It works exactly the same way, except that you now need to run &lt;code&gt;./sail&lt;/code&gt; instead of &lt;code&gt;./vendor/bin/sail&lt;/code&gt; (or update your Bash alias if you created one as suggested in the &lt;a href="https://laravel.com/docs/sail#configuring-a-bash-alias" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;).&lt;/p&gt;




&lt;p&gt;We are now running a fully functional instance of MongoDB as part of our Docker setup, nicely integrated with Laravel Sail. But MongoDB is a mere example here – you can do the same with pretty much any technology you'd like to use.&lt;/p&gt;

&lt;p&gt;Go &lt;a href="https://hub.docker.com" rel="noopener noreferrer"&gt;take a look&lt;/a&gt; now! Most major actors have Docker images – official or maintained by the community – with easy-to-follow instructions. In most cases, you'll have a local instance of the software running in minutes.&lt;/p&gt;

&lt;p&gt;There are probably many more things we could do to customise Laravel Sail, but the three methods described above should get you a long way already.&lt;/p&gt;

&lt;p&gt;At this stage, you may be thinking that Laravel's new environment has a lot going for it, maybe even more so than you initially thought. Yet, the point of this article is to move away from it...&lt;/p&gt;

&lt;p&gt;So where am I going with this?&lt;/p&gt;

&lt;h2&gt;
  
  
  What's wrong with Laravel Sail anyway?
&lt;/h2&gt;

&lt;p&gt;If you made it this far, you're probably wondering what's wrong with Laravel Sail, now that it's clear how far we can push it.&lt;/p&gt;

&lt;p&gt;As it stands, it's a pretty decent solution. In an earlier version of this article, I was pointing to a whole list of issues which have now largely been solved. So why not use it?&lt;/p&gt;

&lt;p&gt;Let me break it to you right now: once you know and understand everything I've explained in the previous sections, you don't need Laravel Sail anymore.&lt;/p&gt;

&lt;p&gt;That's right – you can take that knowledge and walk away.&lt;/p&gt;

&lt;p&gt;But before I elaborate on this, let's review some remaining pain points of Sail, even though I expect the Laravel team to address most of them sooner rather than later.&lt;/p&gt;

&lt;p&gt;The first one concerns the custom &lt;code&gt;sail&lt;/code&gt; commands: while it's possible to extend the &lt;code&gt;sail&lt;/code&gt; script as demonstrated above, the process is a bit ugly and somewhat hacky. Sail's maintainers could fix this with an explicit Bash extension point allowing users to add their own shortcuts, or by publishing the &lt;code&gt;sail&lt;/code&gt; script along with the other files.&lt;/p&gt;

&lt;p&gt;Second, the Laravel application is served by PHP's development server. I won't go into too much detail here, but as mentioned earlier, &lt;a href="http://supervisord.org/" rel="noopener noreferrer"&gt;Supervisor&lt;/a&gt; manages the PHP process in the &lt;code&gt;laravel.test&lt;/code&gt; container. &lt;a href="https://github.com/laravel/sail/blob/1.x/runtimes/8.0/supervisord.conf#L8" rel="noopener noreferrer"&gt;This line&lt;/a&gt; is where Supervisor runs the &lt;code&gt;php artisan serve&lt;/code&gt; command, which starts PHP's development server under the hood.&lt;/p&gt;

&lt;p&gt;The point here is that the environment doesn't use a proper web server (e.g. Nginx), which means we can't easily have local domain names, nor bring HTTPS to the setup. This may be fine for quick prototyping, but more elaborate development will most likely need those.&lt;/p&gt;

&lt;p&gt;The last issue belongs to a separate category, as it relates to Docker overall and not Laravel Sail specifically. It should be carefully considered before going down the Docker path and deserves a section of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whale in the cabin
&lt;/h2&gt;

&lt;p&gt;The one major caveat that appears to be absent from the conversation so far relates to performance. While this shouldn't affect Linux users, if you run Docker Desktop on your system you will most likely experience long execution times, especially on macOS (it seems that using &lt;a href="https://docs.docker.com/docker-for-windows/wsl/" rel="noopener noreferrer"&gt;WSL 2&lt;/a&gt; on Windows can mitigate the slowness).&lt;/p&gt;

&lt;p&gt;While HTTP response times have improved a great deal, running local commands like Yarn or Composer is still very slow. You've probably experienced it yourself while going through this tutorial, when we installed the Laravel MongoDB package with Composer, for instance.&lt;/p&gt;

&lt;p&gt;I won't go into too much detail here, but the reason essentially comes from the host's underlying filesystem, which does not perform well around mounted local directories. As we've seen, this is how Laravel Sail gets the application's source code in the Laravel application's container.&lt;/p&gt;

&lt;p&gt;This is where an approach like &lt;a href="https://tech.osteel.me/posts/you-dont-need-laravel-sail" rel="noopener noreferrer"&gt;Takeout&lt;/a&gt;'s makes sense, as instead of running PHP from a Docker container, they expect developers to run it on their local machine (e.g. via &lt;a href="https://laravel.com/docs/valet" rel="noopener noreferrer"&gt;Valet&lt;/a&gt;), all the while providing instances of services like MySQL or MongoDB, thus offering convenience without sacrificing performance. But from the moment you choose to run PHP via a Docker container (like Sail does), the added value of Takeout decreases, in my opinion.&lt;/p&gt;

&lt;p&gt;There are strategies to mitigate these performance issues, but the Laravel documentation doesn't mention them, let alone that performance might be degraded at all, which I find surprising.&lt;/p&gt;

&lt;p&gt;That being said, you might be comfortable enough with performance as it is; I, for one, have been OK with it for years, even though I use Docker Desktop on macOS. The bottom line is that this aspect should be carefully considered before moving your whole setup to a solution running PHP in a container, be it Laravel Sail or something else.&lt;/p&gt;

&lt;p&gt;But once you've made that decision, and whether or not the other issues are eventually addressed, the main idea of this article remains the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  You don't need Laravel Sail
&lt;/h2&gt;

&lt;p&gt;If you're considering building anything substantial using Laravel Sail as your development environment, sooner or later you will have to extend it. You'll find yourself fumbling around the Dockerfiles and eventually writing your own; you'll have to add some services to &lt;code&gt;docker-compose.yml&lt;/code&gt;, and maybe throw in a few custom Bash commands.&lt;/p&gt;

&lt;p&gt;Once you get there, there's one question you should ask yourself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What's stopping me from building my own setup?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer is &lt;em&gt;nothing&lt;/em&gt;. Once you feel comfortable extending Laravel Sail, you already have the knowledge required to build your own environment.&lt;/p&gt;

&lt;p&gt;Think about it: the &lt;code&gt;docker-compose.yml&lt;/code&gt; file is not specific to Laravel Sail, that's just how Docker Compose works. The same goes for Dockerfiles – they are standard Docker stuff. The Bash layer? That's all there is to it – some Bash code, and as you can see, it's not that complicated.&lt;/p&gt;

&lt;p&gt;So why artificially restrain yourself within the constraints of Sail?&lt;/p&gt;

&lt;p&gt;And more importantly: why limit yourself to using Docker in the context of Laravel?&lt;/p&gt;

&lt;p&gt;Your application may start as a monolith, but it might not always be. Perhaps you've got a separate frontend, and you use Laravel as the API layer. In that case, you might want your development environment to manage them both; to run them simultaneously so they interact with each other like they do on a staging environment or in production.&lt;/p&gt;

&lt;p&gt;If your whole application is a &lt;a href="https://en.wikipedia.org/wiki/Monorepo" rel="noopener noreferrer"&gt;monorepo&lt;/a&gt;, your Docker configuration and Bash script could be at the root of the project, and you could have your frontend and backend applications in separate subfolders, e.g. &lt;code&gt;src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The corresponding tree view would look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-app/
├── bash-script
├── docker-compose.yml
└── src/
    ├── backend/
    │   └── Dockerfile
    └── frontend/
        └── Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; file would declare two services – one for the backend and one for the frontend – both pointing to their respective Dockerfile.&lt;/p&gt;

&lt;p&gt;If the backend and the frontend live in different repositories, you could create a third one, containing your Docker development environment exclusively. Just &lt;em&gt;git-ignore&lt;/em&gt; the &lt;code&gt;src&lt;/code&gt; folder and complete your Bash script so that it pulls both application repositories into it, using the same commands you would normally run by hand.&lt;/p&gt;

&lt;p&gt;Even if your project is a Laravel monolith, this kind of structure is already cleaner than mixing up development-related files with the rest of the source code. Moreover, if your application grows bigger and needs other components besides Laravel, you're already in a good position to support them.&lt;/p&gt;

&lt;p&gt;Once you've made the effort to understand Laravel Sail to extend it, nothing's stopping you from building your own development environments, &lt;em&gt;whether or not Laravel is part of the equation&lt;/em&gt;. That's right, you can build bespoke Docker-based environments for anything.&lt;/p&gt;

&lt;p&gt;And if Laravel is part of the stack, nothing prevents you from reusing Sail's Dockerfiles if you're not comfortable writing your own yet; after all, they are already optimised for Laravel. Likewise, you can draw inspiration from Sail's &lt;code&gt;docker-compose.yml&lt;/code&gt; file if that helps.&lt;/p&gt;

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

&lt;p&gt;Don't get me wrong – Sail has a lot going for it, and I am glad to see an established actor such as Laravel push forward the adoption of Docker for local development.&lt;/p&gt;

&lt;p&gt;We love our frameworks because they offer guidelines to achieve desired results in a way we know to be efficient and battle-tested, and it's only natural that they also seek to provide the environment that will allow their users to build upon them. But one thing that Sail incidentally shows us is that this doesn't have to be part of the framework's mandate anymore.&lt;/p&gt;

&lt;p&gt;Much like Truman's sailboat helps him overcome his fear of the sea and takes him to the edges of the artificial world he lives in, Sail reveals both the confines of Laravel and a way to escape from them.&lt;/p&gt;

&lt;p&gt;You may feel that Sail is more than enough for your needs today, or that you're not yet ready to go on your own. That's fine. But Laravel will always be limited by its monolithic nature, and as you grow as a developer, the day will come where your Laravel application will be but a single component of a larger system, for which Sail won't be enough anymore. Eventually, your small sailboat will bump into a painted backdrop.&lt;/p&gt;

&lt;p&gt;If you'd like to explore this further but feel like you need more guidance, I've published &lt;a href="https://dev.to/posts/docker-for-local-web-development-introduction-why-should-you-care"&gt;a series on the subject&lt;/a&gt; that should get you going. It requires no prior knowledge of Docker and covers web servers, HTTPS, domain names and many other things. It doesn't have all the answers but will get you to a place where you can find your own.&lt;/p&gt;

&lt;p&gt;What you do next is entirely up to you; just know that there's a whole world out there, waiting for you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Truman hesitates. Perhaps he cannot go through with it after all. The camera slowly zooms into Truman's face.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;TRUMAN:&lt;/strong&gt; "In case I don't see you – good afternoon, good evening and good night."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He steps through the door and is gone.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://laravel.com/docs/sail" rel="noopener noreferrer"&gt;Laravel Sail documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/laravel/sail" rel="noopener noreferrer"&gt;Laravel Sail repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/osteel/laravel-sail-extended" rel="noopener noreferrer"&gt;This article's repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com" rel="noopener noreferrer"&gt;Docker documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Overview of Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/engine/reference/builder/" rel="noopener noreferrer"&gt;Dockerfile reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/resources/what-container" rel="noopener noreferrer"&gt;What is a Container?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/posts/docker-for-local-web-development-introduction-why-should-you-care"&gt;Docker for local web development series&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>laravel</category>
      <category>docker</category>
      <category>laravelsail</category>
      <category>php</category>
    </item>
    <item>
      <title>How to Build and Distribute Beautiful Command-Line Applications with PHP and Composer</title>
      <dc:creator>Yannick Chenot</dc:creator>
      <pubDate>Sat, 12 Feb 2022 15:55:00 +0000</pubDate>
      <link>https://dev.to/osteel/how-to-build-and-distribute-beautiful-command-line-applications-with-php-and-composer-2bdi</link>
      <guid>https://dev.to/osteel/how-to-build-and-distribute-beautiful-command-line-applications-with-php-and-composer-2bdi</guid>
      <description>&lt;p&gt;When you think of command-line applications, PHP doesn't immediately come to mind. Yet the language powers many popular tools, either as independent programs or to be used within projects.&lt;/p&gt;

&lt;p&gt;Here are a few examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/laravel/valet" rel="noopener noreferrer"&gt;Valet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vimeo/psalm" rel="noopener noreferrer"&gt;Psalm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/phpstan/phpstan" rel="noopener noreferrer"&gt;PHPStan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nunomaduro/phpinsights" rel="noopener noreferrer"&gt;PHP Insights&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/FriendsOfPHP/PHP-CS-Fixer" rel="noopener noreferrer"&gt;PHP CS Fixer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/squizlabs/PHP_CodeSniffer" rel="noopener noreferrer"&gt;PHP_CodeSniffer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rectorphp/rector" rel="noopener noreferrer"&gt;Rector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tighten/takeout" rel="noopener noreferrer"&gt;Takeout&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be it through its vast ecosystem of libraries and frameworks, its ability to interact with the host, or the versatility of its dependency manager, PHP features everything you need to build and ship powerful CLI applications.&lt;/p&gt;

&lt;p&gt;This tutorial will walk you through the process of creating a simple game running in the terminal, using &lt;a href="https://symfony.com/doc/current/components/console.html" rel="noopener noreferrer"&gt;Symfony's Console Component&lt;/a&gt; as a bedrock, &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; for compatibility checking, and &lt;a href="https://getcomposer.org" rel="noopener noreferrer"&gt;Composer&lt;/a&gt; for distribution.&lt;/p&gt;

&lt;p&gt;We won't use specialised frameworks like &lt;a href="https://github.com/minicli/minicli" rel="noopener noreferrer"&gt;Minicli&lt;/a&gt; or &lt;a href="https://laravel-zero.com" rel="noopener noreferrer"&gt;Laravel Zero&lt;/a&gt; because the goal is not so much to focus on features but to better understand the development, testing, and distribution phases of command-line programs.&lt;/p&gt;

&lt;p&gt;You will find that you can build a lot of things on top of such a simple foundation, but you can always check out these frameworks later on, should you need extra capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  In this post
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Repository and core dependencies&lt;/li&gt;
&lt;li&gt;Entry point&lt;/li&gt;
&lt;li&gt;Command&lt;/li&gt;
&lt;li&gt;Styling the output&lt;/li&gt;
&lt;li&gt;Distribution test&lt;/li&gt;
&lt;li&gt;PHP version support&lt;/li&gt;
&lt;li&gt;Release&lt;/li&gt;
&lt;li&gt;Cleaning up&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;Resources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To complete this tutorial, you will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://github.com" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; account (a free one will do)&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.php.net/manual/en/install.php" rel="noopener noreferrer"&gt;local PHP instal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://getcomposer.org/doc/00-intro.md" rel="noopener noreferrer"&gt;local Composer instal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A terminal to interact with the above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If at any time you feel lost, you can also refer to this article's &lt;a href="https://github.com/osteel/php-cli-demo" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repository and core dependencies
&lt;/h2&gt;

&lt;p&gt;The first thing we need to do is create a GitHub repository named &lt;code&gt;php-cli-demo&lt;/code&gt; (or anything you like, but the rest of this guide will refer to this name so it might be simpler to stick to it).&lt;/p&gt;

&lt;p&gt;You can make the repository public straight away if you want, but I like to do so only when the code is pretty much ready. No matter what you choose, I will let you know when is a good time to make it public.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rfhpb742cxyg81sfdoc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rfhpb742cxyg81sfdoc.png" alt="Create a new repository"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, open a terminal and clone the project locally, and enter its root directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git clone git@github.com:&amp;lt;YOUR GITHUB USERNAME&amp;gt;/php-cli-demo.git
$ cd php-cli-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run the following command to create a &lt;code&gt;composer.json&lt;/code&gt; file interactively:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It will ask you a series of questions – here is some guidance on how to answer them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Package name (&amp;lt;vendor&amp;gt;/&amp;lt;name&amp;gt;)&lt;/code&gt;: I distribute all my packages under the vendor name &lt;code&gt;osteel&lt;/code&gt;, so in my case, it's &lt;code&gt;osteel/php-cli-demo&lt;/code&gt;. If you're not sure, you can just pick your GitHub username&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Description&lt;/code&gt;: shown as optional but will be required for distribution later on. You can set "PHP command-line tool demo"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Author&lt;/code&gt;: make sure you set values that you are comfortable sharing publicly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Minimum stability&lt;/code&gt;: go for &lt;code&gt;stable&lt;/code&gt;. You may sometimes need to pick &lt;code&gt;dev&lt;/code&gt; instead if some of your tool's dependencies aren't marked as &lt;code&gt;stable&lt;/code&gt; (see &lt;a href="https://getcomposer.org/doc/04-schema.md#minimum-stability" rel="noopener noreferrer"&gt;here&lt;/a&gt; for details). That won't be the case in this tutorial though&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Package type&lt;/code&gt;: choose &lt;code&gt;library&lt;/code&gt; (see &lt;a href="https://getcomposer.org/doc/04-schema.md#type" rel="noopener noreferrer"&gt;here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;License&lt;/code&gt;: this is up to you, but since the code will be distributed and available publicly, it has to be open source. &lt;a href="https://choosealicense.com/licenses/mit" rel="noopener noreferrer"&gt;MIT&lt;/a&gt; is fine in most cases, but if you need something more specific &lt;a href="https://choosealicense.com" rel="noopener noreferrer"&gt;this guide&lt;/a&gt; will help. Make sure to set a value that is &lt;a href="https://getcomposer.org/doc/04-schema.md#license" rel="noopener noreferrer"&gt;recognised by Composer&lt;/a&gt;, too. For this article, &lt;code&gt;MIT&lt;/code&gt; will do&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will set our dependencies manually, so answer &lt;code&gt;no&lt;/code&gt; when asked about defining them interactively. Keep the default value (&lt;code&gt;src/&lt;/code&gt;) for PSR-4 autoload mapping.&lt;/p&gt;

&lt;p&gt;The last question is about adding the &lt;code&gt;vendor&lt;/code&gt; directory to the &lt;code&gt;.gitignore&lt;/code&gt; file, to which you should reply &lt;code&gt;yes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That should be it for &lt;code&gt;composer init&lt;/code&gt; – it will generate a &lt;code&gt;composer.json&lt;/code&gt; file at the root of your project based on the values you've selected.&lt;/p&gt;

&lt;p&gt;Before we instal any dependency, let's create a &lt;code&gt;LICENSE&lt;/code&gt; file at the top of our project and paste the content of the chosen license in it (&lt;a href="https://choosealicense.com/licenses/mit" rel="noopener noreferrer"&gt;here's the MIT one&lt;/a&gt; – you will find the others &lt;a href="https://choosealicense.com" rel="noopener noreferrer"&gt;here&lt;/a&gt;). Make sure to edit the placeholders for the name and year.&lt;/p&gt;

&lt;p&gt;Let's now require &lt;a href="https://symfony.com/doc/current/components/console.html" rel="noopener noreferrer"&gt;Symfony's Console Component&lt;/a&gt; as a dependency:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer require symfony/console
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpoqbgqfawypmr11u1uv8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpoqbgqfawypmr11u1uv8.png" alt="composer require symfony/console"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This component is used as a foundation for many frameworks' command-line features such as Laravel's &lt;a href="https://laravel.com/docs/artisan" rel="noopener noreferrer"&gt;Artisan&lt;/a&gt;. It's a robust and battle-tested set of classes for console applications and is pretty much a standard at this stage, so it makes sense to use it as a starting point instead of reinventing the wheel.&lt;/p&gt;

&lt;p&gt;Once the script is done, you should see the dependency listed in the &lt;code&gt;require&lt;/code&gt; section of &lt;code&gt;composer.json&lt;/code&gt; (your version might be different based on when you're completing this tutorial):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"require": {
    "symfony/console": "^6.0"
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Entry point
&lt;/h2&gt;

&lt;p&gt;Console programs, like web applications, need an entry point through which all "requests" come through. In other words, we need an &lt;code&gt;index.php&lt;/code&gt; for our commands.&lt;/p&gt;

&lt;p&gt;To get started, we will reuse the example given in the Console Component's &lt;a href="https://symfony.com/doc/current/components/console.html#creating-a-console-application" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. Create a new folder named &lt;code&gt;bin&lt;/code&gt; at the root of the project and add a &lt;code&gt;demo&lt;/code&gt; file to it, with the following content:&lt;/p&gt;


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



&lt;p&gt;The &lt;code&gt;demo&lt;/code&gt; file will be the entry point of our program, allowing us to call it from a terminal using commands like this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ demo play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you already have a local application named &lt;code&gt;demo&lt;/code&gt;, give it a different name (if you're not sure, run &lt;code&gt;which demo&lt;/code&gt;  and see if it returns a path to an existing application).&lt;/p&gt;

&lt;p&gt;Back to our &lt;code&gt;demo&lt;/code&gt; file – notice the first line:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/usr/bin/env php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is a way for the system to know which executable it should use to run the script, saving us from having to explicitly call it.&lt;/p&gt;

&lt;p&gt;In other words, that's what will allow us to run this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ demo play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Instead of this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ php demo play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;#!/usr/bin/env&lt;/code&gt; means the system will use the first &lt;code&gt;php&lt;/code&gt; executable found in the user's &lt;code&gt;PATH&lt;/code&gt;, regardless of its actual location (see &lt;a href="https://unix.stackexchange.com/questions/29608/why-is-it-better-to-use-usr-bin-env-name-instead-of-path-to-name-as-my" rel="noopener noreferrer"&gt;here&lt;/a&gt; for details).&lt;/p&gt;

&lt;p&gt;Speaking of execution, this will only work if &lt;code&gt;demo&lt;/code&gt; is executable. Let's make sure that is the case:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ chmod +x bin/demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now run the following commands:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ php bin/demo
$ ./bin/demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Both should display the default console menu:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ka0cij5kaei7fteavtc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ka0cij5kaei7fteavtc.png" alt="Default help menu"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Command
&lt;/h2&gt;

&lt;p&gt;Now that the base structure of our application is in place, we can focus on the command that will contain the core of our program. What we're building is a simple game of calculation, giving the user basic sums to solve and returning whether the answer is correct or not.&lt;/p&gt;

&lt;p&gt;Once again, the &lt;a href="https://symfony.com/doc/current/console.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; provides some guidance on how to write commands, but for the sake of simplicity, I will give you the code straight away, followed by a breakdown.&lt;/p&gt;

&lt;p&gt;Create some new folders &lt;code&gt;src&lt;/code&gt; and &lt;code&gt;src/Commands&lt;/code&gt; at the root of the project and add the following &lt;code&gt;Play.php&lt;/code&gt; file to the latter:&lt;/p&gt;


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



&lt;p&gt;Make sure to change the namespace at the top to reflect your vendor name.&lt;/p&gt;

&lt;p&gt;Our class extends Symfony Console's &lt;a href="https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/Console/Command/Command.php" rel="noopener noreferrer"&gt;&lt;code&gt;Command&lt;/code&gt; class&lt;/a&gt; and overwrites some of its static properties. &lt;code&gt;$defaultName&lt;/code&gt; and &lt;code&gt;$defaultDescription&lt;/code&gt; are the command's name and description, which will both appear in the application's menu. The former will also allow us to invoke the command in the terminal later on.&lt;/p&gt;

&lt;p&gt;Next comes the &lt;code&gt;execute&lt;/code&gt; method, that the parent class requires us to implement. It contains the actual logic for the game and receives an instance of the console's input and output.&lt;/p&gt;

&lt;p&gt;There are several ways to interact with those and I admittedly find the official documentation a bit confusing on the topic. My preferred approach is to rely on the &lt;a href="https://symfony.com/doc/current/console/style.html" rel="noopener noreferrer"&gt;&lt;code&gt;SymfonyStyle&lt;/code&gt; class&lt;/a&gt; because it abstracts away most typical console interactions and I like the default style it gives them.&lt;/p&gt;

&lt;p&gt;Let's take a closer look at the body of the &lt;code&gt;execute&lt;/code&gt; method:&lt;/p&gt;


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


&lt;p&gt;First, we randomly generate the two terms of the sum and calculate the result in advance. We instantiate &lt;code&gt;SymfonyStyle&lt;/code&gt;, pass the console's input and output to it and use it to display the question and request an answer (with the &lt;code&gt;ask&lt;/code&gt; method).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If you're unfamiliar with &lt;code&gt;sprintf&lt;/code&gt;, it's a &lt;a href="https://www.php.net/manual/en/function.sprintf" rel="noopener noreferrer"&gt;PHP function&lt;/a&gt; allowing developers to format strings in a way that I find easier to read than the standard concatenation operator (&lt;code&gt;.&lt;/code&gt;). Feel free to use the latter instead, though.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We collect and save the answer in the &lt;code&gt;$answer&lt;/code&gt; variable, cast it to an integer (inputs are strings by default), and compare it to the expected result. If the answer is correct, we display a success message; if not, we display an error.&lt;/p&gt;

&lt;p&gt;We then ask the user if she wants to play another round, this time using the &lt;code&gt;confirm&lt;/code&gt; method since we need a boolean answer ("yes" or "no").&lt;/p&gt;

&lt;p&gt;If the answer is "yes", we call the &lt;code&gt;execute&lt;/code&gt; method recursively, essentially starting the game over. If the answer is "no", we exit the command by returning &lt;code&gt;Command::SUCCESS&lt;/code&gt; (a constant containing the integer &lt;code&gt;0&lt;/code&gt;, which is a code signalling a successful execution).&lt;/p&gt;

&lt;p&gt;The returned value is a way to tell the console whether the program executed correctly or not, and has nothing to do with the answer being right or wrong. These exit codes are &lt;a href="https://tldp.org/LDP/abs/html/exitcodes.html" rel="noopener noreferrer"&gt;standardised&lt;/a&gt; and anything other than &lt;code&gt;0&lt;/code&gt; usually means "failure". You can see &lt;a href="https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/Console/Command/Command.php#L36" rel="noopener noreferrer"&gt;here&lt;/a&gt; which exit codes the &lt;code&gt;Command&lt;/code&gt; class defines by default and come up with your own if you need to.&lt;/p&gt;

&lt;p&gt;The above breakdown is more than enough for this article, but feel free to check out the &lt;a href="https://symfony.com/doc/current/console.html" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; for details and advanced command usage.&lt;/p&gt;

&lt;p&gt;We now need to register our command to make it available from the outside. Update the content of the &lt;code&gt;bin/demo&lt;/code&gt; file to match the following:&lt;/p&gt;


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


&lt;p&gt;The only thing we've changed is we now pass an instance of our command to the application before running it (again, don't forget to update the namespace of the imported class).&lt;/p&gt;

&lt;p&gt;Go back to your terminal and run this from the project's root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./bin/demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The help menu should display again, only this time the &lt;code&gt;play&lt;/code&gt; command should be part of it, alongside its description:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb2z2ekfgiqvthsb6jxbf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb2z2ekfgiqvthsb6jxbf.png" alt="Help menu with the command"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We should now be able to play the game, so let's give it a try:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./bin/demo play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Make sure it behaves as you would expect – if it doesn't, go back a few steps or take a look at the tutorial's &lt;a href="https://github.com/osteel/php-cli-demo" rel="noopener noreferrer"&gt;repository&lt;/a&gt; to check if you missed something.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxcwmctbzh0l45grtd1s1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxcwmctbzh0l45grtd1s1.png" alt="Testing the game"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;As our application will expose a single command &lt;code&gt;play&lt;/code&gt;, we could also have made it the &lt;a href="https://symfony.com/doc/current/components/console/changing_default_command.html" rel="noopener noreferrer"&gt;default one&lt;/a&gt;. Doing so would have allowed us to start the game by running &lt;code&gt;demo&lt;/code&gt; instead of &lt;code&gt;demo play&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Styling the output
&lt;/h2&gt;

&lt;p&gt;The defaults provided by the &lt;code&gt;SymfonyStyle&lt;/code&gt; class are quite good already, but these &lt;code&gt;[OK]&lt;/code&gt; and &lt;code&gt;[ERROR]&lt;/code&gt; messages are a bit dry and not very suitable for a game.&lt;/p&gt;

&lt;p&gt;To make them a bit more user-friendly, let's create a new &lt;code&gt;Services&lt;/code&gt; folder in &lt;code&gt;src&lt;/code&gt;, and add the following &lt;code&gt;InputOutput.php&lt;/code&gt; file to it:&lt;/p&gt;


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



&lt;p&gt;Once again, make sure to change the vendor namespace at the top.&lt;/p&gt;

&lt;p&gt;The purpose of this class is to extend &lt;code&gt;SymfonyStyle&lt;/code&gt; to adapt its behaviour to our needs.&lt;/p&gt;

&lt;p&gt;The program interacts with the player in three ways – it asks questions and indicates whether an answer is right or wrong.&lt;/p&gt;

&lt;p&gt;I've reflected this in the &lt;code&gt;InputOutput&lt;/code&gt; class above by creating three public methods – &lt;code&gt;question&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, and &lt;code&gt;wrong&lt;/code&gt;. &lt;code&gt;question&lt;/code&gt; simply calls the parent class' &lt;code&gt;ask&lt;/code&gt; method and prepends the question with the ✍️ emoji.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;right&lt;/code&gt; and &lt;code&gt;wrong&lt;/code&gt;, I drew inspiration from the parent class' &lt;a href="https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/Console/Style/SymfonyStyle.php#L135" rel="noopener noreferrer"&gt;&lt;code&gt;success&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/Console/Style/SymfonyStyle.php#L143" rel="noopener noreferrer"&gt;&lt;code&gt;error&lt;/code&gt;&lt;/a&gt; methods and adapted their content to replace the default messages with more convivial emojis.&lt;/p&gt;

&lt;p&gt;And that's all there is to it – the only thing left is to adapt the &lt;code&gt;Play&lt;/code&gt; command to use our new service instead of &lt;code&gt;SymfonyStyle&lt;/code&gt;:&lt;/p&gt;


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


&lt;p&gt;As usual, pay attention to namespaces.&lt;/p&gt;

&lt;p&gt;Let's play the game again and appreciate the new look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./bin/demo play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjj3y1kubye090wsrjcul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjj3y1kubye090wsrjcul.png" alt="New look"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, you might be wondering why I created new methods instead of extending the &lt;code&gt;ask&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; methods. The quick answer is that doing so increases compatibility across library versions.&lt;/p&gt;

&lt;p&gt;Extending a method means the signature of the child method must be identical to that of the parent (with some &lt;a href="https://www.npopov.com/2021/11/08/Type-variance-in-PHP.html" rel="noopener noreferrer"&gt;subtleties&lt;/a&gt;), which is fine as long as the parent class doesn't change. For the sake of portability, however, we want our application to be compatible with various PHP and dependency versions, and in the case of the Console Component, method signatures vary from one major version to another.&lt;/p&gt;

&lt;p&gt;In short, creating new methods instead of extending existing ones allows us to avoid errors related to signature mismatch across versions.&lt;/p&gt;

&lt;p&gt;The above is a quick example of how you can customise the style of console outputs. It also gives you an idea of how you can structure your application, with the introduction of the &lt;code&gt;Services&lt;/code&gt; folder where you can put utility classes to decouple your code.&lt;/p&gt;

&lt;p&gt;Concerning styling, if you need to push the appearance of your console program a bit further, you might want to take a look at &lt;a href="https://github.com/nunomaduro/termwind" rel="noopener noreferrer"&gt;Termwind&lt;/a&gt;, a promising new framework described as "Tailwind CSS, but for PHP command-line applications".&lt;/p&gt;
&lt;h2&gt;
  
  
  Distribution test
&lt;/h2&gt;

&lt;p&gt;Let's now take a few steps towards the distribution of our application. First, we need to tweak our &lt;code&gt;demo&lt;/code&gt; script:&lt;/p&gt;


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



&lt;p&gt;The bit we've updated is the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$root = dirname(__DIR__);

if (! is_file(sprintf('%s/vendor/autoload.php', $root))) {
    $root = dirname(__DIR__, 4);
}

require sprintf('%s/vendor/autoload.php', $root);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We're now looking for the &lt;code&gt;autoload.php&lt;/code&gt; file in two different places. Why? Because this file will be located in different folders based on whether we're in development mode or distribution mode.&lt;/p&gt;

&lt;p&gt;So far we've used our application in the former mode only, so we knew exactly where the &lt;code&gt;vendor&lt;/code&gt; folder was – at the project's root. But once it'll be distributed (installed globally on a user's system), that folder will be located a few levels above the current one.&lt;/p&gt;

&lt;p&gt;Here's for instance the path to the application's &lt;code&gt;bin&lt;/code&gt; folder on my system, once I've installed it globally:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.composer/vendor/osteel/php-cli-demo/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And here is the path to the global &lt;code&gt;autoload.php&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.composer/vendor/autoload.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The path to &lt;code&gt;/vendor/autoload.php&lt;/code&gt; is then four levels above &lt;code&gt;bin&lt;/code&gt;, hence the use of &lt;code&gt;dirname(__DIR__, 4)&lt;/code&gt; (this &lt;a href="https://www.php.net/manual/en/function.dirname" rel="noopener noreferrer"&gt;function&lt;/a&gt; returns the path of the given directory's parent, which here is the parent of the current directory &lt;code&gt;__DIR__&lt;/code&gt;. The second parameter is the number of parent directories to go up).&lt;/p&gt;

&lt;p&gt;Save the file and make sure you can still access the application by displaying the menu:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./bin/demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://getcomposer.org/doc/articles/vendor-binaries.md#finding-the-composer-autoloader-from-a-binary" rel="noopener noreferrer"&gt;Since Composer 2.2&lt;/a&gt;, a &lt;code&gt;$_composer_autoload_path&lt;/code&gt; global variable containing the path to the autoloader is available when running the program globally. This variable would allow us to simplify the above script, but also means we'd have to enforce the use of Composer 2.2 as a minimum requirement, potentially impacting compatibility.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another thing we need to do is add &lt;code&gt;composer.lock&lt;/code&gt; to the &lt;code&gt;.gitignore&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/vendor/
composer.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you come from regular PHP application development, you may be used to &lt;a href="https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control" rel="noopener noreferrer"&gt;committing this file&lt;/a&gt; to freeze dependency versions (also called &lt;em&gt;dependency pinning&lt;/em&gt;), thus guaranteeing consistent behaviour across environments. When it comes to command-line tools, however, things are different.&lt;/p&gt;

&lt;p&gt;As mentioned earlier, your CLI application should ideally work with various PHP and dependency versions to maximise compatibility. This flexibility means you can't dictate which versions your users should instal, and instead let client environments figure out the dependency tree for themselves.&lt;/p&gt;

&lt;p&gt;Note that you &lt;a href="https://getcomposer.org/doc/02-libraries.md#lock-file" rel="noopener noreferrer"&gt;&lt;em&gt;can&lt;/em&gt;&lt;/a&gt; commit this file – it will be ignored when your application is installed via Composer. But in practice, explicitly ignoring &lt;code&gt;composer.lock&lt;/code&gt; best reflects real usage conditions, and forces you (and your team) to consider version variation early on.&lt;/p&gt;

&lt;p&gt;By the way, if you'd like to know more about when to and when not to commit &lt;code&gt;composer.lock&lt;/code&gt;, I published a Twitter thread on the subject:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1465285217892114443-192" src="https://platform.twitter.com/embed/Tweet.html?id=1465285217892114443"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1465285217892114443-192');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1465285217892114443&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;Since we don't enforce specific dependency versions anymore, we need to define the range of versions our application will support. Open &lt;code&gt;composer.json&lt;/code&gt; and update the &lt;code&gt;require&lt;/code&gt; section as follows:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"require": {
    "php": "^8.0",
    "symfony/console": "^5.0|^6.0"
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We need to specify the PHP version because down the line we won't know which versions our users are running. By making it explicit, Composer will then be able to check whether a compatible version of PHP is available and refuse to proceed with the installation if that's not the case.&lt;/p&gt;

&lt;p&gt;Right now we're only interested in running a distribution test on our current machine though, so specifying your local PHP version is enough (adjust it if it's different from the above – if you're not sure, run &lt;code&gt;php -v&lt;/code&gt; from a terminal).&lt;/p&gt;

&lt;p&gt;You'll notice that I also added version &lt;code&gt;5.0&lt;/code&gt; of the Console Component – that's because while running my own tests, I experienced some compatibility issues with my local instance of Laravel Valet, which needed Console &lt;code&gt;5.0&lt;/code&gt; at the time.&lt;/p&gt;

&lt;p&gt;Based on when you're completing this tutorial, versions may have changed and this constraint may not be true anymore, but it's a good example of the kind of compatibility issues you may have to deal with.&lt;/p&gt;

&lt;p&gt;There's one last change we need to make to this file, and that is the addition of a &lt;code&gt;bin&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"require": {
    "php": "^8.0",
    "symfony/console": "^5.0|^6.0"
},
"bin": [
    "bin/demo"
],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is how you specify the &lt;a href="https://getcomposer.org/doc/articles/vendor-binaries.md" rel="noopener noreferrer"&gt;vendor binary&lt;/a&gt;, which is a way to tell Composer "this will be the application's entry point, please make it available in the user's &lt;code&gt;vendor/bin&lt;/code&gt; directory". By doing so, our users will be able to call our application globally.&lt;/p&gt;

&lt;p&gt;Commit and push the above changes. Then, if your repository was private so far, now is the time to make it public. Go to the &lt;em&gt;Settings&lt;/em&gt; tab, enter the &lt;em&gt;Options&lt;/em&gt; section, and scroll all the way down to the &lt;em&gt;Danger Zone&lt;/em&gt; (which always reminds me of &lt;a href="https://www.youtube.com/watch?v=k3-zaTr6OUo" rel="noopener noreferrer"&gt;Archer&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;There you can change the repository's visibility to &lt;em&gt;public&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fze9yvzy992rhz9ppooqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fze9yvzy992rhz9ppooqx.png" alt="Change repository visibility"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now head over to &lt;a href="https://packagist.org/" rel="noopener noreferrer"&gt;packagist.org&lt;/a&gt; and create an account (or sign in if you already have one).&lt;/p&gt;

&lt;p&gt;Once you're logged in, click the &lt;em&gt;Submit&lt;/em&gt; button in the top right corner and submit the URL of your repository. Allow a few moments for the crawler to do its job and your repository will appear:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vkhv78wzeavm0ela5lg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vkhv78wzeavm0ela5lg.png" alt="Crawled repository"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don’t worry too much about publishing a dummy package – the worst that can happen is that no one but yourself will instal it (and you will be able to delete it later anyway).&lt;/p&gt;

&lt;p&gt;Let's now try and instal it on your machine. Go to your terminal and run the following command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer global require osteel/php-cli-demo:dev-main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then again, make sure to set the right vendor name. Notice the presence of &lt;code&gt;:dev-main&lt;/code&gt; at the end – this is a way to target the &lt;code&gt;main&lt;/code&gt; branch specifically, as there is no release available yet.&lt;/p&gt;

&lt;p&gt;Once the instal is successful, make sure that the system recognises your application (you may need to open a new terminal window):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ which demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This command should return the path to your application's entry point (the &lt;code&gt;demo&lt;/code&gt; file). If it doesn't, make sure the &lt;code&gt;~/.composer/vendor/bin&lt;/code&gt; directory &lt;a href="https://blackdeerdev.com/how-to-add-composer-vendor-bin-directory-in-your-path" rel="noopener noreferrer"&gt;is in your system's &lt;code&gt;PATH&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Give the game a quick spin to ensure it behaves as expected:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ demo play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Our distribution test is successful!&lt;/p&gt;
&lt;h2&gt;
  
  
  PHP version support
&lt;/h2&gt;

&lt;p&gt;Before we carry on and publish a proper release, we need to tackle something we've now mentioned a few times – PHP version support.&lt;/p&gt;

&lt;p&gt;This will be a two-step process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a test that controls the correct end-to-end execution of our program&lt;/li&gt;
&lt;li&gt;Add a GitHub workflow that will run the test using various PHP versions&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Automated test
&lt;/h3&gt;

&lt;p&gt;We will use &lt;a href="https://phpunit.de/" rel="noopener noreferrer"&gt;PHPUnit&lt;/a&gt; as our testing environment – let's instal it as a development dependency:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer require --dev phpunit/phpunit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Create the folders &lt;code&gt;tests&lt;/code&gt; and &lt;code&gt;tests/Commands&lt;/code&gt; at the root of the project and add a &lt;code&gt;PlayTest.php&lt;/code&gt; file to the latter, with the following content:&lt;/p&gt;


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



&lt;p&gt;As usual, make sure to update the vendor in the namespace and &lt;code&gt;use&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;As per the &lt;a href="https://symfony.com/doc/current/console.html#testing-commands" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, the Console Component comes with a &lt;code&gt;CommandTester&lt;/code&gt; helper class that will simplify testing the &lt;code&gt;Play&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;We instantiate the latter and pass it to the tester, which allows us to &lt;a href="https://symfony.com/doc/current/components/console/helpers/questionhelper.html#testing-a-command-that-expects-input" rel="noopener noreferrer"&gt;simulate a series of inputs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The sequence is as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Answer &lt;code&gt;10&lt;/code&gt; to the first question&lt;/li&gt;
&lt;li&gt;Say &lt;code&gt;yes&lt;/code&gt; to playing another round&lt;/li&gt;
&lt;li&gt;Answer &lt;code&gt;10&lt;/code&gt; again&lt;/li&gt;
&lt;li&gt;Say &lt;code&gt;no&lt;/code&gt; to playing another round&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We then have the tester execute the command and check if it ran successfully using the &lt;code&gt;assertCommandIsSuccessful&lt;/code&gt; method (which controls that the command returned &lt;code&gt;0&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;And that's it. We're not testing the game's outputs – we're merely making sure that the program runs successfully given a series of inputs. In other words, we're ensuring that it doesn't crash.&lt;/p&gt;

&lt;p&gt;Feel free to add more tests to control the game's actual behaviour (e.g. whether it gives the correct answers), but for our immediate purpose, the above is all we need.&lt;/p&gt;

&lt;p&gt;Let's make sure our test is passing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./vendor/bin/phpunit tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotm80c21xpsb7m2fsxm0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotm80c21xpsb7m2fsxm0.png" alt="Test execution"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it doesn't, go back a few steps or take a look at the tutorial's &lt;a href="https://github.com/osteel/php-cli-demo" rel="noopener noreferrer"&gt;repository&lt;/a&gt; to check if you missed anything.&lt;/p&gt;
&lt;h3&gt;
  
  
  GitHub workflow
&lt;/h3&gt;

&lt;p&gt;Now that we have a test controlling the correct execution of our program, we can use it to ensure the latter's compatibility with a range of PHP versions. Doing so manually would be a bit of a pain, but we can automate the process using &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;GitHub Actions allow us to write workflows triggering on selected events, such as the push of a commit or the opening of a pull request. They're available with any GitHub account, including the free ones (they come with a &lt;a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions" rel="noopener noreferrer"&gt;monthly allowance&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Workflows are in YAML format and must be placed in a &lt;code&gt;.github/workflows&lt;/code&gt; folder at the root.&lt;/p&gt;

&lt;p&gt;Create this folder and add the following &lt;code&gt;ci.yml&lt;/code&gt; file to it:&lt;/p&gt;


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



&lt;p&gt;At the very top of the workflow is its name ("CI" stands for &lt;a href="https://www.martinfowler.com/articles/continuousIntegration.html" rel="noopener noreferrer"&gt;Continuous Integration&lt;/a&gt;), followed by the events on which it should trigger, in the &lt;code&gt;on&lt;/code&gt; section. Ours will go off anytime some code is pushed to the &lt;code&gt;main&lt;/code&gt; branch, or whenever a pull request is open towards it.&lt;/p&gt;

&lt;p&gt;We then define jobs to be executed, which in our case is just one – running the test. We specify that we'll use Ubuntu as the environment, and then define the execution &lt;a href="https://docs.github.com/en/actions/learn-github-actions/contexts#strategy-context" rel="noopener noreferrer"&gt;strategy&lt;/a&gt; to apply. Here, we'll be using a &lt;a href="https://docs.github.com/en/actions/using-jobs/using-a-build-matrix-for-your-jobs" rel="noopener noreferrer"&gt;matrix&lt;/a&gt; of PHP versions, each of which will run our test in sequence (the listed versions are the &lt;a href="https://www.php.net/supported-versions.php" rel="noopener noreferrer"&gt;supported ones&lt;/a&gt; at the time of writing).&lt;/p&gt;

&lt;p&gt;Finally, we define the steps of our job, that we break down as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check out the code&lt;/li&gt;
&lt;li&gt;Set up PHP using the current version&lt;/li&gt;
&lt;li&gt;Instal Composer dependencies&lt;/li&gt;
&lt;li&gt;Run the test&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We now need to list the same PHP versions in our application's &lt;code&gt;composer.json&lt;/code&gt; so Composer won't refuse to instal it for some of them. Let's update the &lt;code&gt;require&lt;/code&gt; section again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"require": {
    "php": "^7.4|^8.0",
    "symfony/console": "^5.0|^6.0"
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you're wondering why I didn't list &lt;code&gt;8.1&lt;/code&gt;, that's because as per the rules of &lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;Semantic Versioning&lt;/a&gt; (&lt;em&gt;semver&lt;/em&gt;), &lt;code&gt;^8.0&lt;/code&gt; covers all versions above or equal to &lt;code&gt;8.0&lt;/code&gt; but strictly under &lt;code&gt;9.0&lt;/code&gt;, which includes &lt;code&gt;8.1&lt;/code&gt;. If you're like me and tend to forget the rules of semver when you need them, keep this &lt;a href="https://devhints.io/semver" rel="noopener noreferrer"&gt;cheatsheet&lt;/a&gt; handy and thank me later.&lt;/p&gt;

&lt;p&gt;Save, commit and push the above – if it went well, you should see the workflow running in the &lt;em&gt;Actions&lt;/em&gt; tab of your GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F821ooaxkdsxusnzcfg0q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F821ooaxkdsxusnzcfg0q.png" alt="Workflow execution"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then again, refer to the article's &lt;a href="https://github.com/osteel/php-cli-demo" rel="noopener noreferrer"&gt;repository&lt;/a&gt; if something goes wrong.&lt;/p&gt;

&lt;p&gt;This workflow gives us the confidence that our application will run successfully with any of the listed PHP versions, and will confirm whether this is still the case any time we push some changes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The above is a simplified workflow that doesn't quite cover all dependency versions. We can push the concept of matrix even further, but to avoid making this post longer than it already is, I've published a &lt;a href="https://dev.to/osteel/a-github-workflow-to-check-the-compatibility-of-your-php-package-with-a-range-of-dependency-versions-11ab"&gt;separate blog post&lt;/a&gt; dedicated to the topic.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Release
&lt;/h2&gt;

&lt;p&gt;We now have a tested application that is compatible with a range of PHP versions and an early version of which we managed to instal on our machine. We're ready to publish an official release.&lt;/p&gt;

&lt;p&gt;But before we do that, we need to ensure our future users will know how to instal and use our application – that's what &lt;code&gt;README.md&lt;/code&gt; files are for.&lt;/p&gt;

&lt;p&gt;Let's create one at the root of the project (click "view raw" at the bottom to see the source code):&lt;/p&gt;


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



&lt;p&gt;This is a basic template mixing some HTML and markdown code. I won't spend too much time explaining it, so here is a quick summary of the content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title and subtitle&lt;/li&gt;
&lt;li&gt;Preview image&lt;/li&gt;
&lt;li&gt;Some badges&lt;/li&gt;
&lt;li&gt;Installation instructions&lt;/li&gt;
&lt;li&gt;Usage instructions&lt;/li&gt;
&lt;li&gt;Update instructions&lt;/li&gt;
&lt;li&gt;Deletion instructions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll need to replace instances of &lt;code&gt;osteel&lt;/code&gt; with your vendor name again (and maybe some other things like the application name or entry point if you chose something other than &lt;code&gt;demo&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;For the preview image I like to take a simple screenshot of the application being used:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivvfite17i0xnhxb7thn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivvfite17i0xnhxb7thn.png" alt="Screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that this is also the image I used to illustrate this post.&lt;/p&gt;

&lt;p&gt;Create an &lt;code&gt;art&lt;/code&gt; folder at the root of the project and place a &lt;code&gt;preview.png&lt;/code&gt; image in it (update the code accordingly if you pick a different name).&lt;/p&gt;

&lt;p&gt;You can also use this picture for social media by setting it in the &lt;em&gt;Social preview&lt;/em&gt; section of the repository's &lt;em&gt;Settings&lt;/em&gt; tab.&lt;/p&gt;

&lt;p&gt;That's pretty much it – feel free to reuse and adapt this template as you please.&lt;/p&gt;

&lt;p&gt;Save and push &lt;code&gt;README.md&lt;/code&gt;, and head over to the repository's &lt;em&gt;Releases&lt;/em&gt; section, available from the column on the right-hand side:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwf18hlqxrkz5r4exsh6r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwf18hlqxrkz5r4exsh6r.png" alt="Releases"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create a tag for your release, which should follow the rules of &lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;Semantic Versioning&lt;/a&gt;. We'll make ours version 1:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cslnilf5bfbyic86or9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cslnilf5bfbyic86or9.png" alt="Release tag v1.0"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give your release a name and a description and publish it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhik9eadtvxvdo60gauz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhik9eadtvxvdo60gauz.png" alt="Release name and description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a few moments, you should see it appear on the Packagist page of your repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fknk72rimxpat0mtify9d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fknk72rimxpat0mtify9d.png" alt="Release on Packagist"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now go back to your terminal and run the following command (using the appropriate vendor name):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer global require osteel/php-cli-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that a proper release is available, there's no need to append &lt;code&gt;:dev-main&lt;/code&gt; anymore.&lt;/p&gt;

&lt;p&gt;Make sure you can still play the game:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ demo play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! Your application is now ready to be advertised and distributed.&lt;/p&gt;

&lt;p&gt;You can instal future minor releases by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer global update osteel/php-cli-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And major ones with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer global require osteel/php-cli-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cleaning up
&lt;/h2&gt;

&lt;p&gt;This tutorial is drawing to an end and if after playing with your application you feel like cleaning up after yourself, you can delete your package from Packagist:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvsxciiu53vce3tt7e58w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvsxciiu53vce3tt7e58w.png" alt="Packagist options"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this is how you'd remove it from your local system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer global remove osteel/php-cli-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, you can also delete your GitHub repository at any time.&lt;/p&gt;

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

&lt;p&gt;I hope this will inspire you to write your own command-line tools in PHP.&lt;/p&gt;

&lt;p&gt;While the language's primary application remains for web development, the above should show that it's versatile enough to assist you in other ways.&lt;/p&gt;

&lt;p&gt;Let's be honest – if your goal is the widespread use of a standalone CLI application, PHP is probably not the best language. Users would still need an environment that supports it, a local instal of Composer, and also to edit their local &lt;code&gt;PATH&lt;/code&gt; to include Composer's &lt;code&gt;bin&lt;/code&gt; directory. These are all steps that most people won't be comfortable taking.&lt;/p&gt;

&lt;p&gt;But if you're already a PHP developer, your environment is very likely to satisfy these conditions already, and as a result, the barrier to building your own CLI tools is very low.&lt;/p&gt;

&lt;p&gt;Think of repetitive tasks that you perform regularly, and that could be automated away without the need of a full-blown website. I, for instance, grew tired of having to manually import my Kobo annotations to &lt;a href="https://readwise.io" rel="noopener noreferrer"&gt;Readwise&lt;/a&gt;, so came up with a &lt;a href="https://github.com/osteel/kobwise" rel="noopener noreferrer"&gt;tool&lt;/a&gt; to make the conversion easier.&lt;/p&gt;

&lt;p&gt;PHP has no issue running &lt;a href="https://www.php.net/manual/en/function.system.php" rel="noopener noreferrer"&gt;system commands&lt;/a&gt; and if you need more muscle than what the Console Component alone has to offer, frameworks like &lt;a href="https://laravel-zero.com" rel="noopener noreferrer"&gt;Laravel Zero&lt;/a&gt; will greatly expand the realm of possibility.&lt;/p&gt;

&lt;p&gt;You don't necessarily have to distribute your programs via Packagist for a strictly personal use, but it's a way to share your work with others who might have similar needs and a potential foray into open-source development.&lt;/p&gt;

&lt;p&gt;Besides, the process of writing and distributing your own CLI tools is not so dissimilar to that of application libraries – once you're comfortable with the former, the latter becomes more accessible, and both are ways to give back to the community.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;Here are some of the resources referenced in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/osteel/php-cli-demo" rel="noopener noreferrer"&gt;Article repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getcomposer.org/doc/00-intro.md" rel="noopener noreferrer"&gt;Composer documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://symfony.com/doc/current/components/console.html" rel="noopener noreferrer"&gt;Symfony's Console Component documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://choosealicense.com" rel="noopener noreferrer"&gt;Choose an open source license&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unix.stackexchange.com/questions/29608/why-is-it-better-to-use-usr-bin-env-name-instead-of-path-to-name-as-my" rel="noopener noreferrer"&gt;Why is it better to use "#!/usr/bin/env NAME" instead of "#!/path/to/NAME" as my shebang?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tldp.org/LDP/abs/html/exitcodes.html" rel="noopener noreferrer"&gt;Appendix E. Exit Codes With Special Meanings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npopov.com/2021/11/08/Type-variance-in-PHP.html" rel="noopener noreferrer"&gt;Type variance in PHP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/function.dirname" rel="noopener noreferrer"&gt;PHP's dirname function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/supported-versions.php" rel="noopener noreferrer"&gt;PHP Supported Versions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://phpunit.de/" rel="noopener noreferrer"&gt;PHPUnit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.martinfowler.com/articles/continuousIntegration.html" rel="noopener noreferrer"&gt;Continuous Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://packagist.org/" rel="noopener noreferrer"&gt;Packagist.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;Semantic Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devhints.io/semver" rel="noopener noreferrer"&gt;Semantic Versioning Cheatsheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blackdeerdev.com/how-to-add-composer-vendor-bin-directory-in-your-path" rel="noopener noreferrer"&gt;How to add composer vendor bin directory to path&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/minicli/minicli" rel="noopener noreferrer"&gt;Minicli&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://laravel-zero.com" rel="noopener noreferrer"&gt;Laravel Zero&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nunomaduro/termwind" rel="noopener noreferrer"&gt;Termwind&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>composer</category>
      <category>cli</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>How to Build a Dynamic GitHub Profile with GitHub Actions and PHP</title>
      <dc:creator>Yannick Chenot</dc:creator>
      <pubDate>Sun, 06 Feb 2022 17:59:32 +0000</pubDate>
      <link>https://dev.to/osteel/how-to-build-a-dynamic-github-profile-with-github-actions-and-php-81m</link>
      <guid>https://dev.to/osteel/how-to-build-a-dynamic-github-profile-with-github-actions-and-php-81m</guid>
      <description>&lt;p&gt;In the summer of 2020, GitHub quietly released a feature that was quickly noticed by the community – profile READMEs. A &lt;a href="https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme"&gt;profile README&lt;/a&gt; is a global &lt;code&gt;README&lt;/code&gt; file for your GitHub profile, which you can set up by creating a public repository whose name is identical to your GitHub username. For instance, as my username is &lt;code&gt;osteel&lt;/code&gt;, I created the &lt;code&gt;osteel/osteel&lt;/code&gt; &lt;a href="https://github.com/osteel/osteel"&gt;repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A little box like this one should appear while you add your own:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2fWsjPA6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ptotj3km06fe5xng35iw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2fWsjPA6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ptotj3km06fe5xng35iw.png" alt="Special repository" width="400" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the repository is created, add a &lt;code&gt;README&lt;/code&gt; file with a short description explaining how great you are, and your GitHub profile page will display its content by default:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_hslDCND--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r5z5jn5n9txt0btx5rab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_hslDCND--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r5z5jn5n9txt0btx5rab.png" alt="GitHub profile README" width="880" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Neat and simple.&lt;/p&gt;

&lt;p&gt;As I was &lt;a href="https://www.aboutmonica.com/blog/how-to-create-a-github-profile-readme"&gt;browsing examples&lt;/a&gt; for some inspiration, I stumbled upon Simon Willison's &lt;a href="https://github.com/simonw"&gt;version&lt;/a&gt;, which features some dynamic content like recent work and blog publications. He explained how he used a combination of &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt; and Python to achieve this in a &lt;a href="https://simonwillison.net/2020/Jul/10/self-updating-profile-readme/"&gt;blog post&lt;/a&gt;, and I decided to do something similar with PHP.&lt;/p&gt;

&lt;h2&gt;
  
  
  The placeholder
&lt;/h2&gt;

&lt;p&gt;The first thing to do is to create a placeholder in the &lt;code&gt;README&lt;/code&gt; file where the dynamic content will go. Since I wanted to automatically insert the latest publications of my blog, I used the following tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- posts --&amp;gt;&amp;lt;!-- /posts --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might recognise this format; since Markdown files also support HTML, I used some HTML comment tags to make sure they wouldn't show up on my profile page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PHP script
&lt;/h2&gt;

&lt;p&gt;I can't remember the last time I wrote some PHP without a framework; as a result, I had to do a &lt;a href="https://www.frobiovox.com/posts/2016/08/16/basic-hello-world-with-composer-and-php.html"&gt;quick search&lt;/a&gt; just to get started with a basic PHP script and some Composer dependencies.&lt;/p&gt;

&lt;p&gt;Turn out it's quite simple! The first step is to initialise the project with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, I installed &lt;a href="https://github.com/dg/rss-php"&gt;a lightweight library&lt;/a&gt; to parse my blog's &lt;a href="https://tech.osteel.me/feeds/rss.xml"&gt;RSS feed&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ composer require dg/rss-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then added a &lt;code&gt;posts.php&lt;/code&gt; file at the root of the project, with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

// Load Composer's autoload
require_once __DIR__ . '/vendor/autoload.php';

// Load the RSS feed
$feed = Feed::loadRss('https://tech.osteel.me/feeds/rss.xml')-&amp;gt;toArray();

// Generate the list of blog posts
$posts = '';
foreach (array_slice($feed['item'], 0, 5) as $post) {
    $date   = date('d/m/Y', strtotime($post['pubDate']));
    $posts .= sprintf("\n* **[%s]** [%s](%s \"%s\")", $date, $post['title'], $post['link'], $post['title']);
}

// Generate the new content
$content = preg_replace(
    '#&amp;lt;!-- posts --&amp;gt;.*&amp;lt;!-- /posts --&amp;gt;#s',
    sprintf('&amp;lt;!-- posts --&amp;gt;%s&amp;lt;!-- /posts --&amp;gt;', $posts),
    file_get_contents('README.md')
);

// Overwrite the file
file_put_contents('README.md', $content);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing too complicated here – Composer's &lt;em&gt;autoload&lt;/em&gt; is required at the top, allowing me to load the RSS parser to generate a list of blog posts as a string, in Markdown format.&lt;/p&gt;

&lt;p&gt;The existing content of the &lt;code&gt;README&lt;/code&gt; file is then loaded into the &lt;code&gt;$content&lt;/code&gt; variable, and the Markdown string is inserted between its &lt;code&gt;&amp;lt;!-- posts --&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;!-- /posts --&amp;gt;&lt;/code&gt; tags with &lt;code&gt;preg_replace&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, the file's entire content is replaced with the new one, using the &lt;code&gt;file_put_contents&lt;/code&gt; function.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt; allow developers to write workflows to automate various tasks like running test suites or deploying web services.&lt;/p&gt;

&lt;p&gt;They must be defined using YAML format in a &lt;code&gt;.github/workflows&lt;/code&gt; folder at the root of the project, and contain a list of steps that they are to execute.&lt;/p&gt;

&lt;p&gt;Here's mine, which I named &lt;code&gt;posts.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Update blog posts

on:
  push:
  workflow_dispatch:
  schedule:
    - cron:  '0 0 * * *'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Clone repository
      uses: actions/checkout@v2
    - name: Install PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
    - name: Install Composer dependencies
      run: composer install
    - name: Insert blog posts
      run: php posts.php
    - name: Push changes
      uses: stefanzweifel/git-auto-commit-action@v4
      with:
        commit_message: Updated latest blog posts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, nothing too complicated. We first give the action a name, and then define a list of events that should trigger it – pushing some code to the repository, a manual trigger from the interface (&lt;code&gt;workflow_dispatch&lt;/code&gt;), or periodically like a cron job (here, every day at midnight).&lt;/p&gt;

&lt;p&gt;We then indicate that the action should run on an Ubuntu image, where it will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/actions/checkout"&gt;clone the repository&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shivammathur/setup-php"&gt;instal PHP 8.1&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;instal the Composer dependencies;&lt;/li&gt;
&lt;li&gt;run the PHP script;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/stefanzweifel/git-auto-commit-action"&gt;commit and push the changes&lt;/a&gt;, if any.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it! My GitHub profile is now automatically updated every time I publish a new article.&lt;/p&gt;

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

&lt;p&gt;This was a quick experiment aiming at exploring GitHub Actions, which I expect to use more and more in the future. It was also fun to use PHP as a simple scripting language again, in a procedural way.&lt;/p&gt;

&lt;p&gt;I've voluntarily left out a few things in order to keep this article short and simple – please refer to &lt;a href="https://github.com/osteel/osteel"&gt;the repository&lt;/a&gt; for implementation details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme"&gt;Managing your profile README&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/osteel/osteel"&gt;This article’s repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/checkout"&gt;GitHub Action: Checkout V2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/shivammathur/setup-php"&gt;GitHub Action: Setup PHP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/stefanzweifel/git-auto-commit-action"&gt;GitHub Action: Git Auto Commit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>githubactions</category>
      <category>github</category>
      <category>workflow</category>
    </item>
  </channel>
</rss>
