Andreas MöllerAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/feed.xmlen-us2020-07-16T18:30:00+02:00My name is Andreas Möller, and I am a self-employed Software Engineer and Consultant from Berlin, Germany. What can I do for you?Creating Doctrine entities populated with fake dataAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2020/07/16/creating-doctrine-entities-populated-with-fake-data/2020-07-16T18:30:00+02:00<h1> Creating Doctrine entities populated with fake data </h1> <p>Creating database fixtures in projects using <a target="_blank" href="http://github.com/doctrine/orm" title="doctrine/orm"><code>doctrine/orm</code></a> can be cumbersome - but it doesn't have to be.</p> <p>When you create and populate entities with fake data by hand, your tests can quickly become bloated and difficult to understand. In need of a cure, you might extract helper methods or classes. The chances are that use a third-party library for setting up test fixtures.</p> <p>At the time of writing, <a target="_blank" href="https://github.com/doctrine/data-fixtures" title="doctrine/data-fixtures"><code>doctrine/data-fixtures</code></a> and <a target="_blank" href="https://github.com/nelmio/alice" title="nelmio/alice"><code>nelmio/alice</code></a> are by far the most-downloaded packages for creating and loading fixtures for Doctrine entities, with 33 million and 11 million total downloads, respectively.</p> <p>I have used both of them, but there are a few things that bother me.</p> <p>The primary issue I have with these packages is that there is a considerable disconnect between entities created in a fixture and those used in a test. Developers can establish a connection loosely, using hard-coded values, or indirectly, using constants. I have found tests using these fixtures hard to understand and difficult to maintain.</p> <h2> <code>breerly/factory-girl-php</code> </h2> <p>In 2014, a colleague recommended <a target="_blank" href="https://github.com/GoodPete/factory-girl-php" title="breerly/factory-girl-php"><code>breerly/factory-girl-php</code></a>, a port of Ruby's <a target="_blank" href="https://github.com/thoughtbot/factory_bot" title="factory_bot"><code>factory_bot</code></a> to PHP, and I have been a big fan since.</p> <p>The big difference is that instead of creating fixtures, with <code>breerly/factory-girl-php</code>, I can use a fixture factory to create entity definitions. Based on these definitions, I can then use the fixture factory to create entities. Depending on the nature of the entity definitions, the fixture factory will populate the entities with fake data. Entities can have references to other entities, and the fixture factory establishes associations to these automatically. Similar to the other packages, I can create entities for demonstration or testing purposes. However, the connection between the test code and entities is immediate.</p> <p>I have enjoyed working with <code>breerly/factory-girl-php</code> so much that I started contributing to it. To simplify working with entity definitions, I built <a target="_blank" href="https://github.com/ergebnis/factory-girl-definition" title="ergebnis/factory-girl-definition"><code>ergebnis/factory-girl-definition</code></a>, a companion that allows loading entity definitions from a directory. In March 2020, I sent an email to <a target="_blank" href="https://github.com/GoodPete" title="Grayson Koonce">Grayson Koonce</a>, the owner of <code>breerly/factory-girl-php</code>. I wondered if he would accept me as a collaborator.</p> <h2> <code>ergebnis/factory-bot</code> </h2> <p>Since I never got a reply, I decided to split and started working on <a target="_blank" href="https://github.com/ergebnis/factory-bot" title="ergebnis/factory-bot"><code>ergebnis/factory-bot</code></a>.</p> <p>Splitting <code>ergebnis/factory-bot</code> from <code>breerly/factory-girl-php</code> allows me to work on my terms, make my own decisions, and, more importantly, will enable me to move faster.</p> <p>Since splitting, I have removed code that I never needed and modernized the codebase. I have integrated <code>ergebnis/factory-girl-definition</code>, added features, and foremost, documentation and examples that help you get started.</p> <p>You can install <code>ergebnis/factory-bot</code> by running</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> composer require --dev ergebnis/factory-bot</span> </code></pre> <p>The fixture factory requires an instance of <code>Doctrine\ORM\EntityManagerInterface</code> (for reading class metadata from Doctrine entities, and for persisting Doctrine entities when necessary) and an instance of <code>Faker\Generator</code> for generating fake data.</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); <span class="hljs-keyword">use</span> <span class="hljs-title">Doctrine</span>\<span class="hljs-title">ORM</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Ergebnis</span>\<span class="hljs-title">FactoryBot</span>\<span class="hljs-title">FixtureFactory</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Faker</span>\<span class="hljs-title">Factory</span>; $entityManager = ORM\EntityManager::create(...); $faker = Factory::create(...); $fixtureFactory = <span class="hljs-keyword">new</span> FixtureFactory( $entityManager, $faker ); </code></pre> <p>With the fixture factory set up, you can create definitions for Doctrine entities.</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); <span class="hljs-keyword">use</span> <span class="hljs-title">Ergebnis</span>\<span class="hljs-title">FactoryBot</span>\<span class="hljs-title">Count</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Ergebnis</span>\<span class="hljs-title">FactoryBot</span>\<span class="hljs-title">FieldDefinition</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Ergebnis</span>\<span class="hljs-title">FactoryBot</span>\<span class="hljs-title">FixtureFactory</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Example</span>\<span class="hljs-title">Entity</span>; <span class="hljs-comment">/** <span class="hljs-doctag">@var</span> FixtureFactory $fixtureFactory */</span> $fixtureFactory-&gt;define(Entity\User::class, [ <span class="hljs-string">'avatar'</span> =&gt; FieldDefinition::reference(Entity\Avatar::class), <span class="hljs-string">'id'</span> =&gt; FieldDefinition::closure(<span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(Generator $faker)</span>: <span class="hljs-title">string</span> </span>{ <span class="hljs-keyword">return</span> $faker-&gt;uuid; }), <span class="hljs-string">'location'</span> =&gt; FieldDefinition::optionalClosure(<span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(Generator $faker)</span>: <span class="hljs-title">string</span> </span>{ <span class="hljs-keyword">return</span> $faker-&gt;city; }), <span class="hljs-string">'login'</span> =&gt; FieldDefinition::closure(<span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(Generator $faker)</span>: <span class="hljs-title">string</span> </span>{ <span class="hljs-keyword">return</span> $faker-&gt;userName; }), ]); $fixtureFactory-&gt;define(Entity\Avatar::class, [ <span class="hljs-string">'height'</span> =&gt; FieldDefinition::closure(<span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(Generator $faker)</span>: <span class="hljs-title">int</span> </span>{ <span class="hljs-keyword">return</span> $faker-&gt;numberBetween(<span class="hljs-number">300</span>, <span class="hljs-number">600</span>); }), <span class="hljs-string">'url'</span> =&gt; FieldDefinition::closure(<span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(Generator $faker)</span>: <span class="hljs-title">string</span> </span>{ <span class="hljs-keyword">return</span> $faker-&gt;imageUrl(); }), <span class="hljs-string">'width'</span> =&gt; FieldDefinition::closure(<span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(Generator $faker)</span>: <span class="hljs-title">int</span> </span>{ <span class="hljs-keyword">return</span> $faker-&gt;numberBetween(<span class="hljs-number">400</span>, <span class="hljs-number">900</span>); }), ]); </code></pre> <p>With the fixture factory aware of entity definitions, you can now create entities populated with fake data.</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); <span class="hljs-keyword">use</span> <span class="hljs-title">Ergebnis</span>\<span class="hljs-title">FactoryBot</span>\<span class="hljs-title">Count</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Ergebnis</span>\<span class="hljs-title">FactoryBot</span>\<span class="hljs-title">FieldDefinition</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Ergebnis</span>\<span class="hljs-title">FactoryBot</span>\<span class="hljs-title">FixtureFactory</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Example</span>\<span class="hljs-title">Entity</span>; <span class="hljs-comment">/** <span class="hljs-doctag">@var</span> FixtureFactory $fixtureFactory */</span> $user = $fixtureFactory-&gt;createOne(Entity\User::class, [ <span class="hljs-string">'login'</span> =&gt; FieldDefinition::value(<span class="hljs-string">'localheinz'</span>), ]); var_dump($user-&gt;location()); <span class="hljs-comment">// `null` or random city</span> var_dump($user-&gt;login()); <span class="hljs-comment">// 'localheinz'</span> $users = $fixtureFactory-&gt;createMany( Entity\User::class, Count::between(<span class="hljs-number">0</span>, <span class="hljs-number">10</span>), [ <span class="hljs-string">'login'</span> =&gt; FieldDefinition::sequence(<span class="hljs-string">'user-%d'</span>), ] ); var_dump($users); <span class="hljs-comment">// array with 0-10 instances of Entity\User</span> </code></pre> <p>I have released <a target="_blank" href="https://github.com/ergebnis/factory-bot/tree/0.2.1" title="ergebnis/factory-bot:0.2.0"><code>ergebnis/factory-bot:0.2.0</code></a> yesterday, and you can take a look at the <a target="_blank" href="https://github.com/ergebnis/factory-bot/blob/main/README.md" title="ergebnis/factory-bot: Documentation">documentation</a> and <a target="_blank" href="https://github.com/ergebnis/factory-bot/blob/main/example" title="ergebnis/factory-bot: Examples">examples</a>, and try it out today.</p> Merging pull requests with GitHub ActionsAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2020/06/15/merging-pull-requests-with-github-actions/2020-06-15T10:25:00+02:00<h1> Merging pull requests with GitHub Actions </h1> <p>In May 2019, <a target="_blank" href="https://dependabot.com/blog/hello-github/" title="Dependabot is joining GitHub">GitHub acquired Dependabot</a>, and recently <a target="_blank" href="https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/" title="Keep all your packages up to date with Dependabot">GitHub announced that Dependabot is moving into GitHub</a>.</p> <p>When you follow the instructions, you will find that the migration is straightforward. However, there is one thing missing: after migrating from version 1 to version 2, Dependabot will not automatically merge pull requests anymore. That the feature is currently missing could indicate that GitHub will bring automatic merges onboard - independently from Dependabot.</p> <p>In the meantime, you can configure a job with <a target="_blank" href="https://github.com/features/actions" title="GitHub Actions">GitHub Actions</a> and <a target="_blank" href="https://github.com/actions/github-script" title="actions/github-script"><code>actions/github-script</code></a> that will automatically merge pull requests created by Dependabot. <code>actions/github-script</code> uses <a target="_blank" href="https://github.com/octokit/rest.js" title="octokit/rest.js"><code>octokit/rest.js</code></a>, which is <a target="_blank" href="https://octokit.github.io/rest.js/v17" title="Documentation for octokit/rest.js">well documented</a>, and it is often more convenient to use than maintaining a full-blown GitHub action.</p> <p>Here is an example of a job that will run when the <code>coding-standards</code>, <code>static-code-analysis</code>, and <code>tests</code> jobs have succeeded, and which will then merge a pull request opened by <code>dependabot[bot]</code>:</p> <pre><code class="language-yaml hljs yaml" data-lang="yaml"><span class="hljs-attr">merge:</span> <span class="hljs-attr">name:</span> <span class="hljs-string">"Merge"</span> <span class="hljs-attr">runs-on:</span> <span class="hljs-string">"ubuntu-latest"</span> <span class="hljs-attr">needs:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">"coding-standards"</span> <span class="hljs-bullet">-</span> <span class="hljs-string">"static-code-analysis"</span> <span class="hljs-bullet">-</span> <span class="hljs-string">"tests"</span> <span class="hljs-attr">if:</span> <span class="hljs-string">&gt; github.event_name == 'pull_request' &amp;&amp; github.event.pull_request.draft == false &amp;&amp; ( github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' ) &amp;&amp; ( github.actor == 'dependabot[bot]' ) </span> <span class="hljs-attr">steps:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">"Merge pull request"</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">"actions/github-script@v2"</span> <span class="hljs-attr">with:</span> <span class="hljs-attr">github-token:</span> <span class="hljs-string">"$<span class="hljs-template-variable">{{ secrets.GITHUB_TOKEN }}</span>"</span> <span class="hljs-attr">script:</span> <span class="hljs-string">| const pullRequest = context.payload.pull_request const repository = context.repo </span> <span class="hljs-string">await</span> <span class="hljs-string">github.pulls.merge({</span> <span class="hljs-attr">merge_method:</span> <span class="hljs-string">"merge"</span><span class="hljs-string">,</span> <span class="hljs-attr">owner:</span> <span class="hljs-string">repository.owner,</span> <span class="hljs-attr">pull_number:</span> <span class="hljs-string">pullRequest.number,</span> <span class="hljs-attr">repo:</span> <span class="hljs-string">repository.repo,</span> <span class="hljs-string">})</span> </code></pre> <p>💡 You can see this very simple job in action at <a target="_blank" href="https://github.com/Jan0707/phpstan-prophecy/pull/186" title="Example of merging pull request created by Dependabot with GitHub Actions"><code>jangregor/phpstan-prophecy</code></a>, and you can find a more sophisticated example of using GitHub Actions to merge pull requests in <a target="_blank" href="https://github.com/ergebnis/php-library-template/blob/ee9c37686652c40494602c3a06b1dd553b4682b2/.github/workflows/integrate.yaml#L378-L471" title="Workflow definition for merging pull requests with a bot user"><code>ergebnis/php-library-template</code></a>.</p> <p>Note that this approach has a severe drawback: this job will run when the dependent jobs in the same workflow have completed. The job is entirely unaware of additional workflows or integrations with other apps. It might merge a pull request when any of these additional workflows or integrations are still running or have already failed. This job works a lot better when used with <a target="_blank" href="https://help.github.com/en/github/administering-a-repository/about-protected-branches" title="About protected branches">branch protection</a>.</p> <p>As an alternative, you might want to take a look at the <a target="_blank" href="https://github.com/marketplace?type=apps" title="Apps on the GitHub Marketplace">GitHub Marketplace</a>, where you can find a range of applications that will merge pull requests for you.</p> <div class="note"> <div class="note-body"> <p>❗ As of March 1st, 2021, this job will not work anymore.</p> <p>For reference, see <a target="_blank" href="https://github.blog/changelog/2021-02-19-github-actions-workflows-triggered-by-dependabot-prs-will-run-with-read-only-permissions/" title="GitHub Actions: Workflows triggered by Dependabot PRs will run with read-only permissions">GitHub Actions: Workflows triggered by Dependabot PRs will run with read-only permissions</a>.</p> </div> </div> Black lives matterAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2020/06/12/black-lives-matter/2020-06-12T09:45:00+02:00<h1> Black lives matter </h1> <p>I am a middle-aged white man working in tech. I can afford to spend my spare time working on open-source projects. I am privileged.</p> <p>A few days ago, I spent two hours renaming the default branches of the repositories in the <a target="_blank" href="https://github.com/ergebnis" title="@ergebnis on GitHub"><code>@ergebnis</code><a/> organization from <code>master</code> to <code>main</code>.</p> <blockquote class="twitter-tweet"> <p lang="en" dir="ltr">I have spent the last two hours renaming the default branches of <a href="https://twitter.com/github?ref_src=twsrc%5Etfw">@github</a> repositories in the <a href="https://twitter.com/ergebnis?ref_src=twsrc%5Etfw">@ergebnis</a> organization from &quot;master&quot; to &quot;main&quot;.<br><br>✊🏿 Black lives matter.</p>&mdash; Andreas Möller (@localheinz) <a href="https://twitter.com/localheinz/status/1270781170792517632?ref_src=twsrc%5Etfw">June 10, 2020</a> </blockquote> <p>For a brief moment, I was considering the consequences of renaming the branches - for contributors and users. I don’t know if anyone will be affected by the rename, but for what it’s worth: when a user been affected, then they are still alive. A renamed branch or broken build is only a minor inconvenience compared to being harassed, beaten up, or choked to death because of the color of your skin.</p> <p>If you encounter any issues because of the renaming, I apologize. You will get over it. If you think this change was unnecessary, think again.</p> <p>I am not alone. Friends and neighbors in the PHP and the entire software development community have made efforts in the last weeks, months, and years to acknowledge the use of offensive terminology, and have started to rename things.</p> <p>Naming is hard. Renaming things is easy.</p> <p>✊🏿 Black lives matter.</p> <blockquote class="twitter-tweet"> <p lang="en" dir="ltr">It&#39;s a great idea and we are already working on this! cc <a href="https://twitter.com/billygriffin22?ref_src=twsrc%5Etfw">@billygriffin22</a></p>&mdash; Nat Friedman (@natfriedman) <a href="https://twitter.com/natfriedman/status/1271253144442253312?ref_src=twsrc%5Etfw">June 12, 2020</a> </blockquote> <blockquote class="twitter-tweet"> <p lang="en" dir="ltr">Our team at <a href="https://twitter.com/github?ref_src=twsrc%5Etfw">@GitHub</a> just changed our project&#39;s main branch to `main`! We&#39;re not the only ones, and there&#39;s growing momentum to change the default across the company. 😱 It&#39;s no defunding the police, but it&#39;s nice to see progress against such entrenched practices.</p>&mdash; Lucas Garron (@lgarron) <a href="https://twitter.com/lgarron/status/1271212268294823938?ref_src=twsrc%5Etfw">June 11, 2020</a> </blockquote> <blockquote class="twitter-tweet"> <p lang="en" dir="ltr">I picked the names &quot;master&quot; (and &quot;origin&quot;) in the early Git tooling back in 2005.<br><br>(this probably means you shouldn&#39;t give much weight to my name preferences :) )<br><br>I have wished many times I would have named them &quot;main&quot; (and &quot;upstream&quot;) instead.<br><br>Glad it&#39;s happenning <a href="https://twitter.com/natfriedman?ref_src=twsrc%5Etfw">@natfriedman</a></p>&mdash; Petr Baudis (@xpasky) <a href="https://twitter.com/xpasky/status/1271477451756056577?ref_src=twsrc%5Etfw">June 12, 2020</a> </blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <h2>Resources</h2> <ul class="bibliography"> <li> Moore, Robert B. 1976. Racism in the English Language: a Lesson Plan and Study Essay. New York, NY: The Racism and Sexism Resource Center for Educators. </li> <li> “High-Tech Gripe Makes PCs More PC.” 2003. The Sydney Morning Herald. The Sydney Morning Herald. November 27, 2003. <a target="_blank" href="https://www.smh.com.au/world/high-tech-gripe-makes-pcs-more-pc-20031127-gdhv7x.html">https://www.smh.com.au/world/high-tech-gripe-makes-pcs-more-pc-20031127-gdhv7x.html</a>. </li> <li> “'Master' and 'Slave' Computer Labels Unacceptable, Officials Say.” 2003. CNN. Cable News Network. November 26, 2003. <a target="_blank" href="http://edition.cnn.com/2003/TECH/ptech/11/26/master.term.reut/">http://edition.cnn.com/2003/TECH/ptech/11/26/master.term.reut/</a>. </li> <li> <a target="_blank" href="https://github.com/pasky" title="Petr Baudis">Baudis, Petr</a>. 2005. “Added git fork, which will create a new branch in a given new directory, with the object cache shared with your current branch. The "first" branch originating from the object cache is called "master". git/HEAD is now just a symlink to .git/heads/$branch.” Public Git Hosting - Cogito.git/Commit. April 16, 2005. <a target="_blank" href="https://repo.or.cz/cogito.git/commit/3c4347c8b64fb516bc097ab174c3247113b7603d">https://repo.or.cz/cogito.git/commit/3c4347c8b64fb516bc097ab174c3247113b7603d</a>. </li> <li> <a target="_blank" href="https://github.com/torvalds" title="Petr Baudis">Torvalds, Linus</a>. 2005. “Re: kernel.org and GIT tree rebuilding.” June 25, 2005. <a target="_blank" href="https://marc.info/?l=git&m=111968031816936&w=2">https://marc.info/?l=git&m=111968031816936&w=2</a>. </li> <li> <a target="_blank" href="https://github.com/eglash" title="Ron Eglash">Eglash, Ron</a>. 2007. “Broken Metaphor: The Master-Slave Analogy in Technical Literature.” Technology and Culture. Johns Hopkins University Press. May 21, 2007. <a target="_blank" href="https://muse.jhu.edu/article/215390">https://muse.jhu.edu/article/215390</a>. </li> <li> <a target="_blank" href="https://github.com/dww" title="Derek Wright">Wright, Derek</a>. 2008. “Rename ‘Master/Slave’ Terminology to ‘Client/Server.’” Drupal.org. December 5, 2008. <a target="_blank" href="https://www.drupal.org/project/project_issue_file_review/issues/343414">https://www.drupal.org/project/project_issue_file_review/issues/343414</a>. </li> <li> <a target="_blank" href="https://github.com/fcurella" title="Flavio Curella">Curella, Flavio</a>. 2014. “Replace Occurrences of Master/Slave Terminology with Leader/Follower.” Django. May 20, 2014. <a target="_blank" href="https://code.djangoproject.com/ticket/22667">https://code.djangoproject.com/ticket/22667</a>. </li> <li> <a target="_blank" href="https://github.com/dhh" title="David Heinemeier Hansson">Hansson, David Heinemeier</a>. 2018. “Replace use of whitelist with allowlist and blacklist with denylist · Issue #33677 · <code>rails/rails</code>.” GitHub. August 22, 2018. <a target="_blank" href="https://github.com/rails/rails/issues/33677">https://github.com/rails/rails/issues/33677</a>. </li> <li> <a target="_blank" href="https://github.com/mallory" title="Mallory Knode">Knode, Mallory</a>, and <a target="_blank" href="https://github.com/nllz" title="Niels ten Oever">Niels ten Oever</a>. 2018. “Terminology, Power and Oppressive Language.” IETF Tools. October 22, 2018. <a target="_blank" href="https://tools.ietf.org/id/draft-knodel-terminology-00.html">https://tools.ietf.org/id/draft-knodel-terminology-00.html</a>. </li> <li> <a target="_blank" href="https://github.com/mallory" title="Mallory Knode">Knode, Mallory</a>, and <a target="_blank" href="https://github.com/nllz" title="Niels ten Oever">Niels ten Oever</a>. 2019. “Terminology, Power and Oppressive Language.” IETF Tools. March 11, 2019. <a target="_blank" href="https://tools.ietf.org/id/draft-knodel-terminology-01.html">https://tools.ietf.org/id/draft-knodel-terminology-01.html</a>. </li> <li> <a target="_blank" href="https://wiki.gnome.org/BastienNocera" title="Bastien Nocera">Nocera, Bastien</a>. 2019. “Re: Replacing ‘Master’ Reference in Git Branch Names (Was Re: Proposal: Replace All References to Master/Slave in GNOME Modules).” Gnome Desktop Devel Mailing List. May 4, 2019. <a target="_blank" href="https://mail.gnome.org/archives/desktop-devel-list/2019-May/msg00066.html">https://mail.gnome.org/archives/desktop-devel-list/2019-May/msg00066.html</a>. </li> <li> <a target="_blank" href="https://github.com/GrahamCampbell" title="Graham Campbell">Campbell, Graham</a>. 2020. “[1.10] Replace whitelist with allow list by GrahamCampbell · Pull Request #8957 · <code>composer/composer</code>.” GitHub. June 7, 2020. <a target="_blank" href="https://github.com/composer/composer/pull/8957">https://github.com/composer/composer/pull/8957</a>. </li> <li> <a target="_blank" href="https://github.com/shanselman" title="Scott Hanselmann">Hanselman, Scott</a>. 2020. “Easily Rename Your Git Default Branch from Master to Main.” Scott Hanselman. June 8, 2020. <a target="_blank" href="https://www.hanselman.com/blog/EasilyRenameYourGitDefaultBranchFromMasterToMain.aspx">https://www.hanselman.com/blog/EasilyRenameYourGitDefaultBranchFromMasterToMain.aspx</a>. </li> <li> <a target="_blank" href="https://github.com/brzuchal" title="Michał Marcin Brzuchalski">Brzuchalski, Michał Marcin</a>. 2020. “PHP RFC: Change Terminology to ExcludeList.” PHP. June 9, 2020. <a target="_blank" href="https://wiki.php.net/rfc/change-terminology-to-excludelist">https://wiki.php.net/rfc/change-terminology-to-excludelist</a>. </li> <li> <a target="_blank" href="https://github.com/jeromegamez" title="Jérôme Gamez">Gamez, Jérôme</a>. 2020. “The default branch of <code>kreait/firebase-php</code> has been renamed from <code>master</code> to <code>main</code>.” Twitter. Twitter. June 9, 2020. <a target="_blank" href="https://twitter.com/jeromegamez/status/1270276881468862464">https://twitter.com/jeromegamez/status/1270276881468862464</a>. </li> <li> <a target="_blank" href="https://github.com/bjoleary" title="Brendan O'Leary">O'Leary, Brendan</a>. 2020. “I Was Wrong.” Brendan O'Leary. June 10, 2020. <a target="_blank" href="https://boleary.dev/blog/2020-06-10-i-was-wrong-about-git-master.html">https://boleary.dev/blog/2020-06-10-i-was-wrong-about-git-master.html</a>. </li> <li> <a target="_blank" href="https://github.com/PurpleBooth" title="Billie Thompson">Thompson, Billie</a>. 2020. “<code>PurpleBooth/change-github-default-branch.sh</code>: Quickly change your GitHub repositories default branch something else.” GitHub. June 14, 2020. <a target="_blank" href="https://github.com/PurpleBooth/change-github-default-branch.sh">https://github.com/PurpleBooth/change-github-default-branch.sh</a>. </li> <li> <a target="_blank" href="https://github.com/mheap" title="Michael Heap">Heap, Michael</a>. 2020. “<code>mheap/github-default-branch</code>: Rename your default branch on GitHub.” GitHub. June 15, 2020. <a target="_blank" href="https://github.com/mheap/github-default-branch">https://github.com/mheap/github-default-branch</a>. </li> <li> <a target="_blank" href="https://github.com/geekcom" title="Daniel Rodrigues Lima">Lima, Daniel Rodrigues</a>. 2020. “About the use of the terms master/slave and blacklist, proposal to replace.” Externals - Opening PHP's #Internals to the Outside. June 15, 2020. <a target="_blank" href="https://externals.io/message/110515">https://externals.io/message/110515</a>. </li> <li> <a target="_blank" href="https://github.com/mallory" title="Mallory Knode">Knode, Mallory</a>, and <a target="_blank" href="https://github.com/nllz" title="Niels ten Oever">Niels ten Oever</a>. 2020. “Terminology, Power and Oppressive Language.” IETF Tools. June 16, 2020. <a target="_blank" href="https://tools.ietf.org/id/draft-knodel-terminology-02.html">https://tools.ietf.org/id/draft-knodel-terminology-02.html</a>. </li> <li> <a target="_blank" href="https://github.com/gr2m" title="Gregor Martynus">Martynus, Gregor</a>. 2020. “Rename Master Branch - Resources about how and why to rename the "master" branch in git.” GitHub. June 17, 2020. <a target="_blank" href="https://github.com/rename-master-branch">https://github.com/rename-master-branch</a>. </li> <li> <a target="_blank" href="https://github.com/seldaek" title="Jordi Boggiano">Boggiano, Jordi</a>. “Composer and Default Git Branches.” Private Packagist, Private Packagist, 19 June 2020, <a target="_blank" href="https://blog.packagist.com/composer-and-default-git-branches">https://blog.packagist.com/composer-and-default-git-branches/</a>. </li> <li> <a target="_blank" href="https://github.com/mallory" title="Mallory Knode">Knode, Mallory</a>, and <a target="_blank" href="https://github.com/nllz" title="Niels ten Oever">Niels ten Oever</a>. 2020. “Terminology, Power and Oppressive Language.” IETF Tools. July 8, 2020. <a target="_blank" href="https://tools.ietf.org/id/draft-knodel-terminology-03.html">https://tools.ietf.org/id/draft-knodel-terminology-03.html</a>. </li> <li> <a target="_blank" href="https://github.blog" title="GitHub, Inc">The GitHub Blog</a>. “The Default Branch for Newly-Created Repositories Is Now Main.” The GitHub Blog, October 1, 2020. <a target="_blank" href="https://github.blog/changelog/2020-10-01-the-default-branch-for-newly-created-repositories-is-now-main/">https://github.blog/changelog/2020-10-01-the-default-branch-for-newly-created-repositories-is-now-main/</a>. </li> </ul> <div class="call-to-action"> <h2> Do you have additional resources? </h2> <p><a class="btn-call-to-action-get-in-touch-via-email" target="_blank" href="mailto:hello@localheinz.com?subject=Additional%20resources%20for%20Black%20Lives%20Matter" title="Please share!">Please share!</a></p> </div> Rolling up database migrations with DoctrineAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2020/06/10/rolling-up-database-migrations-with-doctrine/2020-06-10T14:10:00+02:00<h1> Rolling up database migrations with Doctrine </h1> <h2>Introduction</h2> <p>As a user of <a target="_blank" href="https://github.com/doctrine/orm" title="doctrine/orm"><code>doctrine/orm</code><a/> and <a target="_blank" href="https://github.com/doctrine/migrations" title="doctrine/orm"><code>doctrine/migrations</code><a/>, it is likely that you - like me - have encountered one of the following scenarios:</p> <ul> <li>You have started using <code>doctrine/migrations</code> late in a project. Unfortunately, you can not set up a database from scratch using database migrations.</li> <li>You use Doctrine entities or other services in the database migrations. You have realized that this was a mistake, regret it, and you would like to get rid of these database migrations.</li> <li>You populate the database with data using database migrations. Again, you have realized that this was a mistake, regret it, and you would like to get rid of these database migrations.</li> <li>You have accumulated a lot of database migrations in a mature project. Since you run these database migrations frequently to set up databases in development, test, and staging environments, and because you get that extra kick out of things running fast, you would like to collapse them into a single database migration.</li> </ul> <p>If that does not describe you, for example, because you do not work on these kinds of projects or never make any mistakes, please share this article with someone who does.</p> <p>Are you still here? Great!</p> <p><a target="_blank" href="https://github.com/doctrine/migrations/releases/tag/v2.0.0" title="doctrine/migrations:2.0.0"><code>doctrine/migrations:2.0.0</code></a> introduced a <code>DumpSchemaCommand</code> and a <code>RollupCommand</code>. The <code>DumpSchemaCommand</code> reads the schema from our entity mapping and dumps corresponding SQL statements into a single database migration. The <code>RollupCommand</code> verifies that only a single database migration exists, removes information about previously run database migrations from the configured table, and inserts information about this single database migration.</p> <p>Using these commands, we can easily roll up database migrations. In the following, I will show you how.</p> <p>The process consists of four steps:</p> <ul> <li>validate entity mapping and database</li> <li>remove existing migrations</li> <li>dump the schema into a single database migration</li> <li>validate the dumped database migration</li> <li>roll up migrations</li> </ul> <h2>Commands</h2> <p>In a moment, we will run Doctrine console commands. These commands operate with options that have default values, but if our application has a setup that differs from the defaults, we need to specify values for these options.</p> <h3>Options</h3> <ul> <li> <strong>--connection</strong>: The name of the Doctrine DBAL database connection; could be <code>default</code>.</li> <li> <strong>--env</strong>: The name of the environment; could be <code>dev</code>, <code>develop</code>, <code>prod</code>, or <code>production</code>.</li> <li> <strong>--em</strong>: The name of the Doctrine ORM entity manager to use; could be <code>default</code>.</li> </ul> <h3>Environments</h3> <p>We will run these commands in development and production environments. To achieve the best results, our development environment should reflect the production environment. How to create such a development environment is out of the scope of this article.</p> <div class="note"> <div class="note-body"> <p>❗ Make sure to use the appropriate parameters when executing the commands.</p> </div> </div> <h2>Validate entity mapping and database</h2> <p>As mentioned before, the <code>DumpSchemaCommand</code> reads the schema from our entity mapping and dumps corresponding SQL statements into a single database migration. When our entity mapping and the database are not in sync, this will result in a database migration that does not reflect the actual database structure.</p> <p>To avoid that, we need to validate the schema and fix all errors in a development environment.</p> <p>We run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:migrations:status</span> </code></pre> <p>to show the database migration status in our development environment.</p> <p>When the command reports that there are new database migrations, we run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:migrations:execute</span> </code></pre> <p>to run these database migrations in our development environment.</p> <p>When all database migrations have been run, we run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:schema:validate</span> </code></pre> <p>to validate that the entity mapping is correct and in sync with the database in our development environment.</p> <p>When this command reports errors, we need to fix them. Fixing these errors might involve adjusting our entity mapping and creating database migrations. These fixes are useful - even when we do not want to roll up database migrations. Therefore we can apply them to the main line of the project. When we have fixed these errors, we can return to rolling up the database migrations.</p> <p>When this command does not report any errors, we can continue.</p> <div class="note"> <div class="note-body"> <p>💡 To avoid validation errors, we can validate that the entity mapping is correct and in sync with the database in a continuous-integration environment.</p> <p>When we cannot set up a database from database migrations, we validate the mapping only. Once we have rolled up the database migrations, we can set up a database from database migrations. Then we can also verify that the entity mapping is in sync with the database.</p> </div> </div> <h2>Remove existing database migrations</h2> <p>Since our entity mapping is valid and in sync with the database, we can now remove the existing database migrations from the configured migrations directory, and commit the changes.</p> <h2>Dump the schema</h2> <p>After removing the existing database migrations, we can now dump the schema from our entity mapping into a single database migration.</p> <p>We run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:migrations:dump-schema</span> </code></pre> <p>to dump the schema into a single migration in our development environment, and commit the changes.</p> <h2>Validate the dumped migration</h2> <p>We have dumped the schema into a single migration, but before we continue, let us double-check that running this migration really creates a database structure that reflects our entity mapping. We can do that by dropping and recreating the database, running the migration, and validating the schema.</p> <p>We run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:database:drop</span> </code></pre> <p>to drop the database in our development environment. Then we run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:database:create</span> </code></pre> <p>to create the database in our development environment.</p> <p>We now have an empty database in our development environment, and we run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:migrations:status</span> </code></pre> <p>to show the database migration status. This command will show a single new database migration: the database migration we just dumped.</p> <p>We run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:migrations:execute</span> </code></pre> <p>to run this database migration in our development environment.</p> <p>We now run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:schema:validate</span> </code></pre> <p>to validate that the entity mapping is correct and in sync with the database in our development environment.</p> <p>At this point, this command should not report any errors. When it does, we need to fix them, but we should not adjust our entity mapping. However, we can modify the dumped database migration. Remember, the RollupCommand will not run when more than one database migration exists.</p> <p>We run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:migrations:diff</span> </code></pre> <p>to create a database migration from the differences between our entity mapping and the database. Instead of committing the migration created from running this command, we manually merge it into the previously dumped migration and commit the resulting changes.</p> <p>We repeat this process until the single migration creates a database in sync with our entity mapping.</p> <h2>Roll up</h2> <p>We have rolled up the database migrations in our development environment, and can finally roll up the database migrations in our production environment.</p> <p>After deploying the changes, we run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:migrations:rollup</span> </code></pre> <p>to roll up the migrations in our production environment.</p> <p>We run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> bin/console doctrine:migrations:status</span> </code></pre> <p>to verify that there is only a single available migration and that there are more migrations that need to be executed.</p> <h2>Conclusion</h2> <p>We have now collapsed all existing database migrations into a single database migration.</p> <p>We can set up a database from scratch in development, testing, or continuous-integration environments now, and run commands and tests that require the presence of a database. We have also rid ourselves of database migrations that use entities or services or populate the database with data. By reducing the number of migrations, we have sped up the process of running them.</p> Avoiding imports and aliasesAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2020/05/19/avoiding-imports-and-aliases/2020-05-19T12:00:00+02:00<h1> Avoiding imports and aliases </h1> <p><a target="_blank" href="https://www.php.net/releases/5_3_0.php" title="PHP 5.3: Release notes">PHP 5.3</a>, released on June 30, 2009, introduced <a target="blank" href="https://www.php.net/manual/en/language.namespaces.rationale.php" title="PHP: Namespaces overview">namespaces</a> as well as <a target="blank" href="https://www.php.net/manual/en/language.namespaces.importing.php" title="PHP: Using namespaces">imports and aliases</a>.</p> <p>Developers quickly adopted these features. Where previously a class was named <code>Foo_Bar_Baz_Qux</code>, namespaces allow calling it <code>Foo\Bar\Baz\Qux</code>.</p> <h2>Imports</h2> <p>With imports, I can import the fully-qualified class name and reference the class by its terminating class name:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); <span class="hljs-keyword">use</span> <span class="hljs-title">Foo</span>\<span class="hljs-title">Bar</span>\<span class="hljs-title">Baz</span>\<span class="hljs-title">Qux</span>; $qux = <span class="hljs-keyword">new</span> Qux(); </code></pre> <p>Alternatively, I can import a namespace prefix and reference the class relative to it:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); <span class="hljs-keyword">use</span> <span class="hljs-title">Foo</span>\<span class="hljs-title">Bar</span>; $qux = <span class="hljs-keyword">new</span> Bar\Baz\Qux(); </code></pre> <h2>Aliases</h2> <p>Aliases, you guessed it, allow importing either and assigning aliases to it:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); <span class="hljs-keyword">use</span> <span class="hljs-title">Foo</span>\<span class="hljs-title">Bar</span> <span class="hljs-title">as</span> <span class="hljs-title">Quux</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Foo</span>\<span class="hljs-title">Bar</span>\<span class="hljs-title">Baz</span>\<span class="hljs-title">Qux</span> <span class="hljs-title">as</span> <span class="hljs-title">Quuz</span>; $qux = <span class="hljs-keyword">new</span> Quux\Baz\Qux(); $anotherQux = <span class="hljs-keyword">new</span> Quuz(); </code></pre> <h2>Problems</h2> <p>When it comes to the possibilities of how I can use these features, there are two extremes. At one end, I use fully-qualified class names everywhere. At the other end, I import every single class.</p> <p>Using fully-qualified class names everywhere is a bit like writing code as if today is June 29, 2009. It does not make any sense - so let us not get into it.</p> <p>Importing every single class is possible, but potentially leads to a long list of imports. A long list of imports takes a long time to read. For every class I add to this list, developers need to recall the context from which I introduced it. Every time I import a class, I risk conflicts.</p> <p>Aliases can help to avoid these conflicts. However, aliases only make the problem worse. For every alias, developers need to translate between the original class name and the aliased class name.</p> <p>With aliases, navigating the code becomes more difficult. In <a target="_blank" href="https://www.jetbrains.com/phpstorm/" title="PhpStorm: The Lightning-Smart PHP IDE">PhpStorm</a>, clicking on a symbol <a target="_blank" href="https://www.jetbrains.com/help/phpstorm/navigating-through-the-source-code.html" title="PhpStorm: Source code navigation">navigates</a> to where it was declared. When I click on a class name referenced in code, I navigate to the class declaration. However, when I click on an alias, I navigate to the alias declaration. I now need to click again on the class name to navigate to its declaration.</p> <p>Ugh.</p> <h2>Solution</h2> <p>The worst examples of <em>importitis</em> and <em>aliasitis</em> I have seen in a closed-source project included a class with 58 imports, and another class with 12 aliases. If we ignore for a moment that these classes were suffering from other problems, all of these issues could have been avoided by only importing a suitable parent namespace.</p> <p>Here are few examples.</p> <p>Take a look at the <a target="_blank" href="https://github.com/symfony/demo/blob/v1.5.3/src/Controller/UserController.php" title="UserController"><code>UserController</code></a> from the <a target="_blank" href="https://github.com/symfony/demo" title="symfony/demo"><code>symfony/demo</code></a> application:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Controller</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Entity</span>\<span class="hljs-title">Comment</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Entity</span>\<span class="hljs-title">Post</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Events</span>\<span class="hljs-title">CommentCreatedEvent</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Form</span>\<span class="hljs-title">CommentType</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Repository</span>\<span class="hljs-title">PostRepository</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Repository</span>\<span class="hljs-title">TagRepository</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Sensio</span>\<span class="hljs-title">Bundle</span>\<span class="hljs-title">FrameworkExtraBundle</span>\<span class="hljs-title">Configuration</span>\<span class="hljs-title">Cache</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Sensio</span>\<span class="hljs-title">Bundle</span>\<span class="hljs-title">FrameworkExtraBundle</span>\<span class="hljs-title">Configuration</span>\<span class="hljs-title">IsGranted</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Sensio</span>\<span class="hljs-title">Bundle</span>\<span class="hljs-title">FrameworkExtraBundle</span>\<span class="hljs-title">Configuration</span>\<span class="hljs-title">ParamConverter</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Bundle</span>\<span class="hljs-title">FrameworkBundle</span>\<span class="hljs-title">Controller</span>\<span class="hljs-title">AbstractController</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">EventDispatcher</span>\<span class="hljs-title">EventDispatcherInterface</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpFoundation</span>\<span class="hljs-title">Request</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpFoundation</span>\<span class="hljs-title">Response</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Routing</span>\<span class="hljs-title">Annotation</span>\<span class="hljs-title">Route</span>; </code></pre> <p>It has a fairly long list of imports, that can easily be reduced by importing only parent namespaces:</p> <pre><code class="language-diff hljs diff" data-lang="diff">diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 85228f0..82723de 100644 <span class="hljs-comment">--- a/src/Controller/UserController.php</span> <span class="hljs-comment">+++ b/src/Controller/UserController.php</span> <span class="hljs-meta">@@ -2,29 +2,31 @@</span> namespace App\Controller; <span class="hljs-deletion">-use App\Form\Type\ChangePasswordType;</span> <span class="hljs-deletion">-use App\Form\UserType;</span> <span class="hljs-deletion">-use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;</span> <span class="hljs-deletion">-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;</span> <span class="hljs-deletion">-use Symfony\Component\HttpFoundation\Request;</span> <span class="hljs-deletion">-use Symfony\Component\HttpFoundation\Response;</span> <span class="hljs-deletion">-use Symfony\Component\Routing\Annotation\Route;</span> <span class="hljs-deletion">-use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;</span> <span class="hljs-addition">+use App\Form;</span> <span class="hljs-addition">+use Sensio\Bundle\FrameworkExtraBundle;</span> <span class="hljs-addition">+use Symfony\Bundle\FrameworkBundle;</span> <span class="hljs-addition">+use Symfony\Component\HttpFoundation;</span> <span class="hljs-addition">+use Symfony\Component\Routing;</span> <span class="hljs-addition">+use Symfony\Component\Security;</span> /** <span class="hljs-deletion">- * @Route("/profile")</span> <span class="hljs-deletion">- * @IsGranted("ROLE_USER")</span> <span class="hljs-addition">+ * @Routing\Annotation\Route("/profile")</span> <span class="hljs-addition">+ * @FrameworkExtraBundle\Configuration\IsGranted("ROLE_USER")</span> */ <span class="hljs-deletion">-class UserController extends AbstractController</span> <span class="hljs-addition">+class UserController extends FrameworkBundle\Controller\AbstractController</span> { /** <span class="hljs-deletion">- * @Route("/edit", methods="GET|POST", name="user_edit")</span> <span class="hljs-addition">+ * @Routing\Annotation\Route(</span> <span class="hljs-addition">+ * "/edit",</span> <span class="hljs-addition">+ * methods="GET|POST",</span> <span class="hljs-addition">+ * name="user_edit"</span> <span class="hljs-addition">+ * )</span> */ <span class="hljs-deletion">- public function edit(Request $request): Response</span> <span class="hljs-addition">+ public function edit(HttpFoundation\Request $request): HttpFoundation\Response</span> { $user = $this-&gt;getUser(); <span class="hljs-deletion">- $form = $this-&gt;createForm(UserType::class, $user);</span> <span class="hljs-addition">+ $form = $this-&gt;createForm(Form\UserType::class, $user);</span> $form-&gt;handleRequest($request); if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid()) { @@ -42,13 +44,19 @@ class UserController extends AbstractController } /** <span class="hljs-deletion">- * @Route("/change-password", methods="GET|POST", name="user_change_password")</span> <span class="hljs-addition">+ * @Routing\Annotation\Route(</span> <span class="hljs-addition">+ * "/change-password",</span> <span class="hljs-addition">+ * methods="GET|POST",</span> <span class="hljs-addition">+ * name="user_change_password"</span> <span class="hljs-addition">+ * )</span> */ <span class="hljs-deletion">- public function changePassword(Request $request, UserPasswordEncoderInterface $encoder): Response</span> <span class="hljs-deletion">- {</span> <span class="hljs-addition">+ public function changePassword(</span> <span class="hljs-addition">+ HttpFoundation\Request $request,</span> <span class="hljs-addition">+ Security\Core\Encoder\UserPasswordEncoderInterface $encoder</span> <span class="hljs-addition">+ ): HttpFoundation\Response {</span> $user = $this-&gt;getUser(); <span class="hljs-deletion">- $form = $this-&gt;createForm(ChangePasswordType::class);</span> <span class="hljs-addition">+ $form = $this-&gt;createForm(Form\Type\ChangePasswordType::class);</span> $form-&gt;handleRequest($request); if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid()) { </code></pre> <p>We have shortened the list of imports and referenced classes relative to the imported namespace prefixes. We can see immediately the context from which we imported a class. Additionally, we wrapped a few lines. Cognitive load is reduced.</p> <p>Take a look at the <a target="_blank" href="https://github.com/symfony/demo/blob/v1.5.3/src/Entity/Post.php" title="Post"><code>Post</code></a> entity from the same application:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Entity</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Doctrine</span>\<span class="hljs-title">Common</span>\<span class="hljs-title">Collections</span>\<span class="hljs-title">ArrayCollection</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Doctrine</span>\<span class="hljs-title">Common</span>\<span class="hljs-title">Collections</span>\<span class="hljs-title">Collection</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Doctrine</span>\<span class="hljs-title">ORM</span>\<span class="hljs-title">Mapping</span> <span class="hljs-title">as</span> <span class="hljs-title">ORM</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Bridge</span>\<span class="hljs-title">Doctrine</span>\<span class="hljs-title">Validator</span>\<span class="hljs-title">Constraints</span>\<span class="hljs-title">UniqueEntity</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Validator</span>\<span class="hljs-title">Constraints</span> <span class="hljs-title">as</span> <span class="hljs-title">Assert</span>; </code></pre> <p>The list of imports is not so long, but unnecessary aliases are used, and these can be avoided as well:</p> <pre><code class="language-diff hljs diff" data-lang="diff">diff --git a/src/Entity/Post.php b/src/Entity/Post.php index a47a862..a2328fd 100644 <span class="hljs-comment">--- a/src/Entity/Post.php</span> <span class="hljs-comment">+++ b/src/Entity/Post.php</span> <span class="hljs-meta">@@ -11,16 +11,19 @@</span> namespace App\Entity; <span class="hljs-deletion">-use Doctrine\Common\Collections\ArrayCollection;</span> <span class="hljs-deletion">-use Doctrine\Common\Collections\Collection;</span> <span class="hljs-deletion">-use Doctrine\ORM\Mapping as ORM;</span> <span class="hljs-deletion">-use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;</span> <span class="hljs-deletion">-use Symfony\Component\Validator\Constraints as Assert;</span> <span class="hljs-addition">+use Doctrine\Common;</span> <span class="hljs-addition">+use Doctrine\ORM;</span> <span class="hljs-addition">+use Symfony\Bridge\Doctrine;</span> <span class="hljs-addition">+use Symfony\Component\Validator;</span> /** <span class="hljs-deletion">- * @ORM\Entity(repositoryClass="App\Repository\PostRepository")</span> <span class="hljs-deletion">- * @ORM\Table(name="symfony_demo_post")</span> <span class="hljs-deletion">- * @UniqueEntity(fields={"slug"}, errorPath="title", message="post.slug_unique")</span> <span class="hljs-addition">+ * @ORM\Mapping\Entity(repositoryClass="App\Repository\PostRepository")</span> <span class="hljs-addition">+ * @ORM\Mapping\Table(name="symfony_demo_post")</span> <span class="hljs-addition">+ * @Doctrine\Validator\Constraints\UniqueEntity(</span> <span class="hljs-addition">+ * fields={"slug"},</span> <span class="hljs-addition">+ * errorPath="title",</span> <span class="hljs-addition">+ * message="post.slug_unique"</span> <span class="hljs-addition">+ * )</span> */ class Post { @@ -35,88 +38,97 @@ class Post /** * @var int * <span class="hljs-deletion">- * @ORM\Id</span> <span class="hljs-deletion">- * @ORM\GeneratedValue</span> <span class="hljs-deletion">- * @ORM\Column(type="integer")</span> <span class="hljs-addition">+ * @ORM\Mapping\Id</span> <span class="hljs-addition">+ * @ORM\Mapping\GeneratedValue</span> <span class="hljs-addition">+ * @ORM\Mapping\Column(type="integer")</span> */ private $id; /** * @var string * <span class="hljs-deletion">- * @ORM\Column(type="string")</span> <span class="hljs-deletion">- * @Assert\NotBlank</span> <span class="hljs-addition">+ * @ORM\Mapping\Column(type="string")</span> <span class="hljs-addition">+ * @Validator\Constraints\NotBlank</span> */ private $title; /** * @var string * <span class="hljs-deletion">- * @ORM\Column(type="string")</span> <span class="hljs-addition">+ * @ORM\Mapping\Column(type="string")</span> */ private $slug; /** * @var string * <span class="hljs-deletion">- * @ORM\Column(type="string")</span> <span class="hljs-deletion">- * @Assert\NotBlank(message="post.blank_summary")</span> <span class="hljs-deletion">- * @Assert\Length(max=255)</span> <span class="hljs-addition">+ * @ORM\Mapping\Column(type="string")</span> <span class="hljs-addition">+ * @Validator\Constraints\NotBlank(message="post.blank_summary")</span> <span class="hljs-addition">+ * @Validator\Constraints\Length(max=255)</span> */ private $summary; /** * @var string * <span class="hljs-deletion">- * @ORM\Column(type="text")</span> <span class="hljs-deletion">- * @Assert\NotBlank(message="post.blank_content")</span> <span class="hljs-deletion">- * @Assert\Length(min=10, minMessage="post.too_short_content")</span> <span class="hljs-addition">+ * @ORM\Mapping\Column(type="text")</span> <span class="hljs-addition">+ * @Validator\Constraints\NotBlank(message="post.blank_content")</span> <span class="hljs-addition">+ * @Validator\Constraints\Length(</span> <span class="hljs-addition">+ * min=10,</span> <span class="hljs-addition">+ * minMessage="post.too_short_content"</span> <span class="hljs-addition">+ * )</span> */ private $content; /** * @var \DateTime * <span class="hljs-deletion">- * @ORM\Column(type="datetime")</span> <span class="hljs-addition">+ * @ORM\Mapping\Column(type="datetime")</span> */ private $publishedAt; /** * @var User * <span class="hljs-deletion">- * @ORM\ManyToOne(targetEntity="App\Entity\User")</span> <span class="hljs-deletion">- * @ORM\JoinColumn(nullable=false)</span> <span class="hljs-addition">+ * @ORM\Mapping\ManyToOne(targetEntity="App\Entity\User")</span> <span class="hljs-addition">+ * @ORM\Mapping\JoinColumn(nullable=false)</span> */ private $author; /** <span class="hljs-deletion">- * @var Comment[]|ArrayCollection</span> <span class="hljs-addition">+ * @var Comment[]|Common\Collections\ArrayCollection</span> * <span class="hljs-deletion">- * @ORM\OneToMany(</span> <span class="hljs-addition">+ * @ORM\Mapping\OneToMany(</span> * targetEntity="Comment", * mappedBy="post", * orphanRemoval=true, * cascade={"persist"} * ) <span class="hljs-deletion">- * @ORM\OrderBy({"publishedAt": "DESC"})</span> <span class="hljs-addition">+ * @ORM\Mapping\OrderBy({"publishedAt": "DESC"})</span> */ private $comments; /** <span class="hljs-deletion">- * @var Tag[]|ArrayCollection</span> <span class="hljs-addition">+ * @var Tag[]|Common\Collections\ArrayCollection</span> * <span class="hljs-deletion">- * @ORM\ManyToMany(targetEntity="App\Entity\Tag", cascade={"persist"})</span> <span class="hljs-deletion">- * @ORM\JoinTable(name="symfony_demo_post_tag")</span> <span class="hljs-deletion">- * @ORM\OrderBy({"name": "ASC"})</span> <span class="hljs-deletion">- * @Assert\Count(max="4", maxMessage="post.too_many_tags")</span> <span class="hljs-addition">+ * @ORM\Mapping\ManyToMany(</span> <span class="hljs-addition">+ * targetEntity="App\Entity\Tag",</span> <span class="hljs-addition">+ * cascade={"persist"}</span> <span class="hljs-addition">+ * )</span> <span class="hljs-addition">+ * @ORM\Mapping\JoinTable(name="symfony_demo_post_tag")</span> <span class="hljs-addition">+ * @ORM\Mapping\OrderBy({"name": "ASC"})</span> <span class="hljs-addition">+ * @Validator\Constraints\Count(</span> <span class="hljs-addition">+ * max="4",</span> <span class="hljs-addition">+ * maxMessage="post.too_many_tags"</span> <span class="hljs-addition">+ * )</span> */ private $tags; public function __construct() { $this-&gt;publishedAt = new \DateTime(); <span class="hljs-deletion">- $this-&gt;comments = new ArrayCollection();</span> <span class="hljs-deletion">- $this-&gt;tags = new ArrayCollection();</span> <span class="hljs-addition">+ $this-&gt;comments = new Common\Collections\ArrayCollection();</span> <span class="hljs-addition">+ $this-&gt;tags = new Common\Collections\ArrayCollection();</span> } public function getId(): ?int @@ -174,7 +186,7 @@ class Post $this-&gt;author = $author; } <span class="hljs-deletion">- public function getComments(): Collection</span> <span class="hljs-addition">+ public function getComments(): Common\Collections\Collection</span> { return $this-&gt;comments; } @@ -216,7 +228,7 @@ class Post $this-&gt;tags-&gt;removeElement($tag); } <span class="hljs-deletion">- public function getTags(): Collection</span> <span class="hljs-addition">+ public function getTags(): Common\Collections\Collection</span> { return $this-&gt;tags; } </code></pre> <p>Again, we have shortened the list of imports and referenced classes relative to the imported namespace prefixes. We can see immediately the context from which we imported a class. We removed unnecessary aliases Additionally, we wrapped a few lines. Cognitive load is reduced again.</p> <h2>Best practices</h2> <p>The changes I propose here do not conform with the examples found in the documentation for <a target="_blank" href="https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/association-mapping.html#association-mapping" title="Doctrine: Association mapping">Doctrine</a> or <a target="_blank" href="https://symfony.com/doc/current/doctrine.html" title="Symfony: Databases and the Doctrine ORM">Symfony</a>, which might be a problem when you intend to contribute to the core of Doctrine or Symfony or related projects.</p> <p>When you work on other projects, you are free to do whatever you want.</p> <p>Decide for yourself. Find out what works best for you.</p> Quickly switching between PCOV and XdebugAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2020/05/16/quickly-switching-between-pcov-and-xdebug/2020-05-16T12:15:00+02:00<h1> Quickly switching between PCOV and Xdebug </h1> <p>There are at least two PHP extensions I need during the development of a PHP library or application: PCOV and Xdebug.</p> <p><a target="_blank" href="https://github.com/krakjoe/pcov" title="pcov">PCOV</a> is <a target="_blank" href="https://blog.krakjoe.ninja/2019/01/running-for-coverage.html" title="Joe Watkins: Running for Coverage">relatively new</a>. Built by by <a target="_blank" href="https://github.com/krakjoe" title="Joe Watkins">Joe Watkins</a>, it provides fast code coverage for use with <code>phpunit/phpunit</code>.</p> <p><a target="_blank" href="https://xdebug.org" title="Xdebug">Xdebug</a> has been around for <a target="_blank" href="https://derickrethans.nl/xdebug-10.html" title="Derick Rethans: 10 years of Xdebug and Xdebug 2.2.0 released">a while</a>. Built by <a target="_blank" href="https://github.com/derickr">Derick Rethans</a>, it is a development tool that can not only be used for collecting code coverage, but also provides a single-step debugger for use with IDEs, is equipped with a profiler, upgrades <code>var_dump()</code>, and more. Since it does more things differently, it is slower at collecting code coverage.</p> <p>Both are great tools and have their place. When I want to collect code coverage with recent versions of <code>phpunit/phpunit</code>, I use PCOV. When I work on a project with older versions of <code>phpunit/phpunit</code>, I use Xdebug to collect code coverage. When I want to debug production code while running tests in PhpStorm, I use Xdebug.</p> <p>There are drawbacks, however. PCOV does not play well with Xdebug, so I only want one of them to be enabled at a time. Xdebug adds significant overhead when running other tasks in PHP, so I prefer to disable Xdebug when I do not need it.</p> <p>Before I can enable or disable PCOV and Xdebug, I need to install and configure them. I am going to demonstrate the installation of both on PHP 7.4.</p> <h2>Installation of PCOV</h2> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> pecl install pcov</span> </code></pre> <p>and take note of last few lines that are shown when the installation process is finished and the location of <code>pcov.so</code> is revealed (we need it in a moment):</p> <pre><code class="language-plaintext hljs plaintext" data-lang="plaintext">Build process completed successfully Installing '/usr/local/Cellar/php/7.4.6/pecl/20190902/pcov.so' install ok: channel://pecl.php.net/pcov-1.0.6 Extension pcov enabled in php.ini </code></pre> <h2>Configuration of PCOV</h2> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> php --ini</span> </code></pre> <p>to show the names of configuration files loaded by PHP:</p> <pre><code class="language-plaintext hljs plaintext" data-lang="plaintext">Configuration File (php.ini) Path: /usr/local/etc/php/7.4 Loaded Configuration File: /usr/local/etc/php/7.4/php.ini Scan for additional .ini files in: /usr/local/etc/php/7.4/conf.d Additional .ini files parsed: /usr/local/etc/php/7.4/conf.d/ext-opcache.ini </code></pre> <p>Open <code>/usr/local/etc/php/7.4/php.ini</code> and remove</p> <pre><code class="language-diff hljs diff" data-lang="diff"><span class="hljs-deletion">- extension="pcov.so"</span> </code></pre> <p>Create a file <code>/usr/local/etc/php/7.4/conf.d/ext-pcov.ini</code>, but use the exact location of <code>pcov.so</code> we noted earlier:</p> <pre><code class="language-ini hljs ini" data-lang="ini"><span class="hljs-attr">extension</span>=<span class="hljs-string">"/usr/local/Cellar/php/7.4.5_2/pecl/20190902/pcov.so"</span> </code></pre> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> php --ini</span> </code></pre> <p>again and observe that <code>ext-pcov.ini</code> has been loaded:</p> <pre><code class="language-plaintext hljs plaintext" data-lang="plaintext">Configuration File (php.ini) Path: /usr/local/etc/php/7.4 Loaded Configuration File: /usr/local/etc/php/7.4/php.ini Scan for additional .ini files in: /usr/local/etc/php/7.4/conf.d Additional .ini files parsed: /usr/local/etc/php/7.4/conf.d/ext-opcache.ini, /usr/local/etc/php/7.4/conf.d/ext-pcov.ini </code></pre> <h2>Installation of Xdebug</h2> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> pecl install xdebug</span> </code></pre> <p>and take note of the location of <code>xdebug.so</code>:</p> <pre><code class="language-plaintext hljs plaintext" data-lang="plaintext">Build process completed successfully Installing '/usr/local/Cellar/php/7.4.6/pecl/20190902/xdebug.so' install ok: channel://pecl.php.net/xdebug-2.9.5 Extension xdebug enabled in php.ini </code></pre> <h2>Configuration of Xdebug</h2> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> php --ini</span> </code></pre> <p>to show the names of configuration files loaded by PHP:</p> <pre><code class="language-plaintext hljs plaintext" data-lang="plaintext">Configuration File (php.ini) Path: /usr/local/etc/php/7.4 Loaded Configuration File: /usr/local/etc/php/7.4/php.ini Scan for additional .ini files in: /usr/local/etc/php/7.4/conf.d Additional .ini files parsed: /usr/local/etc/php/7.4/conf.d/ext-opcache.ini, /usr/local/etc/php/7.4/conf.d/ext-pcov.ini </code></pre> <p>Open <code>/usr/local/etc/php/7.4/php.ini</code> and remove</p> <pre><code class="language-diff hljs diff" data-lang="diff"><span class="hljs-deletion">- zend_extension="xdebug.so"</span> </code></pre> <p>Create a file <code>/usr/local/etc/php/7.4/conf.d/ext-xdebug.ini</code>, but use the exact location of <code>xdebug.so</code> we noted earlier:</p> <pre><code class="language-ini hljs ini" data-lang="ini"><span class="hljs-attr">zend_extension</span>=<span class="hljs-string">"/usr/local/Cellar/php/7.4.6/pecl/20190902/xdebug.so"</span> </code></pre> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> php --ini</span> </code></pre> <p>again and observe that <code>ext-xdebug.ini</code> has been loaded:</p> <pre><code class="language-plaintext hljs plaintext" data-lang="plaintext">Configuration File (php.ini) Path: /usr/local/etc/php/7.4 Loaded Configuration File: /usr/local/etc/php/7.4/php.ini Scan for additional .ini files in: /usr/local/etc/php/7.4/conf.d Additional .ini files parsed: /usr/local/etc/php/7.4/conf.d/ext-opcache.ini, /usr/local/etc/php/7.4/conf.d/ext-pcov.ini, /usr/local/etc/php/7.4/conf.d/ext-xdebug.ini </code></pre> <h2>Disabling and enabling PCOV and Xdebug</h2> <p>Now that we have installed PCOV and Xdebug, we need a way for disabling and enabling them.</p> <p>The easiest way to disable an extension is to prevent PHP from loading the corresponding configuration file. Since PHP is looking for files ending with a <code>.ini</code> extension, renaming the file seems smart.</p> <p>In <a href="/blog/2020/05/05/switching-between-php-versions-when-using-homebrew/" title="Switching between PHP versions when using Homebrew">Switching between PHP versions when using Homebrew</a>, I collect PHP versions previously installed with Homebrew.</p> <p>Again, I would like to have a command that works regardless of which version of PHP I currently use. I would also prefer to use a short command, especially when I am going to run them often. To achieve this, I have added the following to my <code>.zhsrc</code>:</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">#</span><span class="bash"> Toggle extension</span> function toggle() { name=$1 displayName=${2:-$1} for phpVersion in ${installedPhpVersions[*]}; do config="/usr/local/etc/php/${phpVersion}/conf.d/ext-${name}.ini" if [[ -f "${config}" ]]; then mv "${config}" "${config}.bak" elif [[ -f "${config}.bak" ]]; then mv "${config}.bak" "${config}" fi done php -v if [[ $(php -m | grep ${name}) ]]; then echo "${displayName} extension has been enabled." else echo "${displayName} extension has been disabled." fi } </code></pre> <p>This function expects a single argument, the name of an extension, and optionally a second argument, the <a target="_blank" href="https://twitter.com/derickr/status/1184465999971700736" title="Derick Rethans: What's the correct spelling and capitalisation of Xdebug?">display name</a>. The function iterates over all installed PHP versions, and either adds a <code>.bak</code> extension to the name of a matching configuration file, or removes it - effectively disabling or enabling the corresponding extension.</p> <p>Now I could run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> toggle pcov</span> </code></pre> <p>or</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> toggle xdebug</span> </code></pre> <p>but to be honest, that is still too much work. I have added the following to <code>.zshrc</code> instead:</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">#</span><span class="bash"> Toggle PCOV</span> function p () { toggle pcov PCOV } <span class="hljs-meta"> #</span><span class="bash"> Toggle Xdebug</span> function x () { toggle xdebug Xdebug } </code></pre> <p>Thanks to these small functions, I can run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> p</span> </code></pre> <p>and</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> x</span> </code></pre> <p>to quickly disable or enable PCOV and Xdebug.</p> Using Makefiles in projects where I can not use themAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2020/05/07/using-makefiles-in-projects-where-i-can-not-use-them/2020-05-07T09:45:00+02:00<h1> Using Makefiles in projects where I can not use them </h1> <p>In <a href="/blog/2018/01/24/makefile-for-lazy-developers/" title="Makefile for lazy developers">Makefile for lazy developers</a>, I have shared how I use <code>Makefile</code>s to save time running frequent tasks.</p> <p>As a maintainer of a project, I can use any tool I like to get the job done, and of course, I use Makefiles in all of these projects.</p> <p>As a collaborator of a project, I do not have that luxury. I depend on the owners of a project. Some projects already use an alternative, perhaps <a target="_blank" href="https://getcomposer.org/doc/articles/scripts.md" title="Composer Scripts">composer scripts</a> or <a target="_blank" href="https://www.phing.info" title="PHing">PHing</a>. Occasionally I have suggested the use of <code>Makefile</code>s. Often the owners have stated that they prefer not to use <code>Makefile</code>s. Rarely the owners already have a <code>Makefile</code> but object to modifications.</p> <p>I respect that. However, I can still use <code>Makefile</code>s the way I like in every single project, and as a matter of fact, I do.</p> <p>In <a href="/blog/2019/10/25/project-notes/" title="Project notes">Project notes</a>, I have shared how I keep files in a <code>.notes</code> directory within the root directory of a project - without the need to check them into version control. This directory is where I put a <code>Makefile</code> when necessary.</p> <p>I can now run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> make -f .notes/Makefile</span> </code></pre> <p>to run the first target of the <code>Makefile</code>, but I find that this is too much work.</p> <p>Instead, I have adjusted the alias I previously used from</p> <pre><code class="language-shell hljs shell" data-lang="shell">alias m="make" </code></pre> <p>to</p> <pre><code class="language-shell hljs shell" data-lang="shell">alias m="if [[ -f .notes/Makefile ]]; then make -f .notes/Makefile; else make; fi" </code></pre> <p>Now I can run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> m</span> </code></pre> <p>in every single project and still use <code>Makefile</code>s the way I prefer.</p> Switching between PHP versions when using HomebrewAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2020/05/05/switching-between-php-versions-when-using-homebrew/2020-05-05T13:20:00+02:00<h1> Switching between PHP versions when using Homebrew </h1> <p>Do you use <a target="_blank" href="https://brew.sh" title="Homebrew">Homebrew</a> to install multiple versions of PHP on macOS? Do you often need to switch between PHP versions? Do you prefer short commands, especially when you need to run them often?</p> <p>Since I maintain and contribute to a range of <a href="/open-source/" title="Open Source">open-source projects</a>, I certainly do! These projects often have different PHP version requirements. For running development tools in the terminal, I need to switch between these versions quickly.</p> <p>Currently, when I want to switch from PHP 7.1 to PHP 7.4., I need to run the following commands:</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> brew unlink php@7.1</span> <span class="hljs-meta">$</span><span class="bash"> brew link php@7.4 --force --overwrite</span> </code></pre> <p>Hmm. I need to know what the currently enabled version is, unlink it, then link PHP 7.4. That is a bit much.</p> <p>What about running</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> 7.4</span> </code></pre> <p>to switch to PHP 7.4, regardless of which version of PHP is currently enabled?</p> <p>That would be better, right?</p> <p>In the following, I will show how to achieve that.</p> <p>First, we are going to determine which versions of PHP we installed on our system. Then we are going to create an alias for each. The alias will unlink all other versions, and then link the one we want to use.</p> <div class="note"> <div class="note-body"> <p>💡 Because I use <a target="_blank" href="https://ohmyz.sh" title="Oh My Zsh: Your terminal never felt this good before.">oh-my-zsh</a>, I will add the script to <code>~/.zshrc</code>.</p> </div> </div> <h2> Determine PHP versions </h2> <p>The first option could be to hard-code the versions we know into an array:</p> <pre><code class="language-shell hljs shell" data-lang="shell">installedPhpVersions=('7.1' '7.2' '7.3' '7.4') </code></pre> <p>When we install or uninstall a minor PHP version, we need to update this variable manually. Otherwise, we risk creating aliases that will not work, or miss creating aliases that we need.</p> <p>If you can live with that, you can skip to the next section.</p> <p>Another option could be to hard-code the names of brew formulae used to install PHP, and then use <code>brew ls</code> to verify whether we installed a particular formula:</p> <pre><code class="language-shell hljs shell" data-lang="shell">installedPhpVersions=() brewFormulas=('php@7.1' 'php@7.2' 'php@7.3' 'php@7.4') for brewFormula in ${brewFormulas[*]}; do if [[ -n "$(brew ls --versions "$brewFormula")" ]]; then phpVersion=${brewFormula#php@} installedPhpVersions+=("$phpVersion") fi done </code></pre> <p>We now avoid creating aliases for versions of PHP that we have not previously installed on our system. This script, however, will run every time we open a new terminal. Since executing <code>brew ls</code> takes a bit of time, and we want to get to work quickly, this is not ideal. We also need to update the array of formulae whenever a new major or minor version of PHP comes out. That does not happen very often. Still, it would be nice to avoid manual work.</p> <p>The next option would be to run <code>brew ls</code> once, and obtain the PHP versions from the output:</p> <pre><code class="language-shell hljs shell" data-lang="shell">installedPhpVersions=($(brew ls --versions | ggrep -E 'php(@.*)?\s' | ggrep -oP '(?&lt;=\s)\d\.\d' | uniq | sort)) </code></pre> <p>A lot better, right?</p> <p>We do not have to worry about updating a hard-coded list, and a new terminal will open a lot quicker.</p> <div class="note"> <div class="note-body"> <p>💡 The use of <code>ggrep</code> instead of <code>grep</code> is not an accident.</p> <p>The version of <code>grep</code> that ships with macOS has a smaller feature set, so I installed <code>grep</code> with Homebrew. This version uses the prefix <code>g</code> to avoid conflicts.</p> </div> </div> <h2> Create aliases </h2> <p>Now we can create the aliases:</p> <pre><code class="language-shell hljs shell" data-lang="shell">installedPhpVersions=($(brew ls --versions | ggrep -E 'php(@.*)?\s' | ggrep -oP '(?&lt;=\s)\d\.\d' | uniq | sort)) for phpVersion in ${installedPhpVersions[*]}; do value="{" for otherPhpVersion in ${installedPhpVersions[*]}; do if [ "${otherPhpVersion}" != "${phpVersion}" ]; then value="${value} brew unlink php@${otherPhpVersion};" fi done value="${value} brew link php@${phpVersion} --force --overwrite; } &amp;&gt; /dev/null &amp;&amp; php -v" alias "${phpVersion}"="${value}" done </code></pre> <p>With this script in place, when I open a new terminal on my system and run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> <span class="hljs-built_in">alias</span> | grep php</span> </code></pre> <p>I can see that the script created the following aliases:</p> <pre><code class="language-plaintext hljs plaintext" data-lang="plaintext">7.1='{ brew unlink php@7.2; brew unlink php@7.3; brew unlink php@7.4; brew link php@7.1 --force --overwrite; } &amp;&gt; /dev/null &amp;&amp; php -v' 7.2='{ brew unlink php@7.1; brew unlink php@7.3; brew unlink php@7.4; brew link php@7.2 --force --overwrite; } &amp;&gt; /dev/null &amp;&amp; php -v' 7.3='{ brew unlink php@7.1; brew unlink php@7.2; brew unlink php@7.4; brew link php@7.3 --force --overwrite; } &amp;&gt; /dev/null &amp;&amp; php -v' 7.4='{ brew unlink php@7.1; brew unlink php@7.2; brew unlink php@7.3; brew link php@7.4 --force --overwrite; } &amp;&gt; /dev/null &amp;&amp; php -v' </code></pre> <h2> Switch </h2> <p>To switch from any version of PHP, we now only need to run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> 7.4</span> </code></pre> <p>I hope it helps!</p> From @localheinz to @ergebnisAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2019/12/10/from-localheinz-to-ergebnis/2019-12-10T16:40:00+01:00<h1> From @localheinz to @ergebnis </h1> <p>With 2019 in review and 2020 coming closer, I am sorting out a few things related to open-source projects I am maintaining.</p> <h2> Move </h2> <p>I have decided to move the open-source PHP projects I am currently maintaining on my personal account <a target="_blank" href="https://github.com/localheinz" title="@localheinz on GitHub"><code>@localheinz</code></a> to the organization <a target="_blank" href="https://github.com/ergebnis" title="@ergebnis on GitHub"><code>@ergebnis</code></a> on <a target="_blank" href="https://github.com" title="GitHub">GitHub</a>.</p> <p>The following projects have been moved so far:</p> <ul> <li> <a target="_blank" href="https://github.com/ergebnis/classy" title="ergebnis/classy"><code>ergebnis/classy</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/clock" title="ergebnis/clock"><code>ergebnis/clock</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/composer-normalize" title="ergebnis/composer-normalize"><code>ergebnis/composer-normalize</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/composer-normalize-action" title="ergebnis/composer-normalize-action"><code>ergebnis/composer-normalize-action</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/factory-girl-definition" title="ergebnis/factory-girl-definition"><code>ergebnis/factory-girl-definition</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/factory-muffin-definition" title="ergebnis/factory-muffin-definition"><code>ergebnis/factory-muffin-definition</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/faker-provider" title="ergebnis/faker-provider"><code>ergebnis/faker-provider</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/github-changelog" title="ergebnis/github-changelog"><code>ergebnis/github-changelog</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/http-method" title="ergebnis/http-method"><code>ergebnis/http-method</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/json-normalizer" title="ergebnis/json-normalizer"><code>ergebnis/json-normalizer</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/json-printer" title="ergebnis/json-printer"><code>ergebnis/json-printer</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/php-cs-fixer-config" title="ergebnis/php-cs-fixer-config"><code>ergebnis/php-cs-fixer-config</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/php-library-template" title="ergebnis/php-library-template"><code>ergebnis/php-library-template</code></a> </li> <li> <a target="_blank" href="https://github.com/ergebnis/phpunit-framework-constraint" title="ergebnis/phpunit-framework-constraint"><code>ergebnis/phpunit-framework-constraint</code></a> </li> </ul> <h2> Why </h2> <p>At the moment, I have ~350 public repositories under my personal account. Most of these repositories are <em>forks</em> of projects I have contributed to in the past, and only a few of them are <em>sources</em>.</p> <p>By moving the <em>source</em> repositories to a separate organization:</p> <ul> <li>they become more visible to me and can be more easily be discovered by others</li> <li>they become less attached to my person and maintenance can be can be more easily transferred to collaborators when necessary</li> <li>adding and managing maintainers or collaborators becomes a lot easier</li> </ul> <p>If you are using any of the packages provided by these repositories: thank you! I am happy that I - standing on the shoulders of giants - have built something useful for others.</p> <p>In any case, take a look at <code>CHANGELOG.md</code>; I have provided instructions for updating. In most cases, the only things that have changed are vendor prefixes (from <code>localheinz</code> to <code>ergebnis</code>) and namespaces (from <code>Localheinz</code> to <code>Ergebnis</code>).</p> <h2> How </h2> <p>Perhaps you have been considering to do something similar?</p> <p>Here is an example of how I moved <code>localheinz/test-util</code> to <code>ergebnis/test-util</code>:</p> <ol> <li> <a target="_blank" href="https://help.github.com/en/github/administering-a-repository/transferring-a-repository" title="GitHub Help: Transferring a repository">transfer ownership</a> </li> <li>rename references <code>localheinz/test-util</code> to <code>ergebnis/test-util</code> </li> <li>rename namespace <code>Localheinz\Test\Util</code> to <code>Ergebnis\Test\Util</code> </li> <li>provide details on how to update in <a target="_blank" href="https://github.com/ergebnis/test-util/blob/master/CHANGELOG.md#090" title="Changelog for ergebnis/test-util"><code>CHANGELOG.md</code></a> </li> <li>submit package <code>ergebnis/test-util</code> to <a target="_blank" href="https://packagist.org/packages/submit" title="Packagist: Submit package">Packagist</a> </li> <li>ensure that <a target="_blank" href="https://packagist.org/about#how-to-update-packages" title="Packagist: How to update packages">integration with Packagist</a> works</li> <li>abandon package <a target="_blank" href="https://packagist.org/packages/localheinz/test-util" title="localheinz/test-util on Packagist"><code>localheinz/test-util</code></a> on Packagist and suggest <code>ergebnis/test-util</code> as a replacement</li> <li>tag and push a new major release of <code>ergebnis/test-util</code> </li> <li>update packages previously using <code>localheinz/test-util</code> to using <code>ergebnis/test-util</code>, for example, in <a target="_blank" href="https://github.com/ergebnis/phpstan-rules/pull/154" title="Enhancement: Use ergebnis/test-util instead of localheinz/test-util"><code>ergebnis/phpstan-rules</code></a> </li> <li>keep fingers crossed 🤞</li> </ol> <h2> Archive </h2> <p>At the same time, I am archiving a few projects that I have never brought to a stage that makes sense keeping them around:</p> <ul> <li> <a target="_blank" href="https://github.com/localheinz/composer-json-normalizer" title="localheinz/composer-json-normalizer"><code>localheinz/composer-json-normalizer</code></a> </li> <li> <a target="_blank" href="https://github.com/localheinz/composer-require-checker-action" title="localheinz/composer-require-checker-action"><code>localheinz/composer-require-checker-action</code></a> </li> <li> <a target="_blank" href="https://github.com/localheinz/data-structure" title="localheinz/data-structure"><code>localheinz/data-structure</code></a> </li> <li> <a target="_blank" href="https://github.com/localheinz/github-pulse" title="localheinz/github-pulse"><code>localheinz/github-pulse</code></a> </li> <li> <a target="_blank" href="https://github.com/localheinz/specification" title="localheinz/specification"><code>localheinz/specification</code></a> </li> <li> <a target="_blank" href="https://github.com/localheinz/token" title="localheinz/token"><code>localheinz/token</code></a> </li> </ul> <p>If you find the transition from <a target="_blank" href="https://github.com/localheinz" title="@localheinz on GitHub"><code>@localheinz</code></a> to <a target="_blank" href="https://github.com/ergebnis" title="@ergebnis on GitHub"><code>@ergebnis</code></a> difficult, please accept my apologies.</p> <p>If you need any help, let me know, and I will see what I can do.</p> The grass could be a lot greener on both sides of the fenceAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2019/12/03/the-grass-could-be-a-lot-greener-on-both-sides-of-the-fence/2019-12-03T12:00:00+02:00<h1> The grass could be a lot greener on both sides of the fence </h1> <p>It is the end of 2019, and the invitation to write an article for 24 Days in December is an excellent opportunity to reflect on what has happened this year - and maybe even look back a bit further.</p> <h2>Past</h2> <p>In 1990 I wrote my first line of code on an Atari Portfolio, and at the time, I would have never guessed that one day I would be building and maintaining software for a living.</p> <p>In 1999 I got paid the first time for building a website with static HTML.</p> <p>In 2001 I took up an internship in a startup in Berlin, and there I would write my first line of PHP code. In 2007, shortly before dropping out of business school, I started working full-time with PHP in another startup. In the years to follow, I worked for several startups, always with PHP.</p> <p>In 2012 I found out that PHP developers meet up monthly to give and listen to talks about their experiences at so-called user groups, and I first attended the <a target="_blank" href="https://www.bephpug.de" title="BEPHPUG">Berlin PHP user group</a>.</p> <p>From writing my first line of PHP code in 2001 to attending my first user group in 2012, it took me eleven years to realize that I am part of an actual community! A community of people who not necessarily work in the same companies but a community of people who share their experiences; people who are eager to learn from and eventually help each other to become better developers!</p> <p>In the months and years to follow, a lot of things changed for me. Most of all, seeing that there is a community out there, and that it is easy to become a part of it, made me want to become a better developer.</p> <p>Soon I started following other developers on Twitter. I made my first contributions to open-source software. I attended my first PHP conference. Attending the user groups more or less regularly, I got to know other developers, some of whom introduced me to other people in the PHP community. I took note of books recommended by speakers, and added them to an ever-growing reading list. I overcame my fear of writing tests. Eventually, I started writing tests first. I continued to make contributions to open-source software. I began working for a company with an actual build pipeline and a process that I still admire today. I overcame framework fanboy-ism and got hired over Twitter to work remotely for a company in New York. I attended conferences more regularly. Seeing a lot of developers over and over again, I found it rather easy to get in touch with them - and was surprised that they are very approachable. I became acknowledged with more and more developer tools - tools that made my life as a PHP developer a lot easier. I started contributing to these tools, and even published a few small open-source libraries that have users other than me. I came around to writing blog posts, and at the moment, I’m struggling with writing this article here.</p> <p>Probably none of this would have happened if I had never attended the meetings of the PHP user group - none of it. Up until the moment when I first joined the user group meeting, I was entirely concerned with <i>making things run</i>. By going to these meetings, I had opened a new chapter and began wondering how to <i>make things right</i>.</p> <h2>Present</h2> <p>In the last five years, the focus of my work has shifted from creating to maintaining and modernizing legacy applications. Thanks to excellent tooling experiences made, dealing with legacy applications is not a hard problem.</p> <p>What I have come to realize in 2019, but have heard numerous time before, is, that people are the hard problems - just as Jerry Weinberg, who passed away in August 2018, put it in <a target="_blank" href="https://leanpub.com/b/secretsofconsulting" title="The Secrets of Consulting">The Secrets of Consulting</a>:</p> <blockquote> No matter how it looks at first, it’s always a people problem. <footer> <cite><a target="_blank" href="https://twitter.com/jerryweinberg" title="Gerald M. Weinberg">Gerald M. Weinberg</a></cite> </footer> </blockquote> <p>Setting up a build pipeline, putting developer tools in place, refining a process, making it harder for developers to ship faulty code - this is all excellent.</p> <p>Working <em>on code</em>, however, has only short-term effects: as soon as the project is over, developers are left alone with a code base they hardly understand. When developers have only learned to follow the rules enforced by automated systems, but have not learned why they have been put in place, these automated systems become an annoyance rather than a crutch.</p> <p>Working <em>with developers</em>, on the other hand, can have long-term effects: by asking developers questions instead of giving them directions, by letting developers fail and helping them up again, by showing them alternative ways of developing software, and by encouraging them to question the status quo, they will eventually become better versions of themselves.</p> <h2>Future</h2> <p>Perhaps you are working on a legacy code base, with colleagues who do not care so much. Perhaps you are looking for a new job already. Of course, the grass always looks greener on the other side - but is it?</p> <p>For some of your colleagues, being a software developer is just a 9-to-5 job. Others have already so much on their plate, maybe they really cannot and will not be able to do much more. However, there will always be interested developers who only need a little nudge or a pointer.</p> <p>You could be the person reaching out, go ahead and invite them to the next <a target="_blank" href="https://php.ug" title="PHP user group">user group</a>.</p> <div class="note"> <div class="note-body"> <p>This article was originally published on <a target="_blank" href="https://24daysindecember.net/2019/12/03/the-grass-could-be-a-lot-greener-on-both-sides-of-the-fence/" title="24 Days in December">24 Days in December</a>.</p> </div> </div> Project notesAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2019/10/25/project-notes/2019-10-25T07:55:00+02:00<h1> Project notes </h1> <p>In almost every project I work on, I take notes in Markdown files, keep track of small lists of things that need to be done, or create small scripts - and most of these are not important enough to find their way into a journal, an issue tracker, or version control.</p> <p>I have observed developers who keep these files untracked within or in a separate directory outside of the project. I keep these files within the project. This way, they are always in reach.</p> <p>In 2014, <a target="_blank" href="https://blog.jetbrains.com/idea/2014/09/intellij-idea-14-eap-138-2210-brings-scratch-files-and-better-mercurial-integration/" title="Jetbrains: IntelliJ IDEA 14 EAP 138.2210 Brings Scratch Files and Better Mercurial Integration">Jetbrains introduced</a> the <a target="_blank" href="https://www.jetbrains.com/help/phpstorm/scratches.html" title="PhpStorm: Scratch Files">Scratch Files</a> feature.</p> <p>However, there's something better that you might have been using already and does not depend on features provided by your IDE: a <a target="_blank" href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_core_excludesfile" title="Git Configuration: core.excludesfile">global <code>.gitignore</code> file</a>.</p> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> touch ~/.gitignore_global</span> </code></pre> <p>to create a <code>.gitignore_global</code> file in your home directory.</p> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> git config --global core.excludesfile ~/.gitignore_global</span> </code></pre> <p>to configure <code>git</code> to use <code>.gitignore_global</code> as a global exludes file.</p> <p>Add the following</p> <pre><code class="language-diff hljs diff" data-lang="diff"> .idea/ <span class="hljs-addition">+.notes/</span> .DS_Store </code></pre> <p>to to <code>.gitignore_global</code> to ignore a <code>.notes/</code> directory in any project that is under version control with <code>git</code>.</p> <p>For example, for a project I'm currently working on, my <code>.notes/</code> directory currently looks like this:</p> <pre><code class="language-plaintext hljs plaintext" data-lang="plaintext">.notes ├── XYZ-123 # sub-directory for issue XYZ-123 │ ├── commits.md # list of commit hashes kept as a backup │ ├── test.php # small script for prototyping │ └── todo.md # list of todos for a ticket in Markdown ├── todo.md # list of general todos in Markdown └── XYZ-234.md # notes regarding issue XYZ-234 </code></pre> <p>Hope it helps!</p> Test to the left, production to the rightAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2019/10/14/test-to-the-left-production-to-the-right/2019-10-14T06:40:00+02:00<h1> Test to the left, production to the right </h1> <p>Not only do I speak and read languages where texts are written and read from left-to-right and top-to-bottom, but I also work with programming languages where programs are written in the same order.</p> <p>I'm also a proponent of Test-Driven Development (TDD), a practice that suggests following <a target="_blank" href="http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd" title="Uncle Bob: The three rules of TDD">three simple rules</a>:</p> <ol> <li>You are not allowed to write any production code unless it is to make a failing unit test pass.</li> <li>You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.</li> <li>You are not allowed to write any more production code than is sufficient to pass the one failing unit test.</li> </ol> <p>Consequently, it only feels natural to me to split the editor screen vertically, with</p> <ul> <li>the more important test code on the left side</li> <li>the less important production code on the right side</li> </ul> <figure> <img src="/dist/img/post/test-code-to-the-left-production-code-to-the-right/split-vertically.png?3ede256" alt="Test code to the left, production code to the right"> </figure> <p>In JetBrains IDEs this can be achieved by right-clicking on the editor tab and then selecting <strong>Split Vertically</strong> (find out more at <a target="_blank" href="https://www.jetbrains.com/help/phpstorm/using-code-editor.html#split_screen" title="Editor Basics: Split Screen">Editor Basics: Split Screen</a>.</p> <p>How do you organize yourself working with test and production code?</p> Running tests for PHPUnit itself from within PhpStormAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2019/04/27/running-tests-for-phpunit-itself-from-within-phpstorm/2019-04-27T11:01:00+01:00<h1> Running tests for PHPUnit itself from within PhpStorm </h1> <p>Have you been working on <a target="_blank" href="https://github.com/sebastianbergmann/phpunit" title="phpunit/phpunit"><code>phpunit/phpunit</code></a>? Are you a user of <a target="_blank" href="https://www.jetbrains.com/phpstorm/" title="PhpStorm">PhpStorm</a>?</p> <p>I have and I am, and until yesterday I have been struggling with setting up <code>phpunit/phpunit</code> as test framework for PhpStorm so I can run tests for <code>phpunit/phpunit</code> from within PhpStorm for <code>phpunit/phpunit</code> itself.</p> <h3>Use Composer autoloader</h3> <p>In any project that requires <code>phpunit/phpunit</code> as a development dependency, the setup is fairly simple: pick the <strong>Use Composer autoloader</strong> option and reference the path to <code>vendor/autoload.php</code>.</p> <p>However, for <code>phpunit/phpunit</code> this will not work, as it does not have a dependency on itself:</p> <figure> <img src="/dist/img/post/running-tests-for-phpunit-from-within-phpstorm/01-use-composer-autoloader.png" alt="Use composer autoloader"> </figure> <h3>Path to phpunit.phar (with downloaded phpunit.phar)</h3> <p>Ok, then, let's try the option <strong>Path to phpunit.phar</strong>!</p> <p>Looks like we need to download <code>phpunit.phar</code> here, so let's head over to <a target="_blank" href="https://phpunit.de/getting-started/phpunit-8.html" title="Getting Started with PHPUnit 8">Getting Started with PHPUnit 8</a> and find out how to download phpunit.phar`.</p> <p>Personally, I ran</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> wget -O ~/Sites/phpunit.phar https://phar.phpunit.de/phpunit.phar</span> <span class="hljs-meta">$</span><span class="bash"> chmod +x ~/Sites/phpunit.phar</span> </code></pre> <p>Next, I pick the <strong>Path to phpunit.phar</strong> option and specify the path to the previously downloaded <code>phpunit.phar</code>:</p> <figure> <img src="/dist/img/post/running-tests-for-phpunit-from-within-phpstorm/02-path-to-phpunit-phar-with-downloaded-phar.png?3ede256" alt="Path to phpunit.phar (with downloaded phpunit.phar)"> </figure> <p>Looks like that worked fine, now let's run the tests by right-clicking on <code>phpunit.xml</code>, and selecting the option <strong>Run 'phpunit.xml'</strong>.</p> <p>Great, the tests run! But they fail with</p> <pre><code class="language-plaintext hljs plaintext" data-lang="plaintext">Testing started at 11:25 ... /usr/local/Cellar/php@7.2/7.2.17_1/bin/php /Users/am/Sites/phpunit --configuration /Users/am/Sites/sebastianbergmann/phpunit/phpunit.xml --teamcity PHPUnit 8.1.3 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.17 with Xdebug 2.7.1 Configuration: /Users/am/Sites/sebastianbergmann/phpunit/phpunit.xml TypeError : Return value of PHPUnit\Framework\Constraint\Count::getCountOf() must be of the type integer or null, none returned phar:///Users/am/Sites/phpunit/phpunit/Framework/Constraint/Count.php:86 phar:///Users/am/Sites/phpunit/phpunit/Framework/Constraint/Count.php:44 phar:///Users/am/Sites/phpunit/phpunit/Framework/Constraint/Constraint.php:45 /Users/am/Sites/sebastianbergmann/phpunit/tests/unit/Framework/Constraint/CountTest.php:164 phar:///Users/am/Sites/phpunit/phpunit/Framework/TestCase.php:1172 phar:///Users/am/Sites/phpunit/phpunit/Framework/TestCase.php:854 phar:///Users/am/Sites/phpunit/phpunit/Framework/TestResult.php:685 phar:///Users/am/Sites/phpunit/phpunit/Framework/TestCase.php:808 phar:///Users/am/Sites/phpunit/phpunit/Framework/TestSuite.php:761 phar:///Users/am/Sites/phpunit/phpunit/Framework/TestSuite.php:761 phar:///Users/am/Sites/phpunit/phpunit/Framework/TestSuite.php:761 phar:///Users/am/Sites/phpunit/phpunit/TextUI/TestRunner.php:601 phar:///Users/am/Sites/phpunit/phpunit/TextUI/Command.php:207 phar:///Users/am/Sites/phpunit/phpunit/TextUI/Command.php:163 PHP Fatal error: Class Mock_Exception_19a384fc contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (ExceptionWithThrowable::getAdditionalInformation) in phar:///Users/am/Sites/phpunit/phpunit/Framework/MockObject/Generator.php(608) : eval()'d code on line 1 PHP Stack trace: PHP 1. {main}() /Users/am/Sites/phpunit:0 PHP 2. PHPUnit\TextUI\Command::main() /Users/am/Sites/phpunit:619 PHP 3. PHPUnit\TextUI\Command-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/TextUI/Command.php:163 PHP 4. PHPUnit\TextUI\TestRunner-&gt;doRun() phar:///Users/am/Sites/phpunit/phpunit/TextUI/Command.php:207 PHP 5. PHPUnit\Framework\TestSuite-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/TextUI/TestRunner.php:601 PHP 6. PHPUnit\Framework\TestSuite-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestSuite.php:761 PHP 7. PHPUnit\Framework\TestSuite-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestSuite.php:761 PHP 8. GeneratorTest-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestSuite.php:761 PHP 9. PHPUnit\Framework\TestResult-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestCase.php:808 PHP 10. GeneratorTest-&gt;runBare() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestResult.php:685 PHP 11. GeneratorTest-&gt;runTest() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestCase.php:854 PHP 12. GeneratorTest-&gt;testMockingOfThrowable() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestCase.php:1172 PHP 13. PHPUnit\Framework\MockObject\Generator-&gt;getMock() /Users/am/Sites/sebastianbergmann/phpunit/tests/unit/Framework/MockObject/GeneratorTest.php:190 PHP 14. PHPUnit\Framework\MockObject\Generator-&gt;getObject() phar:///Users/am/Sites/phpunit/phpunit/Framework/MockObject/Generator.php:196 PHP 15. PHPUnit\Framework\MockObject\Generator-&gt;evalClass() phar:///Users/am/Sites/phpunit/phpunit/Framework/MockObject/Generator.php:562 PHP 16. eval() phar:///Users/am/Sites/phpunit/phpunit/Framework/MockObject/Generator.php:608 Fatal error: Class Mock_Exception_19a384fc contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (ExceptionWithThrowable::getAdditionalInformation) in phar:///Users/am/Sites/phpunit/phpunit/Framework/MockObject/Generator.php(608) : eval()'d code on line 1 Call Stack: 0.0057 543992 1. {main}() /Users/am/Sites/phpunit:0 0.1437 9827856 2. PHPUnit\TextUI\Command::main() /Users/am/Sites/phpunit:619 0.1437 9827968 3. PHPUnit\TextUI\Command-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/TextUI/Command.php:163 0.4764 23119992 4. PHPUnit\TextUI\TestRunner-&gt;doRun() phar:///Users/am/Sites/phpunit/phpunit/TextUI/Command.php:207 0.7250 23869248 5. PHPUnit\Framework\TestSuite-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/TextUI/TestRunner.php:601 0.7466 23869440 6. PHPUnit\Framework\TestSuite-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestSuite.php:761 3.7839 25022328 7. PHPUnit\Framework\TestSuite-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestSuite.php:761 3.8201 25324600 8. GeneratorTest-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestSuite.php:761 3.8201 25324600 9. PHPUnit\Framework\TestResult-&gt;run() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestCase.php:808 3.8202 25324600 10. GeneratorTest-&gt;runBare() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestResult.php:685 3.8204 25341232 11. GeneratorTest-&gt;runTest() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestCase.php:854 3.8204 25341288 12. GeneratorTest-&gt;testMockingOfThrowable() phar:///Users/am/Sites/phpunit/phpunit/Framework/TestCase.php:1172 3.8204 25341288 13. PHPUnit\Framework\MockObject\Generator-&gt;getMock() /Users/am/Sites/sebastianbergmann/phpunit/tests/unit/Framework/MockObject/GeneratorTest.php:190 3.8209 25344136 14. PHPUnit\Framework\MockObject\Generator-&gt;getObject() phar:///Users/am/Sites/phpunit/phpunit/Framework/MockObject/Generator.php:196 3.8209 25344136 15. PHPUnit\Framework\MockObject\Generator-&gt;evalClass() phar:///Users/am/Sites/phpunit/phpunit/Framework/MockObject/Generator.php:562 3.8211 25350656 16. eval('class Mock_Exception_19a384fc extends Exception implements ExceptionWithThrowable, PHPUnit\Framework\MockObject\MockObject { private $__phpunit_invocationMocker; private $__phpunit_originalObject; private $__phpunit_configurable = []; private $__phpunit_returnValueGeneration = true; public function expects(\PHPUnit\Framework\MockObject\Matcher\Invocation $matcher): \PHPUnit\Framework\MockObject\Builder\InvocationMocker { return $this-&gt;__phpunit_getInvocationMocker()-&gt;expects($matcher); } public function method() { $any = new \PHPUnit\Framework\MockObject\Matcher\AnyInvokedCount; $expects = $this-&gt;expects($any); return call_user_func_array([$expects, 'method'], func_get_args()); } public function __phpunit_setOriginalObject($originalObject): void { $this-&gt;__phpunit_originalObject = $originalObject; } public function __phpunit_setReturnValueGeneration(bool $returnValueGeneration): void { $this-&gt;__phpunit_returnValueGeneration = $returnValueGeneration; } public function __phpunit_getInvocationMocker(): \PHPUnit\Framework\MockObject\InvocationMocker { if ($this-&gt;__phpunit_invocationMocker === null) { $this-&gt;__phpunit_invocationMocker = new \PHPUnit\Framework\MockObject\InvocationMocker($this-&gt;__phpunit_configurable, $this-&gt;__phpunit_returnValueGeneration); } return $this-&gt;__phpunit_invocationMocker; } public function __phpunit_hasMatchers(): bool { return $this-&gt;__phpunit_getInvocationMocker()-&gt;hasMatchers(); } public function __phpunit_verify(bool $unsetInvocationMocker = true): void { $this-&gt;__phpunit_getInvocationMocker()-&gt;verify(); if ($unsetInvocationMocker) { $this-&gt;__phpunit_invocationMocker = null; } } } ') phar:///Users/am/Sites/phpunit/phpunit/Framework/MockObject/Generator.php:608 Process finished with exit code 255 </code></pre> <p>Looks like we have some issues here because we are using a <em>different</em> version of <code>phpunit/phpunit</code> for testing the version of <code>phpunit/phpunit</code> we have currently checked out.</p> <h3>Path to phpunit.phar (with phpunit binary)</h3> <p>While not obvious, there's a rather simple solution that was suggested by <a target="_blank" href="https://github.com/nickel715" title="Nicolas Hohm">Nicolas Hohm</a>: pick the <strong>Path to phpunit.phar</strong> option and specify the path the <code>phpunit</code> binary in the root of the project!</p> <figure> <img src="/dist/img/post/running-tests-for-phpunit-from-within-phpstorm/03-path-to-phpunit-phar-with-phpunit-binary.png?3ede256" alt="Path to phpunit.phar (with phpunit binary)"> </figure> <p>Running the tests by right-clicking on <code>phpunit.xml</code>, and selecting the option <strong>Run 'phpunit.xml'</strong> now works like a charm!</p> <p>Even better, when enabling <a target="_blank" href="https://xdebug.org" title="Xdebug">Xdebug</a> and selecting the option <strong>Run 'phpunit.xml' with Coverage</strong>, we can now easily identify areas of the code base which are obviously lacking tests - something we have been doing yesterday and today here at <a target="_blank" href="https://www.flyeralarm.com/de/" title="FLYERALARM">FLYERALARAM</a>, where the 5th <a target="_blank" href="https://phpunit.de/code-sprints/index.html" title="PHPUnit Code Sprint">PHPUnit Code Sprint</a> has been taking place.</p> <p>Hope it helps!</p> System-wide terminal accessible via hotkey on macOSAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2018/10/02/system-wide-terminal-accessible-via-hotkey-on-macos/2018-10-02T11:05:00+01:00<h1> System-wide terminal accessible via hotkey on macOS </h1> <p>When pairing with other developers, I oftentimes notice them spending more time than necessary on things that are neither interesting nor should take a lot of time.</p> <p>One of these things is opening a terminal window so commands can be entered.</p> <p>Very often I have observed developers move around windows on the screen in search for a previously opened terminal. While it is possible on macOS to switch between applications using <kbd>⌥</kbd> + <kbd>tab</kbd> or <kbd>shift</kbd> + <kbd>⌥</kbd> + <kbd>tab</kbd>, there are certainly faster and better ways of reaching a terminal. While it is also possible to <a target="_blank" href="https://www.jetbrains.com/help/idea/working-with-system-console.html" title="JetBrains: Working with Embedded Local Terminal">open an internal terminal window in any JetBrains IDE</a> using <kbd>⌥</kbd> + <kbd>F12</kbd>, these windows are usually too small to show a lot of information, and at the same time they take away space that is better suited for displaying code.</p> <p><a target="_blank" href="https://totalterminal.binaryage.com" title="TotalTerminal, a system-wide terminal available on a hot-key">TotalTerminal</a> provided <i>a system-wide terminal available on a hot-key</i> - until OS X El Capitan was released in September 2015. Because of a lack of compatibility out of the box and a lack of interest by the original maintainers, development was stopped. Instead, the maintainers suggested to switch to <a target="_blank" href="https://www.iterm2.com" title="iTerm2">iTerm2</a>, which offers similar functionality.</p> <p>If <a target="_blank" href="https://www.iterm2.com/documentation-hotkey.html" title="iTerm Documentation: Hotkeys">iTerm's documentation for hotkeys</a> doesn't suffice, here's a step-by-step guide for setting up a full-screen, system-wide terminal accessible via hotkey:</p> <h2>Basic Setup</h2> <p>Download <a href="https://www.iterm2.com/downloads.html">iTerm2</a>, move <code>iTerm.app</code> from <code>Downloads</code> to <code>Applications</code>, and open iTerm.</p> <p>In iTerm, press <kbd>⌘</kbd> + <kbd>,</kbd> to open preferences.</p> <figure> <img src="/dist/img/post/system-wide-terminal-accessible-via-hotkey/01-open-preferences.png?3ede256" alt="iTerm preferences"> </figure> <p>Go the <strong>Keys</strong> tab, and click on the <strong>Create a Dedicated Hotkey Window...</strong> button in the lower left.</p> <figure> <img src="/dist/img/post/system-wide-terminal-accessible-via-hotkey/02-go-to-keys-tab.png?3ede256" alt="Go to Keys tab"> </figure> <p>In the panel that opens up, check the <strong>Double-tab key</strong> checkbox, and press <strong>OK</strong>.</p> <figure> <img src="/dist/img/post/system-wide-terminal-accessible-via-hotkey/03-check-double-tab-key-checkbox.png?3ede256" alt="Check the Double-tab Key checkbox"> </figure> <p>Congratulations, your hotkey window has now been configured and can be toggled on and off by double-pressing the <kbd>Control</kbd> key!</p> <figure> <img src="/dist/img/post/system-wide-terminal-accessible-via-hotkey/04-in-action-with-default-settings.gif?3ede256" alt="In action with default settings"> </figure> <h2>Customization</h2> <p>In order to open terminal windows or tabs using the previously used location, go to the <strong>Profiles</strong> tab, select the <strong>Hotkey Window</strong> profile, select the <strong>General</strong> tab, and in the <strong>Working Directory</strong> section, click on <strong>Reuse previous session's directory</strong>.</p> <figure> <img src="/dist/img/post/system-wide-terminal-accessible-via-hotkey/05-hotkey-window-general-settings.png?3ede256" alt="Click on Reuse previous session's directory"> </figure> <p>In order to change the font, go to the <strong>Profiles</strong> tab, select the <strong>Hotkey Window</strong> profile, select the <strong>Text</strong> tab, and and in the <strong>Font</strong> section, click on <strong>Change Font</strong> to select your favorite font. Personally, I use <a target="_blank" href="https://www.typography.com/fonts/operator/styles/">Operator Mono</a> by <a target="_blank" href="https://www.typography.com" title="Hoefler & Co.">Hoefler &amp; Co.</a>.</p> <figure> <img src="/dist/img/post/system-wide-terminal-accessible-via-hotkey/06-hotkey-window-text-settings-font.png?3ede256" alt="Click on Change Font"> </figure> <p>In order to reduce distraction and increase the size of the hotkey window, go to the <strong>Profiles</strong> tab, select the <strong>Hotkey Window</strong> profile, select the <strong>Windows</strong> tab, and in the <strong>Window Appearance</strong> section, reduce <strong>Transparency</strong> to opaque, then in the <strong>Settings for New Windows</strong> section, click on the <strong>Style</strong> options, and select <strong>Fullscreen</strong>.</p> <figure> <img src="/dist/img/post/system-wide-terminal-accessible-via-hotkey/07-hotkey-window-window-settings-transparency-and-style.png?3ede256" alt="Reduce transparency and set style to fullscreen"> </figure> <p>Congratulations, your hotkey window is now full-screen!</p> <figure> <img src="/dist/img/post/system-wide-terminal-accessible-via-hotkey/08-in-action-opaque-and-fullscreen.gif?3ede256" alt="In action without transparency and in fullscreen"> </figure> <p>Hope it helps!</p> <p>If you think iTerm makes you more productive, please consider <a target="_blank" href="https://www.iterm2.com/donate.html">donating</a>.</p> Cost and value of DocBlocksAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2018/05/06/cost-and-value-of-docblocks/2018-05-06T07:25:00+02:00<h1> Cost and value of DocBlocks </h1> <p>Over the years I have added, updated, and removed a lot of DocBlocks. I have also suggested to add, update, or remove DocBlocks many times, as well as explained why I believe a DocBlock makes sense in one case, and doesn't in another. In order to have a document I can refer to if I need to explain my reasoning again, I'm writing this blog post - maybe it's of use for you as well.</p> <p>As a user of products that run software, the products I like the most are those that can be used and explored without needing a lot of documentation. As a user of open-source software I have been surprised by excellent and up-to-date documentation, and disappointed by outdated or even entirely missing documentation. As a contributor to open-source software I have witnessed the struggles and challenges of projects to keep documentation up-to-date. As a software engineer working on closed-source software I understand that there are different documentation needs for open- and closed-source software.</p> <p>Regardless of whether software is open- or closed-source software, its maintenance, and the maintenance of its documentation, incurs costs. Since resources are limited, it's in the interest of everyone involved to focus on activities that provide value.</p> <p><a target="_blank" href="https://docs.phpdoc.org/latest/references/phpdoc/basic-syntax.html#what-is-a-docblock" title="phpDocumentor: What is a DocBlock?">DocBlocks</a> can provide value as documentation that lives inline with the code: it can easily be understood by developers, and interpreted by tools. The information present in DocBlocks can guide developers on the what, how and why of a piece of software. However, the value of DocBlocks diminishes quickly when they only repeat information that is already present on the structural elements they intend to document. Worse, when not kept up-to-date, they can become misleading.</p> <blockquote class="twitter-tweet" data-lang="en"><p lang="fr" dir="ltr">Code comments <a href="http://t.co/2KDRdfFE9u">pic.twitter.com/2KDRdfFE9u</a></p>&mdash; Michael Koziarski (@nzkoz) <a href="https://twitter.com/nzkoz/status/538892801941848064?ref_src=twsrc%5Etfw">November 30, 2014</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <p>Personally, when it comes to DocBlocks, I follow two rules:</p> <ol> <li>Add a DocBlock when it adds value.</li> <li>Remove a DocBlock when it does not add value.</li> </ol> <p>DocBlocks can be applied to a number of structural elements:</p> <ul> <li> <a href="#file">files</a> </li> <li> <a href="#class">classes</a> (also: interfaces and traits)</li> <li>constants</li> <li> <a href="#property">properties</a> </li> <li> <a href="#method">methods</a> (also: functions)</li> <li> <a href="#variable">variables</a> </li> </ul> <h2>File</h2> <p>Frequently found in open-source software, file-level DocBlocks document copyright information, refer to a license document, and link to the source repository:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); <span class="hljs-comment">/** * Copyright (c) 2017 Andreas Möller. * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. * * <span class="hljs-doctag">@see</span> https://github.com/ergebnis/test-util */</span> <span class="hljs-keyword">namespace</span> <span class="hljs-title">Ergebnis</span>\<span class="hljs-title">Test</span>\<span class="hljs-title">Util</span>; </code></pre> <p>So far I have not found a use for file-level DocBlocks in closed-source software.</p> <p>However, I have worked on a closed-source project where dependencies had actually been checked in into version control. When introducing <a target="_blank" href="https://getcomposer.org" title="composer">composer</a> and replacing the checked-in dependencies with requirements, file-level DocBlocks proved to be extremely useful to identify some of these dependencies.</p> <p>When using <a target="_blank" href="https://github.com/FriendsOfPHP/PHP-CS-Fixer" title="friendsofphp/php-cs-fixer"><code>friendsofphp/php-cs-fixer</code></a>, file-level DocBlocks similar to above can be easily added or replaced with a configuration similar to the following:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); $header = <span class="hljs-string">&lt;&lt;&lt;EOF Copyright (c) 2017 Andreas Möller For the full copyright and license information, please view the LICENSE file that was distributed with this source code. @see https://github.com/ergebnis/test-util EOF;</span> $config = PhpCsFixer\Config::create()-&gt;setRules([ <span class="hljs-string">'header_comment'</span> =&gt; [ <span class="hljs-string">'commentType'</span> =&gt; <span class="hljs-string">'PHPDoc'</span>, <span class="hljs-string">'header'</span> =&gt; $header, <span class="hljs-string">'location'</span> =&gt; <span class="hljs-string">'after_declare_strict'</span>, <span class="hljs-string">'separate'</span> =&gt; <span class="hljs-string">'both'</span>, ], ]); </code></pre> <p>File-level DocBlocks can easily be removed with a configuration similar to the following:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); $config = PhpCsFixer\Config::create()-&gt;setRules([ <span class="hljs-string">'header_comment'</span> =&gt; [ <span class="hljs-string">'header'</span> =&gt; <span class="hljs-string">''</span>, ], ]); </code></pre> <h2>Class</h2> <p>I have rarely found a need for class-level DocBlocks, and in particular, class-level DocBlocks such as</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); <span class="hljs-deletion">-/**</span> <span class="hljs-deletion">- * Represents a user in the system.</span> <span class="hljs-deletion">- */</span> final class User { } </code></pre> <p>do not add any value - so I remove them.</p> <p>Oftentimes, a need for a description of a class is a <a target="_blank" href="http://wiki.c2.com/?CodeSmell" title="Code Smell">smell</a>. Sometimes the only thing that is required to enhance clarity is a <a target="_blank" href="http://wiki.c2.com/?IntentionRevealingNames" title="Intention-revealing Names">name that reveals intent</a>.</p> <h2>Property</h2> <p>Depending on how properties are initialized (for example, via constructor injection), some IDEs are capable of inferring their type - either from <a href="#method">method-level</a> DocBlocks or type declarations on constructors, or from initializations of properties within constructors. Adding DocBlocks for properties with <a target="_blank" href="https://docs.phpdoc.org/latest/references/phpdoc/tags/var.html" title="PHPDoc Tag Reference: @var"><code>@var</code></a> tags assists with auto-completion when using an IDE, but also helps when reading the code elsewhere, so I always add DocBlocks to properties.</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); final class User { <span class="hljs-addition">+ /**</span> <span class="hljs-addition">+ * @var UserId</span> <span class="hljs-addition">+ */</span> private $id; <span class="hljs-addition">+ /**</span> <span class="hljs-addition">+ * @var Login</span> <span class="hljs-addition">+ */</span> private $login; } </code></pre> <p>It is worth mentioning that there have been a few RFCs related to typed properties</p> <ul> <li> <a target="_blank" href="https://wiki.php.net/rfc/property_type_hints" title="PHP RFC: Property type-hints">PHP RFC: Property type-hints</a> (2015-07-19, in draft)</li> <li> <a target="_blank" href="https://wiki.php.net/rfc/typed-properties" title="PHP RFC: Typed Properties">PHP RFC: Typed Properties</a> (2016-03-16, declined)</li> <li> <a target="_blank" href="https://wiki.php.net/rfc/typed_properties_v2" title="PHP RFC: Typed Properties 2.0">PHP RFC: Typed Properties 2.0</a> (2018-06-15, accepted)</li> </ul> <p>an on September 27, 2018, the <a target="_blank" href="https://wiki.php.net/rfc/typed_properties_v2" title="PHP RFC: Typed Properties 2.0">PHP RFC: Typed Properties 2.0</a> was accepted!</p> <blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">🎉 Typed properties have been accepted for PHP 7.4 🎉<a href="https://t.co/64kgmIw7TQ">https://t.co/64kgmIw7TQ</a></p>&mdash; Nikita Popov (@nikita_ppv) <a href="https://twitter.com/nikita_ppv/status/1044947172771418112?ref_src=twsrc%5Etfw">September 26, 2018</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <p>That is, starting with PHP 7.4, a lot of DocBlocks on properties can be removed.</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); final class User { <span class="hljs-deletion">- /**</span> <span class="hljs-deletion">- * @var UserId</span> <span class="hljs-deletion">- */</span> <span class="hljs-deletion">- private $id;</span> <span class="hljs-addition">+ private UserId $id;</span> <span class="hljs-deletion">- /**</span> <span class="hljs-deletion">- * @var Login</span> <span class="hljs-deletion">- */</span> <span class="hljs-deletion">- private $login;</span> <span class="hljs-addition">+ private Login $login;</span> } </code></pre> <p>Descriptions on property-level DocBlocks such as</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); final class User { /** <span class="hljs-deletion">- * The identifier of the user.</span> <span class="hljs-deletion">- *</span> * @var UserId */ private $id; /** <span class="hljs-deletion">- * The login of the user.</span> <span class="hljs-deletion">- *</span> * @var Login */ private $login; } </code></pre> <p>do not add any value - so I remove them.</p> <p>Again, a need for a description of a property is a smell.</p> <h2>Method</h2> <p>Method-level DocBlocks which only repeat information that is entirely present in type and return type declarations, such as</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); class User { /** * @var UserId */ private $id; /** * @var Login */ private $login; <span class="hljs-deletion">- /**</span> <span class="hljs-deletion">- * @param UserId $id</span> <span class="hljs-deletion">- * @param Login $login</span> <span class="hljs-deletion">- */</span> public function __construct(UserId $id, Login $login) { $this-&gt;id = $id; $this-&gt;login = $login; } <span class="hljs-deletion">- /**</span> <span class="hljs-deletion">- * @return UserId</span> <span class="hljs-deletion">- */</span> public function id(): UserId { return $this-&gt;id; } <span class="hljs-deletion">- /**</span> <span class="hljs-deletion">- * @return Login</span> <span class="hljs-deletion">- */</span> public function login(): Login { return $this-&gt;login; } } </code></pre> <p>do not add any value - so I remove them.</p> <p>Again, oftentimes a need for a description of a method is a smell. Consider renaming instead to clarify the intent.</p> <p>With the introduction of <a target="_blank" href="http://php.net/manual/en/migration70.new-features.php#migration70.new-features.scalar-type-declarations" title="PHP 7.0 New Features: Scalar type declarations">scalar</a> and <a target="_blank" href="http://php.net/manual/en/migration70.new-features.php#migration70.new-features.return-type-declarations" title="PHP 7.0 New Features: Return type declarations">return type declarations</a> in PHP 7.0, as well as with the introduction of <a target="_blank" href="http://php.net/manual/en/migration71.new-features.php#migration71.new-features.nullable-types" title="PHP 7.1 New Features: Nullable types">nullable type and return declarations</a> and <a target="_blank" href="http://php.net/manual/en/migration71.new-features.php#migration71.new-features.void-functions" title="PHP 7.1 New Features: Void functions">void return type declarations</a> in PHP 7.1, a lot of DocBlocks can easily be replaced by corresponding type declarations:</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); final class Login { /** * @var string */ private $value; <span class="hljs-deletion">- /**</span> <span class="hljs-deletion">- * @param string $value</span> <span class="hljs-deletion">- */</span> <span class="hljs-deletion">- public function __construct($value)</span> <span class="hljs-addition">+ public function __construct(string $value)</span> { $this-&gt;value = $value; } <span class="hljs-deletion">- /**</span> <span class="hljs-deletion">- * @return string</span> <span class="hljs-deletion">- */</span> <span class="hljs-deletion">- public function __toString()</span> <span class="hljs-addition">+ public function __toString(): string</span> { return $this-&gt;value; } } </code></pre> <p>However, oftentimes not all of the required information is present in type and return type declarations. For example, when a method throws exceptions</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); final class Login { /** * @var string */ private $value; <span class="hljs-addition">+ /**</span> <span class="hljs-addition">+ * @param string $value</span> <span class="hljs-addition">+ *</span> <span class="hljs-addition">+ * @throws \InvalidArgumentException</span> <span class="hljs-addition">+ */</span> public function __construct(string $value) { if ('' <span class="hljs-comment">=== trim($login)) {</span> throw new \InvalidArgumentException('Value cannot be an empty string'); } $this-&gt;value = $value; } public function __toString(): string { return $this-&gt;value; } } </code></pre> <p>or when a method returns an array (of objects or scalars)</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); class UserRepository { <span class="hljs-addition">+ /**</span> <span class="hljs-addition">+ * @return array&lt;int, User&gt;</span> <span class="hljs-addition">+ */</span> public function all(): array { // ... } } </code></pre> <p>then DocBlocks add value - so I add them.</p> <p>When an interface is extracted (or an implementation of an interface added) and the method on the interface already has all of the information</p> <pre><code class="language-diff hljs diff" data-lang="diff"><span class="hljs-addition">+&lt;?php</span> <span class="hljs-addition">+</span> <span class="hljs-addition">+declare(strict_types=1);</span> <span class="hljs-addition">+</span> <span class="hljs-addition">+interface UserRepository</span> <span class="hljs-addition">+{</span> <span class="hljs-addition">+ /**</span> <span class="hljs-addition">+ * @return array&lt;int, User&gt;</span> <span class="hljs-addition">+ */</span> <span class="hljs-addition">+ public function all(): array</span> <span class="hljs-addition">+}</span> </code></pre> <p>then a DocBlock on the corresponding method in the implementations such as</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); <span class="hljs-deletion">-class UserRepository</span> <span class="hljs-addition">+final class DoctrineUserRepository implements UserRepository</span> { <span class="hljs-deletion">- /**</span> <span class="hljs-deletion">- * @return array&lt;int, User&gt;</span> <span class="hljs-deletion">- */</span> public function all(): array { // ... } } </code></pre> <p>doesn't add any value - so I remove them.</p> <p>Similarly, when an abstract class is extracted (or extending a class) and implementing an abstract method (overriding a method) the method on the interface already has all of the information, then a DocBlock on the corresponding method in the child class doesn't add any value - so I remove them as well.</p> <h2>Variable</h2> <p>Sometimes return types of methods are not clear</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); use PHPUnit\Framework; final class DoctrineUserRepositoryTest extends Framework\TestCase { public function testByLoginReturnsUser(): void { // ... <span class="hljs-addition">+ /** @var User $user */</span> $user = $this-&gt;fixtureFactory-&gt;get(User::class, [ 'login' =&gt; $login, ]); // ... } } </code></pre> <p>and a clarifying inline DocComment adds value - so I add them.</p> <h2>Generated Code</h2> <p>When creating files and classes in IDEs, they often add unnecessary DocBlocks. However, DocBlocks such as</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); <span class="hljs-deletion">-/**</span> <span class="hljs-deletion">- * Created by PhpStorm.</span> <span class="hljs-deletion">- * User: localheinz</span> <span class="hljs-deletion">- * Date: 05.05.18</span> <span class="hljs-deletion">- * Time: 22:18.</span> <span class="hljs-deletion">- */</span> final class User { } </code></pre> <p>or</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); <span class="hljs-deletion">-/**</span> <span class="hljs-deletion">- * Class User</span> <span class="hljs-deletion">- */</span> final class User { <span class="hljs-deletion">- /**</span> <span class="hljs-deletion">- * User constructor.</span> <span class="hljs-deletion">- *</span> <span class="hljs-deletion">- * @param Uuid $id</span> <span class="hljs-deletion">- * @param string $login</span> <span class="hljs-deletion">- */</span> public function __construct(Uuid $id, string $login) { $this-&gt;id = $id; $this-&gt;login = $login; } } </code></pre> <p>do not add any value - so I remove them.</p> <p>When using <a target="_blank" href="https://www.jetbrains.com/phpstorm/" title="PhpStorm">PhpStorm</a>, the <a target="_blank" href="https://www.jetbrains.com/help/phpstorm/using-file-and-code-templates.html" title="PhpStorm Help: File and Code Templates">file and code templates</a> used when creating files and classy constructs can be easily configured (and unnecessary comments removed).</p> <p>When creating code with command-line tools, for example, when creating a <a target="_blank" href="https://github.com/doctrine/migrations" title="doctrine/migrations">Doctrine migration</a>, the generated code often contains helpful comments for getting started, such as</p> <pre><code class="language-diff hljs diff" data-lang="diff"> &lt;?php declare(strict_types=1); use Doctrine\DBAL\Migrations\AbstractMigration; use Doctrine\DBAL\Schema\Schema; <span class="hljs-deletion">-/**</span> <span class="hljs-deletion">- * Auto-generated Migration: Please modify to your needs!</span> <span class="hljs-deletion">- */</span> class Version20180426054008 extends AbstractMigration { public function up(Schema $schema) { <span class="hljs-deletion">- // this up() migration is auto-generated, please modify it to your needs</span> } public function down(Schema $schema) { <span class="hljs-deletion">- // this down() migration is auto-generated, please modify it to your needs</span> } } </code></pre> <p>but other than that, these DocBlocks (and additional comments) add no value - so I remove them.</p> <p>When code is generated from external sources, or its structure is dictated by external sources which we do not control, for example, when generating <a target="_blank" href="https://martinfowler.com/eaaCatalog/dataTransferObject.html" title="P of EAA Catalog: Data Transfer Object">Data Transfer Objects</a> or similar from API definitions or documentation, then DocBlocks with descriptions may add value. Especially when there is control over the mechanisms that generate the code, then including descriptions adds little maintenance cost.</p> <p>If you have different opinions, that is fine - just make sure you don't blindly add DocBlocks everywhere, but weigh costs against value.</p> <p>Hope it helps!</p> Test coverage is a meaningless metricAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2018/02/01/test-coverage-is-meaningless/2018-02-01T14:25:00+01:00<h1> Test coverage is a meaningless metric </h1> <p>Oftentimes people ask: How much test coverage is sufficient? While I believe that this is a legitimate question to ask, especially when just getting started with testing, there is a better, a counter-question: Do you want to do Test-Driven Development or not?</p> <p>This question has three possible answers:</p> <ol type="a"> <li>Yes</li> <li>No</li> <li>What is Test-Driven Development?</li> </ol> <p>Test-Driven Development (TDD), is a practice that suggests following <a target="_blank" href="http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd" title="Uncle Bob: The three rules of TDD">three simple rules</a>:</p> <blockquote> <ol> <li>You are not allowed to write any production code unless it is to make a failing unit test pass.</li> <li>You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.</li> <li>You are not allowed to write any more production code than is sufficient to pass the one failing unit test.</li> </ol> </blockquote> <p>If developers follow TDD, not a single line of production code is written without a failing test first. Consequently, the resulting test coverage will <strong>always be 100%</strong>, and test coverage becomes a meaningless metric.</p> <p>If developers do not follow TDD, they likely write production code, and either never or only occasionally write tests afterwards. Consequently, test coverage will <strong>vary between 0% and 100%</strong>. Test coverage will decrease when</p> <ul> <li>tests are deleted</li> <li>untested code is added</li> <li>tested code is reformatted or refactored (the same problem is solved with fewer lines of code)</li> </ul> <p>and it will increase when</p> <ul> <li>tests are added</li> <li>untested code is removed</li> <li>tested code is reformatted or refactored (the same problem is solved with more lines of code)</li> </ul> <p>While you can achieve 100% test coverage when writing tests after the fact, how confident will you be that the tests are meaningful? How confident will you be that the code works as expected in production?</p> <p>For example, I recently <a target="_blank" href="https://github.com/ergebnis/composer-normalize/pull/38" title="Enhancement: Add --dry-run option">added a <code>--dry-run</code> option</a> to <a target="_blank" href="https://github.com/ergebnis/composer-normalize" title="ergebnis/composer-normalize"><code>ergebnis/composer-normalize</code></a>. Since a <a target="_blank" href="https://github.com/sebastianbergmann/diff/blob/2.0.1/src/Differ.php" title="Sebastian\Diff\Differ">collaborator</a> from a 3rd-party package is marked as <code>final</code> and does not implement an interface, I did not have a possibility to create a test double for it. If you look closely, the code I added to output a diff in <code>--dry-run</code> mode is largely untested. Still, I have 100% test coverage.</p> <p>While the <strong>changes</strong> in test coverage might tell you something (what exactly, you have to find out for yourself), do you still think test coverage itself is a meaningful metric?</p> <p>Personally, I have decided to follow the three rules of TDD. Following these rules simplifies a lot of things for me:</p> <ol> <li>There's no need to discuss what to test - every component is equally important and will be thoroughly tested.</li> <li>There's no need to discuss when to test - tests are always written first.</li> <li>There's no need to discuss how much test coverage is sufficient - it will always be 100%.</li> </ol> <p>You might have a different point of view, and if that works for you and your clients, fine.</p> <p>However, if you are still asking how much coverage you should aim for, here is a tweet by <a target="_blank" href="https://github.com/Ocramius" title="Marco Pivetta">Marco Pivetta</a> for you:</p> <blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Broke production because I didn&#39;t bother covering &gt;90%, and affected code path was in the 10%.<br><br>To those that say testing everything isn&#39;t worth doing: STFU.</p>&mdash; Software Onion Peeler (@Ocramius) <a href="https://twitter.com/Ocramius/status/958737872613462017?ref_src=twsrc%5Etfw">January 31, 2018</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <p>If you are still hesitant, the hardest part with testing is <a target="_blank" href="https://phpunit.de/getting-started/phpunit-8.html" title="Getting Started with PHPUnit 7">getting started</a>.</p> <p>Good luck!</p> Makefile for lazy developersAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2018/01/24/makefile-for-lazy-developers/2018-01-24T09:30:00+01:00<h1> Makefile for lazy developers </h1> <p>Whatever the size of the software project, I believe in, subscribe to, and promote <a target="_blank" href="https://www.martinfowler.com/articles/continuousIntegration.html" title="Continuous Integration">Continuous Integration</a>. Personally, I rely on <a target="_blank" href="https://github.com/features/actions" title="GitHub Actions">GitHub Actions</a> as an automated build system. Even this blog here is built with, and eventually deployed into production using GitHub Actions.</p> <p>Regardless of whether an automated build system can be set up and used for a project or not, I prefer to be able to run build steps locally. This prevents stress-testing the automated build system and taking away resources from other developers. Also, it gives me more confidence before committing and pushing changes upstream.</p> <p>The first time I heard of an automated build tool was when I was working on a crowd investment project in 2011. Back then a developer set up <a target="_blank" href="http://capistranorb.com" title="Capistrano">Capistrano</a>, and I didn't understand a thing. Then had used <a target="_blank" href="https://www.phing.info" title="PHing Is Not GNU make">Phing</a> for a while. For a couple of years now I have been using <a target="_blank" href="http://pubs.opengroup.org/onlinepubs/9699919799/utilities/make.html" title="make"><code>make</code></a>, after having been introduced to it when working on a project in 2014. While it has its limitations, it's short and simple, and most of all, it get's the job done.</p> <p>In most of my projects I use a <code>Makefile</code> similar to the following:</p> <pre><code class="language-makefile hljs makefile" data-lang="makefile"><span class="hljs-meta"><span class="hljs-meta-keyword">.PHONY</span>: it</span> <span class="hljs-section">it: coding-standards tests</span> <span class="hljs-meta"><span class="hljs-meta-keyword">.PHONY</span>: code-coverage</span> <span class="hljs-section">code-coverage: vendor</span> vendor/bin/phpunit --configuration=test/Unit/phpunit.xml --coverage-text <span class="hljs-meta"><span class="hljs-meta-keyword">.PHONY</span>: coding-standards</span> <span class="hljs-section">coding-standards: vendor</span> vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --verbose <span class="hljs-meta"><span class="hljs-meta-keyword">.PHONY</span>: mutation-tests</span> <span class="hljs-section">mutation-tests: vendor</span> vendor/bin/infection --min-covered-msi=80 --min-msi=80 <span class="hljs-meta"><span class="hljs-meta-keyword">.PHONY</span>: tests</span> <span class="hljs-section">tests: vendor</span> vendor/bin/phpunit --configuration=test/AutoReview/phpunit.xml vendor/bin/phpunit --configuration=test/Unit/phpunit.xml vendor/bin/phpunit --configuration=test/Integration/phpunit.xml <span class="hljs-section">vendor: composer.json composer.lock</span> composer validate composer install composer normalize </code></pre> <p>As you can see, I have defined a few <a target="_blank" href="https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html" title="phony targets">phony targets</a>, which may run any number of commands, or depend on so-called <em>prerequisites</em>.</p> <p>For example, running</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> make tests</span> </code></pre> <p>will first execute the <code>vendor</code> target, followed by running unit and integration tests. Running</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> make coding-standards</span> </code></pre> <p>will first execute the <code>vendor</code> target, followed by running <code>php-cs-fixer</code>. You get the idea.</p> <p>A note on the <code>vendor</code> target: I previously used to have a <code>composer</code> target. After posting this article on <a target="_blank" href="https://www.reddit.com/r/PHP/comments/7sltmh/makefile_for_lazy_developers/" title="Reddit">Reddit</a>, <a target="_blank" href="https://github.com/nicwortel" title="Nic Wortel">Nic Wortel</a> provided an <a target="_blank" href="https://www.reddit.com/r/PHP/comments/7sltmh/makefile_for_lazy_developers/dt5rba0/" title="Reddit: Comment on 'Makefile for lazy developers'">excellent suggestion</a>:</p> <blockquote> <p>I use the following recipe in my Makefiles:</p> <pre><code class="language-makefile hljs makefile" data-lang="makefile"><span class="hljs-section">vendor: composer.json composer.lock</span> composer install </code></pre> <p>This will compare the timestamp of the <code>vendor</code> directory with those of <code>composer.json</code> and <code>composer.lock</code>, and will only execute the recipe if either of the composer files is newer than the <code>vendor</code> directory, or if the vendor directory is missing.</p> </blockquote> <p>Excellent, this saves even more time!</p> <p>Finally, I have added an <code>it</code> target. Running</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> make it</span> </code></pre> <p>will execute all of the targets I consider important enough to be run before I commit and push changes upstream.</p> <p>Now, you might have noticed that while I have a penchant for <a href="/blog/2018/01/15/normalizing-composer.json/" title="Normalizing composer.json">keeping things sorted</a>, I have intentionally defined <code>it</code> as the first target. There's a good reason to it: unless a target has been specified explicitly, the first target defined in the <code>Makefile</code> will be executed. That is, by making <code>it</code> the first target, I can run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> make</span> </code></pre> <p>to execute all of the important targets.</p> <p>Nonetheless, as I have gotten into the habit of running these tasks many, many (dozends? hundreds?) of times throughout the day, it's still a bit too much typing, right? Therefore, I have created an <a target="_blank" href="http://www.linfo.org/alias.html" title="alias">alias</a> in my shell configuration:</p> <pre><code class="language-bash hljs bash" data-lang="bash"><span class="hljs-comment"># Makefile</span> <span class="hljs-built_in">alias</span> m=<span class="hljs-string">"make"</span> </code></pre> <p>Running</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> m</span> </code></pre> <p>achieves the same now. I believe that's the closest thing to <a target="_blank" href="https://www.joelonsoftware.com/2000/08/09/the-joel-test-12-steps-to-better-code/" title="The Joel Test: 12 Steps to Better Code">making a build in one step</a>, with only two key strokes.</p> <p>Hope it helps to increase your productivity!</p> Normalizing composer.jsonAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2018/01/15/normalizing-composer.json/2018-01-15T08:30:00+01:00<h1> Normalizing composer.json </h1> <p>If you are using <a target="_blank" href="https://getcomposer.org" title="composer">composer</a>, you have probably modified <code>composer.json</code> at least once to keep things nice and tidy.</p> <p>In fact, when I started using composer in late summer 2012, I of course edited <code>composer.json</code> manually, even when I wanted to add or update dependencies. I soon learned that one should run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> composer require foo/bar:~x.y.z</span> </code></pre> <p>instead, as this prevents pulling in undesired updates. I did that, but in order to keep the list of dependencies sorted, I actually had to do it twice:</p> <ol> <li>require package</li> <li>move package in <code>require</code> or <code>require-dev</code> section so packages are sorted</li> <li>run command again (to keep the lock file updated, as the order of dependencies matters for the calculation of the hash)</li> </ol> <p>That's a bit crazy, right? And just to keep things nice and tidy!</p> <p>Then I came across a tweet by <a target="_blank" href="https://github.com/HenrikJoreteg" title="Henrik Joreteg">Henrik Joreteg</a>:</p> <blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Favorite little OCD module that I use *all the time* fixpack: <a href="https://t.co/amXS7th05U">https://t.co/amXS7th05U</a><br><br>Alphabetizes dependencies, and cleans up package.json</p>&mdash; Henrik Joreteg (@HenrikJoreteg) <a href="https://twitter.com/HenrikJoreteg/status/448366378836180993?ref_src=twsrc%5Etfw">March 25, 2014</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <p>Nice, I wasn't alone with liking things nice and tidy!</p> <p>At the time I was contracting for a company here in Berlin. One of the best parts working there was that a lot of respected members of the PHP community stopped by the office oftentimes, for example, <a target="_blank" href="https://github.com/beberlei" title="Benjamin Eberlei">Benjamin Eberlei</a>, <a target="_blank" href="https://github.com/dzuelke" title="David Zuelke">David Zuelke</a>, and <a target="_blank" href="https://github.com/naderman" title="Nils Aderman">Nils Aderman</a>. Nils (whom you probably know as one of the maintainers of composer, together with <a target="_blank" href="https://github.com/seldaek" title="Jordi Boggiano">Jordi Boggiano</a>) even regularly worked out of the office. Hesitant at first, I approached him about this issue, and asked him whether they would consider a pull request. I think he liked it, so I was quite happy and figured it should be ok to do something about it.</p> <p>Nonetheless, it took me until December 2014 until I eventually opened a <a target="_blank" href="https://github.com/composer/composer/pull/3549" title="Enhancement: Add sort-packages option which allows sorting of packages">pull request</a> to add the <a target="_blank" href="https://getcomposer.org/doc/03-cli.md#require" title="Composer Command Line Interface: require"><code>--sort-packages</code></a> option to composer.</p> <p>Since then it has been possible to run, for example,</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> composer require --dev --sort-packages phpunit/phpunit</span> </code></pre> <p>to require a package and keep packages sorted (if you already have that package required, you can run the command to trigger sorting).</p> <p>With a bit of <a target="_blank" href="https://github.com/composer/composer/pull/3872" title="Enhancement: Sort packages by importance, then alphabetically">tweaking</a> not only packages and platform requirements would be sorted, but platform requirements listed <em>before</em> packages. Thanks to <a target="_blank" href="https://github.com/hanovruslan" title="Hanov Ruslan">Hanov Ruslan</a>, this behaviour can be <a target="_blank" href="https://github.com/composer/composer/pull/4716" title="Added sort-packages into config">configured</a>, making it unnecessary to use the option on the command line.</p> <p>Not only have a lot of people blogged about the feature, but a quick <a target="_blank" href="https://github.com/search?utf8=✓&q=sort-packages+filename%3A%22composer.json%22&type=Code" title="Searcg for sort-packages in composer.json">search on GitHub</a> reveals that the configuration option is used at least <strong>327,598</strong> times! Excellent!</p> <p>Now, how about taking it a step further? Today I present to you <a target="_blank" href="https://github.com/ergebnis/composer-normalize" title=""><code>ergebnis/composer-normalize</code></a>, a composer plugin for tidying up <em>all of</em> <code>composer.json</code>!</p> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> composer global require ergebnis/composer-normalize</span> </code></pre> <p>to install the composer plugin globally.</p> <p>Run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> composer normalize</span> </code></pre> <p>to normalize <code>composer.json</code> in the working directory or run</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> composer normalize ~/Sites/foo/bar/composer.json</span> </code></pre> <p>to normalize <code>composer.json</code> in the <code>~/Sites/foo/bar</code> directory.</p> <p>The <code>NormalizeCommand</code> provided by the NormalizePlugin within this package will</p> <ul> <li>determine whether a <code>composer.json</code> exists</li> <li>determine whether a <code>composer.lock</code> exists, and if so, whether it is up to date</li> <li>use the <code>ComposerJsonNormalizer</code> to normalize the content of <code>composer.json</code> </li> <li>format the normalized content (either as sniffed, or as specified using the <code>--indent-size</code> and <code>--indent-style</code> options)</li> <li>write the normalized and formatted content of composer.json back to the file</li> <li>update the hash in <code>composer.lock</code> if it exists and if an update is necessary</li> </ul> <p>Currently, the following normalizations will be applied</p> <ul> <li>restructure <code>composer.json</code> according to the underlying <a target="_blank" href="https://getcomposer.org/schema.json" title="JSON Schema for composer.json">JSON schema</a> </li> <li>sort entries in the <code>bin</code> section (by value)</li> <li>sort entries in the <code>config</code>, <code>extra</code>, and <code>scripts-descriptions</code> sections (by key)</li> <li>sort entries in the <code>conflict</code>, <code>provide</code>, <code>replace</code>, <code>require</code>, <code>require-dev</code>, and <code>suggest</code> sections (as you are used to from the <a target="_blank" href="https://getcomposer.org/doc/06-config.md#sort-packages" title="sort-packages"><code>sort-packages</code></a> configuration)</li> <li>normalize version constraints in <code>conflict</code>, <code>provide</code>, <code>replace</code>, <code>require</code>, and <code>require-dev</code> sections</li> </ul> <p>The command accepts the following argument</p> <ul> <li> <code>file</code>: Path to <code>composer.json</code> file (optional, defaults to <code>composer.json</code> in working directory)</li> </ul> <p>The command comes with the following options</p> <ul> <li> <code>--dry-run</code>: Show the results of normalizing, but do not modify any files</li> <li> <code>--indent-size</code>: Indent size (an integer greater than 0); should be used with the <code>--indent-style</code> option</li> <li> <code>--indent-style</code>: Indent style (one of &quot;space&quot;, &quot;tab&quot;); should be used with the <code>--indent-size</code> option</li> <li> <code>--no-update-lock</code>: Do not update lock file if it exists</li> </ul> <p>Here is an example of running</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> composer normalize</span> </code></pre> <p>on <code>composer/composer</code> itself:</p> <pre><code class="language-diff hljs diff" data-lang="diff">diff --git a/composer.json b/composer.json index dd672c3b..330f73d0 100644 <span class="hljs-comment">--- a/composer.json</span> <span class="hljs-comment">+++ b/composer.json</span> <span class="hljs-meta">@@ -1,9 +1,13 @@</span> { "name": "composer/composer", <span class="hljs-addition">+ "type": "library",</span> "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", <span class="hljs-deletion">- "keywords": ["package", "dependency", "autoload"],</span> <span class="hljs-addition">+ "keywords": [</span> <span class="hljs-addition">+ "package",</span> <span class="hljs-addition">+ "dependency",</span> <span class="hljs-addition">+ "autoload"</span> <span class="hljs-addition">+ ],</span> "homepage": "https://getcomposer.org/", <span class="hljs-deletion">- "type": "library",</span> "license": "MIT", "authors": [ { <span class="hljs-meta">@@ -17,52 +21,58 @@</span> "homepage": "http://seld.be" } ], <span class="hljs-deletion">- "support": {</span> <span class="hljs-deletion">- "irc": "irc://irc.freenode.org/composer",</span> <span class="hljs-deletion">- "issues": "https://github.com/composer/composer/issues"</span> <span class="hljs-deletion">- },</span> "require": { "php": "^5.3.2 || ^7.0", <span class="hljs-deletion">- "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",</span> "composer/ca-bundle": "^1.0", "composer/semver": "^1.0", "composer/spdx-licenses": "^1.2", <span class="hljs-addition">+ "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",</span> <span class="hljs-addition">+ "psr/log": "^1.0",</span> <span class="hljs-addition">+ "seld/cli-prompt": "^1.0",</span> "seld/jsonlint": "^1.4", <span class="hljs-addition">+ "seld/phar-utils": "^1.0",</span> "symfony/console": "^2.7 || ^3.0 || ^4.0", <span class="hljs-deletion">- "symfony/finder": "^2.7 || ^3.0 || ^4.0",</span> <span class="hljs-deletion">- "symfony/process": "^2.7 || ^3.0 || ^4.0",</span> "symfony/filesystem": "^2.7 || ^3.0 || ^4.0", <span class="hljs-deletion">- "seld/phar-utils": "^1.0",</span> <span class="hljs-deletion">- "seld/cli-prompt": "^1.0",</span> <span class="hljs-deletion">- "psr/log": "^1.0"</span> <span class="hljs-addition">+ "symfony/finder": "^2.7 || ^3.0 || ^4.0",</span> <span class="hljs-addition">+ "symfony/process": "^2.7 || ^3.0 || ^4.0"</span> }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.7", "phpunit/phpunit-mock-objects": "^2.3 || ^3.0" }, <span class="hljs-addition">+ "suggest": {</span> <span class="hljs-addition">+ "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",</span> <span class="hljs-addition">+ "ext-zip": "Enabling the zip extension allows you to unzip archives",</span> <span class="hljs-addition">+ "ext-zlib": "Allow gzip compression of HTTP requests"</span> <span class="hljs-addition">+ },</span> "config": { "platform": { "php": "5.3.9" } }, <span class="hljs-deletion">- "suggest": {</span> <span class="hljs-deletion">- "ext-zip": "Enabling the zip extension allows you to unzip archives",</span> <span class="hljs-deletion">- "ext-zlib": "Allow gzip compression of HTTP requests",</span> <span class="hljs-deletion">- "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages"</span> <span class="hljs-addition">+ "extra": {</span> <span class="hljs-addition">+ "branch-alias": {</span> <span class="hljs-addition">+ "dev-master": "1.7-dev"</span> <span class="hljs-addition">+ }</span> }, "autoload": { <span class="hljs-deletion">- "psr-4": { "Composer\\": "src/Composer" }</span> <span class="hljs-addition">+ "psr-4": {</span> <span class="hljs-addition">+ "Composer\\": "src/Composer"</span> <span class="hljs-addition">+ }</span> }, "autoload-dev": { <span class="hljs-deletion">- "psr-4": { "Composer\\Test\\": "tests/Composer/Test" }</span> <span class="hljs-deletion">- },</span> <span class="hljs-deletion">- "bin": ["bin/composer"],</span> <span class="hljs-deletion">- "extra": {</span> <span class="hljs-deletion">- "branch-alias": {</span> <span class="hljs-deletion">- "dev-master": "1.7-dev"</span> <span class="hljs-addition">+ "psr-4": {</span> <span class="hljs-addition">+ "Composer\\Test\\": "tests/Composer/Test"</span> } }, <span class="hljs-addition">+ "bin": [</span> <span class="hljs-addition">+ "bin/composer"</span> <span class="hljs-addition">+ ],</span> "scripts": { "test": "phpunit" <span class="hljs-addition">+ },</span> <span class="hljs-addition">+ "support": {</span> <span class="hljs-addition">+ "issues": "https://github.com/composer/composer/issues",</span> <span class="hljs-addition">+ "irc": "irc://irc.freenode.org/composer"</span> } } </code></pre> <p>If you need to change the indentation, you can use the <code>--indent-size</code> and <code>--indent-style</code> options. Here's an example of running</p> <pre><code class="language-shell hljs shell" data-lang="shell"><span class="hljs-meta">$</span><span class="bash"> composer normalize --indent-size=2 --indent-style=space</span> </code></pre> <p>on <code>composer/composer</code> itself:</p> <pre><code class="language-diff hljs diff" data-lang="diff">diff --git a/composer.json b/composer.json index dd672c3b..071c99c6 100644 <span class="hljs-comment">--- a/composer.json</span> <span class="hljs-comment">+++ b/composer.json</span> <span class="hljs-meta">@@ -1,68 +1,78 @@</span> { <span class="hljs-deletion">- "name": "composer/composer",</span> <span class="hljs-deletion">- "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",</span> <span class="hljs-deletion">- "keywords": ["package", "dependency", "autoload"],</span> <span class="hljs-deletion">- "homepage": "https://getcomposer.org/",</span> <span class="hljs-deletion">- "type": "library",</span> <span class="hljs-deletion">- "license": "MIT",</span> <span class="hljs-deletion">- "authors": [</span> <span class="hljs-deletion">- {</span> <span class="hljs-deletion">- "name": "Nils Adermann",</span> <span class="hljs-deletion">- "email": "naderman@naderman.de",</span> <span class="hljs-deletion">- "homepage": "http://www.naderman.de"</span> <span class="hljs-deletion">- },</span> <span class="hljs-deletion">- {</span> <span class="hljs-deletion">- "name": "Jordi Boggiano",</span> <span class="hljs-deletion">- "email": "j.boggiano@seld.be",</span> <span class="hljs-deletion">- "homepage": "http://seld.be"</span> <span class="hljs-deletion">- }</span> <span class="hljs-deletion">- ],</span> <span class="hljs-deletion">- "support": {</span> <span class="hljs-deletion">- "irc": "irc://irc.freenode.org/composer",</span> <span class="hljs-deletion">- "issues": "https://github.com/composer/composer/issues"</span> <span class="hljs-addition">+ "name": "composer/composer",</span> <span class="hljs-addition">+ "type": "library",</span> <span class="hljs-addition">+ "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",</span> <span class="hljs-addition">+ "keywords": [</span> <span class="hljs-addition">+ "package",</span> <span class="hljs-addition">+ "dependency",</span> <span class="hljs-addition">+ "autoload"</span> <span class="hljs-addition">+ ],</span> <span class="hljs-addition">+ "homepage": "https://getcomposer.org/",</span> <span class="hljs-addition">+ "license": "MIT",</span> <span class="hljs-addition">+ "authors": [</span> <span class="hljs-addition">+ {</span> <span class="hljs-addition">+ "name": "Nils Adermann",</span> <span class="hljs-addition">+ "email": "naderman@naderman.de",</span> <span class="hljs-addition">+ "homepage": "http://www.naderman.de"</span> }, <span class="hljs-deletion">- "require": {</span> <span class="hljs-deletion">- "php": "^5.3.2 || ^7.0",</span> <span class="hljs-deletion">- "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",</span> <span class="hljs-deletion">- "composer/ca-bundle": "^1.0",</span> <span class="hljs-deletion">- "composer/semver": "^1.0",</span> <span class="hljs-deletion">- "composer/spdx-licenses": "^1.2",</span> <span class="hljs-deletion">- "seld/jsonlint": "^1.4",</span> <span class="hljs-deletion">- "symfony/console": "^2.7 || ^3.0 || ^4.0",</span> <span class="hljs-deletion">- "symfony/finder": "^2.7 || ^3.0 || ^4.0",</span> <span class="hljs-deletion">- "symfony/process": "^2.7 || ^3.0 || ^4.0",</span> <span class="hljs-deletion">- "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",</span> <span class="hljs-deletion">- "seld/phar-utils": "^1.0",</span> <span class="hljs-deletion">- "seld/cli-prompt": "^1.0",</span> <span class="hljs-deletion">- "psr/log": "^1.0"</span> <span class="hljs-deletion">- },</span> <span class="hljs-deletion">- "require-dev": {</span> <span class="hljs-deletion">- "phpunit/phpunit": "^4.8.35 || ^5.7",</span> <span class="hljs-deletion">- "phpunit/phpunit-mock-objects": "^2.3 || ^3.0"</span> <span class="hljs-deletion">- },</span> <span class="hljs-deletion">- "config": {</span> <span class="hljs-deletion">- "platform": {</span> <span class="hljs-deletion">- "php": "5.3.9"</span> <span class="hljs-deletion">- }</span> <span class="hljs-deletion">- },</span> <span class="hljs-deletion">- "suggest": {</span> <span class="hljs-deletion">- "ext-zip": "Enabling the zip extension allows you to unzip archives",</span> <span class="hljs-deletion">- "ext-zlib": "Allow gzip compression of HTTP requests",</span> <span class="hljs-deletion">- "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages"</span> <span class="hljs-deletion">- },</span> <span class="hljs-deletion">- "autoload": {</span> <span class="hljs-deletion">- "psr-4": { "Composer\\": "src/Composer" }</span> <span class="hljs-deletion">- },</span> <span class="hljs-deletion">- "autoload-dev": {</span> <span class="hljs-deletion">- "psr-4": { "Composer\\Test\\": "tests/Composer/Test" }</span> <span class="hljs-deletion">- },</span> <span class="hljs-deletion">- "bin": ["bin/composer"],</span> <span class="hljs-deletion">- "extra": {</span> <span class="hljs-deletion">- "branch-alias": {</span> <span class="hljs-deletion">- "dev-master": "1.7-dev"</span> <span class="hljs-deletion">- }</span> <span class="hljs-deletion">- },</span> <span class="hljs-deletion">- "scripts": {</span> <span class="hljs-deletion">- "test": "phpunit"</span> <span class="hljs-addition">+ {</span> <span class="hljs-addition">+ "name": "Jordi Boggiano",</span> <span class="hljs-addition">+ "email": "j.boggiano@seld.be",</span> <span class="hljs-addition">+ "homepage": "http://seld.be"</span> <span class="hljs-addition">+ }</span> <span class="hljs-addition">+ ],</span> <span class="hljs-addition">+ "require": {</span> <span class="hljs-addition">+ "php": "^5.3.2 || ^7.0",</span> <span class="hljs-addition">+ "composer/ca-bundle": "^1.0",</span> <span class="hljs-addition">+ "composer/semver": "^1.0",</span> <span class="hljs-addition">+ "composer/spdx-licenses": "^1.2",</span> <span class="hljs-addition">+ "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",</span> <span class="hljs-addition">+ "psr/log": "^1.0",</span> <span class="hljs-addition">+ "seld/cli-prompt": "^1.0",</span> <span class="hljs-addition">+ "seld/jsonlint": "^1.4",</span> <span class="hljs-addition">+ "seld/phar-utils": "^1.0",</span> <span class="hljs-addition">+ "symfony/console": "^2.7 || ^3.0 || ^4.0",</span> <span class="hljs-addition">+ "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",</span> <span class="hljs-addition">+ "symfony/finder": "^2.7 || ^3.0 || ^4.0",</span> <span class="hljs-addition">+ "symfony/process": "^2.7 || ^3.0 || ^4.0"</span> <span class="hljs-addition">+ },</span> <span class="hljs-addition">+ "require-dev": {</span> <span class="hljs-addition">+ "phpunit/phpunit": "^4.8.35 || ^5.7",</span> <span class="hljs-addition">+ "phpunit/phpunit-mock-objects": "^2.3 || ^3.0"</span> <span class="hljs-addition">+ },</span> <span class="hljs-addition">+ "suggest": {</span> <span class="hljs-addition">+ "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",</span> <span class="hljs-addition">+ "ext-zip": "Enabling the zip extension allows you to unzip archives",</span> <span class="hljs-addition">+ "ext-zlib": "Allow gzip compression of HTTP requests"</span> <span class="hljs-addition">+ },</span> <span class="hljs-addition">+ "config": {</span> <span class="hljs-addition">+ "platform": {</span> <span class="hljs-addition">+ "php": "5.3.9"</span> <span class="hljs-addition">+ }</span> <span class="hljs-addition">+ },</span> <span class="hljs-addition">+ "extra": {</span> <span class="hljs-addition">+ "branch-alias": {</span> <span class="hljs-addition">+ "dev-master": "1.7-dev"</span> <span class="hljs-addition">+ }</span> <span class="hljs-addition">+ },</span> <span class="hljs-addition">+ "autoload": {</span> <span class="hljs-addition">+ "psr-4": {</span> <span class="hljs-addition">+ "Composer\\": "src/Composer"</span> <span class="hljs-addition">+ }</span> <span class="hljs-addition">+ },</span> <span class="hljs-addition">+ "autoload-dev": {</span> <span class="hljs-addition">+ "psr-4": {</span> <span class="hljs-addition">+ "Composer\\Test\\": "tests/Composer/Test"</span> } <span class="hljs-addition">+ },</span> <span class="hljs-addition">+ "bin": [</span> <span class="hljs-addition">+ "bin/composer"</span> <span class="hljs-addition">+ ],</span> <span class="hljs-addition">+ "scripts": {</span> <span class="hljs-addition">+ "test": "phpunit"</span> <span class="hljs-addition">+ },</span> <span class="hljs-addition">+ "support": {</span> <span class="hljs-addition">+ "issues": "https://github.com/composer/composer/issues",</span> <span class="hljs-addition">+ "irc": "irc://irc.freenode.org/composer"</span> <span class="hljs-addition">+ }</span> } </code></pre> <p>Hope you like it!</p> Pretty-printing JSON with custom indentAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2018/01/04/pretty-printing-json-with-custom-indent/2018-01-04T11:25:00+01:00<h1> Pretty-printing JSON with custom indent </h1> <p>Recently I was in need of pretty-printing JSON in PHP with custom indentation.</p> <p>While PHP 5.4 has added the constants <code>JSON_PRETTY_PRINT</code>, <code>JSON_UNESCAPED_SLASHES</code>, and <code>JSON_UNESCAPED_UNICODE</code> which could be combined and passed in into <a target="_blank" href="http://php.net/manual/en/function.json-encode.php" title="json_encode()"><code>json_encode()</code></a> to configure encoding options, there's unfortunately no <em>built-in</em> way to print JSON with an indent other than 4 spaces.</p> <p>For example:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); $data = [ <span class="hljs-string">'name'</span> =&gt; <span class="hljs-string">'Andreas Möller'</span>, <span class="hljs-string">'emoji'</span> =&gt; <span class="hljs-string">'🤓'</span>, <span class="hljs-string">'urls'</span> =&gt; [ <span class="hljs-string">'https://localheinz.com'</span>, <span class="hljs-string">'https://github.com/localheinz'</span>, <span class="hljs-string">'https://twitter.com/localheinz'</span>, ], ]; $json = json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); </code></pre> <p>will result in</p> <pre><code class="language-json hljs json" data-lang="json">{ <span class="hljs-attr">"name"</span>:<span class="hljs-string">"Andreas Möller"</span>, <span class="hljs-attr">"emoji"</span>:<span class="hljs-string">"🤓"</span>, <span class="hljs-attr">"urls"</span>:[ <span class="hljs-string">"https://localheinz.com"</span>, <span class="hljs-string">"https://github.com/localheinz"</span>, <span class="hljs-string">"https://twitter.com/localheinz"</span> ] } </code></pre> <p>But what if - for some reason - I need to indent with 2 spaces, or with tabs? Or with 13 spaces?</p> <p>I first turned to <a target="_blank" href="https://github.com/zendframework/zend-json" title="zendframework/zend-json"><code>zendframework/zend-json</code></a>. Unfortunately it doesn't cover all of the cases I need it to cover, so I started to search. First thing I found was a <a target="_blank" href="https://www.daveperrett.com/articles/2008/03/11/format-json-with-php/" title="Format JSON With PHP">blog post</a> from 2008, by <a target="_blank" href="https://github.com/recurser" title="Dave Perrett">Dave Perrett</a>. Unfortunately, I felt uncomfortable using it (I would have preferred to find a PHP package, with tests). So I continued to search. Next stop, Stack Overflow: I found a <a target="_blank" href="https://stackoverflow.com/q/6054033/1172545" title="Pretty-Printing JSON with PHP">question</a> from 2011. Of course, one <a target="_blank" href="https://stackoverflow.com/a/6054389/1172545" title="An answer to Pretty-Printing JSON with PHP">answer</a> recommended the blog post by Dave Perrett again. A dead end, it seemed. So I continued to search. Then I took a look at the source code of <a target="_blank" href="https://github.com/composer/composer" title="composer/composer"><code>composer/composer</code></a> and found <a target="_blank" href="https://github.com/composer/composer/blob/1.6.0/src/Composer/Json/JsonFormatter.php" title="Composer\Json\JsonFormatter"><code>Composer\Json\JsonFormatter</code></a>. Guess what? Of course it is based on the blog post by Dave Perrett.</p> <p>Well, then, off I go and take the code, grab some tests, add more tests, cut away the parts that I don't need (back-porting <code>JSON_UNESCAPED_SLASHES</code> and <code>JSON_UNESCAPED_UNICODE</code> for PHP versions prior to 5,4), adjust it so it can process already pretty-printed JSON, allow to pass in an indent string, and publish it as <a target="_blank" href="https://github.com/ergebnis/json-printer" title="ergebnis/json-printer"><code>ergebnis/json-printer</code></a>.</p> <p>Now you can print JSON with 2 spaces, or tabs, or 13 spaces indentation, if you please:</p> <pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>); <span class="hljs-keyword">use</span> <span class="hljs-title">Ergebnis</span>\<span class="hljs-title">Json</span>\<span class="hljs-title">Printer</span>; $indent = <span class="hljs-string">' '</span>; $printer = <span class="hljs-keyword">new</span> Printer\Printer(); $printer-&gt;print( $json, $indent ); </code></pre> <p>which will then result in</p> <pre><code class="language-json hljs json" data-lang="json">{ <span class="hljs-attr">"name"</span>:<span class="hljs-string">"Andreas Möller"</span>, <span class="hljs-attr">"emoji"</span>:<span class="hljs-string">"🤓"</span>, <span class="hljs-attr">"urls"</span>:[ <span class="hljs-string">"https://localheinz.com"</span>, <span class="hljs-string">"https://github.com/localheinz"</span>, <span class="hljs-string">"https://twitter.com/localheinz"</span> ] } </code></pre> <p>Hope it helps!</p> Essential PhpStorm pluginsAndreas Möllerhttp://localhost/am@localheinz.comhttp://localhost/blog/2017/10/27/essential-phpstorm-plugins/2017-10-27T09:30:00+01:00<h1> Essential PhpStorm plugins </h1> <p> If you are like me and have been programming for a bit, you probably have worked with several editors and IDEs. Over the years, I have tried a bunch of them - with varying degrees of successes. Some of them were just the right tool to get started, others were recommended by friends and developers along the way. Most of them were quickly outgrown by the projects I worked on at the time. </p> <p> So I kept looking for alternatives. </p> <h2> PhpStorm </h2> <p>In January 2012 I attended a meeting of <a target="_blank" href="https://www.bephpug.de/">BEPHPUG</a>, the Berlin PHP user group. At that meeting, a few IDEs and editors (some of which I had worked with before) were demonstrated. <a target="_blank" href="https://github.com/bashofmann">Bastian Hoffmann</a> gave a demonstration of <a target="_blank" href="https://www.jetbrains.com/phpstorm/">PhpStorm</a>, and I was amazed! Directly after the meeting I went home, installed it, and have been using it ever since. As advertised, it's lightning fast and smart, and I can't image I could be any happier or more productive with any other IDE!</p> <h2> Plugins </h2> <p> Nonetheless, while PhpStorm already <em>is</em> the perfect development environment, there are plugins that make it even better. I have tried a few, and here's a list of what I see as essential PhpStorm plugins: </p> <ul> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/9525--env-files-support" title=".env files support">.env files support</a> by <a target="_blank" href="https://github.com/adelf" title="Adel Fayzrakhmanov">Adel Fayzrakhmanov</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/7495--ignore" title=".ignore">.ignore</a> by <a target="_blank" href="https://github.com/hsz" title="Jakub Chrzanowski">Jakub Chrzanowski</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/6834-apache-config--htaccess-support" title="Apache config (.htaccess) support">Apache config (.htaccess) support</a> by <a target="_blank" href="https://github.com/neuro159" title="Alexey Gopachenko">Alexey Gopachenko</a>, <a target="_blank" href="https://github.com/MaXal" title="Maxim Kolmakov">Maxim Kolmakov</a> and <a target="_blank" href="https://github.com/SvetlanaZem" title="Svetlana Zemlyanskaya">Svetlana Zemlyanskaya</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/4230-bashsupport" title="BashSupport">BashSupport</a> by <a target="_blank" href="https://github.com/jansorg" title="Joachim Ansorg">Joachim Ansorg</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/7160-camelcase" title="CamelCase">CamelCase</a> by <a target="_blank" href="https://github.com/johannespfeiffer" title="Johannes Pfeiffer">Johannes Pfeiffer</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/14896-code-with-me" title="Code With Me">Code With Me</a> by <a target="_blank" href="https://www.jetbrains.com" title="JetBrains s.r.o">JetBrains s.r.o</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/7275-codeglance" title="CodeGlance">CodeGlance</a> by <a target="_blank" href="https://github.com/Vektah" title="Adam Scarr">Adam Scarr</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/7499-gittoolbox" title="GitToolBox">GitToolBox</a> by <a target="_blank" href="https://github.com/zielu" title="Lukasz Zielinski">Lukasz Zielinski</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/9792-key-promoter-x" title="Key Promoter X">Key Promoter X</a> by <a target="_blank" href="https://github.com/halirutan" title="Patrick Scheibe">Patrick Scheibe</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/5919-lines-sorter" title="Lines Sorter">Lines Sorter</a> by <a target="_blank" href="https://github.com/syllant" title="Sylvain Francois">Sylvain Francois</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/9333-makefile-support" title="Makefile support">Makefile support</a> by <a target="_blank" href="https://github.com/kropp" title="Victor Kropp">Victor Kropp</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced" title="Markdown Navigator Enhanced">Markdown Navigator Enhanced</a> by <a target="_blank" href="https://github.com/vsch" title="Vladimir Schneider">Vladimir Schneider</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/7060-neon-support" title="NEON support">NEON support</a> by <a target="_blank" href="https://github.com/juzna" title="Jan Dolecek">Jan Dolecek</a> and <a target="_blank" href="https://github.com/matej21" title="David Matějka">David Matějka</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/7320-php-annotations" title="PHP Annotations">PHP Annotations</a> by <a target="_blank" href="https://github.com/Haehnchen" title="Daniel Espendiller">Daniel Espendiller</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/12754-phpstan--psalm--generics" title="PHPStan / Psalm / Generics">PHPStan / Psalm / Generics</a> by <a target="_blank" href="https://github.com/Haehnchen" title="Daniel Espendiller">Daniel Espendiller</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/9674-phpunit-enhancement" title="PHPUnit Enhancement">PHPUnit Enhancement</a> by <a target="_blank" href="https://github.com/Haehnchen" title="Daniel Espendiller">Daniel Espendiller</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/7622-php-inspections-ea-extended-" title="Php Inspections (EA Extended)">Php Inspections (EA Extended)</a> by <a target="_blank" href="https://github.com/kalessil" title="Vladimir Reznichenko">Vladimir Reznichenko</a>, <a target="_blank" href="https://github.com/funivan" title="Ivan Scherbak">Ivan Scherbak</a> and <a target="_blank" href="https://github.com/rentalhost" title="David Rodrigues">David Rodrigues</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/10215-php-inspections-ea-ultimate-" title="Php Inspections (EA Ultimate)">Php Inspections (EA Ultimate)</a> by <a target="_blank" href="https://github.com/kalessil" title="Vladimir Reznichenko">Vladimir Reznichenko</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/2917-regexptester" title="RegexpTester">RegexpTester</a> by <a target="_blank" href="https://github.com/sevdokimov" title="Sergey Evdokimov">Sergey Evdokimov</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/2162-string-manipulation" title="String Manipulation">String Manipulation</a> by <a target="_blank" href="https://github.com/krasa" title="Vojtěch Krása">Vojtěch Krása</a> </li> <li> <a target="_blank" href="https://plugins.jetbrains.com/plugin/7219-symfony-plugin" title="Symfony Plugin">Symfony Plugin</a> by <a target="_blank" href="https://github.com/Haehnchen" title="Daniel Espendiller">Daniel Espendiller</a> </li> </ul> <p> You can find more plugins from within the IDE or by browsing the <a target="_blank" href="https://plugins.jetbrains.com/phpstorm" title="JetBrains PHPStorm Plugin Repository">JetBrains PHPStorm Plugin Repository</a>. </p> <h2> Not using PHPStorm yet? </h2> <p> If you haven't been using PhpStorm before, I highly recommend giving it a try. If you are a student, you can even use PhpStorm or any JetBrains IDE <a target="_blank" href="https://www.jetbrains.com/student/">free of charge</a>. JetBrains also offer a free 30-day-trial, so you can find out if PHPStorm will work for you without any strings attached. If you need some more time, you can also participate in the <a target="_blank" href="https://confluence.jetbrains.com/display/PhpStorm/PhpStorm+Early+Access+Program">PhpStorm Early Access Program</a>, which allows you to use the early releases of upcoming versions in exchange for living with (and ideally reporting) an occasional bug or two. </p> <p> Don't waste any more time with mediocre editors or IDEs! </p> <figure> <a target="_blank" href="https://www.jetbrains.com/phpstorm/" title="PhpStorm"> <img class="webfeedsFeaturedVisual" src="/dist/img/post/essential-phpstorm-plugins/boxshot.png?3ede256" alt="Boxshot of PhpStorm"> </a> </figure>