<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lynn Romich</title>
    <description>The latest articles on DEV Community by Lynn Romich (@lynnntropy).</description>
    <link>https://dev.to/lynnntropy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F169994%2F398bd462-a499-407c-a0e2-e0f307b4e223.png</url>
      <title>DEV Community: Lynn Romich</title>
      <link>https://dev.to/lynnntropy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lynnntropy"/>
    <language>en</language>
    <item>
      <title>E2E Testing an App with Clerk Authentication in Cypress</title>
      <dc:creator>Lynn Romich</dc:creator>
      <pubDate>Thu, 21 Apr 2022 00:39:25 +0000</pubDate>
      <link>https://dev.to/lynnntropy/e2e-testing-an-app-with-clerk-authentication-in-cypress-3ae7</link>
      <guid>https://dev.to/lynnntropy/e2e-testing-an-app-with-clerk-authentication-in-cypress-3ae7</guid>
      <description>&lt;p&gt;Background: &lt;a href="https://clerk.dev/"&gt;Clerk&lt;/a&gt; is a hosted authentication and user management product.&lt;/p&gt;




&lt;p&gt;I recently started writing E2E tests in Cypress for an app that uses &lt;a href="https://clerk.dev/"&gt;Clerk&lt;/a&gt; for authentication, and there wasn’t anything out there to guide me, so here’s what I ended up with after fiddling with it for a bit.&lt;/p&gt;

&lt;p&gt;(Note: In my case, this is a Next.js app using Clerk’s Next.js SDK, but my understanding is that this code will work everywhere, because their client SDKs all ultimately use ClerkJS under the hood.)&lt;/p&gt;

&lt;p&gt;I wrote a &lt;a href="https://docs.cypress.io/api/cypress-api/custom-commands"&gt;custom Cypress command&lt;/a&gt; that waits for Clerk to load, signs the user out if they aren’t signed out already, and then signs in with test credentials (see &lt;a href="https://docs.cypress.io/guides/guides/environment-variables"&gt;here&lt;/a&gt; for how you can set these so that they’re accessible via &lt;code&gt;Cypress.env()&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`initializeAuth`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Initializing auth state.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Clerk`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Clerk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isReady&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Clerk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signOut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Clerk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`TEST_USER_IDENTIFIER`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`TEST_USER_PASSWORD`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you’re using TypeScript to write your tests (which I recommend!), you’ll also want to add this command to your &lt;a href="https://docs.cypress.io/guides/tooling/typescript-support#Types-for-custom-commands"&gt;custom command types&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Chainable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="cm"&gt;/**
       * Initialize auth to a state where you're logged in as the test user.
       * @example cy.initializeAuth()
       */&lt;/span&gt;
      &lt;span class="nx"&gt;initializeAuth&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Chainable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ultimately, you’ll probably want to use this command in a &lt;code&gt;before&lt;/code&gt; or &lt;code&gt;beforeEach&lt;/code&gt; hook to reset the auth state before every test, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Test Page`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resetDatabase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// another custom command&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initializeAuth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// ... tests go here&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Happy testing! Please let me know if you run into problems with this approach.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Yes, PHP is Worth Learning/Using in $CURRENT_YEAR</title>
      <dc:creator>Lynn Romich</dc:creator>
      <pubDate>Wed, 17 Nov 2021 23:25:40 +0000</pubDate>
      <link>https://dev.to/lynnntropy/yes-php-is-worth-learningusing-in-currentyear-l85</link>
      <guid>https://dev.to/lynnntropy/yes-php-is-worth-learningusing-in-currentyear-l85</guid>
      <description>&lt;p&gt;Welcome, dear reader! If you're reading this, you're probably one of the many people who find themselves wondering how much of what they've heard about PHP (a lot of which isn't super positive, I'm sure) is still relevant today. Is PHP a dying language? Should you learn PHP in $CURRENT_YEAR, and/or use it to build your next app? Hopefully, by the end of this post, you'll have the answers to these questions and more.&lt;/p&gt;

&lt;p&gt;Rather than yet another generic overview of the language or a point-by-point refutation of the things people say is wrong with it, what I want this post to be more than anything else is kind of a comprehensive list of ✨good things about PHP✨ (or, well, at least things that I think are good).&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Got Here
&lt;/h2&gt;

&lt;p&gt;If you have any preconceptions about PHP at all, they're probably largely shaped by what the discourse around it was during the PHP 4 and PHP 5 days. This was an era where PHP was increasingly seen as a "legacy" platform in contrast to cool new projects like Ruby on Rails and Node.js, and conventional wisdom was that PHP was simply &lt;a href="https://eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design/"&gt;a "bad" language&lt;/a&gt;, or at least a language a lot of people were writing bad code in.&lt;/p&gt;

&lt;p&gt;You might also have heard that PHP 7 was a big step forward for PHP, though, and this is true. While the story of how PHP got to where it is today is largely one of many incremental improvements, a lot of people would probably agree that the release of PHP 7 in 2015 was the start of the "modern" PHP era. PHP 7 &lt;a href="https://www.php.net/manual/en/migration70.new-features.php"&gt;included&lt;/a&gt;, among other things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.archive.org/web/20160413094701/http://zend.com/en/resources/php7_infographic"&gt;dramatic performance improvements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;a significantly expanded type system (scalar types and return types)&lt;/li&gt;
&lt;li&gt;anonymous classes&lt;/li&gt;
&lt;li&gt;the null coalescing operator (&lt;code&gt;??&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the spaceship operator (&lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;unicode codepoint escape syntax (&lt;code&gt;echo "\u{2764}";&lt;/code&gt; → ❤️)&lt;/li&gt;
&lt;li&gt;a &lt;a href="https://www.php.net/manual/en/ref.csprng.php"&gt;built-in CSPRNG API&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What PHP Code Looks Like Today
&lt;/h2&gt;

&lt;p&gt;Over the last several years, PHP has become a significantly more ergonomic language, as examples like &lt;a href="https://stitcher.io/blog/evolution-of-a-php-object"&gt;this post&lt;/a&gt; illustrate very well. Let's do a rundown of some of the most significant features PHP has gained over the years, some of which you may or may not recognize from other languages (and some you might even wish your favorite language had!).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.php.net/manual/en/migration70.new-features.php#migration70.new-features.null-coalesce-op"&gt;The null coalescing operator&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// equivalent to:&lt;/span&gt;
&lt;span class="c1"&gt;// $username = isset($_GET['user']) ? $_GET['user'] : 'anonymous';&lt;/span&gt;

&lt;span class="nv"&gt;$username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'anonymous'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://stitcher.io/blog/php-8-nullsafe-operator"&gt;The nullsafe operator&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// equivalent to:&lt;/span&gt;
&lt;span class="c1"&gt;// $dateTime = $event-&amp;gt;getDateTime();&lt;/span&gt;
&lt;span class="c1"&gt;// $timestamp = $dateTime ? $dateTime-&amp;gt;getTimestamp() : null;&lt;/span&gt;

&lt;span class="nv"&gt;$timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDateTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTimestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://stitcher.io/blog/constructor-promotion-in-php-8"&gt;Constructor property promotion&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WidgetManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WidgetManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://stitcher.io/blog/php-8-match-or-switch"&gt;The &lt;code&gt;match&lt;/code&gt; expression&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SomeError&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OtherError&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnknownError&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://stitcher.io/blog/php-8-named-arguments"&gt;Named arguments&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$third&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$fourth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;testFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'second value'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'first value'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fourth&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'fourth value'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://stitcher.io/blog/short-closures-in-php"&gt;Arrow functions&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayCollection&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$incremented&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// $incremented is [2, 3, 4]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://stitcher.io/blog/attributes-in-php-8"&gt;Attributes&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;#[Route('/greetings')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GreetingController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;#[Route('/hello')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'hello'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://www.php.net/manual/en/migration70.new-features.php#migration70.new-features.spaceship-op"&gt;The spaceship operator&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The spaceship operator is a little esoteric (and possibly a little controversial, depending on how confident you are that people reading your code will know what it does without looking it up), but the one thing it's very useful for is writing clear and succinct comparison/sorting functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// -1&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// -1&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://wiki.php.net/rfc/spread_operator_for_array"&gt;The spread operator&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'a'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'b'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'c'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'d'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$second&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// $merged is ['a' =&amp;gt; 1, 'b' =&amp;gt; 2, 'c' =&amp;gt; 3, 'd' =&amp;gt; 4]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://wiki.php.net/rfc/numeric_literal_separator"&gt;The numeric literal separator&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$withoutSeparators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$withSeparators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$withoutSeparators&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$withSeparators&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://stitcher.io/blog/array-destructuring-with-list-in-php"&gt;Array destructuring&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$second&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$list&lt;/span&gt;

&lt;span class="c1"&gt;// $first is 1, $second is 2&lt;/span&gt;

&lt;span class="nv"&gt;$array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'a'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'b'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'a'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'b'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// $a is 1, $b is 2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  PHP's Type System
&lt;/h2&gt;

&lt;p&gt;While it's by no means the strictest type system out there (not to mention entirely optional, much like TypeScript), modern PHP has a robust type system that includes features like &lt;strong&gt;interfaces, scalar and object types, nullable types, union and intersection types, and more&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Code speaks louder than words, so here's an example of what code that takes full advantage of the type system in PHP 8.1 can look like:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyClass&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;DateTimeInterface&lt;/span&gt; &lt;span class="nv"&gt;$dateTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nf"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;useUnionTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// $input is guaranteed to be either an int or a string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;useIntersectionTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Traversable&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;Countable&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// $input is guaranteed to satisfy the constraints&lt;/span&gt;
        &lt;span class="c1"&gt;// of both `Traversable` AND `Countable`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Package Management
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://getcomposer.org/"&gt;Composer&lt;/a&gt; is the de facto standard package manager for modern-day PHP, and has been for about a decade now. It's strongly inspired by other popular package managers, such as &lt;a href="https://www.npmjs.com/"&gt;npm&lt;/a&gt; for JavaScript, so if you've used a modern package manager in any other language, chances are you'll feel right at home with Composer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://packagist.org/"&gt;Packagist&lt;/a&gt; is the main public package repository for Composer. Like npm, you can also use Packagist to host your private packages for &lt;a href="https://packagist.com/pricing"&gt;a reasonable monthly fee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What I'd consider the main difference between Composer and npm is actually one of culture, rather than a technical difference — the PHP community doesn't generally have the preference for micropackages that the JavaScript community does, so for better or worse, the average PHP project is more likely to have dozens of larger dependencies than hundreds of smaller ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frameworks
&lt;/h2&gt;

&lt;p&gt;The current PHP landscape is dominated by two web application frameworks: &lt;a href="https://laravel.com/"&gt;Laravel&lt;/a&gt; and &lt;a href="https://symfony.com/"&gt;Symfony&lt;/a&gt;. While a detailed breakdown of the differences and similarities between these is out of scope for this post, suffice to say that they're both modern, expressive frameworks that aim to make it easier to write robust, fast, and maintainable web applications while reducing the need to write boilerplate code as much as possible.&lt;/p&gt;

&lt;p&gt;If you're concerned about frameworks perhaps being "overkill" for what you want to do with PHP, you'll be happy to hear that Symfony is a microframework out of the box (all components outside of the core framework are 100% optional), and Laravel also has a microframework variant called &lt;a href="https://lumen.laravel.com/"&gt;Lumen&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To help you get a bit of a feel for these, here's what some typical modern PHP code written with one of these frameworks might look like:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\Annotation\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExampleController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// the RandomNumberGenerator is automatically&lt;/span&gt;
    &lt;span class="c1"&gt;// injected by a service container&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;RandomNumberGenerator&lt;/span&gt; &lt;span class="nv"&gt;$randomNumberGenerator&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="c1"&gt;#[Route('/number')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;randomNumberGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;"&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;Your lucky number is: &lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;"&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Open-Source Ecosystem
&lt;/h2&gt;

&lt;p&gt;PHP has an incredibly robust open-source ecosystem that I honestly think even a lot of more "respectable" languages can envy.&lt;/p&gt;

&lt;p&gt;There are high-quality, well-maintained open-source libraries available for pretty much everything the average PHP application is likely to need, and many of the most popular packages in the ecosystem are maintained by established vendors or projects rather than &lt;a href="https://xkcd.com/2347/"&gt;individual maintainers&lt;/a&gt;, though of course there's plenty of those, too.&lt;/p&gt;

&lt;p&gt;Anyhow, the following is a &lt;em&gt;very&lt;/em&gt; surface-level overview of some of the most significant packages in the PHP ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;symfony/*&lt;/code&gt; - the &lt;a href="https://symfony.com/components"&gt;Symfony Components&lt;/a&gt;, a set of incredibly popular PHP packages and the foundation for the &lt;a href="https://symfony.com/doc/current/index.html"&gt;Symfony framework&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;symfony/cache&lt;/code&gt; - a production-ready caching library with support for many different backing stores&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/console&lt;/code&gt; - a CLI library used by many notable PHP projects&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/dependency-injection&lt;/code&gt; - a &lt;a href="https://www.php-fig.org/psr/psr-11/"&gt;PSR-11&lt;/a&gt;-compatible service container&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/dotenv&lt;/code&gt; - a &lt;code&gt;.env&lt;/code&gt; file parser&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/event-dispatcher&lt;/code&gt; - an event dispatcher&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/form&lt;/code&gt; - a library for creating and processing forms (HTML or otherwise)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/http-client&lt;/code&gt; - an HTTP client library&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/mailer&lt;/code&gt; - a multi-transport library for creating and sending emails&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/messenger&lt;/code&gt; - a message bus with support for sync and async message processing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/notifier&lt;/code&gt; - a tool for sending notifications with first-party support for email, SMS, Slack, Discord, Telegram, push notifications, and more&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/routing&lt;/code&gt; - the router used by the Symfony framework&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/security&lt;/code&gt; - utilities for authentication, authorization, CSRF protection, and other common security needs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/serializer&lt;/code&gt; - a serialization/deserialization library with support for JSON, XML, YAML, CSV, and more&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;symfony/validator&lt;/code&gt; - a data validation library&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;league/*&lt;/code&gt; - &lt;a href="https://thephpleague.com/"&gt;The League of Extraordinary Packages&lt;/a&gt;, a set of modern, standards-compliant PHP packages developed with the explicit mission of improving the PHP ecosystem

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;league/commonmark&lt;/code&gt; - a CommonMark-compliant Markdown parser&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;league/csv&lt;/code&gt; - read and write CSV documents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;league/flysystem&lt;/code&gt; - a filesystem abstraction with support for local filesystems, object storage, FTP, and more&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;league/oauth2-server&lt;/code&gt; - an OAuth 2.0 authorization server implementation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;league/oauth2-client&lt;/code&gt; - an OAuth 2.0 client library with built-in and community support for many common OAuth 2.0 providers, as well as custom providers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;league/omnipay&lt;/code&gt; - a multi-gateway payment processing library&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;doctrine/*&lt;/code&gt; - packages from &lt;a href="https://www.doctrine-project.org/"&gt;the Doctrine Project&lt;/a&gt;, largely but not exclusively related to working with databases

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;doctrine/collections&lt;/code&gt; - utilities for working with arrays of data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;doctrine/dbal&lt;/code&gt; - a &lt;strong&gt;d&lt;/strong&gt;ata*&lt;em&gt;b&lt;/em&gt;&lt;em&gt;ase **a&lt;/em&gt;&lt;em&gt;bstraction **l&lt;/em&gt;*ayer with support for MySQL, Oracle, Microsoft SQL Server, PostgreSQL, and SQLite databases&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;doctrine/orm&lt;/code&gt; - the Doctrine ORM, a popular PHP ORM based on the Data Mapper pattern&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;doctrine/migrations&lt;/code&gt; - utilities for database schema versioning (i.e. &lt;a href="https://en.wikipedia.org/wiki/Schema_migration"&gt;database migrations&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;phpoffice/*&lt;/code&gt; - packages from the &lt;a href="https://github.com/PHPOffice"&gt;PHPOffice&lt;/a&gt; project, a set of libraries for working with file formats produced by Microsoft Office and other office suites

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;phpoffice/phppresentation&lt;/code&gt; - read and write presentation file formats (e.g. &lt;code&gt;.pptx&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;phpoffice/phpspreadsheet&lt;/code&gt; - read and write spreadsheet file formats (e.g. &lt;code&gt;.xlsx&lt;/code&gt;, &lt;code&gt;.csv&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;phpoffice/phpword&lt;/code&gt; - read and write document file formats (e.g. &lt;code&gt;.docx&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;guzzlehttp/guzzle&lt;/code&gt; - an HTTP client library based on &lt;a href="https://www.php-fig.org/psr/psr-7/"&gt;PSR-7&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;monolog/monolog&lt;/code&gt; - a very widely-used logging library based on &lt;a href="https://www.php-fig.org/psr/psr-3/"&gt;PSR-3&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;phpunit/phpunit&lt;/code&gt; - the de facto standard PHP testing framework, based on the &lt;a href="https://en.wikipedia.org/wiki/XUnit"&gt;xUnit&lt;/a&gt; architecture&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pestphp/pest&lt;/code&gt; - a modern testing framework built on top of PHPUnit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nesbot/carbon&lt;/code&gt; - an extension of the PHP &lt;code&gt;DateTime&lt;/code&gt; API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;phpstan/phpstan&lt;/code&gt; - a static analysis tool, the most popular of several active projects&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;twig/twig&lt;/code&gt; - a modern template engine, inspired by Python's Jinja template engine&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  PHP-FIG
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.php-fig.org/"&gt;PHP-FIG&lt;/a&gt; — short for "PHP &lt;strong&gt;F&lt;/strong&gt;ramework &lt;strong&gt;I&lt;/strong&gt;nterop &lt;strong&gt;G&lt;/strong&gt;roup" — is a group of influential projects in the PHP community working to push PHP forward by standardizing a bunch of things every project was doing in its own, ever-so-slightly incompatible way.&lt;/p&gt;

&lt;p&gt;Unfortunately, while pretty much all of the most influential projects in the PHP ecosystem &lt;em&gt;were&lt;/em&gt; once members of PHP-FIG, many have since left over concerns that the project was headed in the direction of building a "framework-by-committee" rather than working on relatively simple standards everyone could actually implement. &lt;a href="https://xkcd.com/1095/"&gt;Some things&lt;/a&gt; never change, I guess.&lt;/p&gt;

&lt;p&gt;That being said, PHP-FIG still very much deserves a place in this post, both because it's frankly a pretty unique project I don't think I've seen attempted anywhere else, and also because it's still produced a number of incredibly useful &lt;a href="https://www.php-fig.org/psr/"&gt;PSRs&lt;/a&gt; (&lt;strong&gt;P&lt;/strong&gt;HP &lt;strong&gt;S&lt;/strong&gt;tandard &lt;strong&gt;R&lt;/strong&gt;ecommendations) over the years. These include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an autoloading standard (&lt;a href="https://www.php-fig.org/psr/psr-4/"&gt;PSR-4&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;interfaces for common app/framework components (&lt;a href="https://www.php-fig.org/psr/psr-3/"&gt;PSR-3&lt;/a&gt;, &lt;a href="https://www.php-fig.org/psr/psr-6/"&gt;PSR-6&lt;/a&gt;, &lt;a href="https://www.php-fig.org/psr/psr-11/"&gt;PSR-11&lt;/a&gt;, &lt;a href="https://www.php-fig.org/psr/psr-14/"&gt;PSR-14&lt;/a&gt;, &lt;a href="https://www.php-fig.org/psr/psr-16/"&gt;PSR-16&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;standards for HTTP request/response objects and code that handles them (&lt;a href="https://www.php-fig.org/psr/psr-7/"&gt;PSR-7&lt;/a&gt;, &lt;a href="https://www.php-fig.org/psr/psr-18/"&gt;PSR-18&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;style guides (&lt;a href="https://www.php-fig.org/psr/psr-1/"&gt;PSR-1&lt;/a&gt;, &lt;a href="https://www.php-fig.org/psr/psr-12/"&gt;PSR-12&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  No Compiling/Transpiling
&lt;/h2&gt;

&lt;p&gt;One thing you'll probably find refreshing about PHP compared to many comparable languages is that it doesn't require a build step (at least, not one that you have to think about). In fact, due to PHP's typical execution model (more on that later), not only is there no build step, but you don't even have to restart your web server when you change your code — hit save, send the request again and the response you get will be from the code you just saved.&lt;/p&gt;

&lt;p&gt;One major benefit of this (to me, anyway) is that when you install a package through Composer, you're simply downloading the source code of the package, not compiled artifacts or code mangled by some build tool before publishing. What this means for you is that any time you click into, say, a function that comes from a third-party package, you're going to be looking at the actual source code — formatting, comments and all.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP is Fast
&lt;/h2&gt;

&lt;p&gt;While it's hard to make apples-to-apples performance comparisons between programming languages, and you probably shouldn't be worrying about language runtime performance that much anyway (your code is almost always going to be I/O-bound, after all), it's worth pointing out that modern PHP is very fast, handily beating "slow" languages like Ruby in &lt;a href="https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/php-ruby.html"&gt;synthetic benchmarks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;More than anything else, this is thanks to lots of hard work put into improving performance by the PHP team over the years, up to and including adding entirely new features, such as the &lt;a href="https://php.watch/versions/8.0/JIT"&gt;JIT compiler&lt;/a&gt; introduced in PHP 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ups and Downs of PHP's Unique Execution Model
&lt;/h2&gt;

&lt;p&gt;You might have heard people say that PHP was "doing serverless before serverless was a thing", and this is &lt;em&gt;kind of&lt;/em&gt; true.&lt;/p&gt;

&lt;p&gt;The way people used to write PHP was they'd have a bunch of PHP files where each file corresponded to a page or route (e.g. &lt;code&gt;index.php&lt;/code&gt; for a homepage, &lt;code&gt;item.php&lt;/code&gt; for another page, and so on) and output some HTML. Then they'd upload this code to their server — often a shared hosting provider where all a developer had to do was FTP the files up to the server, and it'd just work. The web server would handle all the parts external to your code, like starting up a PHP process for the request and giving it the right script based on the path.&lt;/p&gt;

&lt;p&gt;There's a lot of sites on the internet that still work this way, and while PHP has since evolved past some parts of this approach, you can probably see how it resembles some patterns that have become popular in recent years, like &lt;a href="https://nextjs.org/docs/routing/introduction"&gt;filesystem-based routing&lt;/a&gt; and &lt;a href="https://vercel.com/docs/concepts/functions/serverless-functions"&gt;serverless functions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The part of this that's most relevant today is the idea that &lt;strong&gt;your app gets initialized and torn down for every request&lt;/strong&gt;. Any variables you set, anything you do to the objects in your app, everything gets wiped out at the end of the request — there's no way to persist data between requests without relying on some sort of external resource, like a database.&lt;/p&gt;

&lt;p&gt;There are naturally some drawbacks to this setup, not the least of which being that it means that putting some data in memory to keep it around for a while is less trivial in PHP than it is in something like Node.js. On the other hand, this setup makes whole classes of bugs impossible at the application level (like meaningful memory leaks), and perhaps more importantly, it means you don't have to worry about writing async code nearly as much as other languages, because you're only ever going to be handling one request per process anyway.&lt;/p&gt;

&lt;p&gt;For instance, making an HTTP request is as simple as writing a blocking call to an HTTP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpClient\HttpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStatusCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In most other languages, code like this would be a huge no-no because you'd be blocking the runtime from handling any other requests while you wait for that call to &lt;code&gt;HttpClient::request()&lt;/code&gt; to finish. In PHP, though, because the assumption that you have the process "to yourself" is built into the language, you can write blocking code like this safely, while avoiding the mental overhead that async code can sometimes involve.&lt;/p&gt;

&lt;p&gt;I should probably cap this section off by mentioning that there &lt;em&gt;has&lt;/em&gt; been a push in recent years to make asynchronous code viable in PHP, and frameworks like &lt;a href="https://amphp.org/"&gt;Amp&lt;/a&gt; have made significant progress in this area. That being said, the vast majority of PHP code is still synchronous, and will be for the foreseeable future.&lt;/p&gt;

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

&lt;p&gt;If you came into this post with the mindset that PHP is a legacy language nobody in their right mind would want to write code for today, well, I hope I've been able to at least somewhat change your mind on that.&lt;/p&gt;

&lt;p&gt;PHP, of course, still has its share of warts, but more than anything else, what I wanted to convey with this post is that &lt;strong&gt;it's absolutely possible to write reliable, clean, maintainable code in PHP that you and/or your team will be happy with&lt;/strong&gt;. Not only that, but there are parts of PHP that you might even &lt;em&gt;prefer&lt;/em&gt; to how things work in other languages you're familiar with.&lt;/p&gt;

&lt;p&gt;Still not convinced? You're more than welcome to peruse the rest of my site (&lt;a href="https://bulletproofphp.dev"&gt;https://bulletproofphp.dev&lt;/a&gt;) to perhaps get a better feel for what real-world PHP looks like, and if you have concerns you think I overlooked in this post, I'd be happy to talk about it &lt;a href="https://twitter.com/omegavesko"&gt;on Twitter&lt;/a&gt; or in the comments here.&lt;/p&gt;

</description>
      <category>php</category>
      <category>programming</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Simple, Privacy-Friendly, and Free Analytics Using Serverless Functions</title>
      <dc:creator>Lynn Romich</dc:creator>
      <pubDate>Wed, 14 Apr 2021 23:25:18 +0000</pubDate>
      <link>https://dev.to/lynnntropy/simple-privacy-friendly-and-free-analytics-using-serverless-functions-28j5</link>
      <guid>https://dev.to/lynnntropy/simple-privacy-friendly-and-free-analytics-using-serverless-functions-28j5</guid>
      <description>&lt;p&gt;Whether it's for work or play, most of us usually want &lt;em&gt;some&lt;/em&gt; analytics on our websites, but the available options typically come with one or more pretty major downsides:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Massive JavaScript payload&lt;/strong&gt; -- Most of the big analytics products have this problem. &lt;code&gt;gtag.js&lt;/code&gt; is ~35kB minzipped, ~90kB uncompressed (at time of writing); some others are even worse. Yes, these days this is loaded asynchronously and doesn't directly hold up your page while it gets loaded and parsed, but that's still an absolutely indefensible amount of overhead just to get some super basic data on how your site is being used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy implications&lt;/strong&gt; -- Again, this is one you're almost certainly going to hit if you're looking at a free, hosted analytics product, like Google Analytics or Yandex.Metrica. Third-party cookies may thankfully be going the way of the dodo, but tools like Google Analytics -- if you use the tracking code they give you -- will still insist on giving your users at least first-party cookies they can use to identify them across multiple requests (and visits), regardless of whether you want that feature or not.&lt;/p&gt;

&lt;p&gt;From a practical perspective, this means you now need to ask your users to consent to these tracking cookies, and from an ethical perspective, you may just want to avoid tracking your users like this altogether, especially if it means doing it through someone like Google.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inaccuracy due to people blocking analytics&lt;/strong&gt; -- Whether it's through privacy-enhancing browser features, browser extensions like &lt;a href="https://github.com/gorhill/uBlock"&gt;uBlock Origin&lt;/a&gt; or even something like &lt;a href="https://github.com/pi-hole/pi-hole"&gt;Pi-hole&lt;/a&gt;, lots of people these days are running something that blocks obvious analytics requests. Depending on how privacy-conscious your audience is, as many as &lt;a href="https://blog.wesleyac.com/posts/google-analytics"&gt;~45%&lt;/a&gt; of your visitors could be blocking analytics entirely.&lt;/p&gt;

&lt;p&gt;Serverside analytics naturally don't suffer from this problem, and some privacy-focused analytics products like &lt;a href="https://usefathom.com/support/custom-domains"&gt;Fathom Analytics&lt;/a&gt; and &lt;a href="https://plausible.io/docs/custom-domain"&gt;Plausible Analytics&lt;/a&gt; give you ways to use your own domain for analytics to avoid being caught by this, but if you're using a typical big-name analytics product (unless you're being particularly clever about it, which we'll get to), this is a huge drawback you're kind of just stuck with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt; -- If you're looking to add analytics to something like a personal site, this is probably your biggest problem. Modern, privacy-focused analytics products like &lt;a href="https://usefathom.com/"&gt;Fathom Analytics&lt;/a&gt;, &lt;a href="https://plausible.io/"&gt;Plausible Analytics&lt;/a&gt; and &lt;a href="https://www.netlify.com/products/analytics/"&gt;Netlify Analytics&lt;/a&gt; give you ways to avoid many or even all of the other problems on this list, but they're not free -- and there's nothing wrong with that, but I can't justify paying a monthly subscription just to see how many people read my blog posts, and you probably can't either.&lt;/p&gt;

&lt;p&gt;One way to get around this could be self-hosting an open-source analytics product like &lt;a href="https://matomo.org/"&gt;Matomo Analytics&lt;/a&gt;, but unless you already have somewhere you can put that without incurring any extra costs, that costs money, too.&lt;/p&gt;

&lt;p&gt;That being said, if you're looking for an analytics solution for your business or something else that actually makes money, I think you should strongly consider just paying for one of these products and being done with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Serverless
&lt;/h2&gt;

&lt;p&gt;Okay, so what can we do about this? Well, we can be smart about the way we use analytics, and take control of how our analytics are collected while still leaving the bulk of the work to a third-party analytics product (if we want to).&lt;/p&gt;

&lt;p&gt;How do we do that? With a serverless function.&lt;/p&gt;

&lt;p&gt;More specifically, here's what this approach entails:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Write a serverless function&lt;/strong&gt; that takes an analytics event -- whatever that means to you -- and pipes it wherever you want. This gives you complete control over what and how much data you send, what you do with it before sending it to a third party, and where it ends up.&lt;/p&gt;

&lt;p&gt;The most immediately useful thing you could do here is pipe the event to a traditional analytics product like Google Analytics (giving you the benefits of a proper analytics product without many of the downsides we talked about), and I imagine this is what most people will want to do, but the place you store your events can be literally anything -- a log file, a database you can later build your own analytics tools on top of, hell, it could even be a spreadsheet.&lt;/p&gt;

&lt;p&gt;You could also do stuff like pipe events to your own database while still sending them to Google Analytics, so you still own all of your data if you ever want to do something with it that Google Analytics doesn't let you do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Write a tiny bit of clientside code&lt;/strong&gt; (which, again, you now have complete control over) to send events to your analytics function.&lt;/p&gt;

&lt;p&gt;If we go over those downsides we mentioned at the start of this post again, we can see that this approach allows us to pretty much completely sidestep them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Massive JavaScript payload&lt;/strong&gt; -- There is none, our clientside code can be as simple as a single &lt;code&gt;fetch()&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Privacy implications&lt;/strong&gt; -- More-or-less none, we don't have to track anything we don't want to. Google will still have your data if that's where you choose to pipe it, but there's not a lot they can do with it if it's all anonymized and not tied to a specific user in any way (though you can still do that if you want to, of course).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inaccuracy due to people blocking analytics&lt;/strong&gt; -- Our analytics requests won't be blocked because they're not tied to a known analytics product. As long as we don't do something like use the word &lt;code&gt;analytics&lt;/code&gt; in the name of our endpoint, they'll just look like any other request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt; -- On the analytics side this allows us to use free products like Google Analytics without many of the downsides they usually come with, and as for our serverless function, chances are you won't ever have to pay a dime to run it with how generous pretty much every platform's free tier is. For instance, AWS Lambda gives you &lt;a href="https://aws.amazon.com/lambda/pricing/"&gt;one million free requests&lt;/a&gt; every month (at time of writing).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What This Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;I have to stress that this post is much more about the general concept than it is about a specific implementation -- you don't &lt;em&gt;have&lt;/em&gt; to do anything here the same way I did it. That being said, I'm including an example of how I implemented this pattern for my own website (&lt;a href="https://veselin.dev"&gt;veselin.dev&lt;/a&gt;) because I think it'll be helpful, and because I think my usecase is common enough that many people should be able to take what I did here and carry it over to their own website(s) with few or no changes.&lt;/p&gt;

&lt;p&gt;Here's my analytics function. It's a Netlify Functions function that mostly just proxies events to Google Analytics using the &lt;a href="https://developers.google.com/analytics/devguides/collection/protocol/v1"&gt;Measurement Protocol&lt;/a&gt; (note: I'm using Universal Analytics here because the Measurement Protocol for GA4 is &lt;a href="https://developers.google.com/analytics/devguides/collection/protocol/ga4"&gt;still in alpha&lt;/a&gt;). It anonymizes IP addresses before sending them to Google (though it also enables Google Analytics' &lt;a href="https://support.google.com/analytics/answer/2763052?hl=en"&gt;IP anonymization&lt;/a&gt; feature that does the same thing, because why not), and sidesteps Google's requirement of a user identifier (a &lt;code&gt;cid&lt;/code&gt; or &lt;code&gt;uid&lt;/code&gt;) by just generating a new one for every event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;URLSearchParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;anonymizeIP&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ip-anonymize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;RequestBody&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pageview&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequestBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// enable IP anonymization, even though we're doing it here anyway&lt;/span&gt;
  &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// GA makes us send a cid parameter, so we send a new UUID every time&lt;/span&gt;
  &lt;span class="c1"&gt;// because we don't actually want to track users across requests&lt;/span&gt;
  &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="c1"&gt;// Override user agent&lt;/span&gt;
  &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ua&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="c1"&gt;// Override user IP (but anonymize it first)&lt;/span&gt;
  &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;anonymizeIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;client-ip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

  &lt;span class="c1"&gt;// Set event data&lt;/span&gt;

  &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_ANALYTICS_TRACKING_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;t&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paramName&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paramName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;paramName&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Send event to Google Analytics&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.google-analytics.com/collect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;analyticsRequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the time being I'm happy with this solution, but you could expand on this by implementing something like Plausible's daily identifier method (described &lt;a href="https://plausible.io/data-policy#how-we-count-unique-users-without-cookies"&gt;here&lt;/a&gt;) to anonymously identify unique users, and sending that to Google as your &lt;code&gt;uid&lt;/code&gt;. (&lt;strong&gt;Update:&lt;/strong&gt; I've since published &lt;a href="https://github.com/omegavesko/anonymous-user-id"&gt;anonymous-user-id&lt;/a&gt;, an npm package that helps you do this!)&lt;/p&gt;

&lt;p&gt;My clientside code is basically a single &lt;code&gt;fetch()&lt;/code&gt; call that sends the same data Google Analytics usually sends for a &lt;code&gt;pageview&lt;/code&gt; event. You'll want to put this code somewhere it'll be called both on the initial pageload and whenever the user navigates to a different page -- in my case, that's Gatsby's &lt;a href="https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/#onRouteUpdate"&gt;onRouteUpdate&lt;/a&gt; hook.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s2"&gt;`production`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendPageView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pagePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;

  &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/.netlify/functions/event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pageview&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;dh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pagePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;dr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;referrer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// wrap inside a rAF to make sure react-helmet is done with its changes&lt;/span&gt;
&lt;span class="c1"&gt;// (https://github.com/gatsbyjs/gatsby/issues/11592)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`requestAnimationFrame`&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendPageView&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// simulate 2 rAF calls&lt;/span&gt;
  &lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendPageView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not sending any custom events on my site right now, but those would be handled similarly. If you use analytics heavily on your site, you'll probably want to extract this code into a utility function rather than duplicating it every time you want to send an event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Serverless?
&lt;/h2&gt;

&lt;p&gt;You wouldn't be wrong to notice that everything we're doing here could be implemented just fine in a traditional backend architecture. So, why am I focusing on serverless specifically? Well, there's a few reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More and more sites (like the one I talk about in my example!) are being built on &lt;a href="https://jamstack.org/"&gt;Jamstack&lt;/a&gt; or serverless architectures without a traditional backend. Using a serverless function for this works equally well regardless of what your existing stack is.&lt;/li&gt;
&lt;li&gt;Even if you have an existing backend you could integrate this functionality into, using a serverless function has the benefit of ensuring that you don't have to worry about the performance impact of handling all these extra requests that would typically be handled directly by a third-party service.&lt;/li&gt;
&lt;li&gt;While this isn't a usecase I talk about a lot in this post, a serverless function not tied to an existing backend is easier to use to collect events from a bunch of different places at once.&lt;/li&gt;
&lt;li&gt;Circling back to the Jamstack point again, serverless functions are inherently infinitely scalable. If you have a site deployed to something like a CDN -- where your site will never get hugged to death -- the last thing you want to have to worry about is whether your analytics can keep up with your traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That being said, of course you could apply all of these ideas to a traditional backend, too. I just focus on serverless as a solution in this post because I think it's a great fit for this usecase.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ethics of Circumventing Blockers
&lt;/h2&gt;

&lt;p&gt;To address a bit of an elephant in the room, yes, a significant part of why this pattern is useful is that it allows you to do analytics for users running browser extensions or other tools that would typically block them.&lt;/p&gt;

&lt;p&gt;My opinion on this overlaps significantly with &lt;a href="https://usefathom.com/blog/bypass-adblockers"&gt;Fathom's answer to this question&lt;/a&gt; in their post announcing their custom domain feature (which is explicitly designed to do this) -- I don't believe this is unethical because the reason people block analytics is that they're traditionally extremely invasive, and if you're doing what I've described here, you're doing everything possible to protect your users' privacy.&lt;/p&gt;

&lt;p&gt;Or, to put it a different way -- you're not collecting any more data than serverside analytics would be, and nobody thinks serverside analytics are unethical because they can't be blocked by a browser extension, right?&lt;/p&gt;

&lt;p&gt;That being said, it's possible to use the techniques I've talked about here in an unethical way, for example by proxying requests to Google Analytics through your own domain, but not doing anything to mitigate the privacy concerns of using Google Analytics. All I can do there is ask that you please don't do that.&lt;/p&gt;

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

&lt;p&gt;That's pretty much it! I'm very curious to see how many other people have stumbled their way into similar solutions before (I know I've seen stuff like people &lt;code&gt;proxy_pass&lt;/code&gt;ing requests to Google Analytics with nginx to make them less obvious), and I'm also curious to see if anyone comes up with more creative ways of building on top of this concept than just piping events to Google Analytics.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>serverless</category>
      <category>analytics</category>
      <category>privacy</category>
    </item>
    <item>
      <title>PHP Debugging With Xdebug 3 Inside a Docker Container</title>
      <dc:creator>Lynn Romich</dc:creator>
      <pubDate>Tue, 13 Apr 2021 02:17:27 +0000</pubDate>
      <link>https://dev.to/lynnntropy/php-debugging-with-xdebug-3-inside-a-docker-container-2mpe</link>
      <guid>https://dev.to/lynnntropy/php-debugging-with-xdebug-3-inside-a-docker-container-2mpe</guid>
      <description>&lt;p&gt;I recently spent a good few hours getting Xdebug to work with my development setup (which is &lt;a href="https://www.jetbrains.com/phpstorm/"&gt;PhpStorm&lt;/a&gt; running &lt;a href="https://susi.dev/dev-env-2020"&gt;inside WSL 2&lt;/a&gt; on Windows 10, and PHP/Xdebug running inside a Docker container, inside WSL 2, with &lt;a href="https://docs.docker.com/docker-for-windows/install/"&gt;Docker Desktop&lt;/a&gt;), so here I am writing up the surprisingly simple solution I ended up with -- partially for my own future reference, but also to help out anyone who finds themselves in a similar situation.&lt;/p&gt;

&lt;p&gt;(Don't get discouraged by my ultra-convoluted setup -- this configuration &lt;em&gt;should&lt;/em&gt; actually theoretically work for pretty much any environment, for reasons I'll get into a little later on.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Preamble
&lt;/h2&gt;

&lt;p&gt;Xdebug can be tricky to configure, because it works in reverse from the way you're probably used to interacting with your PHP application -- instead of sending requests &lt;em&gt;to&lt;/em&gt; your PHP code, Xdebug needs to know how to send requests &lt;em&gt;from&lt;/em&gt; where your code is running &lt;em&gt;to&lt;/em&gt; your client application (i.e. probably your editor or IDE), in order to establish a debugging connection.&lt;/p&gt;

&lt;p&gt;This is fine if your setup is relatively simple (i.e. Xdebug can just go to &lt;code&gt;localhost:9003&lt;/code&gt; and hit your client), but if your setup is a little more complicated than that -- i.e. if PHP/Xdebug is running on a different physical machine, in a VM, in a container, or similar -- it won't work out of the box.&lt;/p&gt;

&lt;p&gt;Xdebug 3 makes this whole exercise a little easier for us by generally overhauling the way Xdebug is configured to make it simpler, but if your setup is a little convoluted (like mine), it can still be finicky to get right, hence this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Meat and Potatoes
&lt;/h2&gt;

&lt;p&gt;Okay, so here's what I ended up with.&lt;/p&gt;

&lt;p&gt;This is the bit you need to put somewhere in your PHP configuration (i.e. your &lt;code&gt;php.ini&lt;/code&gt;, or wherever you usually configure your PHP extensions):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Xdebug]&lt;/span&gt;
&lt;span class="py"&gt;xdebug.mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;debug&lt;/span&gt;
&lt;span class="py"&gt;xdebug.start_with_request&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;xdebug.discover_client_host&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;xdebug.client_host&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;host.docker.internal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over this line-by-line:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;xdebug.mode=debug&lt;/code&gt; enables &lt;a href="https://xdebug.org/docs/step_debug"&gt;step debugging&lt;/a&gt; (which is probably what you want to use Xdebug for.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;xdebug.start_with_request=yes&lt;/code&gt; tells Xdebug that we want to activate step debugging at the start of every request, for simplicity's sake. I recommend setting this to &lt;code&gt;yes&lt;/code&gt; and forgetting about it, but if you've done this before and you know you prefer to activate step debugging only for specific requests, leave this line out and Xdebug will default to &lt;code&gt;trigger&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The last two lines are where it gets interesting. &lt;code&gt;xdebug.discover_client_host=true&lt;/code&gt; tells Xdebug to attempt to extract the IP of the client from the HTTP request, and &lt;code&gt;xdebug.client_host&lt;/code&gt; tells it what to try if that doesn't work.&lt;/p&gt;

&lt;p&gt;With all of these set the way we've set them, here's what happens every time you send a request to your PHP application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Xdebug activates step debugging (because our &lt;code&gt;xdebug.mode&lt;/code&gt; is &lt;code&gt;debug&lt;/code&gt;, and &lt;code&gt;xdebug.start_with_request&lt;/code&gt; is set to &lt;code&gt;yes&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Xdebug attempts to automatically connect to the host the HTTP request came from (using &lt;code&gt;$_SERVER['REMOTE_ADDR']&lt;/code&gt;, &lt;code&gt;$_SERVER['HTTP_X_FORWARDED_FOR']&lt;/code&gt;, or a custom HTTP header you've configured).&lt;/li&gt;
&lt;li&gt;If the above didn't work, it falls back to trying to connect to our &lt;code&gt;xdebug.client_host&lt;/code&gt;, which is &lt;code&gt;host.docker.internal&lt;/code&gt;, a DNS name for the host helpfully set for us by Docker Desktop on Windows or macOS (though unfortunately not on Linux).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fact that Xdebug lets us set both &lt;code&gt;xdebug.discover_client_host&lt;/code&gt; and &lt;code&gt;xdebug.client_host&lt;/code&gt; as a fallback is the key to making this configuration work for just about any setup, whether you're on Linux, macOS or Windows, and even whether you're using Docker or not.&lt;/p&gt;

&lt;p&gt;If you're using Docker Desktop, having &lt;code&gt;host.docker.internal&lt;/code&gt; set as a fallback host means Xdebug will always be able to find its way to the host, and on most other setups, client discovery (via &lt;code&gt;xdebug.discover_client_host&lt;/code&gt;) should just work. Theoretically, the only scenario this config should break in is if you're using something that breaks client discovery that &lt;em&gt;isn't&lt;/em&gt; Docker (e.g. a Vagrant virtual machine), in which case there's probably something else you can set &lt;code&gt;xdebug.client_host&lt;/code&gt; to to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Xdebug in Your Docker Image
&lt;/h2&gt;

&lt;p&gt;If you're using Docker (and if you're not, I highly recommend thinking about it!), the below is an example of what I do to install and enable Xdebug in development without carrying it over to the production image, using &lt;a href="https://docs.docker.com/develop/develop-images/multistage-build/"&gt;multi-stage builds&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# (or your base image)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;php:8.0-fpm-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="c"&gt;# whatever you do to set up your image&lt;/span&gt;
&lt;span class="c"&gt;# for example:&lt;/span&gt;
&lt;span class="c"&gt;#   - copying your source code&lt;/span&gt;
&lt;span class="c"&gt;#   - installing extensions&lt;/span&gt;
&lt;span class="c"&gt;#   - doing a `composer install`&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;development&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;pecl &lt;span class="nb"&gt;install &lt;/span&gt;xdebug &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker-php-ext-enable xdebug

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;

&lt;span class="c"&gt;# whatever you do to prepare your image for production&lt;/span&gt;
&lt;span class="c"&gt;# for example: removing packages you don't need in production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;That's it! You should now have an Xdebug config that works for everything from Docker Desktop to just running PHP on the same Linux machine as your editor.&lt;/p&gt;

&lt;p&gt;If you're using this config and you've found a setup it doesn't work for, please yell at me on Twitter and I'll probably add a note to this post!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>php</category>
      <category>xdebug</category>
      <category>wsl</category>
    </item>
    <item>
      <title>On Docs, DX, and Developer Happiness</title>
      <dc:creator>Lynn Romich</dc:creator>
      <pubDate>Sat, 31 Aug 2019 23:06:37 +0000</pubDate>
      <link>https://dev.to/lynnntropy/on-docs-dx-and-developer-happiness-2pib</link>
      <guid>https://dev.to/lynnntropy/on-docs-dx-and-developer-happiness-2pib</guid>
      <description>&lt;p&gt;(This post was originally published on &lt;a href="https://blog.veselin.dev/on-docs-dx-and-developer-happiness/"&gt;my blog&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;One thing I've increasingly taken notice of recently is that a lot of developers, especially in corporate environments, seem to tend to put a very low priority on the DX of their projects. Documentation is poor or non-existent; setting up a working development environment is like pulling teeth. You've got tickets to close and you're sure they'll figure it out, so who cares, right?&lt;/p&gt;

&lt;p&gt;To be clear -- I totally understand why this happens, and I'm not here to judge anyone who might've recognized some of their own projects in that description. I do, however, strongly believe that you &lt;em&gt;should&lt;/em&gt; care, and I'm here to tell you why.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Little Work Now = A Lot Less Work Later
&lt;/h2&gt;

&lt;p&gt;Think about how many times in your life you're going to find yourself setting up a local dev environment for a given project. At the very least, you're probably going to do it every time you get a new dev machine and &lt;code&gt;git clone&lt;/code&gt; all the stuff you're working on, right? You're also going to do it every time you help someone do the same, e.g. a new person on your team.&lt;/p&gt;

&lt;p&gt;If your project is a pain to set up, that quickly adds up to a &lt;em&gt;lot&lt;/em&gt; of unnecessary frustration and wasted time. Doing this by hand is also bound to breed subtle inconsistencies in each environment, potentially leading to bugs. &lt;/p&gt;

&lt;p&gt;On top of that, if &lt;em&gt;running&lt;/em&gt; your app involves more than a single command (e.g. you need to run a web server &lt;em&gt;and&lt;/em&gt; something like a module bundler), you have to remember to do this every single time you work on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate, Automate, Automate
&lt;/h2&gt;

&lt;p&gt;Okay, so we're aware that some of our processes may be inefficient. How do we fix it?&lt;/p&gt;

&lt;p&gt;The absolute best way you can choose to deal with this issue is by codifying your app's environment and other setup into some sort of automated tooling that can do all of this for you. Luckily, we live in the age of &lt;a href="https://www.docker.com"&gt;Docker&lt;/a&gt; and &lt;a href="https://www.docker.com/resources/what-container"&gt;containerization&lt;/a&gt;, which means the tools for this we have at our disposal today are immensely more powerful than they were barely a few years ago.&lt;/p&gt;

&lt;p&gt;This post isn't really the place for a deep dive into using Docker as a development environment, but the gist of what you want to do is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a &lt;code&gt;Dockerfile&lt;/code&gt; to codify as much as you can about the &lt;em&gt;immediate&lt;/em&gt; environment your app needs to run -- this should include things like your language runtime, any binaries it needs (e.g. &lt;code&gt;imagemagick&lt;/code&gt;), config files, and so on.&lt;/li&gt;
&lt;li&gt;Use a &lt;code&gt;docker-compose.yml&lt;/code&gt; to codify as much as you can about your app's &lt;em&gt;external&lt;/em&gt; dependencies, such as cache servers, database servers, mail servers, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will undoubtedly take some time, especially so if you first have to take the time to understand what this Docker thing is all about. Once everything's said and done, however, if you did everything right, you'll have reduced the process to set up a working instance of your app to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &amp;lt;your-repo&amp;gt; &lt;span class="nb"&gt;.&lt;/span&gt;
docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that, if you've already done this once, it'll take you a lot less time to do it in the future -- especially if your apps share a common stack. It takes me maybe an hour to set all this stuff up for a typical new project these days. &lt;/p&gt;

&lt;h2&gt;
  
  
  Learning by Example
&lt;/h2&gt;

&lt;p&gt;If the above seems a little too abstract, let's look at a simple example of a &lt;code&gt;docker-compose.yml&lt;/code&gt; you could use for a WordPress project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wordpress:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;8080:80&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./wordpress:/var/www/html:rw&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;WORDPRESS_DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;WORDPRESS_DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wordpress&lt;/span&gt;
      &lt;span class="na"&gt;WORDPRESS_DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
      &lt;span class="na"&gt;WORDPRESS_DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;links&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;3306:3306&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--default_authentication_plugin=mysql_native_password'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--character-set-server=utf8mb4'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--collation-server=utf8mb4_unicode_ci'&lt;/span&gt;
    &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wordpress&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's self-explanatory enough -- The &lt;code&gt;wp&lt;/code&gt; service is the actual WordPress site, and the &lt;code&gt;db&lt;/code&gt; service is a MySQL database (what we called an "external" dependency above). For the purposes of this example, this is all we need to get us to a point where you can just do a &lt;code&gt;docker-compose up&lt;/code&gt; to run the project.&lt;/p&gt;

&lt;p&gt;Now, you'll notice that I didn't provide a &lt;code&gt;Dockerfile&lt;/code&gt; -- That's because, in this instance, the official &lt;code&gt;wordpress&lt;/code&gt; image does all we need. For a more complex project, however (or if you wanted to deploy a Docker image to production, too), you'd probably want to write a Dockerfile that extends the official image.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Word of Warning
&lt;/h2&gt;

&lt;p&gt;This is a bit of a tangent in the context of this article, but one thing about Docker that I &lt;em&gt;really&lt;/em&gt; need to mention, but nobody ever seems to talk about, is that the way you want to use Docker differs pretty significantly depending on whether you're using it for development, or deploying Docker images to production.&lt;/p&gt;

&lt;p&gt;The main thing it comes down to is where your code is. Take the volume mounted onto the WordPress container in the example above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./wordpress:/var/www/html:rw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WordPress code on your host machine is mounted onto the container -- Any changes you make to it will immediately be reflected in your running app.&lt;/p&gt;

&lt;p&gt;On the other hand, if you're building an image to deploy to production, you &lt;em&gt;have&lt;/em&gt; to copy your code into your image at build time, so that it's self-contained, and so that it can do anything it needs to do with your code at build time (building binaries, module bundling, etc.).&lt;/p&gt;

&lt;p&gt;I find it really important to point out this distinction whenever I get the chance to, because for some reason, a lot of high-profile tutorials out there will have you make a "development" Docker setup that has you rebuild your image every time you want it to reflect the changes in your code, even though this is totally impractical if you're actually using Docker as your dev environment. I dread to think how many people have given up on using Docker for development because they found the idea of constantly rebuilding your app ridiculous (and rightfully so).&lt;/p&gt;

&lt;p&gt;Another thing worth mentioning is that, depending on your stack, you may need to have separate images for development and production (you can use &lt;a href="https://docs.docker.com/develop/develop-images/multistage-build/"&gt;multi-stage builds&lt;/a&gt; to do this without having to maintain multiple Dockerfiles). PHP, for instance, doesn't care -- you're using the exact same code and runtime in every environment. On the other hand, if you're using something like a static site generator, or a language like Go that doesn't have an external runtime, your production image is going to need to be much more lean than the one you use for development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation Isn't Everything
&lt;/h2&gt;

&lt;p&gt;We've just spent a bunch of time talking about Docker and Docker Compose, and while containerizing your app absolutely makes a huge difference and is probably the single most helpful thing you can do to improve the onboarding experience, we should take a moment to remember that reducing your setup to a &lt;code&gt;docker-compose up&lt;/code&gt; isn't the be-all and end-all of making your app friendly to work with. &lt;/p&gt;

&lt;p&gt;Here are a few other things to keep in mind.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your app or library has any sort of public API, you &lt;strong&gt;must&lt;/strong&gt; make it easy for people to see what it actually is; expecting people to dive into your code doesn't count. For HTTP APIs, tools like &lt;a href="https://swagger.io/docs/specification/about/"&gt;the OpenAPI spec&lt;/a&gt; and &lt;a href="https://swagger.io/tools/swagger-ui/"&gt;Swagger UI&lt;/a&gt; make it easy to automatically generate API docs and expose them in a friendly way; use them.&lt;/li&gt;
&lt;li&gt;Even if you've simplified your dev environment setup and documented any public APIs you may have, there's probably still a bunch of stuff you could put in your README. What is this app? Who's it for? What standards/protocols does it implement? Is it a monorepo, or are there other projects one should look at to get the full picture? Hell, take a screenshot (if applicable) and put it at the top of the README. The more context people can glean from your docs, the better.&lt;/li&gt;
&lt;li&gt;If you can't go the full-on containerization route for whatever reason (please make the effort to try, though!), make sure &lt;em&gt;all&lt;/em&gt; the steps one needs to go through to set up a dev environment for your app are well-documented. Don't assume people will just know what to do with your code, even if your app uses a similar stack to other projects in your team or company.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Think Like a FOSS Maintainer
&lt;/h2&gt;

&lt;p&gt;To conclude this post, I think the single most important piece of advice I can give on this topic is -- &lt;strong&gt;Think like a FOSS maintainer&lt;/strong&gt;. Pretend for a second that you're not developing an internal project that people are obligated to use, but a personal open-source project nobody will ever use if the DX is poor and the extent of your docs is a near-blank README.&lt;/p&gt;

&lt;p&gt;To an extent, you don't even have to pretend -- Yeah, of course you don't have to sell people on your project if they're obligated to work with it, but that doesn't mean the atittude you take towards the stuff in this post doesn't matter. Making the effort to make your coworkers' jobs easier makes a huge difference in how they see you, your project, your team, and even affects the culture of the company as a whole. &lt;/p&gt;

&lt;p&gt;To be blunt, animosity between teams can grow from the most trivial of things; we have to make the extra effort to extend that olive branch when we can.&lt;/p&gt;

&lt;p&gt;The way I personally see this (with the disclaimer that this is absolutely my personal opinion -- I can hardly pretend to speak for everyone) is that making sure your projects are well-documented is just a sign of basic respect towards your fellow devs. I wouldn't dream of making someone bang their head against a wall trying to work on a project of mine just because I couldn't be bothered to write proper docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;Let's go over the important stuff one more time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
Do your best to automate your dev environments. This makes as much of a difference for you as it does for everyone else, especially if you switch between projects a lot.&lt;/li&gt;
&lt;li&gt;
Document your APIs. If you maintain an API other people are supposed to use, you can't expect them to dive into your code to understand it.&lt;/li&gt;
&lt;li&gt;
Put &lt;em&gt;everything&lt;/em&gt; in your READMEs. The more comprehensive your docs are, the less often you'll have people tapping you on your shoulder to ask you things. &lt;/li&gt;
&lt;li&gt;
Think like a FOSS maintainer. Sell people on using your project, even if you don't have to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And, of course: Be excellent to each other.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>dx</category>
      <category>documentation</category>
    </item>
  </channel>
</rss>
