<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Sean Kegel]]></title><description><![CDATA[Lead Software Engineer @ Curology with 14+ years of experience designing and building responsive websites and applications.]]></description><link>https://seankegel.com</link><generator>RSS for Node</generator><lastBuildDate>Sat, 11 Apr 2026 20:59:31 GMT</lastBuildDate><atom:link href="https://seankegel.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Accessing Databases in PhpStorm]]></title><description><![CDATA[Did you know PhpStorm can access your databases, and it actually gives you a fantastic UI for managing? I was a long-time TablePlus user, which is a great tool, but lately, I've been finding myself using PhpStorm's built-in tools more and more. It le...]]></description><link>https://seankegel.com/accessing-databases-in-phpstorm</link><guid isPermaLink="true">https://seankegel.com/accessing-databases-in-phpstorm</guid><category><![CDATA[PHP]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[PhpStorm]]></category><category><![CDATA[Databases]]></category><category><![CDATA[Jetbrains]]></category><category><![CDATA[intellij]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sat, 16 Nov 2024 15:30:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731769978065/fcc66ae4-4889-457f-bedf-123b9d72ad81.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Did you know PhpStorm can access your databases, and it actually gives you a fantastic UI for managing? I was a long-time TablePlus user, which is a great tool, but lately, I've been finding myself using PhpStorm's built-in tools more and more. It lets me stay right in my editor without having to open and switch to another application. If you are familiar with JetBrains, you may have heard of or even used Datagrip, which is their IDE for databases. Many of those same features from Datagrip are built right into PhpStorm. Today, I want to explain how to get started and some of my favorite features. Let's get started!</p>
<h2 id="heading-connecting-to-the-database">Connecting to the Database</h2>
<p>First, I created a simple Laravel application and selected to use a SQLite database. In PhpStorm, pull up the database tools:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770074650/c06c5647-d9be-45bc-adcb-f68d3f7d693d.png" alt="Access PhpStorm database tools" /></p>
<p>When the tool window appears, we can add our SQLite database:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770110083/0201e142-1580-4715-b5e6-ca6b6016a057.png" alt="Add SQLite database to PhpStorm" /></p>
<blockquote>
<p>We are using SQLite in this post, but PhpStorm has support for many different data sources.</p>
</blockquote>
<p>Set up the connection to the database, for SQLite, just need to point to the appropriate file in the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770149198/cf16575f-05c3-404d-9e0b-a6a52534e15c.png" alt="Connect SQLite File to PhpStorm" /></p>
<p>After hitting OK, we are connected to our database!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770175837/57d18fb7-c229-4b0e-8cd7-8b49c84a9745.png" alt="SQLite Database in PhpStorm" /></p>
<h2 id="heading-viewing-table-data">Viewing Table Data</h2>
<p>I used the <code>UserFactory</code> to create users. By double-clicking the <code>users</code> table in the database tools, we can see all the users.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770210811/d6b8f50f-6c85-45b3-84e5-f0ec2a4e5cc1.png" alt="Users table in PhpStorm" /></p>
<blockquote>
<p>Looks like there are a lot of professors using our site! 😂</p>
</blockquote>
<h2 id="heading-transposing-the-data">Transposing the Data</h2>
<p>The first feature I really like is being able to transpose the data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770241931/418da9e4-ef7a-4d56-b163-0532aafcc5e5.png" alt="Transpose table in PhpStorm" /></p>
<p>This lets you view the table with the columns as rows. It makes it really nice to see the data for individual records.</p>
<h2 id="heading-query-console">Query Console</h2>
<p>One of the nice features of having databases right in PhpStorm, is you get the advantage of your editor, autocomplete, syntax highlighting, and even AI completion if you use something like Copilot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770285075/1b0c673e-f966-4d3c-94b9-562665f18271.png" alt="PhpStorm database console completion" /></p>
<p>The console also persists across sessions per database, so it's easy to just leave previous queries there if you need to re-run them later.</p>
<p>The console also allows defining variables that can be modified each time the query is run:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770310125/51c9de38-386d-4fc0-a64c-53d836c881c2.png" alt="PhpStorm database console parameters" /></p>
<p>You can refer to the PhpStorm docs for more information about using parameters: <a target="_blank" href="https://www.jetbrains.com/help/phpstorm/settings-tools-database-user-parameters.html">User Parameters | PhpStorm Documentation</a>.</p>
<h2 id="heading-data-extractors">Data Extractors</h2>
<p>Data extractors change how copied data is handled. It allows copying rows and columns as a CSV, JSON, Markdown, or even custom extractors.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770336237/3e4eee9d-4422-4274-9e96-93d5ac3aec6c.png" alt="PhpStorm database data extractors" /></p>
<p>For example, if we select Markdown, then select data, copy and paste it, we get a nicely formatted Markdown table in the clipboard.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770371801/a3ff9af2-00da-45f7-bb5b-ae7d94a81ca8.png" alt="Copying data from PhpStorm database" /></p>
<pre><code class="lang-markdown">| id | name | email |
| :--- | :--- | :--- |
| 1 | Ana Lesch IV | lrogahn@example.net |
| 2 | Mateo Trantow | ullrich.deborah@example.com |
| 3 | Jeanie Altenwerth | marcelle00@example.com |
| 4 | Prof. Brad Koch | elise.paucek@example.net |
| 5 | Prof. Reva Jast IV | jmayert@example.com |
| 6 | Buford Stracke | ora07@example.net |
| 7 | Agnes O'Conner | lillian95@example.org |
| 8 | Korey Spencer PhD | schmeler.fleta@example.com |
| 9 | Dr. Celestino Fisher | grimes.ada@example.com |
| 10 | Orin Keeling | bgrant@example.org |
</code></pre>
<p>If you've used Laravel's job queue and ever needed to retry a lot of jobs, you have to use something like the following command:</p>
<pre><code class="lang-bash">php artisan queue:retry 825d691e-8a75-403a-98d4-afc0da6474c0 565faa3f-dad4-40c2-b0d5-cf9c783f6643 7aec1085-11a8-441d-8218-d65f5f60382a
</code></pre>
<p>This retries three jobs using their UUID from the <code>failed_jobs</code> table. With a custom data extractor, we can easily copy the UUID's from the table in the proper format (no quotes, no commas, one space between UUIDs).</p>
<p>If we use the “One-row” data extractor, this gets us close, but notice it includes quotes and commas, which don't work in the retry command.</p>
<pre><code class="lang-bash"><span class="hljs-string">'825d691e-8a75-403a-98d4-afc0da6474c0'</span>, <span class="hljs-string">'565faa3f-dad4-40c2-b0d5-cf9c783f6643'</span>, <span class="hljs-string">'7aec1085-11a8-441d-8218-d65f5f60382a'</span>
</code></pre>
<p>So, let's create a new version of the extractor.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770427267/764aec03-3bc3-4fd4-9041-bb31edda125c.png" alt="Create data extractor in PhpStorm" /></p>
<p>Copy the <code>One-row.sql.groovy</code> script, and then we can easily modify for our needs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770470579/d55e3fe9-a769-470e-b65a-9a2ad47659f1.png" alt="Copy existing data extractor in PhpStorm" /></p>
<blockquote>
<p>Make sure to move the new file into the <code>extractors</code> folder so PhpStorm can find it.</p>
</blockquote>
<p>It might look complicated, but all we really need to do is change the separator and quote to a space and empty, respectively.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770523110/448d5d1f-58a8-4478-a254-88a4939c7c79.png" alt="Edit data extractor in PhpStorm" /></p>
<p>Now, if we select our new extractor and copy the UUIDs, we get the format we require for the command.</p>
<pre><code class="lang-bash">825d691e-8a75-403a-98d4-afc0da6474c0 565faa3f-dad4-40c2-b0d5-cf9c783f6643 7aec1085-11a8-441d-8218-d65f5f60382a
</code></pre>
<h2 id="heading-diagrams">Diagrams</h2>
<p>The last feature I will discuss in this article is auto-generated database diagrams. For example, to create a diagram for our entire database, select the database and choose diagrams.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731770550555/1fbecae0-9068-4e4d-8fd2-d400b0b7dabb.png" alt="Database diagrams in PhpStorm" /></p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>PhpStorm is extremely powerful and offers a ton of different features and tools. I keep learning new things all the time, especially with the database tools. I highly recommend further exploring the database tools on your own to help refine your workflows.</p>
<p>Let me know if you have any other helpful tips about the database features in PhpStorm or any questions I can help answer. Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Laravel Factories: Tips for Handling Dependent Data]]></title><description><![CDATA[Something I recently came across while working on a Laravel app was an unoptimized factory. I’ve seen this many times and have even been guilty of it. Let’s say we have three models, a User, a Team, and a Project. A team can have a team owner (teams....]]></description><link>https://seankegel.com/laravel-factories-tips-for-handling-dependent-data</link><guid isPermaLink="true">https://seankegel.com/laravel-factories-tips-for-handling-dependent-data</guid><category><![CDATA[Laravel]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Databases]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sun, 22 Sep 2024 15:16:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727018379618/7c371a74-8858-49a6-b182-056fd0f1784c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Something I recently came across while working on a Laravel app was an unoptimized factory. I’ve seen this many times and have even been guilty of it. Let’s say we have three models, a <code>User</code>, a <code>Team</code>, and a <code>Project</code>. A team can have a team owner (<code>teams.user_id</code>), a project can have an owner (<code>projects.user_id</code>) and a team (<a target="_blank" href="http://projects.team"><code>projects.team</code></a><code>_id</code>). In the <code>ProjectFactory</code>, I want to create a user and a team, and that team should have the user as the owner. The first attempt might look something like this:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProjectFactory</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Factory</span>
</span>{
    <span class="hljs-keyword">protected</span> $model = Project::class;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">definition</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;sentence(),
            <span class="hljs-string">'description'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;paragraph(),
            <span class="hljs-string">'team_id'</span> =&gt; Team::factory(),
            <span class="hljs-string">'user_id'</span> =&gt; User::factory(),
        ];
    }
}
</code></pre>
<p>The problem with this is the <code>Team</code> factory is creating its own <code>User</code> and now the <code>User</code> factory created a user, and they are not related.</p>
<p>The next iteration could look like this:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProjectFactory</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Factory</span>
</span>{
    <span class="hljs-keyword">protected</span> $model = Project::class;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">definition</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        $team = Team::factory()-&gt;create();

        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;sentence(),
            <span class="hljs-string">'description'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;paragraph(),
            <span class="hljs-string">'team_id'</span> =&gt; $team-&gt;id,
            <span class="hljs-string">'user_id'</span> =&gt; $team-&gt;user_id,
        ];
    }
}
</code></pre>
<p>This works and gives us what we want, or does it?</p>
<p>It will give us a <code>Project</code> with a <code>User</code> and <code>Team</code> that are related. However, what happens if we were using the factory and we already knew the team? Something like the following:</p>
<pre><code class="lang-php">$project = Project::factory()
    -&gt;for($previouslyCreatedTeam, <span class="hljs-string">'team'</span>)
    -&gt;for($previouslyCreatedTeam-&gt;owner, <span class="hljs-string">'user'</span>)
    -&gt;create();
</code></pre>
<p>If we look in the database, we should see a single user, team, and project record. However, looking at the <code>users</code> table, I have two users:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727017774085/2eeac220-252b-4913-b187-eb9c84e1a4c9.png" alt="Users table" class="image--center mx-auto" /></p>
<p>In the <code>teams</code> table, I have two teams!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727017786070/75727b63-91f3-4327-ac72-aa2761e9236e.png" alt="Teams table" class="image--center mx-auto" /></p>
<p>What happened?</p>
<p>The issue is, any time the factory runs, it calls the <code>definition</code> method, and then it looks at the fields that were passed to the factory to see which fields of the definition it needs to use. When the <code>definition</code> method is called, though, nothing is preventing it from calling the <code>Team</code> factory and creating a <code>Team</code> and <code>User</code>.</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProjectFactory</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Factory</span>
</span>{
    <span class="hljs-keyword">protected</span> $model = Project::class;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">definition</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-comment">// This runs regardless of the fields provided</span>
        <span class="hljs-comment">// to the factory, creating extra teams and users</span>
        <span class="hljs-comment">// when not necessary.</span>
        $team = Team::factory()-&gt;create();

        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;sentence(),
            <span class="hljs-string">'description'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;paragraph(),
            <span class="hljs-string">'team_id'</span> =&gt; $team-&gt;id,
            <span class="hljs-string">'user_id'</span> =&gt; $team-&gt;user_id,
        ];
    }
}
</code></pre>
<p>How do we fix this?</p>
<p>It’s actually right in the <a target="_blank" href="https://laravel.com/docs/11.x/eloquent-factories#defining-relationships-within-factories">Laravel docs</a>.</p>
<blockquote>
<p>If the relationship's columns depend on the factory that defines it you may assign a closure to an attribute.</p>
</blockquote>
<p>So to fix it, we can use a closure to get the <code>User</code> from the <code>Team</code> factory used in the definition:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProjectFactory</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Factory</span>
</span>{
    <span class="hljs-keyword">protected</span> $model = Project::class;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">definition</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;sentence(),
            <span class="hljs-string">'description'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;paragraph(),
            <span class="hljs-string">'team_id'</span> =&gt; Team::factory(),
            <span class="hljs-comment">// Fetch the user_id from the team that was </span>
            <span class="hljs-comment">// generated in the factory above.</span>
            <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-function"><span class="hljs-keyword">fn</span> (<span class="hljs-params"><span class="hljs-keyword">array</span> $attributes</span>) =&gt; <span class="hljs-title">Team</span>::<span class="hljs-title">find</span>(<span class="hljs-params">$attributes[<span class="hljs-string">'team_id'</span>]</span>)-&gt;<span class="hljs-title">user_id</span>,
        ]</span>;
    }
}
</code></pre>
<p>This is a bit of an edge case and doesn’t frequently happen, so it’s easy to miss in the docs. I hope this helps, and now you can go and improve your factories. Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Laravel: Casting Eloquent JSON Fields]]></title><description><![CDATA[I have a short post today to cover something I recently used in a project. I had a table using a JSON column and though they are extremely flexible, I like to know what data exists in the column. To accomplish this, I use a simple data transfer objec...]]></description><link>https://seankegel.com/laravel-casting-eloquent-json-fields</link><guid isPermaLink="true">https://seankegel.com/laravel-casting-eloquent-json-fields</guid><category><![CDATA[Laravel]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Databases]]></category><category><![CDATA[eloquent]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sun, 28 Jul 2024 14:07:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722174606890/7bd3620e-c234-4261-9941-455445b6ce21.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have a short post today to cover something I recently used in a project. I had a table using a JSON column and though they are extremely flexible, I like to know what data exists in the column. To accomplish this, I use a simple data transfer object.</p>
<p>In my project, I have a <code>checkout_events</code> table where I track various options and properties of a user's checkout. It’s primarily for debugging purposes, but maybe one day, it could grow into an event sourcing flow.</p>
<p>In the table, I have a <code>metadata</code> column and I want to track things like <code>processor</code>, <code>checkoutSource</code>, <code>checkoutType</code>, <code>errorMessage</code>, and <code>exceptionClass</code>. As things change or grow, I knew the different properties could change, which is why I opted for a JSON column versus a dedicated column for each property.</p>
<p>To make sure I have the correct data going in and out of the table, I created the DTO below:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

readonly <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CheckoutMetadata</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> Processor $processor,
        <span class="hljs-keyword">public</span> CheckoutSource $checkoutSource,
        <span class="hljs-keyword">public</span> CheckoutType $checkoutType,
        <span class="hljs-keyword">public</span> ?<span class="hljs-keyword">string</span> $errorMessage = <span class="hljs-literal">null</span>,
        <span class="hljs-keyword">public</span> ?<span class="hljs-keyword">string</span> $exceptionClass = <span class="hljs-literal">null</span>,

    </span>) </span>{}
}
</code></pre>
<p>Notice, the <code>$errorMessage</code> and <code>$exception</code> properties are nullable since the event won’t always have that metadata.</p>
<p>Now, for the model, we need to add a cast for this:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CheckoutEvents</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span>
</span>{
    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">casts</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'metadata'</span> =&gt; CheckoutMetadata::class,
        ];
    }
}
</code></pre>
<p>Now, to actually make this work, we could create a <code>Cast</code> in Laravel as shown in the <a target="_blank" href="https://laravel.com/docs/11.x/eloquent-mutators#custom-casts">docs</a>. However, I am going to opt for making the DTO itself be <code>Castable</code> (<a target="_blank" href="https://laravel.com/docs/11.x/eloquent-mutators#castables">docs</a>) and use anonymous cast classes (<a target="_blank" href="https://laravel.com/docs/11.x/eloquent-mutators#anonymous-cast-classes">docs</a>). To achieve this, <code>CheckoutMetadata</code> needs to implement <code>Illuminate\Contracts\Database\Eloquent\Castable</code>. The contract requires a <code>castUsing</code> method that returns a new class instance that implements <code>Illuminate\Contracts\Database\Eloquent\CastsAttributes</code>:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CheckoutMetadata</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Castable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> Processor $processor,
        <span class="hljs-keyword">public</span> CheckoutSource $checkoutSource,
        <span class="hljs-keyword">public</span> CheckoutType $checkoutType,
        <span class="hljs-keyword">public</span> ?<span class="hljs-keyword">string</span> $errorMessage = <span class="hljs-literal">null</span>,
        <span class="hljs-keyword">public</span> ?<span class="hljs-keyword">string</span> $exceptionClass = <span class="hljs-literal">null</span>,
    </span>) </span>{}

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">castUsing</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $arguments</span>): <span class="hljs-title">CastsAttributes</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">CastsAttributes</span>
        </span>{
            <span class="hljs-comment">// Get method is called when getting metadata from the model.</span>
            <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get</span>(<span class="hljs-params">Model $model, <span class="hljs-keyword">string</span> $key, mixed $value, <span class="hljs-keyword">array</span> $attributes</span>)
            </span>{
                <span class="hljs-comment">// Convert the JSON data from the database into an object.</span>
                $data = json_decode($attributes[$key]);

                <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> CheckoutMetadata(
                    processor: Processor::from($data-&gt;processor),
                    checkoutSource: CheckoutSource::from($data-&gt;checkout_source),
                    checkoutType: CheckoutType::from($data-&gt;checkout_type),
                    errorMessage: data_get($data, <span class="hljs-string">'error_message'</span>),
                    exceptionClass: data_get($data, <span class="hljs-string">'error_message'</span>),
                );
            }

            <span class="hljs-comment">// Set method is called when updating the metadata field on the model.</span>
            <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">set</span>(<span class="hljs-params">Model $model, <span class="hljs-keyword">string</span> $key, mixed $value, <span class="hljs-keyword">array</span> $attributes</span>): <span class="hljs-title">array</span>
            </span>{
                <span class="hljs-comment">// Throw an exception if trying to set a value that is not an instance of CheckoutMetadata.</span>
                <span class="hljs-keyword">if</span> (! $value <span class="hljs-keyword">instanceof</span> CheckoutMetadata) {
                    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">InvalidArgumentException</span>(<span class="hljs-string">'A CheckoutMetadata instance is required.'</span>);
                }

                $data = [
                    <span class="hljs-string">'processor'</span> =&gt; $value-&gt;processor-&gt;value,
                    <span class="hljs-string">'checkout_source'</span> =&gt; $value-&gt;checkoutSource-&gt;value,
                    <span class="hljs-string">'checkout_type'</span> =&gt; $value-&gt;checkoutType-&gt;value,
                    <span class="hljs-string">'error_message'</span> =&gt; $value-&gt;errorMessage,
                    <span class="hljs-string">'exception_class'</span> =&gt; $value-&gt;exceptionClass,
                ];

                <span class="hljs-comment">// JSON encode the data to store in the database.</span>
                <span class="hljs-keyword">return</span> [$key =&gt; json_encode($data)];
            }
        };
    }
}
</code></pre>
<p>Quite a lot of new stuff here. The <code>get</code> method of the anonymous class gets called when we try to get the <code>metadata</code> property from the model, like <code>$checkoutModel-&gt;metadata</code>. We need to decode the JSON from the database and then instantiate a new <code>CheckoutMetadata</code> class instead. Now, anytime we call <code>$checkoutModel-&gt;metadata</code>, we get the <code>CheckoutMetadata</code> class and know what properties we have available.</p>
<p>The <code>set</code> method is used when we try to update the <code>metadata</code> property, so: <code>$checkoutModel-&gt;metadata = $checkoutMetadata</code>. Since we always want to require data to be in the form of <code>CheckoutMetadata</code>, we throw an <code>InvalidArgumentException</code> if the value passed in is not an instance of <code>CheckoutMetadata</code>.</p>
<p>Now, our model can be used like the following:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

$metadata = <span class="hljs-keyword">new</span> CheckoutMetadata(
  checkoutSource: CheckoutSource::Brick,
  checkoutType: CheckoutType::OneTime,
  processor: Processor::Braintree
);

$model = <span class="hljs-keyword">new</span> CheckoutEvent();
$model-&gt;name = <span class="hljs-string">'New Event'</span>;
$model-&gt;user_id = <span class="hljs-number">1</span>;
$model-&gt;metadata = $metadata;
$model-&gt;save();

$model-&gt;metadata;

<span class="hljs-comment">// App\CheckoutMetadata {#2744</span>
<span class="hljs-comment">//    +processor: App\Processor {#2746</span>
<span class="hljs-comment">//      +name: "Braintree",</span>
<span class="hljs-comment">//      +value: 2,</span>
<span class="hljs-comment">//    },</span>
<span class="hljs-comment">//    +checkoutSource: App\CheckoutSource {#2750</span>
<span class="hljs-comment">//      +name: "Brick",</span>
<span class="hljs-comment">//      +value: 2,</span>
<span class="hljs-comment">//    },</span>
<span class="hljs-comment">//    +checkoutType: App\CheckoutType {#2748</span>
<span class="hljs-comment">//      +name: "OneTime",</span>
<span class="hljs-comment">//      +value: 2,</span>
<span class="hljs-comment">//    },</span>
<span class="hljs-comment">//    +errorMessage: null,</span>
<span class="hljs-comment">//    +exceptionClass: null,</span>
<span class="hljs-comment">//  }</span>
</code></pre>
<p>In the future, if we wish to add properties that we will track, we can add new optional properties to the DTO and update the anonymous <code>CastAttributes</code> class. If we want to remove properties, we can just delete them from the DTO and update the anonymous <code>CastsAttributes</code> class.</p>
<p>For further learning, look into the <code>Arrayable</code> and <code>JsonSerializable</code> interfaces in Laravel so you can return the DTO to an expected structure when converting a model to an array or JSON. You can also read about <a target="_blank" href="https://laravel.com/docs/11.x/eloquent-mutators#array-json-serialization">Array / JSON Serialization</a> in Laravel.</p>
<p>I hope you enjoyed this post and learned something new about Eloquent attribute casting and JSON fields. They can be powerful, but it’s important to make sure the data being passed in and out of the field is what you expect. You don’t want it to blow up with countless unknown properties and different structures in each row.</p>
<p>Thanks for reading!</p>
<p>Furthermore, if you’d like to learn more about data transfer objects, read my post <a target="_blank" href="https://seankegel.com/streamlining-api-responses-in-laravel-with-dtos?source=more_series_bottom_blogs">Streamlining API Responses in Laravel with DTOs</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Neovim for PHP and Laravel]]></title><description><![CDATA[If you’ve ever wanted to use Neovim for PHP and Laravel development, this guide should help get you started. If you use VSCode, then check out this post.
In this post, I will be using Neovim with LazyVim. LazyVim is a fantastic Neovim setup with many...]]></description><link>https://seankegel.com/neovim-for-php-and-laravel</link><guid isPermaLink="true">https://seankegel.com/neovim-for-php-and-laravel</guid><category><![CDATA[PHP]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[neovim]]></category><category><![CDATA[vim]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sat, 13 Jul 2024 20:28:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1720902232438/4a818268-f926-435e-8410-8c556e97f9c4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you’ve ever wanted to use Neovim for PHP and Laravel development, this guide should help get you started. If you use VSCode, then check out this <a target="_blank" href="https://seankegel.com/vscode-for-php-and-laravel">post</a>.</p>
<p>In this post, I will be using Neovim with LazyVim. LazyVim is a fantastic Neovim setup with many features and tools out of the box. I’ve spent more time than I’d like to admit configuring Vim and Neovim. LazyVim cuts almost all that time out. As you start to become comfortable configuring LazyVim, you can consider creating your own configuration from scratch.</p>
<h2 id="heading-installation">Installation</h2>
<p>This post assumes you are using the latest version of Neovim (v0.10.0 at the time of publishing). If you need to install it, you can grab it for your OS <a target="_blank" href="https://github.com/neovim/neovim/blob/master/INSTALL.md">here</a>.</p>
<p>Once you have Neovim installed, we are going to use <a target="_blank" href="https://www.lazyvim.org/">LazyVim</a>. You are welcome to use your configuration if you would rather not use LazyVim, but that setup is a lot more complex for this post.</p>
<p>To install LazyVim, you’ll want to clone the repo and move it to your Neovim configuration folder, (<code>~/.config/nvim</code> on MacOS/Linux).</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/LazyVim/starter ~/.config/nvim
</code></pre>
<p>Once cloned, you can remove the <code>.git</code> folder since you won’t need it now and may want to version control your configuration changes.</p>
<pre><code class="lang-bash">rm -rf ~/.config/nvim/.git
</code></pre>
<p>If you are using Windows, you can follow the installation instructions <a target="_blank" href="https://www.lazyvim.org/installation">here</a>.</p>
<p>Once that is done, you can run Neovim, and it will pull down all the plugins and dependencies for LazyVim</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720901683016/9635da4f-118e-4e4c-af54-58ae56bc0825.png" alt="LazyVim Home Screen" class="image--center mx-auto" /></p>
<p>Out of the box, LazyVim gives a nice menu to navigate around as needed.</p>
<h2 id="heading-lazyvim-php-extras">LazyVim PHP Extras</h2>
<p>LazyVim recently added an update to quickly add PHP support. You just need to click <code>x</code> from the home screen to get to the extras menu or type <code>:LazyExtras</code>.</p>
<p>From the menu, you can search for PHP by first typing a slash, then <code>php</code>, so <code>/php</code>. The <code>/</code> begins searches in Neovim.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720901727689/d0a11d14-ce24-4102-a0df-05d0db2b3cac.png" alt="LazyExtras lang.php" class="image--center mx-auto" /></p>
<p>With your cursor on the <code>lang.php</code> line, hit <code>x</code> to toggle the extra. Then restart Neovim. Now, Neovim will support PHP syntax and install the <a target="_blank" href="https://github.com/phpactor/phpactor">Phpactor</a> LSP.</p>
<p>To test the LSP, I created a new Laravel project with Laravel Breeze. From the project directory, open Neovim and open the <code>ProfileController</code> by using <code>&lt;leader&gt;ff</code>. In Neovim, many keyboard commands are prefixed with the <code>&lt;leader&gt;</code> key, which is set to <code>space</code> by default. So to find a file, type <code>space + f + f</code>. Then, you can just search using the fuzzy finder.</p>
<p>When loading the controller for the first time, Phpactor will start to index your codebase, typically from the Git root of the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720901758738/47824ee0-d962-4750-b863-32e7c402e57e.png" alt="Phpactor Indexing" class="image--center mx-auto" /></p>
<p>You’ll also see many errors and other diagnostics. These are being provided by the LSP along with code completion, go to definition, refactoring, and many other features.</p>
<p>If you want to modify the base controller, you can navigate to <code>Controller</code> and click <code>gd</code> for Goto Definition.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720902754210/5a452ce7-a873-4348-b250-7dd4f4428c93.gif" alt="Navigating Around Symbols" class="image--center mx-auto" /></p>
<p>After Goto Definition, you can go back down to the class definition and click <code>gr</code> to Goto References for the <code>Controller</code> class. Next, you can use <code>ctrl+o</code> to jump back to your previous locations.</p>
<p>Please don't hesitate to keep using Phpactor if it works for you. It struggles with some of the magic and missing types in Laravel but it is completely free and open source. You can improve this by using something like <a target="_blank" href="https://github.com/barryvdh/laravel-ide-helper">Laravel IDE Helper</a> which generates stubs for models, facades, and other framework features to give better auto-completion.</p>
<p>Personally, I’ve had a better experience using the <a target="_blank" href="https://intelephense.com">Intelephense</a> LSP, which you're probably familiar with if you’re coming from VSCode. Unfortunately, you do miss some features of Phpactor without the premium version of Intelephense, so I do recommend the purchase if you use Intelephense. A single license works VSCode, Neovim, and any other editors that support LSPs.</p>
<p>To set up Intelephense, you’ll need to modify the LazyVim configuration. In the <code>~/.config/nvim</code> folder, open <code>options.lua</code> and add the following line:</p>
<pre><code class="lang-lua">vim.g.lazyvim_php_lsp = "intelephense"
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720901829570/663db4c9-4872-473d-a3d1-a1f7294acda1.png" alt="Using Intelephense" class="image--center mx-auto" /></p>
<p>This tells LazyVim to set up Intelephense. You may need to remove Phpactor, though. To accomplish that, you can type <code>&lt;leader&gt;cm</code> which opens Mason. Mason is a tool for installing various formatters, linters, and LSPs. From the Mason menu, find Phpactor and uninstall it by using <code>X</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720902803106/c76e60a9-c977-449e-a958-8b3f8cb928bc.gif" alt="Use Mason to Remove Phpactor" class="image--center mx-auto" /></p>
<h2 id="heading-setup-laravel-pint-formatting">Setup Laravel Pint Formatting</h2>
<p>Since we installed the Lazy Extras for PHP, Laravel Pint and PHP-CS-Fixer have been installed. However, PHP-CS-Fixer is set as the default. To change this, we can create a new file in the Neovim config: <code>~/.config/nvim/lua/plugins/php.lua</code>. You can name this file whatever you want, but for this post, we’ll use it for all of our PHP/Laravel related configuration.</p>
<p>In the file, you can include the following:</p>
<pre><code class="lang-lua">return {
  {
    "stevearc/conform.nvim",
    optional = true,
    opts = {
      formatters_by_ft = {
        php = { { "pint", "php_cs_fixer" } },
      },
    },
  },
}
</code></pre>
<p>This makes Pint the default formatter, and it will fall back to PHP-CS-Fixer if not found. With this change, I can go back to the <code>ProfileController</code> and add an unused import and mess up indentation, and saving will trigger a format.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720902862599/46e8c63c-9c5c-4af2-81c6-5107a5d77819.gif" alt="Automatic Formatting" class="image--center mx-auto" /></p>
<p>Another optional change you can make is to remove phpcs if you don’t use it. In the <code>php.lua</code> file, just add another block like the following:</p>
<pre><code class="lang-lua">return {
  {
    -- Set Laravel Pint as the default PHP formatter with PHP CS Fixer as a fall back.
    "stevearc/conform.nvim",
    optional = true,
    opts = {
      formatters_by_ft = {
        php = { { "pint", "php_cs_fixer" } },
      },
    },
  },
  {
    -- Remove phpcs linter.
    "mfussenegger/nvim-lint",
    optional = true,
    opts = {
      linters_by_ft = {
        php = {},
      },
    },
  },
}
</code></pre>
<p>I am grabbing these configurations right from the <a target="_blank" href="https://www.lazyvim.org/extras/lang/php">LazyVim docs</a>.</p>
<h2 id="heading-testing">Testing</h2>
<p>Next, we’ll set up Neatest so we can run tests from right in Neovim. This is another LazyVim extra that can be added by typing <code>:LazyExtras</code> and then searching for “test.core” and toggling with <code>X</code>.</p>
<p>Then, we need to install the Neotest Pest plugin. Add the following block to the <code>php.lua</code> configuration.</p>
<pre><code class="lang-lua">return {
  {
    ...
  },
  {
    -- Add neotest-pest plugin for running PHP tests.
    -- A package is also available for PHPUnit if needed.
    "nvim-neotest/neotest",
    dependencies = { "V13Axel/neotest-pest" },
    opts = { adapters = { "neotest-pest" } },
  }
}
</code></pre>
<p>With the testing config in place, load up a test file, and you can run individual tests with <code>&lt;leader&gt;tr</code> or the whole file with <code>&lt;leader&gt;tt</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720902931527/b06ce46f-dd08-4d31-8600-214c53b454cd.gif" alt="Running Tests" class="image--center mx-auto" /></p>
<p>Use <code>&lt;leader&gt;to</code> to toggle a summary of the test results.</p>
<h2 id="heading-blade-syntax">Blade Syntax</h2>
<p>Adding support for Laravel Blade is a little more involved. LazyVim has Treesitter setup to support syntax highlighting for most languages. However, Blade is not installed by default, so we need to add it.</p>
<pre><code class="lang-lua">return {
  {
    ...
  },  
  {
    -- Add a Treesitter parser for Laravel Blade to provide Blade syntax highlighting.
    "nvim-treesitter/nvim-treesitter",
    opts = function(_, opts)
      vim.list_extend(opts.ensure_installed, {
        "blade",
        "php_only",
      })
    end,
    config = function(_, opts)
      vim.filetype.add({
        pattern = {
          [".*%.blade%.php"] = "blade",
        },
      })

      require("nvim-treesitter.configs").setup(opts)
      local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
      parser_config.blade = {
        install_info = {
          url = "https://github.com/EmranMR/tree-sitter-blade",
          files = { "src/parser.c" },
          branch = "main",
        },
        filetype = "blade",
      }
    end,
  },
}
</code></pre>
<p>We extend the default Treesitter config to set a new filetype and pull down the Blade parser.</p>
<p>Once we restart Neovim, you can run <code>:TSInstall blade</code> to download the parser.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720901999193/3e47850b-b3f0-44db-b443-2a6820c6d626.png" alt="Install Blade Parser for Treesitter" class="image--center mx-auto" /></p>
<p>Next, we need to add some Treesitter queries for better code support. To achieve this, we have to create some new files from within Neovim.</p>
<h3 id="heading-blade-injections">Blade Injections</h3>
<p>Type <code>:TSEditQuery injections blade</code> and add the following contents:</p>
<pre><code class="lang-bash">((text) @injection.content
    (<span class="hljs-comment">#not-has-ancestor? @injection.content "envoy")</span>
    (<span class="hljs-comment">#set! injection.combined)</span>
    (<span class="hljs-comment">#set! injection.language php))</span>

; tree-sitter-comment injection
; <span class="hljs-keyword">if</span> available
((comment) @injection.content
 (<span class="hljs-comment">#set! injection.language "comment"))</span>

; could be bash or zsh
; or whatever tree-sitter grammar you have.
((text) @injection.content
    (<span class="hljs-comment">#has-ancestor? @injection.content "envoy")</span>
    (<span class="hljs-comment">#set! injection.combined)</span>
    (<span class="hljs-comment">#set! injection.language bash))</span>

((php_only) @injection.content
    (<span class="hljs-comment">#set! injection.language php_only))</span>

((parameter) @injection.content                                                                                                 
    (<span class="hljs-comment">#set! injection.include-children) ; You may need this, depending on your editor e.g Helix                                                                                          </span>
    (<span class="hljs-comment">#set! injection.language "php-only"))</span>
</code></pre>
<h3 id="heading-blade-highlights">Blade Highlights</h3>
<p>Type <code>:TSEditQuery highlights blade</code> and add:</p>
<pre><code class="lang-bash">(directive) @tag
(directive_start) @tag
(directive_end) @tag
(comment) @comment
</code></pre>
<h3 id="heading-blade-folds">Blade Folds</h3>
<p>Type <code>:TSEditQuery folds blade</code> and add:</p>
<pre><code class="lang-bash">((directive_start) @start
    (directive_end) @end.after
    (<span class="hljs-comment">#set! role block))</span>


((bracket_start) @start
    (bracket_end) @end
    (<span class="hljs-comment">#set! role block))</span>
</code></pre>
<h3 id="heading-html-injections">HTML Injections</h3>
<p>Finally, we’ll add some injection queries for Alpine support. Type <code>:TSEditQuery</code> and add:</p>
<pre><code class="lang-bash">;; extends

; AlpineJS attributes
(attribute
  (attribute_name) @_attr
    (<span class="hljs-comment">#lua-match? @_attr "^x%-%l")</span>
  (quoted_attribute_value
    (attribute_value) @injection.content)
  (<span class="hljs-comment">#set! injection.language "javascript"))</span>

; Blade escaped JS attributes
; &lt;x-foo ::bar=<span class="hljs-string">"baz"</span> /&gt;
(element
  (_
    (tag_name) @_tag
      (<span class="hljs-comment">#lua-match? @_tag "^x%-%l")</span>
  (attribute
    (attribute_name) @_attr
      (<span class="hljs-comment">#lua-match? @_attr "^::%l")</span>
    (quoted_attribute_value
      (attribute_value) @injection.content)
    (<span class="hljs-comment">#set! injection.language "javascript"))))</span>

; Blade PHP attributes
; &lt;x-foo :bar=<span class="hljs-string">"<span class="hljs-variable">$baz</span>"</span> /&gt;
(element
  (_
    (tag_name) @_tag
      (<span class="hljs-comment">#lua-match? @_tag "^x%-%l")</span>
    (attribute
      (attribute_name) @_attr
        (<span class="hljs-comment">#lua-match? @_attr "^:%l")</span>
      (quoted_attribute_value
        (attribute_value) @injection.content)
      (<span class="hljs-comment">#set! injection.language "php_only"))))</span>
</code></pre>
<p>Now, after adding everything, saving and restarting, you should now have syntax highlighting for Blade files!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720902028600/dfa57691-83d5-4ecf-9f6e-5877b0d1daae.png" alt="Blade Syntax Highlighting" class="image--center mx-auto" /></p>
<p>For more information and installation instructions, visit the <a target="_blank" href="https://github.com/EmranMR/tree-sitter-blade">repo</a> for the Blade Treesitter parser.</p>
<h2 id="heading-snippets">Snippets</h2>
<p>LazyVim comes with a plugin to easily create snippets. To create PHP snippets, you can create a new file: <code>~/.config/nvim/snippets/php.json</code> similar to the example below:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"strict types"</span>: {
    <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"strict"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Add strict types declaration"</span>,
    <span class="hljs-attr">"body"</span>: [
      <span class="hljs-string">"declare(strict_types=1);"</span>
    ]
  },
  <span class="hljs-attr">"inv"</span>: {
    <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"inv"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Create PHP __invoke method"</span>,
    <span class="hljs-attr">"body"</span>: [
      <span class="hljs-string">"public function __invoke(${1}): ${2:void}"</span>,
      <span class="hljs-string">"{"</span>,
      <span class="hljs-string">"    ${3}"</span>,
      <span class="hljs-string">"}"</span>,
      <span class="hljs-string">""</span>
    ]
  },
  <span class="hljs-attr">"public method"</span>: {
    <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"pubf"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Create a public method"</span>,
    <span class="hljs-attr">"body"</span>: [
      <span class="hljs-string">"public function ${1}(${2}): ${3:void}"</span>,
      <span class="hljs-string">"{"</span>,
      <span class="hljs-string">"    ${0}"</span>,
      <span class="hljs-string">"}"</span>,
      <span class="hljs-string">""</span>
    ]
  },
  <span class="hljs-attr">"protected method"</span>: {
    <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"prof"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Create a protected method"</span>,
    <span class="hljs-attr">"body"</span>: [
      <span class="hljs-string">"protected function ${1}(${2}): ${3:void}"</span>,
      <span class="hljs-string">"{"</span>,
      <span class="hljs-string">"    ${0}"</span>,
      <span class="hljs-string">"}"</span>,
      <span class="hljs-string">""</span>
    ]
  },
  <span class="hljs-attr">"private method"</span>: {
    <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"prif"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Create a private method"</span>,
    <span class="hljs-attr">"body"</span>: [
      <span class="hljs-string">"private function ${1}(${2}): ${3:void}"</span>,
      <span class="hljs-string">"{"</span>,
      <span class="hljs-string">"    ${0}"</span>,
      <span class="hljs-string">"}"</span>,
      <span class="hljs-string">""</span>
    ]
  },
  <span class="hljs-attr">"public static method"</span>: {
    <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"pubsf"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Create a public static method"</span>,
    <span class="hljs-attr">"body"</span>: [
      <span class="hljs-string">"public static function ${1}(${2}): ${3:void}"</span>,
      <span class="hljs-string">"{"</span>,
      <span class="hljs-string">"    ${0}"</span>,
      <span class="hljs-string">"}"</span>,
      <span class="hljs-string">""</span>
    ]
  },
  <span class="hljs-attr">"pest test (it) method"</span>: {
    <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"it"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Create a pest test"</span>,
    <span class="hljs-attr">"body"</span>: [
      <span class="hljs-string">"it('${1}', function () {"</span>,
      <span class="hljs-string">"    // Arrange"</span>,
      <span class="hljs-string">"    ${0}"</span>,
      <span class="hljs-string">""</span>,
      <span class="hljs-string">"    // Act"</span>,
      <span class="hljs-string">""</span>,
      <span class="hljs-string">"    // Assert"</span>,
      <span class="hljs-string">""</span>,
      <span class="hljs-string">"});"</span>
    ]
  }
}
</code></pre>
<p>You can add any other snippets you might want to this file, or create files for other languages to add snippets.</p>
<h2 id="heading-additional-plugins">Additional Plugins</h2>
<h3 id="heading-laravelnvimhttpsgithubcomadalessalaravelnvim"><a target="_blank" href="https://github.com/adalessa/laravel.nvim">Laravel.nvim</a></h3>
<p>This plugin can be used to quickly run Artisan commands with a great search feature using <code>&lt;leader&gt;la</code>. Or you can list all the routes in your application using <code>&lt;leader&gt;lr</code>.</p>
<pre><code class="lang-lua">return { 
  {
    ...
  },
  {
    -- Add the Laravel.nvim plugin which gives the ability to run Artisan commands
    -- from Neovim.
    "adalessa/laravel.nvim",
    dependencies = {
      "nvim-telescope/telescope.nvim",
      "tpope/vim-dotenv",
      "MunifTanjim/nui.nvim",
      "nvimtools/none-ls.nvim",
    },
    cmd = { "Sail", "Artisan", "Composer", "Npm", "Yarn", "Laravel" },
    keys = {
      { "&lt;leader&gt;la", ":Laravel artisan&lt;cr&gt;" },
      { "&lt;leader&gt;lr", ":Laravel routes&lt;cr&gt;" },
      { "&lt;leader&gt;lm", ":Laravel related&lt;cr&gt;" },
    },
    event = { "VeryLazy" },
    config = true,
    opts = {
      lsp_server = "intelephense",
      features = { null_ls = { enable = false } },
    },
  },
}
</code></pre>
<h3 id="heading-blade-navnvimhttpsgithubcomricardoramirezrblade-navnvim"><a target="_blank" href="https://github.com/RicardoRamirezR/blade-nav.nvim">Blade-Nav.nvim</a></h3>
<p>Adds the ability to use Goto File on Blade views to jump to components and other views using <code>gf</code>.</p>
<pre><code class="lang-lua">return { 
  {
    ...
  },
  {
    -- Add the blade-nav.nvim plugin which provides Goto File capabilities
    -- for Blade files.
    "ricardoramirezr/blade-nav.nvim",
    dependencies = {
      "hrsh7th/nvim-cmp",
    },
    ft = { "blade", "php" },
  },
}
</code></pre>
<h2 id="heading-additional-tips">Additional Tips</h2>
<p>One giant benefit of LazyVim is the documentation. If there’s something you’re used to doing in VSCode and you want it in Neovim, there’s a chance LazyVim may already have that built-in. I recommend just going through each section in LazyVim to learn more about it.</p>
<p>Probably one of the most important sections is the keymaps. LazyVim uses which-key.nvim to help you remember the configured keycaps while you’re in the editor, but for a complete list, click <a target="_blank" href="https://www.lazyvim.org/keymaps">here</a>.</p>
<p>Do you want Git support? LazyVim has that covered with <a target="_blank" href="https://github.com/jesseduffield/lazygit">LazyGit</a> which is a really nice terminal UI for Git. Use <code>&lt;leader&gt;gg</code> to open it. You can even <a target="_blank" href="https://github.com/jesseduffield/lazygit?tab=readme-ov-file#installation">install</a> LazyGit directly using something like Homebrew to just run right on the command line using <code>lazygit</code>.</p>
<p>Need additional tooling like PHPStan or Psalm? Use <code>&lt;leader&gt;cm</code> or <code>:Mason</code> to bring up the Mason menu and search for what you need. It has may popular linters and formatters available to install.</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<h3 id="heading-lazyvim-docshttpswwwlazyvimorg"><a target="_blank" href="https://www.lazyvim.org/">LazyVim Docs</a></h3>
<p>Like I mentioned above, LazyVim provides amazing documentation and it’s definitely worth a read.</p>
<h3 id="heading-neovim-as-a-php-and-javascript-idehttpslaracastscomseriesneovim-as-a-php-ide"><a target="_blank" href="https://laracasts.com/series/neovim-as-a-php-ide">Neovim as a PHP and JavaScript IDE</a></h3>
<p>Jess Archer has a fantastic <a target="_blank" href="https://laracasts.com/series/neovim-as-a-php-ide">course</a> for setting up Neovim on Laracasts. If you are not subscribed to Laracasts, I cannot recommend it enough. I’ve been a lifetime user since 2017. If it’s something you’re interested in, please use my <a target="_blank" href="https://laracasts.com/referral/skegel13">referral link</a>.</p>
<h3 id="heading-omakubhttpsomakuborg"><a target="_blank" href="https://omakub.org/">Omakub</a></h3>
<p>DHH (the creator of Ruby on Rails) created the Omakub package as a quick way to set up a development environment on Ubuntu Linux. Even if you’re not using Ubuntu, the repo for Omakub is worth checking out as it has a lot of nice configurations and tools, one of which being Neovim with LazyVim.</p>
<h3 id="heading-kickstartnvimhttpsgithubcomnvim-luakickstartnvim"><a target="_blank" href="https://github.com/nvim-lua/kickstart.nvim">Kickstart.nvim</a></h3>
<p>If you want something a bit more minimal than LazyVim, then Kickstart.nvim is a good start. You can watch this video by TJ DeVries about how to get started. Even if you want to keep using LazyVim, this is still a great resource to learn more about configuring Neovim.</p>
<h2 id="heading-summary">Summary</h2>
<p>I hope this helps get you started on your Neovim journey. It’s a powerful editor that is infinitely configurable. Though not as powerful as something like PhpStorm, it can get you pretty close for free and requires less CPU resources.</p>
<p>Even if you don’t plan to use Neovim as your primary editor, I think it can still be beneficial to learn the keybindings. I typically split my time between Neovim and PhpStorm and try to keep my keybindings as similar as possible. Luckily, the IdeaVim plugin for JetBrains IDEs makes this simple.</p>
<p>For your reference, I created a <a target="_blank" href="https://github.com/skegel13/laravel-neovim-config">repo</a> with all the files we created in this post.</p>
<p>Please let me know if you have any other questions about the setup or any additional features I might have missed.</p>
<p>Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Elevate Your Laravel Eloquent Queries with Tappable Scopes]]></title><description><![CDATA[In this article, I want to go over how to use tappable scopes in Laravel. I’ve used similar patterns in Java Spring Boot, but never really considered using it in Laravel until I read Unorthodox Eloquent by Muhammed Sari which is an excellent guide to...]]></description><link>https://seankegel.com/elevate-your-laravel-eloquent-queries-with-tappable-scopes</link><guid isPermaLink="true">https://seankegel.com/elevate-your-laravel-eloquent-queries-with-tappable-scopes</guid><category><![CDATA[Laravel]]></category><category><![CDATA[Databases]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sat, 20 Apr 2024 19:01:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713727827833/a803befd-8b92-444c-b156-a0ff2e4e64c1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, I want to go over how to use tappable scopes in Laravel. I’ve used similar patterns in Java Spring Boot, but never really considered using it in Laravel until I read <a target="_blank" href="https://muhammedsari.me/unorthodox-eloquent">Unorthodox Eloquent</a> by Muhammed Sari which is an excellent guide to many advanced features in Laravel Eloquent.</p>
<p>Typically, when using query scopes in Laravel, the simple way is to use the <code>scope</code> prefix on a method in the model, like the following:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Posts</span> 
</span>{        
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">scopePublished</span>(<span class="hljs-params">Builder $query</span>): <span class="hljs-title">void</span>
    </span>{
        $query-&gt;where(<span class="hljs-string">'published_at'</span>, <span class="hljs-string">'&lt;='</span>, now());
    }
}

$publishedPosts = Posts::published()-&gt;get();
</code></pre>
<p>This works well, but it does make it harder for the IDE to handle unless you’re using something like the <a target="_blank" href="https://github.com/barryvdh/laravel-ide-helper">Laravel IDE Helper package</a>.</p>
<p>To convert this into a tappable scope, we can do something like the following:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Scopes/Published.php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Published</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__invoke</span>(<span class="hljs-params">Builder $query</span>): <span class="hljs-title">void</span>
    </span>{
        $query-&gt;where(<span class="hljs-string">'published_at'</span>, <span class="hljs-string">'&lt;='</span>, now());
    }
}

$publishedPosts = Posts::tap(<span class="hljs-keyword">new</span> Published)-&gt;get();
</code></pre>
<p>Using the tappable scope changes the following:</p>
<pre><code class="lang-php"><span class="hljs-comment">// With regular query scope</span>
$publishedPosts = Posts::published()-&gt;get();

<span class="hljs-comment">// With tappable scope</span>
$publishedPosts = Posts::tap(<span class="hljs-keyword">new</span> Published)-&gt;get();
</code></pre>
<p>The top one looks nicer, however, the IDE will not be able to easily see what the <code>published</code> method does since it using the magic <code>scope</code> prefix, whereas with the tappable scope version, you can easily click into <code>Published</code> and see exactly what’s happening.</p>
<p>Also, using the tappable scope allows it to be easily reused. For example, if you had a <code>Comment</code> model, that also included a <code>published_at</code> column, then to get just published comments, you can use the same scope from before:</p>
<pre><code class="lang-php">$comments = Comment::tap(<span class="hljs-keyword">new</span> Published)-&gt;get();
</code></pre>
<p>Now, let’s take these scopes to the next level by adding custom parameters.</p>
<p>Using are same <code>Post</code> and <code>Comment</code> models, let’s assume both include a <code>user_id</code> field. To handle that with a tappable scope, create the following:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Scopes/ByUser.php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ByUser</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> readonly <span class="hljs-keyword">int</span> $userId</span>)
    </span>{
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__invoke</span>(<span class="hljs-params">Builder $query</span>): <span class="hljs-title">void</span>
    </span>{
        $query-&gt;where(<span class="hljs-string">'user_id'</span>, <span class="hljs-keyword">$this</span>-&gt;userId);
    }
}
</code></pre>
<p>With the new tappable scope, we can fetch posts and comments for a user shown below:</p>
<pre><code class="lang-php">$userId = <span class="hljs-number">1</span>;

$posts = Post::tap(<span class="hljs-keyword">new</span> ByUser($userId))-&gt;get();

$comments = Comment::tap(<span class="hljs-keyword">new</span> ByUser($userId))-&gt;get();
</code></pre>
<p>The above examples are relatively simple, and maybe it’s easier to just use normal <code>where</code> methods for those, so maybe they are not the best cases for tappable scopes, but I wanted to use the simple examples as an introduction. Now let’s create a tappable scope for something a little more complex.</p>
<p>On our home page, we want to show the latest published posts with the author and comment count. This query could look like the following:</p>
<pre><code class="lang-php">$posts = Post::query()
    -&gt;with([<span class="hljs-string">'user'</span>, <span class="hljs-string">'comments'</span>])
    -&gt;where(<span class="hljs-string">'published_at'</span>, <span class="hljs-string">'&lt;='</span>, now())
    -&gt;latest(<span class="hljs-string">'published_at'</span>)
    -&gt;limit(<span class="hljs-number">10</span>)
    -&gt;get();
</code></pre>
<p>This works, but the query is starting to get kind of large. We also fetch the entire user model for each post and all the comments, when really all we want is a name and count. Also, we are counting unpublished comments which we don’t want. So let’s adjust:</p>
<pre><code class="lang-php">$posts = Post::query()
    -&gt;select(<span class="hljs-string">'posts.*'</span>)
    -&gt;join(<span class="hljs-string">'users'</span>, <span class="hljs-string">'users.id'</span>, <span class="hljs-string">'='</span>, <span class="hljs-string">'posts.user_id'</span>)
    -&gt;withAggregate(<span class="hljs-string">'user'</span>, <span class="hljs-string">'name'</span>)
    -&gt;withCount([<span class="hljs-string">'comments'</span> =&gt; <span class="hljs-function"><span class="hljs-keyword">fn</span> (<span class="hljs-params">Builder $query</span>) =&gt; $<span class="hljs-title">query</span>-&gt;<span class="hljs-title">where</span>(<span class="hljs-params"><span class="hljs-string">'published_at'</span>, <span class="hljs-string">'&gt;='</span>, now(<span class="hljs-params"></span>)</span>)])
    -&gt;<span class="hljs-title">where</span>(<span class="hljs-params"><span class="hljs-string">'published_at'</span>, <span class="hljs-string">'&lt;='</span>, now(<span class="hljs-params"></span>)</span>)
    -&gt;<span class="hljs-title">latest</span>(<span class="hljs-params"><span class="hljs-string">'published_at'</span></span>)
    -&gt;<span class="hljs-title">limit</span>(<span class="hljs-params"><span class="hljs-number">10</span></span>)
    -&gt;<span class="hljs-title">get</span>(<span class="hljs-params"></span>)</span>;
</code></pre>
<p>This gives us exactly what we want, an array of posts with the following structure:</p>
<pre><code class="lang-php">[
    <span class="hljs-number">0</span> =&gt; [    
        <span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">69</span>,
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-number">360</span>,
        <span class="hljs-string">'name'</span> =&gt; <span class="hljs-string">'...'</span>,
        <span class="hljs-string">'body'</span> =&gt; <span class="hljs-string">'...'</span>,
        <span class="hljs-string">'published_at'</span> =&gt; <span class="hljs-string">'2024-04-20 03:18:37'</span>,
        <span class="hljs-string">'created_at'</span> =&gt; <span class="hljs-string">'2024-04-21T18:44:24.000000Z'</span>,
        <span class="hljs-string">'updated_at'</span> =&gt; <span class="hljs-string">'2024-04-21T18:44:24.000000Z'</span>,
        <span class="hljs-string">'user_name'</span> =&gt; <span class="hljs-string">'Janae Luettgen'</span>,
        <span class="hljs-string">'comments_count'</span> =&gt; <span class="hljs-number">2</span>,
    ],
    <span class="hljs-number">1</span> =&gt; [...]
    ...
]
</code></pre>
<p>This is great, but now our query is pretty complex. Imagine different parts of our application need to show a limit of 5 posts instead of 10. Or maybe we want to only find a count of unpublished comments. Let’s create a tappable scope:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Scopes/LatestPosts.php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LatestPosts</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> readonly <span class="hljs-keyword">int</span> $limit = <span class="hljs-number">10</span>, <span class="hljs-keyword">private</span> readonly <span class="hljs-keyword">bool</span> $publishedComments = <span class="hljs-literal">true</span></span>)
    </span>{
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__invoke</span>(<span class="hljs-params">Builder $query</span>): <span class="hljs-title">void</span>
    </span>{
        $query-&gt;select(<span class="hljs-string">'posts.*'</span>)
            -&gt;join(<span class="hljs-string">'users'</span>, <span class="hljs-string">'users.id'</span>, <span class="hljs-string">'='</span>, <span class="hljs-string">'posts.user_id'</span>)
            -&gt;withAggregate(<span class="hljs-string">'user'</span>, <span class="hljs-string">'name'</span>)
            -&gt;withCount([
                    <span class="hljs-string">'comments'</span> =&gt; <span class="hljs-function"><span class="hljs-keyword">fn</span> (<span class="hljs-params">Builder $query</span>) =&gt; $<span class="hljs-title">query</span>
                        -&gt;<span class="hljs-title">when</span>(<span class="hljs-params">
                            $this-&gt;publishedComments,
                            fn(<span class="hljs-params">Builder $query</span>) =&gt; $query-&gt;where(<span class="hljs-params"><span class="hljs-string">'published_at'</span>, <span class="hljs-string">'&gt;='</span>, now(<span class="hljs-params"></span>)</span>)
                        </span>)
                ]
            )
            -&gt;<span class="hljs-title">where</span>(<span class="hljs-params"><span class="hljs-string">'published_at'</span>, <span class="hljs-string">'&lt;='</span>, now(<span class="hljs-params"></span>)</span>)
            -&gt;<span class="hljs-title">latest</span>(<span class="hljs-params"><span class="hljs-string">'published_at'</span></span>)
            -&gt;<span class="hljs-title">limit</span>(<span class="hljs-params">$this-&gt;limit</span>)</span>;
    }
}
</code></pre>
<p>Now, instead of having to copy and paste this large query wherever we need it, it is encapsulated in a single place and we can fetch our latest posts like below:</p>
<pre><code class="lang-php">$latestPosts = Post::tap(<span class="hljs-keyword">new</span> LatestPosts(limit: <span class="hljs-number">10</span>, publishedComments: <span class="hljs-literal">true</span>))-&gt;get();
</code></pre>
<p>I hope this helps you in your Laravel career. It’s a clean way to remove some of the magic of the built-in Laravel query scopes and allows for easy reuse and abstracting complex queries.</p>
<p>Thanks for reading!</p>
<h2 id="heading-related-links">Related Links</h2>
<ul>
<li><p><a target="_blank" href="https://muhammedsari.me/unorthodox-eloquent">Unorthodox Eloquent - Muhammed Sari</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/barryvdh/laravel-ide-helper">Laravel IDE Helper</a></p>
</li>
<li><p><a target="_blank" href="https://laravel.com/docs/11.x/eloquent#local-scopes">Laravel - Local Scopes</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Laravel Cache Classes]]></title><description><![CDATA[I’ve been working on a Laravel project that requires numerous database calls, so I have started implementing caching to try to improve performance and reduce database queries.
To start with, I was just using Laravel’s Cache facade, like below:
Cache:...]]></description><link>https://seankegel.com/laravel-cache-classes</link><guid isPermaLink="true">https://seankegel.com/laravel-cache-classes</guid><category><![CDATA[Laravel]]></category><category><![CDATA[PHP]]></category><category><![CDATA[caching]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Tue, 12 Mar 2024 22:31:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710282646196/dae0b68c-eead-494d-82a5-176e079bb24f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve been working on a Laravel project that requires numerous database calls, so I have started implementing caching to try to improve performance and reduce database queries.</p>
<p>To start with, I was just using Laravel’s <code>Cache</code> facade, like below:</p>
<pre><code class="lang-php">Cache::remember(
    <span class="hljs-string">'unique-key'</span>, 
    <span class="hljs-number">60</span>, <span class="hljs-comment">// cache lives for 60 seconds</span>
    <span class="hljs-function"><span class="hljs-keyword">fn</span> (<span class="hljs-params"></span>) =&gt; $<span class="hljs-title">this</span>-&gt;<span class="hljs-title">operationToBeCached</span>(<span class="hljs-params"></span>)
)</span>;
</code></pre>
<p><code>Cache::remember</code> is really nice because it will pull from the cache if it exists or regenerate the value using the closure, put it in the cache, and return it.</p>
<p>Let’s say this cache is for a user and when the data involved changes, I need to forget the cache. It’s not too hard, just use the <code>forget</code> method.</p>
<pre><code class="lang-php">Cache::forget(<span class="hljs-string">'unique-key'</span>);
</code></pre>
<p>The problem I started running into though is trying to remember that unique key. I don’t like having magic strings used throughout the app to set or remove items from the cache. It gets even more difficult when the unique key needs to include additional data like model IDs.</p>
<p>To solve this, I created a <code>CacheHelper</code> class.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Cache</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Cache</span>;

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CacheHelper</span>
</span>{
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">string</span> $key; <span class="hljs-comment">// Cache key</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">int</span> $ttl = <span class="hljs-number">60</span>; <span class="hljs-comment">// Time to live</span>

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getKey</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;key;
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">value</span>(<span class="hljs-params"></span>): <span class="hljs-title">mixed</span> // <span class="hljs-title">Store</span> <span class="hljs-title">and</span> <span class="hljs-title">fetch</span> <span class="hljs-title">value</span> <span class="hljs-title">from</span> <span class="hljs-title">cache</span>
    </span>{
        <span class="hljs-keyword">return</span> Cache::remember(
            <span class="hljs-keyword">$this</span>-&gt;getKey(),
            <span class="hljs-keyword">$this</span>-&gt;ttl,
            <span class="hljs-function"><span class="hljs-keyword">fn</span> (<span class="hljs-params"></span>) =&gt; $<span class="hljs-title">this</span>-&gt;<span class="hljs-title">generate</span>(<span class="hljs-params"></span>)
        )</span>;
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">forget</span>(<span class="hljs-params"></span>): <span class="hljs-title">bool</span> // <span class="hljs-title">Forget</span> <span class="hljs-title">value</span> <span class="hljs-title">from</span> <span class="hljs-title">the</span> <span class="hljs-title">cache</span>
    </span>{
        <span class="hljs-keyword">return</span> Cache::forget(<span class="hljs-keyword">$this</span>-&gt;getKey());
    }

    <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generate</span>(<span class="hljs-params"></span>): <span class="hljs-title">mixed</span></span>; <span class="hljs-comment">// Abstract method for generating cache value</span>
}
</code></pre>
<p>The <code>CacheHelper</code> class is abstract, so it must be extended and include a <code>generate</code> method. I also created a trait that allows easily setting unique keys using an ID.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Cache</span>;

<span class="hljs-keyword">trait</span> CacheIdentifier
{
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">int</span>|<span class="hljs-keyword">string</span>|<span class="hljs-literal">null</span> $identifier = <span class="hljs-literal">null</span>;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getKey</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span>
    </span>{
        <span class="hljs-keyword">return</span> sprintf(<span class="hljs-keyword">$this</span>-&gt;key, <span class="hljs-keyword">$this</span>-&gt;identifier); <span class="hljs-comment">// Attach an identifier to the cache key.</span>
    }
}
</code></pre>
<p>Here’s an example below on how this can be extended.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Cache</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Models</span>\<span class="hljs-title">User</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Collection</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserExpensiveComputationCache</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CacheHelper</span>
</span>{
    <span class="hljs-keyword">use</span> <span class="hljs-title">CacheIdentifier</span>;

    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">string</span> $key = <span class="hljs-string">'expensive_computation_:%s'</span>;
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">int</span> $ttl = <span class="hljs-number">60</span>;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"><span class="hljs-keyword">protected</span> readonly User $user</span>)
    </span>{
        <span class="hljs-keyword">$this</span>-&gt;identifier = $user-&gt;id; <span class="hljs-comment">// The identifier is the user ID and is attached to the key.</span>
    }

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">make</span>(<span class="hljs-params">User $user</span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">static</span>($user); <span class="hljs-comment">// Simple helper to create a new instance</span>
    }

    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generate</span>(<span class="hljs-params"></span>): <span class="hljs-title">Collection</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;user-&gt;expensiveComputation();
    }
}
</code></pre>
<p>With this set, whenever I need to access the expensive computation, I can use my cache class:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

$user = User::find(<span class="hljs-number">1</span>);
$value = UserExpensiveComputationCache::make($user);
</code></pre>
<p>The <code>UserExpensiveComputationCache</code>class will remember the value for the user I passed to it.</p>
<p>When it comes time to clear the cache because a related record was modified, I can just do the following:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

UserExpensiveComputationCache::make($user)-&gt;forget();
</code></pre>
<p>I no longer need to remember a magic string throughout my application. I just have a simple class that I can instantiate and use it to handle my unique keys, setting, and forgetting the cache. If I have to change the key for whatever reason, it only happens in this one place in the application and I don’t have to search around for other instances of the key.</p>
<p>I was able to throw this <code>CacheHelper</code> class together pretty quickly, and it solved my problem of avoiding magic strings across my application and being able to easily manage forgetting the cache when needed. Using dedicated classes to cache items isn’t something I have seen used before. Please let me know if you’ve seen something similar or have other ideas and strategies for managing cache. Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[What I'm Reading - February 2024]]></title><description><![CDATA[I feel like I didn't get as much reading done in February but I am back with another list of interesting articles I read throughout the month.
Starting off, here's something I read at the end of the month from Aaron Francis: Do literally anything - A...]]></description><link>https://seankegel.com/what-im-reading-february-2024</link><guid isPermaLink="true">https://seankegel.com/what-im-reading-february-2024</guid><category><![CDATA[Web Development]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[cli]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sun, 03 Mar 2024 03:49:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709437516603/84b6df11-b227-497d-b55f-7bbf74226a68.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I feel like I didn't get as much reading done in February but I am back with another list of interesting articles I read throughout the month.</p>
<p>Starting off, here's something I read at the end of the month from Aaron Francis: <a target="_blank" href="https://aaronfrancis.com/2024/do-literally-anything">Do literally anything - Aaron Francis</a>. This one resonates with me. I am always thinking of new projects I want to work on and hobbies I want to start. I've been thinking about getting into woodworking, I've been thinking about landscape improvements, and this is on top of my job, my side work, and my family. What ends up happening is I overwhelm myself and then sit in frustration thinking I can't get anything done. Instead, if I just do something, even if it isn't a lot, it is still a step in the right direction and builds momentum to keep doing more. So this will be something I keep in the back of my mind when I get overwhelmed.</p>
<p>Here's an article by Rob Owen: <a target="_blank" href="https://robbowen.digital/wrote-about/abandoned-side-projects/">It's OK to abandon your side-project - Robb Owen</a>. In the article, Rob describes working on a project and once he had it launched, he abandoned it. He was building an application to help him practice with a foreign language and by doing all the work to build the application, he ended up learning what he was hoping to use the application to teach. So it was more about the experience of building the application itself versus having a completed application.</p>
<p>On to a different topic, here's one by Nikita Prokopov: <a target="_blank" href="https://tonsky.me/blog/js-bloat/">JavaScript Bloat in 2024 @</a> <a target="_blank" href="http://tonsky.me">tonsky.me</a>. It's shocking to see how much JavaScript is being loaded on different sites. What is Slack doing downloading 55 MB worth of JavaScript?! It seems like over the last few years, we are starting to see a shift in frontend development with tools like Laravel Livewire and React server components, we are starting to move back to servers doing more of the work versus the client. I spend more time working on backend code, so this is exciting for me. Hopefully, we'll start to see this JavaScript bloat start to go down. However, I'd be more excited for just the complexity of the front end to go down. Over the last year and a half, I've been primarily doing backend work and I feel like I am so behind on the latest frontend tools, but when I start to look into it, I just think, why has this become so difficult and I have years of experience in Vue, React, Angular, jQuery, etc. Is it time to get back on jQuery now that 4.0 is coming: <a target="_blank" href="https://blog.jquery.com/2024/02/06/jquery-4-0-0-beta/">jQuery 4.0.0 BETA! | Official jQuery Blog</a> 😂?</p>
<p>This article from Cory Doctorow: <a target="_blank" href="https://ft.com/content/6fb1602d-a08b-4a8c-bac0-047b7d64aba5/">‘Enshittification’ is coming for absolutely everything</a> is a bit of a depressing read. It discusses how platforms, that start great and provide a lot of value to their users, start to degrade. It does end on a positive by discussing the "anti-enshittification" movement.</p>
<p>The last article I have for this month is from Paul Redmond at Laravel News: <a target="_blank" href="https://laravel-news.com/command-line-productivity">Five Tools That Will Make You More Productive on the Command Line - Laravel News</a>. I learned about some new CLI tools I haven't used before. Zoxide has been an amazing find to make moving around paths more efficient. Also, Paul mentions fzf-tab. I have been using fzf for a long time with Vim/Neovim, but fzf-tab is great for completing items in the CLI.</p>
<h2 id="heading-article-list">Article List</h2>
<p>Here's a quick list of the articles I described above:</p>
<ul>
<li><p><a target="_blank" href="https://aaronfrancis.com/2024/do-literally-anything">Do literally anything - Aaron Francis</a></p>
</li>
<li><p><a target="_blank" href="https://robbowen.digital/wrote-about/abandoned-side-projects/">It's OK to abandon your side-project - Robb Owen</a></p>
</li>
<li><p><a target="_blank" href="https://tonsky.me/blog/js-bloat/">JavaScript Bloat in 2024 @</a> <a target="_blank" href="http://tonsky.me">tonsky.me</a></p>
</li>
<li><p><a target="_blank" href="https://ft.com/content/6fb1602d-a08b-4a8c-bac0-047b7d64aba5/">‘Enshittification’ is coming for absolutely everything</a></p>
</li>
<li><p><a target="_blank" href="https://laravel-news.com/command-line-productivity">Five Tools That Will Make You More Productive on the Command Line - Laravel News</a></p>
</li>
</ul>
<h2 id="heading-rss-feeds">RSS Feeds</h2>
<p>If you found any of the above articles interesting, here's a list of RSS feeds to subscribe to:</p>
<ul>
<li><p><a target="_blank" href="http://aaronfrancis.com/feed">aaronfrancis.com/feed</a></p>
</li>
<li><p><a target="_blank" href="http://robbowen.digital/feed.xml">robbowen.digital/feed.xml</a></p>
</li>
<li><p><a target="_blank" href="http://tonsky.me/atom.xml">tonsky.me/atom.xml</a></p>
</li>
<li><p><a target="_blank" href="https://feed.laravel-news.com/">Laravel News</a></p>
</li>
</ul>
<h2 id="heading-other-articles">Other Articles</h2>
<p>Here are some other interesting articles I read over February.</p>
<ul>
<li><p><a target="_blank" href="https://chriscoyier.net/2024/02/28/where-im-at-on-the-whole-css-tricks-thing/">Where I’m at on the whole CSS-Tricks thing – Chris Coyier</a></p>
</li>
<li><p><a target="_blank" href="https://peakd.com/php/@crell/php-never-type-hint-on-arrays">PeakD</a></p>
</li>
<li><p><a target="_blank" href="https://daverupert.com/2024/02/robo-barf/">A dozen thoughts about AI -</a> <a target="_blank" href="http://daverupert.com">daverupert.com</a></p>
</li>
<li><p><a target="_blank" href="https://sebastiandedeyne.com/how-i-take-notes-structure-with-now-next-notes">How I take notes: Structure with Now Next Notes | Sebastian De Deyne</a></p>
</li>
<li><p><a target="_blank" href="https://masteringlaravel.io/daily/2024-02-09-watch-out-for-this-when-testing-artisan-commands?ck_subscriber_id=2343584513">Watch out for this when testing Artisan commands | Mastering Laravel</a></p>
</li>
<li><p><a target="_blank" href="https://hamatti.org/posts/please-dont-force-me-to-log-in/?utm_source=cassidoo&amp;utm_medium=email&amp;utm_campaign=people-who-are-truly-strong-lift-others-up-people">Please, don’t force me to log in : Juha-Matti Santala</a></p>
</li>
<li><p><a target="_blank" href="https://sqlfordevs.com/for-each-loop-lateral-join">For each loops with LATERAL Joins - Database Tip</a></p>
</li>
</ul>
<p>Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Fix Flaky Tests with Pest Repeat]]></title><description><![CDATA[I recently ran into an issue with a flaky test in our CI process. Most of the time, it would pass, but when it failed, it meant running all the tests again and hoping it would pass on the next try.
When I was finally fed up enough with the waiting, I...]]></description><link>https://seankegel.com/fix-flaky-tests-with-pest-repeat</link><guid isPermaLink="true">https://seankegel.com/fix-flaky-tests-with-pest-repeat</guid><category><![CDATA[PHP]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[pestphp]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sun, 25 Feb 2024 20:34:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708893137210/7e879510-00fe-43c7-803a-44dad943e449.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently ran into an issue with a flaky test in our CI process. Most of the time, it would pass, but when it failed, it meant running all the tests again and hoping it would pass on the next try.</p>
<p>When I was finally fed up enough with the waiting, I would decide to run the test locally, and when I would see it pass, I’d be confused about what was going on in CI.</p>
<p>That’s when I started using the <code>repeat</code> method with Pest to debug what was going on.</p>
<p>I will use a simple <code>Post</code> model as an example of what I was running into.</p>
<pre><code class="lang-php"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">up</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    Schema::create(<span class="hljs-string">'posts'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Blueprint $table</span>) </span>{
        $table-&gt;id();
        $table-&gt;string(<span class="hljs-string">'title'</span>);
        $table-&gt;string(<span class="hljs-string">'content'</span>)-&gt;nullable();
        $table-&gt;unsignedBigInteger(<span class="hljs-string">'user_id'</span>);
        $table-&gt;string(<span class="hljs-string">'status'</span>);
        $table-&gt;dateTime(<span class="hljs-string">'published_at'</span>)-&gt;nullable();
        $table-&gt;timestamps();
    });
}
</code></pre>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Post</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span>
</span>{
    <span class="hljs-keyword">use</span> <span class="hljs-title">HasFactory</span>;

    <span class="hljs-keyword">protected</span> $casts = [
        <span class="hljs-string">'status'</span> =&gt; PostStatus::class,
        <span class="hljs-string">'published_at'</span> =&gt; <span class="hljs-string">'datetime'</span>,
    ];

    <span class="hljs-keyword">protected</span> $attributes = [
        <span class="hljs-string">'status'</span> =&gt; PostStatus::Idea,
    ];

    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isPublished</span>(<span class="hljs-params"></span>): <span class="hljs-title">Attribute</span>
    </span>{
        <span class="hljs-keyword">return</span> Attribute::make(
            get: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;status === PostStatus::Published &amp;&amp; <span class="hljs-keyword">$this</span>-&gt;published_at?-&gt;lt(now());
            }
        );
    }
}
</code></pre>
<p><code>PostStatus</code> is an enum with the following cases:</p>
<pre><code class="lang-php">enum PostStatus: <span class="hljs-keyword">string</span>
{
    <span class="hljs-keyword">case</span> Idea = <span class="hljs-string">'idea'</span>;
    <span class="hljs-keyword">case</span> Draft = <span class="hljs-string">'draft'</span>;
    <span class="hljs-keyword">case</span> Published = <span class="hljs-string">'published'</span>;
    <span class="hljs-keyword">case</span> Hidden = <span class="hljs-string">'hidden'</span>;
}
</code></pre>
<p>So far, everything is pretty straightforward. A post is considered published if it has a <code>status</code> of <code>PostStatus::Published</code> and a <code>published_at</code> date before the current date/time. I have two simple tests set up for this.</p>
<pre><code class="lang-php">uses(RefreshDatabase::class);

it(<span class="hljs-string">'is published'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $post = Post::factory()-&gt;create([
        <span class="hljs-string">'status'</span> =&gt; PostStatus::Published
    ]);

    expect($post-&gt;is_published)-&gt;toBeTrue();
});

it(<span class="hljs-string">'is not published'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $post = Post::factory()-&gt;create();

    expect($post-&gt;is_published)-&gt;toBeFalse();
});
</code></pre>
<p>I run the tests locally and everything passes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708892742124/2e916d8a-0aa4-4a75-8e66-bd5782c71c3f.png" alt="Flaky Tests Passing" class="image--center mx-auto" /></p>
<p>Everything’s looking good, however, when merging my PR, CI runs and one of these tests fails. Or even worse, it passes, and another teammate comes along with a new PR and one of these tests now fails even though nothing related to these tests was changed. What just happened?</p>
<p>I decide I need to fix this flaky test, so I start looking into it using the <code>repeat</code> method.</p>
<pre><code class="lang-php">it(<span class="hljs-string">'is published'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $post = Post::factory()-&gt;create([
        <span class="hljs-string">'status'</span> =&gt; PostStatus::Published
    ]);

    expect($post-&gt;is_published)-&gt;toBeTrue();
})-&gt;repeat(<span class="hljs-number">100</span>);

it(<span class="hljs-string">'is not published'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $post = Post::factory()-&gt;create();

    expect($post-&gt;is_published)-&gt;toBeFalse();
})-&gt;repeat(<span class="hljs-number">100</span>);
</code></pre>
<p>Now, when I run the tests, I see something like the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708892775724/d585e900-6b27-4cec-aed8-b060896359b4.png" alt="Repeated Flaky Tests Not Passing" class="image--center mx-auto" /></p>
<p>So, our tests pass most of the time, but not all the time. So we know something is going on here. Since these tests are fairly simple, the first thing I will check is the factory.</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PostFactory</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Factory</span>
</span>{
    <span class="hljs-keyword">protected</span> $model = Post::class;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">definition</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;word(),
            <span class="hljs-string">'content'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;paragraph(),
            <span class="hljs-string">'status'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;randomElement(PostStatus::cases()),
            <span class="hljs-string">'published_at'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;dateTimeBetween(now()-&gt;subYear(), now()-&gt;addWeek()),
            <span class="hljs-string">'user_id'</span> =&gt; User::factory(),
        ];
    }
}
</code></pre>
<p>Look at that, we are randomly picking a status in the factory as well as a date, that could be in the past or the future. So, depending on the randomly picked selections, the test can fail. For this example, the best thing to do would probably be to modify the factory to take out the randomness, and then various adjustments into states, like the following.</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PostFactory</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Factory</span>
</span>{
    <span class="hljs-keyword">protected</span> $model = Post::class;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">definition</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;faker-&gt;word(),
            <span class="hljs-string">'status'</span> =&gt; PostStatus::Idea,
            <span class="hljs-string">'user_id'</span> =&gt; User::factory(),
        ];
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">published</span>(<span class="hljs-params"></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;state(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
            <span class="hljs-keyword">return</span> [
                <span class="hljs-string">'status'</span> =&gt; PostStatus::Published,
                <span class="hljs-string">'published_at'</span> =&gt; now()-&gt;subWeek(),
            ];
        });
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">scheduled</span>(<span class="hljs-params"></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;state(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
            <span class="hljs-keyword">return</span> [
                <span class="hljs-string">'status'</span> =&gt; PostStatus::Published,
                <span class="hljs-string">'published_at'</span> =&gt; now()-&gt;addWeek(),
            ];
        });
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">idea</span>(<span class="hljs-params"></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;state(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
            <span class="hljs-keyword">return</span> [
                <span class="hljs-string">'content'</span> =&gt; <span class="hljs-literal">null</span>,
                <span class="hljs-string">'status'</span> =&gt; PostStatus::Idea,
                <span class="hljs-string">'published_at'</span> =&gt; <span class="hljs-literal">null</span>,
            ];
        });
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">draft</span>(<span class="hljs-params"></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;state(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
            <span class="hljs-keyword">return</span> [
                <span class="hljs-string">'status'</span> =&gt; PostStatus::Draft,
                <span class="hljs-string">'published_at'</span> =&gt; <span class="hljs-literal">null</span>,
            ];
        });
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hidden</span>(<span class="hljs-params"></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;state(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
            <span class="hljs-keyword">return</span> [
                <span class="hljs-string">'status'</span> =&gt; PostStatus::Hidden,
                <span class="hljs-string">'published_at'</span> =&gt; now()-&gt;addWeek(),
            ];
        });
    }
}
</code></pre>
<p>Notice that I changed the base definition to be much simpler with no randomness and then added the various states I would expect. For large projects where a factory may already be used in many places, you may need to be careful changing the base definition because it could break other tests.</p>
<p>With the factory updates in place, I updated the tests to the following:</p>
<pre><code class="lang-php">uses(RefreshDatabase::class);

it(<span class="hljs-string">'is published'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $post = Post::factory()-&gt;published()-&gt;create();

    expect($post-&gt;is_published)-&gt;toBeTrue();
})-&gt;repeat(<span class="hljs-number">100</span>);

it(<span class="hljs-string">'is not published'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">$post</span>) </span>{
    expect($post-&gt;is_published)-&gt;toBeFalse();
})-&gt;with([
    <span class="hljs-string">'idea post'</span> =&gt; <span class="hljs-function"><span class="hljs-keyword">fn</span> (<span class="hljs-params"></span>) =&gt; <span class="hljs-title">Post</span>::<span class="hljs-title">factory</span>(<span class="hljs-params"></span>)-&gt;<span class="hljs-title">idea</span>(<span class="hljs-params"></span>)-&gt;<span class="hljs-title">create</span>(<span class="hljs-params"></span>),
    '<span class="hljs-title">draft</span> <span class="hljs-title">post</span>' =&gt; <span class="hljs-title">fn</span> (<span class="hljs-params"></span>) =&gt; <span class="hljs-title">Post</span>::<span class="hljs-title">factory</span>(<span class="hljs-params"></span>)-&gt;<span class="hljs-title">draft</span>(<span class="hljs-params"></span>)-&gt;<span class="hljs-title">create</span>(<span class="hljs-params"></span>),
    '<span class="hljs-title">scheduled</span> <span class="hljs-title">post</span>' =&gt; <span class="hljs-title">fn</span> (<span class="hljs-params"></span>) =&gt; <span class="hljs-title">Post</span>::<span class="hljs-title">factory</span>(<span class="hljs-params"></span>)-&gt;<span class="hljs-title">scheduled</span>(<span class="hljs-params"></span>)-&gt;<span class="hljs-title">create</span>(<span class="hljs-params"></span>),
    '<span class="hljs-title">hidden</span> <span class="hljs-title">post</span>' =&gt; <span class="hljs-title">fn</span> (<span class="hljs-params"></span>) =&gt; <span class="hljs-title">Post</span>::<span class="hljs-title">factory</span>(<span class="hljs-params"></span>)-&gt;<span class="hljs-title">hidden</span>(<span class="hljs-params"></span>)-&gt;<span class="hljs-title">create</span>(<span class="hljs-params"></span>),
])-&gt;<span class="hljs-title">repeat</span>(<span class="hljs-params"><span class="hljs-number">100</span></span>)</span>;
</code></pre>
<p>Now when I run them, I get the output below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708892814909/36f2e70c-e8d2-4eb3-9d53-309833f7030d.png" alt="Repeated Tests Passing" class="image--center mx-auto" /></p>
<p>Since the second test is now using a dataset, it repeats 100 times for each item in the dataset, which is why there are 500 assertions versus the original 200.</p>
<p>Now that I am confident with the tests, I can remove the repeat method.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708892834859/2ff8823b-4ba0-4866-a05b-180c188a1de5.png" alt="Fixed Flaky Tests" class="image--center mx-auto" /></p>
<p>Flaky tests can be extremely frustrating, so having some more tools in place to fix them is always useful. An issue with a factory is what caused my initial look into using <code>repeat</code>, but there can be a variety of other causes, too. One of the biggest culprits I’ve seen is data not being cleaned up properly between tests, so something created in one test is affecting the next test. Another is timestamps. I’ve seen a lot of tests start failing at the start/end of a month, issues with daylight savings, or even where a test set a date that was not far enough into the future and now that date has happened, the test starts failing. You can use something like <code>Carbon::setTestNow(today());</code> or <a target="_blank" href="https://laravel.com/docs/10.x/mocking#interacting-with-time">other time methods in Laravel</a> to try to avoid those issues.</p>
<p>Before Pest 2.0 or PHPUnit 10, you could use the <code>--repeat=100</code> argument in the CLI to get similar results. However, this option was removed in PHPUnit 10, so Pest is needed. This Github issue has some workarounds if you can’t use Pest: <a target="_blank" href="https://github.com/sebastianbergmann/phpunit/issues/5174">Repeating tests · Issue #5174 · sebastianbergmann/phpunit</a>.</p>
<p>Now, go and fix those flaky tests! Let me know if you have any other strategies for flaky tests. Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[What I’m Reading - January 2024]]></title><description><![CDATA[We finally made it through January! It always seems like such a long month and it was a busy one for me. I’ve been working on a large project at work, freelancing, side projects, and this blog.
This is the first of my “What I’m Reading” posts that I ...]]></description><link>https://seankegel.com/what-im-reading-january-2024</link><guid isPermaLink="true">https://seankegel.com/what-im-reading-january-2024</guid><category><![CDATA[Laravel]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[React]]></category><category><![CDATA[rss]]></category><category><![CDATA[devtools]]></category><category><![CDATA[Event Sourcing]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sat, 03 Feb 2024 20:23:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706991684868/64c47d26-301e-4f2b-9957-09b791463925.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We finally made it through January! It always seems like such a long month and it was a busy one for me. I’ve been working on a large project at work, freelancing, side projects, and this blog.</p>
<p>This is the first of my “What I’m Reading” posts that I hope to do monthly. This was inspired by some articles I read by <a target="_blank" href="https://chriscoyier.net/">Chris Coyier</a> and <a target="_blank" href="https://cassidoo.co/">Cassidy Williams</a> about RSS feeds, personal blogs, and reading what you think is interesting or what other humans think is interesting versus what the social media algorithms want you to read. So with that, let’s get started!</p>
<h2 id="heading-rss-and-blogging">RSS and Blogging</h2>
<p>I’ve been an avid RSS user since the time of Google Reader. After that was shut down, I moved to Feedly, and then on and on and on. I now use <a target="_blank" href="https://readwise.io/read">Readwise Reader</a> which I have been enjoying quite a bit for both RSS feeds, read later, and combining it with Readwise itself to track highlights. RSS feeds are still my preferred way of discovering new articles directly from the sources I follow. This is why I found the following articles interesting and hope they help lead to an RSS resurgence.</p>
<ul>
<li><p><a target="_blank" href="https://chriscoyier.net/2024/01/22/where-have-all-the-websites-gone/">Where have all the websites gone?</a></p>
</li>
<li><p><a target="_blank" href="https://blog.cassidoo.co/post/human-curation/">I miss human curation</a></p>
</li>
</ul>
<p>Please comment if you have any interesting RSS feeds you’d like to share.</p>
<h2 id="heading-modular-laravel">Modular Laravel</h2>
<p>This one was more of a watch than a read, but I found it extremely informative. It was a course on <a target="_blank" href="https://laracasts.com/referral/skegel13">Laracasts</a> about Modular Laravel by <a target="_blank" href="https://mateusguimaraes.com/">Mateus Guimarães</a>.</p>
<ul>
<li><a target="_blank" href="https://laracasts.com/series/modular-laravel">Modular Laravel</a></li>
</ul>
<p>Mateus does an excellent job talking about modularizing a Laravel application. After finishing the course, I want to build a simple Laravel package to create modules, but I know there are already a few of those and do I have the time to take something like that on?</p>
<h2 id="heading-productivity-and-maintainability">Productivity and Maintainability</h2>
<p>Speaking of not having time, I read an interesting article from <a target="_blank" href="https://daverupert.com/">Dave Rupert</a> where he discusses having only one big project and one little project at a time and pushing everything else off to the side. It is an interesting idea for someone like me who has a lot of unfinished side projects lying around. I’ll be interested to see a follow-up and whether or not it worked.</p>
<ul>
<li><a target="_blank" href="https://daverupert.com/2024/01/one-big-one-little/">One big, one little</a></li>
</ul>
<p>Another Dave Rupert article I read was about how quickly a project can go south and end up with a lot of tech debt when trying to move too fast. He says:</p>
<blockquote>
<p>a key factor of sustainability is making sure maintainability stays on par with growth</p>
</blockquote>
<p>A fun read and it discusses the current trend of AI for everything.</p>
<ul>
<li><a target="_blank" href="https://daverupert.com/2024/01/time-to-unmaintainable/">The time to unmaintainable is very low</a></li>
</ul>
<p>If you’d like to hear more from Dave Rupert, he co-hosts the <a target="_blank" href="https://shoptalkshow.com/">Shop Talk Show</a> with Chris Coyier. I’ve been listening on and off for a few years now and always enjoy it.</p>
<h2 id="heading-react-complexity">React Complexity</h2>
<p>Here’s another Cassidy Williams post. This time about React and how confusing it has become.</p>
<ul>
<li><a target="_blank" href="https://blog.cassidoo.co/post/annoyed-at-react/">Kind of annoyed at React</a></li>
</ul>
<p>In the last year or so, I’ve been primarily focused on the backend in Laravel, but before that, I was doing quite a bit of React, and I enjoyed it but I always felt like it made it way too easy to get into a bad state with poor performance, too many re-renders, etc.</p>
<p>Recently, I’ve been working on a project using Vue 2 and it has been so refreshing. I’m not sure if I am enjoying it because I really like Vue or because I’ve been so focused on backend work. I have yet to work on a Vue 3 project. If you have worked with Vue 3, how do you compare it to React? Are you using the Options API or Composition API?</p>
<h2 id="heading-dev-tooling">Dev Tooling</h2>
<p>It’s hard to resist trying out shiny new tools as a developer and two I came across recently are the Helix editor and Aspen HTTP client.</p>
<ul>
<li><p><a target="_blank" href="https://helix-editor.com/">Helix</a></p>
</li>
<li><p><a target="_blank" href="https://blog.treblle.com/meet-aspen-api-testing-tool/">Meet Aspen: Speedier &amp; Smarter API Testing, Powered by AI</a></p>
</li>
</ul>
<p>Helix is a cool little terminal editor similar to Vim/Neovim but with a lot of helpful features built-in like LSP support. It’s nearly ready to go out of the box, you just need to install some LSP’s using npm. The keybindings are different from Vim, so that takes some time to get used to but they seem mostly straightforward. I am a PhpStorm user primarily but I like to work with other editors from time to time and getting Helix working was so much easier than my Neovim configuration using <a target="_blank" href="https://www.lazyvim.org/">LazyVim</a>, though some things are missing and Neovim is still a lot more flexible. As much as I want to like Helix, since I use Vim keybindings in all my editors, I likely won’t make the switch until I see Helix extensions in JetBrains, VSCode, etc.</p>
<p>With the recent changes from Insomnia, a lot of people have been looking at other HTTP clients. Insomnia is still my primary but I’ve been trying out Hoppscotch and using PhpStorm which you can read about in my post: <a target="_blank" href="https://seankegel.com/simplify-api-testing-with-phpstorm-http-requests">Simplify API Testing with PhpStorm HTTP Requests</a>. Aspen is fast and has a nice interface, but I can’t find anywhere to save variables and requests outside of the history it stores. It’s a good first start, but it will need some more before it can become my go-to HTTP client. However, I did recently see it can take the response to your request and turn it into a DTO for a variety of languages. This will save me a lot of time.</p>
<h2 id="heading-event-sourcing">Event Sourcing</h2>
<p>I have recently started diving into event sourcing. I haven’t actually done anything with it yet, but I am very intrigued. I started to look into it with the announcement of <a target="_blank" href="https://verbs.thunk.dev/docs/getting-started/quickstart">Verbs</a> for Laravel by <a target="_blank" href="https://cmorrell.com/">Chris Morrell</a> and <a target="_blank" href="https://coulb.com/">Daniel Coulbourne</a>. Verbs isn’t quite ready for production usage yet and though it looks great, I decided to dive into a few other packages to start to learn more about it.</p>
<ul>
<li><p><a target="_blank" href="https://verbs.thunk.dev/docs/getting-started/quickstart">Quickstart - Verbs</a></p>
</li>
<li><p><a target="_blank" href="https://spatie.be/docs/laravel-event-sourcing/v7/introduction">Introduction | laravel-event-sourcing</a></p>
</li>
<li><p><a target="_blank" href="https://eventsauce.io/">EventSauce - Event sourcing for PHP</a></p>
</li>
</ul>
<p>I can’t wait to try out event sourcing, even at a small scale. Have you used any of these packages or event sourcing in general? What do you think about it?</p>
<p>That’s a good chunk of my reading from January, I hope you found this informative and entertaining. Comment with any interesting links or RSS feeds you’ve come across lately. Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Handling Errors with Third-Party APIs]]></title><description><![CDATA[This is the fourth part of my series on Integrating Third-Party APIs in Laravel. If you haven’t been following along, I highly recommend checking out the previous posts to gain the necessary context for a better understanding of this post.
In the pre...]]></description><link>https://seankegel.com/handling-errors-with-third-party-apis</link><guid isPermaLink="true">https://seankegel.com/handling-errors-with-third-party-apis</guid><category><![CDATA[PHP]]></category><category><![CDATA[APIs]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Tue, 30 Jan 2024 00:13:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706573456619/ed245a00-b249-40b3-ab6c-6bea5615a5ca.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the fourth part of my series on <a target="_blank" href="https://seankegel.com/series/laravel-api-integration">Integrating Third-Party APIs in Laravel</a>. If you haven’t been following along, I highly recommend checking out the previous posts to gain the necessary context for a better understanding of this post.</p>
<p>In the previous posts of the series, we learned how to build the following:</p>
<ul>
<li><p>Simple API client class using Laravel’s built-in <code>Http</code> facade</p>
</li>
<li><p>Custom request class to generate API requests with a URL, method type, data, query strings, etc.</p>
</li>
<li><p>API resources to use CRUD-like methods to fetch resources from the APIs</p>
</li>
<li><p>Custom DTOs with simple classes</p>
</li>
<li><p>DTOs using the Spatie <a target="_blank" href="https://spatie.be/docs/laravel-data/v3/introduction">Laravel-data</a> package</p>
</li>
</ul>
<p>Throughout all of these posts, we also learned how to test these various integrations using the <code>Http</code> facade and fake responses. However, we only tested best-case scenarios and didn’t go into error handling. So that’s what I hope to accomplish in this post.</p>
<h2 id="heading-creating-a-custom-exception">Creating a Custom Exception</h2>
<p>To get started, I recommend creating a custom exception. For now, we can call it <code>ApiException</code>. Either create this manually or use the Artisan command:</p>
<pre><code class="lang-bash">php artisan make:exception ApiException
</code></pre>
<p>Our new exception class can extend the <code>Exception</code> class and take three parameters.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Exceptions</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Exception</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Response</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Throwable</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiException</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Exception</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> readonly ?ApiRequest $request = <span class="hljs-literal">null</span>,
        <span class="hljs-keyword">public</span> readonly ?Response $response = <span class="hljs-literal">null</span>,
        <span class="hljs-built_in">Throwable</span> $previous = <span class="hljs-literal">null</span>,
    </span>) </span>{
        <span class="hljs-comment">// Typically, we will just pass in the message from the previous exception, but provide a default if for some reason we threw this exception without a previous one.</span>
        $message = $previous?-&gt;getMessage() ?: <span class="hljs-string">'An error occurred making an API request'</span>;

        <span class="hljs-built_in">parent</span>::__construct(
            message: $message,
            code: $previous?-&gt;getCode(),
            previous: $previous,
        );
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">context</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'uri'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;request?-&gt;getUri(),
            <span class="hljs-string">'method'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;request?-&gt;getMethod(),
        ];
    }
}
</code></pre>
<p>The constructor takes a <code>$request</code> property, <code>$response</code> property, and a <code>$previous</code> property.</p>
<p>The <code>$request</code> property is an instance of the <code>ApiRequest</code> class we created in a previous post to store information like URL, method, body data, query strings, etc.</p>
<p>The <code>$response</code> is an instance of Laravel’s default <code>Illuminate\Http\Client\Response</code> class which is returned when using the <code>Http</code> facade to make a request. By adding the response to the exception, we can gather a lot more information if needed when handing the exception, like an error object from the third-party API.</p>
<p>Finally, using the previous exception, if it exists, we throw the <code>ApiException</code> using data from the previous exception or simple defaults.</p>
<p>I also added a context class to provide a little more information which is pulled from the <code>$request</code> property. Depending on your application, data and query parameters could include sensitive information, so be sure you understand what is being added. For some applications, the URL itself could be sensitive, so adjust as needed or make a context parameter and pass in whatever data works for you.</p>
<h2 id="heading-throwing-the-apiexception">Throwing the ApiException</h2>
<p>Now that we have our new exception class, let’s look at actually throwing it when there is an error. We can update the <code>ApiClient</code> class from the previous posts to now catch exceptions, use the <code>ApiException</code> as a wrapper, and include information about the request.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Exceptions</span>\<span class="hljs-title">ApiException</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Exception</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">PendingRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Response</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiClient</span>
</span>{
    <span class="hljs-comment">/**
     * Send an ApiRequest to the API and return the response.
     * <span class="hljs-doctag">@throws</span> ApiException
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params">ApiRequest $request</span>): <span class="hljs-title">Response</span>
    </span>{
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;getBaseRequest()
                -&gt;withHeaders($request-&gt;getHeaders())
                -&gt;{$request-&gt;getMethod()-&gt;value}(
                    $request-&gt;getUri(),
                    $request-&gt;getMethod() === HttpMethod::GET
                        ? $request-&gt;getQuery()
                        : $request-&gt;getBody()
                );
        } <span class="hljs-keyword">catch</span> (<span class="hljs-built_in">Exception</span> $exception) {
            <span class="hljs-comment">// Create our new exception and throw it.</span>
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ApiException(
                request: $request,
                response: $exception?-&gt;response,
                previous: $exception,
            );
        }
    }

    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getBaseRequest</span>(<span class="hljs-params"></span>): <span class="hljs-title">PendingRequest</span>
    </span>{
        $request = Http::acceptJson()
            -&gt;contentType(<span class="hljs-string">'application/json'</span>)
            -&gt;throw()
            -&gt;baseUrl(<span class="hljs-keyword">$this</span>-&gt;baseUrl());

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;authorize($request);
    }

    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">authorize</span>(<span class="hljs-params">PendingRequest $request</span>): <span class="hljs-title">PendingRequest</span>
    </span>{
        <span class="hljs-keyword">return</span> $request;
    }

    <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">baseUrl</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span></span>;
}
</code></pre>
<p>To throw the exception, all we need to do is wrap the <code>return</code> in our <code>send</code> method with a <code>try…catch</code> and then throw our new exception. When setting the <code>$response</code> in our exception, we attempt to pull it from the caught exception’s <code>response</code> property. If our request was made but failed during the process, the <code>Http</code> facade will throw an <code>Illuminate\Http\Client\RequestException</code> which has a <code>response</code> property that is an instance of <code>Illuminate\Http\Client\Response</code>. If a different exception is caught, we will just set the response to <code>null</code>.</p>
<h2 id="heading-testing">Testing</h2>
<h3 id="heading-testing-the-client">Testing the Client</h3>
<p>To test our new exception, we’ll create an <code>ApiClientTest.php</code> file and add the following test.</p>
<pre><code class="lang-php">it(<span class="hljs-string">'throws an api exception'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Arrange</span>
    Http::fakeSequence()-&gt;pushStatus(<span class="hljs-number">500</span>);
    $request = ApiRequest::get(<span class="hljs-string">'foo'</span>);

    <span class="hljs-comment">// Act</span>
    <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);
})-&gt;throws(ApiException::class, exceptionCode: <span class="hljs-number">500</span>);
</code></pre>
<p>This test uses the <code>Http::fakeSequence()</code> call and pushes a response with a 500 status code. Then, we expect the client to throw an <code>ApiException</code> with a 500 exception code.</p>
<p>You might notice that this test fails. This occurs because we used <code>Http::fake()</code> in the <code>beforeEach</code> method of the test.</p>
<pre><code class="lang-php">beforeEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    Http::fake();

    <span class="hljs-keyword">$this</span>-&gt;client = <span class="hljs-keyword">new</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ApiClient</span> </span>{
        <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">baseUrl</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span>
        </span>{
            <span class="hljs-keyword">return</span> <span class="hljs-string">'https://example.com'</span>;
        }
    };
});
</code></pre>
<p>When calling <code>Http::fake()</code> essentially, we tell it to fake any request made with the facade. It does this by pushing an entry into an internal collection. Even when we add additional items to <code>Http::fake()</code> or our <code>Http::fakeSequence()</code>, the fake response will still pull from the first item in the collection since we didn’t specify a specific URL. It works kind of like the router where it finds the first viable route that can be used to fake the response.</p>
<p>To solve this, we can either move <code>Http::fake()</code> into the various tests themselves. However, I like another approach, which is adding a macro to the <code>Http</code> facade to be able to reset the internal collection, which is named <code>stubCallbacks</code>. To do that, open your <code>AppServiceProvider</code> and add the macro in the <code>boot</code> method.</p>
<pre><code class="lang-php"><span class="hljs-comment">// AppServiceProvider</span>

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">boot</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    Http::macro(<span class="hljs-string">'resetStubs'</span>, <span class="hljs-function"><span class="hljs-keyword">fn</span> (<span class="hljs-params"></span>) =&gt; $<span class="hljs-title">this</span>-&gt;<span class="hljs-title">stubCallbacks</span> = <span class="hljs-title">collect</span>(<span class="hljs-params"></span>))</span>;
}
</code></pre>
<p>Now, instead of having to add <code>Http::fake()</code> to all of our previous tests, we can update our new test to call <code>Http::resetStubs</code>.</p>
<pre><code class="lang-php">it(<span class="hljs-string">'throws an api exception'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Arrange</span>
    Http::resetStubs();
    Http::fakeSequence()-&gt;pushStatus(<span class="hljs-number">500</span>);
    $request = ApiRequest::get(<span class="hljs-string">'foo'</span>);

    <span class="hljs-comment">// Act</span>
    <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);
})-&gt;throws(ApiException::class, exceptionCode: <span class="hljs-number">500</span>);
</code></pre>
<h3 id="heading-testing-the-exception">Testing the Exception</h3>
<p>Now that we tested that our client throws the API exception, let’s add some tests for the exception itself.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Exceptions</span>\<span class="hljs-title">ApiException</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">RequestException</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Response</span>;

it(<span class="hljs-string">'sets default message and code'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Act</span>
    $apiException = <span class="hljs-keyword">new</span> ApiException();

    <span class="hljs-comment">// Assert</span>
    expect($apiException)
        -&gt;getMessage()-&gt;toBe(<span class="hljs-string">'An error occurred making an API request.'</span>)
        -&gt;getCode()-&gt;toBe(<span class="hljs-number">0</span>);
});

it(<span class="hljs-string">'sets context based on request'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Arrange</span>
    $request = ApiRequest::get(fake()-&gt;url);

    <span class="hljs-comment">// Act</span>
    $apiException = <span class="hljs-keyword">new</span> ApiException($request);

    <span class="hljs-comment">// Assert</span>
    expect($apiException)-&gt;context()-&gt;toBe([
        <span class="hljs-string">'uri'</span> =&gt; $request-&gt;getUri(),
        <span class="hljs-string">'method'</span> =&gt; $request-&gt;getMethod(),
    ]);
});

it(<span class="hljs-string">'gets response from RequestException'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Arrange</span>
    $requestException = <span class="hljs-keyword">new</span> RequestException(
        <span class="hljs-keyword">new</span> Response(
            <span class="hljs-keyword">new</span> GuzzleHttp\Psr7\Response(
                <span class="hljs-number">422</span>,
                [],
                json_encode([<span class="hljs-string">'message'</span> =&gt; <span class="hljs-string">'Something went wrong.'</span>]),
            ),
        )
    );

    <span class="hljs-comment">// Act</span>
    $apiException = <span class="hljs-keyword">new</span> ApiException(response: $requestException-&gt;response, previous: $requestException);

    <span class="hljs-comment">// Assert</span>
    expect($apiException-&gt;getCode())-&gt;toBe(<span class="hljs-number">422</span>)
        -&gt;and($apiException-&gt;response)-&gt;toBeInstanceOf(Response::class)
        -&gt;and($apiException-&gt;response-&gt;json(<span class="hljs-string">'message'</span>))-&gt;toBe(<span class="hljs-string">'Something went wrong.'</span>);
});
</code></pre>
<h2 id="heading-using-the-response-in-the-exception">Using the Response in the Exception</h2>
<p>Having the response from the request be part of the <code>ApiException</code> is extremely helpful for a variety of purposes. For example, our application could have a UI to allow a user to add a product to the store. When submitting the request, we would likely validate as much as we could in our application, but maybe the third-party API has some additional validation that we can’t handle locally. We would likely want to return that information to our UI so the user knows what needs to be fixed.</p>
<p>If we make a call to create a product with our <code>ProductResource</code> that we created in a previous post, and we receive an <code>ApiClientException</code>, in our controller, we could catch that exception and return any errors received to the user in the frontend.</p>
<p>For simplicity, I created a very simple controller example. In a production application, you would likely have more validation for the request data using a <code>FormRequest</code> class or <code>$request-&gt;validate()</code>. For this example, we are assuming the third-party API returns validation error messages using a 422 and a response similar to how Laravel returns errors.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Controllers</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">ApiResources</span>\<span class="hljs-title">ProductResource</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Data</span>\<span class="hljs-title">SaveProductData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Exceptions</span>\<span class="hljs-title">ApiException</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Request</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"><span class="hljs-keyword">public</span> readonly ProductResource $productResource</span>)
    </span>{

    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">create</span>(<span class="hljs-params">Request $request</span>)
    </span>{
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;productResource-&gt;create(SaveProductData::from($request-&gt;all());
        } <span class="hljs-keyword">catch</span> (ApiException $exception) {
            <span class="hljs-keyword">if</span> ($exception-&gt;getCode() === <span class="hljs-number">422</span>) {
                <span class="hljs-comment">// 422 is typically the HTTP status code used for validation errors.</span>
                <span class="hljs-comment">// Let's assume that the API returns an 'errors' property similar to Laravel.</span>
                $errors = $exception-&gt;response?-&gt;json(<span class="hljs-string">'errors'</span>);
            }

            <span class="hljs-keyword">return</span> response()-&gt;json([
                <span class="hljs-string">'message'</span> =&gt; $exception-&gt;getMessage(),
                <span class="hljs-string">'errors'</span> =&gt; $errors ?? <span class="hljs-literal">null</span>,
            ], $exception-&gt;getCode());
        }
    }
}
</code></pre>
<h2 id="heading-additional-techniques">Additional Techniques</h2>
<p>In your application, let’s say you have integrations with multiple third-party APIs. This means you likely have multiple client classes extending the base <code>ApiClient</code> class. Instead of having a single <code>ApiException</code>, it could be nice to have specific exceptions for each client. To do that, we can introduce a new <code>$exceptionClass</code> property to the <code>ApiClient</code> class.</p>
<pre><code class="lang-php"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiClient</span>
</span>{
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">string</span> $exceptionClass = ApiException::class;

    ...
}
</code></pre>
<p>Now, when throwing the exception, we can throw an instance of whatever is set by the <code>$exceptionClass</code>.</p>
<pre><code class="lang-php"><span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">$this</span>-&gt;exceptionClass(
    request: $request,
    response: $exception?-&gt;response,
    previous: $exception,
);
</code></pre>
<p>If we go back to the <code>StoreApiClient</code> we created in a previous post, we can create a new exception for it and set it on the client. The exception can just simply extend the <code>ApiException</code> class.</p>
<pre><code class="lang-php"><span class="hljs-comment">// StoreApiException</span>
<span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Exceptions</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StoreApiException</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ApiException</span>
</span>{
}
</code></pre>
<p>Then, we can update the client.</p>
<pre><code class="lang-php"><span class="hljs-comment">// StoreApiClient</span>
<span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Exceptions</span>\<span class="hljs-title">StoreApiException</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StoreApiClient</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ApiClient</span>
</span>{
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">string</span> $exceptionClass = StoreApiException::class;

    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">baseUrl</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span>
    </span>{
        <span class="hljs-keyword">return</span> config(<span class="hljs-string">'services.store_api.url'</span>);
    }
}
</code></pre>
<p>Let’s add a test to make sure the <code>StoreApiClient</code> is throwing our new <code>StoreApiException</code>.</p>
<pre><code class="lang-php">it(<span class="hljs-string">'throws a StoreApiException'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Arrange</span>
    Http::resetStubs();
    Http::fakeSequence()-&gt;pushStatus(<span class="hljs-number">404</span>);
    $request = ApiRequest::get(<span class="hljs-string">'products'</span>);

    <span class="hljs-comment">// Act</span>
    app(StoreApiClient::class)-&gt;send($request);
})-&gt;throws(StoreApiException::class, exceptionCode: <span class="hljs-number">404</span>);
</code></pre>
<p>What happens if someone decides to use an exception that doesn’t extend our <code>ApiException</code> class? When our client tries to throw, it will fail if the <code>$exceptionClass</code> is not expecting the same parameters. To handle that, let’s create an interface and use that to check the <code>$exceptionClass</code>.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Exceptions</span>\<span class="hljs-title">Contracts</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Response</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Throwable</span>;

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ApiExceptionInterface</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        ?ApiRequest $request = <span class="hljs-literal">null</span>,
        ?Response $response = <span class="hljs-literal">null</span>,
        <span class="hljs-built_in">Throwable</span> $previous = <span class="hljs-literal">null</span>,
    </span>)</span>;
}
</code></pre>
<p>Now, update the <code>ApiException</code> class to implement the interface.</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiException</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Exception</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">ApiExceptionInterface</span>
</span>{
    ...
}
</code></pre>
<p>Finally, let’s update the <code>ApiClient</code> to throw the <code>$exceptionClass</code> only if it implements the <code>ApiExceptionInterface</code>. Otherwise, let’s just throw the exception that was caught since we may not know how to instantiate a different type of exception.</p>
<pre><code class="lang-php"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiClient</span>
</span>{
    ...

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params">ApiRequest $request</span>): <span class="hljs-title">Response</span>
    </span>{
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;getBaseRequest()
                -&gt;withHeaders($request-&gt;getHeaders())
                -&gt;{$request-&gt;getMethod()-&gt;value}(
                    $request-&gt;getUri(),
                    $request-&gt;getMethod() === HttpMethod::GET
                        ? $request-&gt;getQuery()
                        : $request-&gt;getBody()
                );
        } <span class="hljs-keyword">catch</span> (<span class="hljs-built_in">Exception</span> $exception) {
            <span class="hljs-keyword">if</span> (! is_subclass_of(<span class="hljs-keyword">$this</span>-&gt;exceptionClass, ApiExceptionInterface::class)) {
                <span class="hljs-comment">// If the exceptionClass does not implement the ApiExceptionInterface,</span>
                <span class="hljs-comment">// let's just throw the caught exception since we don't know how to instantiate</span>
                <span class="hljs-comment">// the exceptionClass.</span>
                <span class="hljs-keyword">throw</span> $exception;
            }

            <span class="hljs-comment">// Create our new exception and throw it.</span>
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">$this</span>-&gt;exceptionClass(
                request: $request,
                response: $exception?-&gt;response,
                previous: $exception,
            );
        }
    }

    ...
}
</code></pre>
<p>We use the <code>is_subclass_of</code> method which checks if the <code>$exceptionClass</code> is a child of or implements the provided class. Since our <code>$exceptionClass</code> for the <code>StoreApiClient</code> extends the <code>ApiException</code> class and does not overwrite the constructor, it implements the <code>ApiExceptionInterface</code>.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this post, we learned how to create a custom exception to make it easier to track and debug issues with third-party APIs. We created a custom <code>ApiException</code> that was integrated with our <code>ApiClient</code>. The exception included information about the request and response to make it easier to track down the cause of the issue.</p>
<p>As always, let me know if you have any questions or comments, and thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Creating API Resources in Laravel]]></title><description><![CDATA[Welcome back to my series for Integrating Third-Party APIs in Laravel. In this post, I will discuss creating API Resources. An API resource in this case is like a RESTful controller. It adds CRUD-like methods for communicating with the API when deali...]]></description><link>https://seankegel.com/creating-api-resources-in-laravel</link><guid isPermaLink="true">https://seankegel.com/creating-api-resources-in-laravel</guid><category><![CDATA[Laravel]]></category><category><![CDATA[APIs]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Tue, 16 Jan 2024 01:58:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1705370122667/6a9811d1-cfcb-4d2a-b40e-c550d0b296f8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome back to my series for Integrating Third-Party APIs in Laravel. In this post, I will discuss creating API Resources. An API resource in this case is like a RESTful controller. It adds CRUD-like methods for communicating with the API when dealing with a specific resource, like books, products, users, etc. If you haven’t read the previous posts, I suggest reading them first.</p>
<ul>
<li><p><a target="_blank" href="https://seankegel.com/simplifying-api-integration-with-laravels-http-facade?source=more_series_bottom_blogs">Simplifying API Integration with Laravel's Http Facade</a></p>
</li>
<li><p><a target="_blank" href="https://seankegel.com/streamlining-api-responses-in-laravel-with-dtos?source=more_series_bottom_blogs">Streamlining API Responses in Laravel with DTOs</a></p>
</li>
</ul>
<p>In the first two parts of the series, I used the Google Books API for my examples. For simplification and to have more routes readily available without needing to setup OAuth 2.0, I will be using <a target="_blank" href="https://fakestoreapi.com/docs">Fake Store API</a> and querying products. I will also be using the <a target="_blank" href="https://spatie.be/docs/laravel-data/v3/introduction">Spatie Laravel-data</a> package for my data-transfer objects (DTOs) instead of creating custom DTOs from simple PHP classes like I did in the previous posts. This helps to remove some of the boilerplate of defining <code>fromArray</code> and <code>toArray</code> methods.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>In the previous posts in the series, we had an <code>ApiRequest</code> class and <code>ApiClient</code> class. To create the API client for the Fake Store API; we can extend the <code>ApiClient</code> class.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StoreApiClient</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ApiClient</span>
</span>{
    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">baseUrl</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span>
    </span>{
        <span class="hljs-keyword">return</span> config(<span class="hljs-string">'services.store_api.url'</span>);
    }
}
</code></pre>
<p>Since the Fake Store API does not have any authentication required, this class can be pretty simple. For the test, we can just make sure the base URL is getting set as expected.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">StoreApiClient</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Request</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;

beforeEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    Http::fake();
    config([
        <span class="hljs-string">'services.store_api.url'</span> =&gt; <span class="hljs-string">'https://example.com'</span>,
    ]);
});

it(<span class="hljs-string">'sets the base url'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = ApiRequest::get(<span class="hljs-string">'products'</span>);

    app(StoreApiClient::class)-&gt;send($request);

    Http::assertSent(<span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)-&gt;url()-&gt;toStartWith(<span class="hljs-string">'https://example.com/products'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});
</code></pre>
<p>For this test, I manually set a test URL in my configuration. It is also possible to just use an <code>.env.test</code> file to define the value if you prefer. For simple tests, I like the approach of defining the config in the test so I can easily see what was expected in the tests versus having to compare it to another file.</p>
<p>Now, let’s create our API resource. What I am calling an API resource is a simple class to treat the product resource similar to a REST controller in Laravel. The API resource will allow me to list products, show a product, create a product, update a product, and delete a product. In the previous post, we had an action for fetching books from the Google Books API, but by using a resource, I combined the various calls for a resource into a single class.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">ApiResources</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Data</span>\<span class="hljs-title">ProductData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">StoreApiClient</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Spatie</span>\<span class="hljs-title">LaravelData</span>\<span class="hljs-title">DataCollection</span>;

<span class="hljs-comment">/**
 * ApiResource for products.
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductResource</span>
</span>{
    <span class="hljs-comment">/**
     * Use dependency injection to get the StoreApiClient.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> readonly StoreApiClient $client</span>)
    </span>{
    }

    <span class="hljs-comment">/**
     * List all products.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">list</span>(<span class="hljs-params"></span>)
    </span>{
        ...
    }

    <span class="hljs-comment">/**
     * Show a single product.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">show</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> $id</span>)
    </span>{
        ...
    }

    <span class="hljs-comment">/**
     * Create a new product.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">create</span>(<span class="hljs-params">$data</span>)
    </span>{
        ...
    }

    <span class="hljs-comment">/**
     * Update a product.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">update</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> $id, $data</span>)
    </span>{
        ...
    }

    <span class="hljs-comment">/**
     * Delete a product.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">delete</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> $id</span>)
    </span>{
        ...
    }
}
</code></pre>
<h2 id="heading-filling-out-the-api-resource">Filling out the API Resource</h2>
<h3 id="heading-list-method">List Method</h3>
<p>We’ll start with the list method. The first thing I like to do is model the data that we will be receiving from the API. I will use Laravel-data for this and create a <code>ProductData</code> class. If you haven’t installed Laravel-data, install it with composer:</p>
<pre><code class="lang-bash">composer require spatie/laravel-data
</code></pre>
<p>The <code>ProductData</code> class can be created manually by extending the <code>Spatie\LaravelData\Data</code> class or by using Artisan:</p>
<pre><code class="lang-bash">php artisan make:data ProductData
</code></pre>
<p>Looking at the documentation for the Fake Store API, we can map that to a DTO like the following:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Data</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Spatie</span>\<span class="hljs-title">LaravelData</span>\<span class="hljs-title">Data</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $title,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">float</span> $price,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $description,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $category,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $image,
        <span class="hljs-keyword">public</span> ?RatingData $rating = <span class="hljs-literal">null</span>,
    </span>) </span>{}
}
</code></pre>
<p>Notice the <code>$rating</code> property has a type of <code>RatingData</code>. This is another DTO:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Data</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Spatie</span>\<span class="hljs-title">LaravelData</span>\<span class="hljs-title">Data</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RatingData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">float</span> $rate,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $count,
    </span>) </span>{}
}
</code></pre>
<p>Now, in the resource <code>list</code> method, we can add the following to fetch the products and map them to a collection of <code>ProductData</code> instances.</p>
<pre><code class="lang-php"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">list</span>(<span class="hljs-params"></span>): <span class="hljs-title">DataCollection</span>
</span>{
    <span class="hljs-comment">// Create the request to the products endpoint.</span>
    $request = ApiRequest::get(<span class="hljs-string">'/products'</span>);

    <span class="hljs-comment">// Send the request using the client.</span>
    $response = <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    <span class="hljs-comment">// Map the response to the ProductData DTO.</span>
    <span class="hljs-keyword">return</span> ProductData::collection($response-&gt;json());
}
</code></pre>
<p>Looking at the API documentation for the Fake Store API, it is possible to limit the number of results and sort the results that are returned. We can map this using a DTO as well.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Data</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Enums</span>\<span class="hljs-title">SortDirection</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Spatie</span>\<span class="hljs-title">LaravelData</span>\<span class="hljs-title">Data</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ListProductsData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> readonly ?<span class="hljs-keyword">int</span> $limit = <span class="hljs-literal">null</span>,
        <span class="hljs-keyword">public</span> readonly ?SortDirection $sort = <span class="hljs-literal">null</span>,
    </span>) </span>{}

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toArray</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> collect(<span class="hljs-built_in">parent</span>::toArray())
            -&gt;filter()
            -&gt;toArray();
    }
}
</code></pre>
<p>The <code>SortDirection</code> type is just a simple enum with <code>asc</code> and <code>desc</code> cases. I added a custom <code>toArray</code> method which uses Laravel collections and the <code>filter</code> method to remove any null properties from the array. This prevents sending things like <code>?sort=null</code> to the API.</p>
<p>Let’s add this to our <code>list</code> method.</p>
<pre><code class="lang-php"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">list</span>(<span class="hljs-params">?ListProductsData $data = <span class="hljs-literal">null</span></span>): <span class="hljs-title">DataCollection</span>
</span>{
    $request = ApiRequest::get(<span class="hljs-string">'/products'</span>);

    <span class="hljs-keyword">if</span> ($data) {
        <span class="hljs-comment">// Add the ListProductsData to the query string of the request.</span>
        $request-&gt;setQuery($data-&gt;toArray());
    }

    $response = <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    <span class="hljs-keyword">return</span> ProductData::collection($response-&gt;json());
}
</code></pre>
<p>Now, if we want to request a list of five products in descending order, we can do something like the following:</p>
<pre><code class="lang-php">$resource = resolve(ProductResource::class);
$requestData = <span class="hljs-keyword">new</span> ListProductsData(
  limit: <span class="hljs-number">5</span>,
  sort: SortDirection::DESC,
);

$response = $resource-&gt;list($requestData);
</code></pre>
<p>Let’s add a few tests for this method.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">ApiResources</span>\<span class="hljs-title">ProductResource</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Data</span>\<span class="hljs-title">ListProductsData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Data</span>\<span class="hljs-title">ProductData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Enums</span>\<span class="hljs-title">SortDirection</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Request</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Spatie</span>\<span class="hljs-title">LaravelData</span>\<span class="hljs-title">DataCollection</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Tests</span>\<span class="hljs-title">Helpers</span>\<span class="hljs-title">StoreApiTestHelper</span>;

uses(StoreApiTestHelper::class);

it(<span class="hljs-string">'shows a list of products'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Fake the response from the API.</span>
    Http::fake([
        <span class="hljs-string">'*/products'</span> =&gt; Http::response([
            <span class="hljs-keyword">$this</span>-&gt;getFakeProduct([<span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">1</span>]),
            <span class="hljs-keyword">$this</span>-&gt;getFakeProduct([<span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">2</span>]),
            <span class="hljs-keyword">$this</span>-&gt;getFakeProduct([<span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">3</span>]),
            <span class="hljs-keyword">$this</span>-&gt;getFakeProduct([<span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">4</span>]),
            <span class="hljs-keyword">$this</span>-&gt;getFakeProduct([<span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">5</span>]),
        ]),
    ]);

    $resource = resolve(ProductResource::class);

    $response = $resource-&gt;list();

    <span class="hljs-comment">// Assert that the response is a collection of product data objects.</span>
    expect($response)
        -&gt;toBeInstanceOf(DataCollection::class)
        -&gt;count()-&gt;toBe(<span class="hljs-number">5</span>)
        -&gt;getIterator()-&gt;each-&gt;toBeInstanceOf(ProductData::class);

    <span class="hljs-comment">// Assert that a GET request was sent to the correct endpoint.</span>
    Http::assertSent(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)
            -&gt;url()-&gt;toEndWith(<span class="hljs-string">'/products'</span>)
            -&gt;method()-&gt;toBe(<span class="hljs-string">'GET'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});

it(<span class="hljs-string">'limits and sorts products'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Fake the response from the API.</span>
    Http::fake([
        <span class="hljs-string">'*/products?*'</span> =&gt; Http::response([
            <span class="hljs-keyword">$this</span>-&gt;getFakeProduct([<span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">3</span>]),
            <span class="hljs-keyword">$this</span>-&gt;getFakeProduct([<span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">2</span>]),
            <span class="hljs-keyword">$this</span>-&gt;getFakeProduct([<span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">1</span>]),
        ]),
    ]);

    $resource = resolve(ProductResource::class);

    <span class="hljs-comment">// Create a request data object with a three-item limit and descending direction.</span>
    $requestData = <span class="hljs-keyword">new</span> ListProductsData(<span class="hljs-number">3</span>, SortDirection::DESC);

    $response = $resource-&gt;list($requestData);

    <span class="hljs-comment">// Assert that the response is a collection of product data objects.</span>
    expect($response)
        -&gt;toBeInstanceOf(DataCollection::class)
        -&gt;count()-&gt;toBe(<span class="hljs-number">3</span>)
        -&gt;getIterator()-&gt;each-&gt;toBeInstanceOf(ProductData::class);

    <span class="hljs-comment">// Assert that a GET request was sent to the correct endpoint with the correct query data.</span>
    Http::assertSent(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        parse_str(parse_url($request-&gt;url(), PHP_URL_QUERY), $queryParams);
        $path =  (parse_url($request-&gt;url(), PHP_URL_PATH));

        expect($queryParams)-&gt;toMatchArray([<span class="hljs-string">'limit'</span> =&gt; <span class="hljs-number">3</span>, <span class="hljs-string">'sort'</span> =&gt; <span class="hljs-string">'desc'</span>])
            -&gt;and($path)-&gt;toEndWith(<span class="hljs-string">'/products'</span>)
            -&gt;and($request)-&gt;method()-&gt;toBe(<span class="hljs-string">'GET'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});
</code></pre>
<p>You’ll notice the <code>uses(StoreApiTestHelper::class);</code> call in the test. That loads a simple trait to provide the <code>getFakeProduct()</code> method which is used to generate fake product responses.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">Tests</span>\<span class="hljs-title">Helpers</span>;

<span class="hljs-keyword">trait</span> StoreApiTestHelper
{
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getFakeProduct</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $data = []</span>): <span class="hljs-title">array</span> </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'id'</span> =&gt; data_get($data, <span class="hljs-string">'id'</span>, fake()-&gt;numberBetween(<span class="hljs-number">1</span>, <span class="hljs-number">1000</span>)),
            <span class="hljs-string">'title'</span> =&gt; data_get($data, <span class="hljs-string">'title'</span>, fake()-&gt;text()),
            <span class="hljs-string">'price'</span> =&gt; data_get($data, <span class="hljs-string">'price'</span>, fake()-&gt;randomFloat(<span class="hljs-number">2</span>, <span class="hljs-number">0</span>, <span class="hljs-number">100</span>)),
            <span class="hljs-string">'description'</span> =&gt; data_get($data, <span class="hljs-string">'description'</span>, fake()-&gt;paragraph()),
            <span class="hljs-string">'category'</span> =&gt; data_get($data, <span class="hljs-string">'category'</span>, fake()-&gt;text()),
            <span class="hljs-string">'image'</span> =&gt; data_get($data, <span class="hljs-string">'image'</span>, fake()-&gt;url()),
            <span class="hljs-string">'rating'</span> =&gt; data_get($data, <span class="hljs-string">'rating'</span>, [
                <span class="hljs-string">'rate'</span> =&gt; fake()-&gt;randomFloat(<span class="hljs-number">2</span>, <span class="hljs-number">0</span>, <span class="hljs-number">5</span>),
                <span class="hljs-string">'count'</span> =&gt; fake()-&gt;numberBetween(<span class="hljs-number">1</span>, <span class="hljs-number">1000</span>),
            ]),
        ];
    }
}
</code></pre>
<p>I like using traits like this in tests to try and make it easier to fake API responses. This can easily be expanded on, too, like I did in my previous <a target="_blank" href="https://seankegel.com/streamlining-api-responses-in-laravel-with-dtos">post</a>.</p>
<p>In the tests, we ensure the proper endpoints are being called and the expected responses are being returned.</p>
<h3 id="heading-show-and-delete-methods">Show and Delete Methods</h3>
<p>I am combining the <code>show</code> and <code>delete</code> methods here since they will be very similar. According to the API documentation, they both return the product for the ID provided, so they have the same return value. They also accept an <code>id</code> URL parameter to fetch the specific product.</p>
<pre><code class="lang-php"><span class="hljs-comment">/**
 * Show a single product.
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">show</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> $id</span>): <span class="hljs-title">ProductData</span>
</span>{
    $request = ApiRequest::get(<span class="hljs-string">"/products/<span class="hljs-subst">{$id}</span>"</span>);
    $response = <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    <span class="hljs-keyword">return</span> ProductData::from($response-&gt;json());
}

<span class="hljs-comment">/**
 * Delete a product.
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">delete</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> $id</span>): <span class="hljs-title">ProductData</span>
</span>{
    $request = ApiRequest::delete(<span class="hljs-string">"/products/<span class="hljs-subst">$id</span>"</span>);
    $response = <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    <span class="hljs-keyword">return</span> ProductData::from($response-&gt;json());
}
</code></pre>
<p>Now, let’s add the following tests:</p>
<pre><code class="lang-php">it(<span class="hljs-string">'fetches a product'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Create a fake product</span>
    $fakeProduct = <span class="hljs-keyword">$this</span>-&gt;getFakeProduct();

    <span class="hljs-comment">// Fake the response from the API.</span>
    Http::fake([<span class="hljs-string">"*/products/<span class="hljs-subst">{$fakeProduct['id']}</span>"</span> =&gt; Http::response($fakeProduct)]);

    $resource = resolve(ProductResource::class);

    <span class="hljs-comment">// Request a product</span>
    $response = $resource-&gt;show($fakeProduct[<span class="hljs-string">'id'</span>]);
    expect($response)
        -&gt;toBeInstanceOf(ProductData::class)
        -&gt;id-&gt;toBe($fakeProduct[<span class="hljs-string">'id'</span>]);

    <span class="hljs-comment">// Assert that a GET request was sent to the correct endpoint with the correct method.</span>
    Http::assertSent(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) <span class="hljs-title">use</span> (<span class="hljs-params">$fakeProduct</span>) </span>{
        expect($request)
            -&gt;url()-&gt;toEndWith(<span class="hljs-string">"/products/<span class="hljs-subst">{$fakeProduct['id']}</span>"</span>)
            -&gt;method()-&gt;toBe(<span class="hljs-string">'GET'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});

it(<span class="hljs-string">'deletes a product'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Create a fake product</span>
    $fakeProduct = <span class="hljs-keyword">$this</span>-&gt;getFakeProduct();

    <span class="hljs-comment">// Fake the response from the API.</span>
    Http::fake([<span class="hljs-string">"*/products/<span class="hljs-subst">{$fakeProduct['id']}</span>"</span> =&gt; Http::response($fakeProduct)]);

    $resource = resolve(ProductResource::class);

    <span class="hljs-comment">// Request a product</span>
    $response = $resource-&gt;delete($fakeProduct[<span class="hljs-string">'id'</span>]);
    expect($response)
        -&gt;toBeInstanceOf(ProductData::class)
        -&gt;id-&gt;toBe($fakeProduct[<span class="hljs-string">'id'</span>]);

    <span class="hljs-comment">// Assert that a DELETE request was sent to the correct endpoint.</span>
    Http::assertSent(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) <span class="hljs-title">use</span> (<span class="hljs-params">$fakeProduct</span>) </span>{
        expect($request)
            -&gt;url()-&gt;toEndWith(<span class="hljs-string">"/products/<span class="hljs-subst">{$fakeProduct['id']}</span>"</span>)
            -&gt;method()-&gt;toBe(<span class="hljs-string">'DELETE'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});
</code></pre>
<h3 id="heading-create-and-update-methods">Create and Update Methods</h3>
<p>For the create and update methods, we know we need to send data to the API. Sometimes it is possible to reuse the same DTO that the API returns, but oftentimes, I like to create dedicated DTOs for the request. So I will create the <code>SaveProductData</code> DTO:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Data</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Spatie</span>\<span class="hljs-title">LaravelData</span>\<span class="hljs-title">Data</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SaveProductData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $title,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">float</span> $price,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $description,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $category,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $image,
    </span>) </span>{}
}
</code></pre>
<p>For this API, this single DTO is sufficient for both the <code>create</code> and <code>update</code> methods, however, sometimes it is necessary to have a dedicated DTO for each method. For example, using this specific DTO for the <code>update</code> method requires all the fields to be specified. However, you may want a DTO that allows optional properties and filters out the ones not set so you can easily update specific properties instead of everything.</p>
<p>With that in place, let’s update the <code>create</code> method:</p>
<pre><code class="lang-php"><span class="hljs-comment">/**
 * Create a new product.
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">create</span>(<span class="hljs-params">SaveProductData $data</span>): <span class="hljs-title">ProductData</span>
</span>{
    $request = ApiRequest::post(<span class="hljs-string">'/products'</span>)-&gt;setBody($data-&gt;toArray());
    $response = <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    <span class="hljs-keyword">return</span> ProductData::from($response-&gt;json());
}
</code></pre>
<p>The update method is similar but with an additional <code>id</code> parameter and a <code>PUT</code> request instead of a <code>POST</code> request.</p>
<pre><code class="lang-php"><span class="hljs-comment">/**
 * Update a product.
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">update</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> $id, SaveProductData $data</span>): <span class="hljs-title">ProductData</span>
</span>{
    $request = ApiRequest::put(<span class="hljs-string">"/products/<span class="hljs-subst">$id</span>"</span>)-&gt;setBody($data-&gt;toArray());
    $response = <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    <span class="hljs-keyword">return</span> ProductData::from($response-&gt;json());
}
</code></pre>
<p>Just like the <code>show</code> and <code>delete</code> methods, we are returning a <code>ProductData</code> instance for <code>create</code> and <code>update</code>.</p>
<p>Add the following tests for the methods.</p>
<pre><code class="lang-php">it(<span class="hljs-string">'creates a product'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Create a fake product</span>
    $fakeProduct = <span class="hljs-keyword">$this</span>-&gt;getFakeProduct();

    <span class="hljs-comment">// Fake the response from the API.</span>
    Http::fake([<span class="hljs-string">"*/products"</span> =&gt; Http::response($fakeProduct)]);

    $resource = resolve(ProductResource::class);

    <span class="hljs-comment">// Data for creating a product.</span>
    $data = <span class="hljs-keyword">new</span> SaveProductData(
        title: $fakeProduct[<span class="hljs-string">'title'</span>],
        price: $fakeProduct[<span class="hljs-string">'price'</span>],
        description: $fakeProduct[<span class="hljs-string">'description'</span>],
        category: $fakeProduct[<span class="hljs-string">'category'</span>],
        image: $fakeProduct[<span class="hljs-string">'image'</span>],
    );

    <span class="hljs-comment">// Request a product</span>
    $response = $resource-&gt;create($data);
    expect($response)
        -&gt;toBeInstanceOf(ProductData::class)
        -&gt;id-&gt;toBe($fakeProduct[<span class="hljs-string">'id'</span>]);

    <span class="hljs-comment">// Assert that a POST request was sent to the correct endpoint.</span>
    Http::assertSent(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)
            -&gt;url()-&gt;toEndWith(<span class="hljs-string">'/products'</span>)
            -&gt;method()-&gt;toBe(<span class="hljs-string">'POST'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});

it(<span class="hljs-string">'updates a product'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Create a fake product</span>
    $fakeProduct = <span class="hljs-keyword">$this</span>-&gt;getFakeProduct();

    <span class="hljs-comment">// Fake the response from the API.</span>
    Http::fake([<span class="hljs-string">"*/products/<span class="hljs-subst">{$fakeProduct['id']}</span>"</span> =&gt; Http::response($fakeProduct)]);

    $resource = resolve(ProductResource::class);

    $data = <span class="hljs-keyword">new</span> SaveProductData(
        title: $fakeProduct[<span class="hljs-string">'title'</span>],
        price: $fakeProduct[<span class="hljs-string">'price'</span>],
        description: $fakeProduct[<span class="hljs-string">'description'</span>],
        category: $fakeProduct[<span class="hljs-string">'category'</span>],
        image: $fakeProduct[<span class="hljs-string">'image'</span>],
    );

    <span class="hljs-comment">// Request a product</span>
    $response = $resource-&gt;update($fakeProduct[<span class="hljs-string">'id'</span>], $data);
    expect($response)
        -&gt;toBeInstanceOf(ProductData::class)
        -&gt;id-&gt;toBe($fakeProduct[<span class="hljs-string">'id'</span>]);

    <span class="hljs-comment">// Assert that a PUT request was sent to the correct endpoint.</span>
    Http::assertSent(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) <span class="hljs-title">use</span> (<span class="hljs-params">$fakeProduct</span>) </span>{
        expect($request)
            -&gt;url()-&gt;toEndWith(<span class="hljs-string">"/products/<span class="hljs-subst">{$fakeProduct['id']}</span>"</span>)
            -&gt;method()-&gt;toBe(<span class="hljs-string">'PUT'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});
</code></pre>
<h2 id="heading-summary">Summary</h2>
<p>In this post, we discussed a method of combining requests related to a resource into a single class. When using the combination of Laravel’s <code>Http</code> facade and data transfer objects, the methods of these classes can be made similar to a Laravel controller and be kept small and concise. I hope you enjoyed this series on integrating third-party APIs in Laravel. You can view the repository with all the code we went through in this post, <a target="_blank" href="https://github.com/skegel13/api-resources">here</a>.</p>
<p>Thanks for reading and as always, feel free to comment or ask questions!</p>
]]></content:encoded></item><item><title><![CDATA[Streamlining API Responses in Laravel with DTOs]]></title><description><![CDATA[Introduction
Handling API responses effectively is crucial for integrating third-party APIs. In my previous post, I discussed setting up simple client and request classes using the Http facade. If that's not something that you've already read, I reco...]]></description><link>https://seankegel.com/streamlining-api-responses-in-laravel-with-dtos</link><guid isPermaLink="true">https://seankegel.com/streamlining-api-responses-in-laravel-with-dtos</guid><category><![CDATA[Laravel]]></category><category><![CDATA[APIs]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[http]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Tue, 05 Dec 2023 02:57:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701744969894/ef97cd4f-6195-4b19-8a05-dafffb48d50b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Handling API responses effectively is crucial for integrating third-party APIs. In my previous <a target="_blank" href="https://seankegel.com/simplifying-api-integration-with-laravels-http-facade">post</a>, I discussed setting up simple client and request classes using the <code>Http</code> facade. If that's not something that you've already read, I recommend you take a look at it.</p>
<p>Amplifying these concepts, this post brings you an in-depth guide on how to create custom data transfer objects (DTOs) that can map the data from the API responses. I’ll be using an ongoing Google Books API integration scenario as a practical example to make things more relatable.</p>
<h2 id="heading-map-the-response-data-to-a-dto">Map the Response Data to a DTO</h2>
<p>To start, let’s look at a sample response from the Google Books API when we perform a search. To do this, I call the <code>QueryBooksByTitle</code> action I created previously and search for the book “The Ferryman”:</p>
<pre><code class="lang-php">$response = app(QueryBooksByTitle::class)(<span class="hljs-string">"The Ferryman"</span>);

dump($response-&gt;json());
</code></pre>
<p>This dumps out the following JSON which I have narrowed down to fields I’d like to track:</p>
<pre><code class="lang-json">[
    'kind' =&gt; 'books#volumes',
    'totalItems' =&gt; <span class="hljs-number">367</span>,
    'items' =&gt; [
        <span class="hljs-number">0</span> =&gt; [
            ...
        ],
        <span class="hljs-number">1</span> =&gt; [
            ...
        ],
        <span class="hljs-number">2</span> =&gt; [
            'kind' =&gt; 'books#volume',
            'id' =&gt; 'dO5-EAAAQBAJ',
            'volumeInfo' =&gt; [
                'title' =&gt; 'The Ferryman',
                'subtitle' =&gt; 'A Novel',
                'authors' =&gt; [
                    <span class="hljs-number">0</span> =&gt; 'Justin Cronin',
                ],
                'publisher' =&gt; 'Doubleday Canada',
                'publishedDate' =&gt; '<span class="hljs-number">2023</span><span class="hljs-number">-05</span><span class="hljs-number">-02</span>',
                'description' =&gt; 'From the #<span class="hljs-number">1</span> New York Times bestselling author of The Passage comes a riveting standalone novel about a group of survivors on a hidden island utopia--where the truth isn\'t what it seems. Founded by a mysterious genius, the archipelago of Prospera lies hidden from the horrors of a deteriorating outside world. In this island paradise, Prospera\'s lucky citizens enjoy long, fulfilling lives until the monitors embedded in their forearms, meant to measure their physical health and psychological well-being, fall below <span class="hljs-number">10</span> percent. Then they retire themselves, embarking on a ferry ride to the island known as the Nursery, where their failing bodies are renewed, their memories are wiped clean, and they are readied to restart life afresh. Proctor Bennett, of the Department of Social Contracts, has a satisfying career as a ferryman, gently shepherding people through the retirement process--and, when necessary, enforcing it. But all is not well with Proctor. For one thing, he\'s been dreaming--which is supposed to be impossible in Prospera. For another, his monitor percentage has begun to drop alarmingly fast. And then comes the day he is summoned to retire his own father, who gives him a disturbing and cryptic message before being wrestled onto the ferry. Meanwhile, something is stirring. The support staff, ordinary men and women who provide the labor to keep Prospera running, have begun to question their place in the social order. Unrest is building, and there are rumors spreading of a resistance group--known as Arrivalists--who may be fomenting revolution. Soon Proctor finds himself questioning everything he once believed, entangled with a much bigger cause than he realized--and on a desperate mission to uncover the truth.',
                'pageCount' =&gt; <span class="hljs-number">507</span>,
                'categories' =&gt; [
                    <span class="hljs-number">0</span> =&gt; 'Fiction',
                ],
                'imageLinks' =&gt; [
                    'smallThumbnail' =&gt; 'http:<span class="hljs-comment">//books.google.com/books/content?id=dO5-EAAAQBAJ&amp;printsec=frontcover&amp;img=1&amp;zoom=5&amp;edge=curl&amp;source=gbs_api',</span>
                    'thumbnail' =&gt; 'http:<span class="hljs-comment">//books.google.com/books/content?id=dO5-EAAAQBAJ&amp;printsec=frontcover&amp;img=1&amp;zoom=1&amp;edge=curl&amp;source=gbs_api',</span>
                ],
                ...
            ],
            ...
        ],
        ...
    ]
</code></pre>
<p>Now that we know the format of the response, let’s create the necessary DTOs to map the data. Let’s start with <code>BookListData</code> which can be a simple PHP class.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Arrayable</span>;

<span class="hljs-comment">/**
 * Stores the top-level data from the Google Books volumes API.
 */</span>
readonly <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BooksListData</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Arrayable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $kind,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $totalItems,
    </span>) </span>{
    }

    <span class="hljs-comment">/**
     * Creates a new instance of the class from an array of data.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fromArray</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $data</span>): <span class="hljs-title">BooksListData</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">self</span>(
            data_get($data, <span class="hljs-string">'kind'</span>),
            data_get($data, <span class="hljs-string">'id'</span>),
            data_get($data, <span class="hljs-string">'totalItems'</span>),
        );
    }

    <span class="hljs-comment">/**
     * Implements Laravel's Arrayable interface to allow the object to be
     * serialized to an array.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toArray</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'kind'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;kind,
            <span class="hljs-string">'items'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;items,
            <span class="hljs-string">'totalItems'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;totalItems,
        ];
    }
}
</code></pre>
<p>With the DTO created, we can update the <code>QueryBooksByTitle</code> action that we created in the previous <a target="_blank" href="https://seankegel.com/simplifying-api-integration-with-laravels-http-facade">post</a>.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Actions</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">BooksListData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">GoogleBooksApiClient</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Response</span>;

<span class="hljs-comment">/**
 * The QueryBooksByTitle class is an action for querying books by title from the
 * Google Books API.
 * It provides an __invoke method that takes a title and returns the response
 * from the API.
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">QueryBooksByTitle</span>
</span>{
    <span class="hljs-comment">/**
     * Query books by title from the Google Books API and return the BookListData.
     * This method creates a GoogleBooksApiClient and an ApiRequest for the
     * 'volumes' endpoint
     * with the given title as the 'q' query parameter and 'books' as the
     * 'printType' query parameter.
     * It then sends the request using the client and returns the book list data.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__invoke</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $title</span>): <span class="hljs-title">BooksListData</span>
    </span>{
        $client = app(GoogleBooksApiClient::class);

        $request = ApiRequest::get(<span class="hljs-string">'volumes'</span>)
            -&gt;setQuery(<span class="hljs-string">'q'</span>, <span class="hljs-string">'intitle:'</span>.$title)
            -&gt;setQuery(<span class="hljs-string">'printType'</span>, <span class="hljs-string">'books'</span>);

        $response = $client-&gt;send($request);

        <span class="hljs-keyword">return</span> BooksListData::fromArray($response-&gt;json());
    }
}
</code></pre>
<h2 id="heading-test-the-response-data">Test the Response Data</h2>
<p>We can create a test to make sure we return a <code>BooksListData</code> object when calling the action:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Actions</span>\<span class="hljs-title">QueryBooksByTitle</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">BooksListData</span>;

it(<span class="hljs-string">'fetches books by title'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $title = <span class="hljs-string">'The Lord of the Rings'</span>;

    $response = resolve(QueryBooksByTitle::class)($title);

    expect($response)-&gt;toBeInstanceOf(BooksListData::class);
});
</code></pre>
<p>You might not have noticed, but there is an issue with the test above. We are reaching out to the Google Books API. This might be okay for an integration test that is not run often, but in our Laravel tests, this should be fixed. We can use the power of the <code>Http</code> facade for this since our <code>Client</code> class is built using the facade.</p>
<h2 id="heading-prevent-http-requests-in-tests">Prevent HTTP Requests in Tests</h2>
<p>The first step I like to do is make sure none of my tests are making external HTTP requests that I am not expecting. We can add <code>Http::preventStrayRequests();</code> to the <code>Pest.php</code> file. Then, in any test using the <code>Http</code> facade to make a request, an exception will be thrown unless we mock the request.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Foundation</span>\<span class="hljs-title">Testing</span>\<span class="hljs-title">TestCase</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Tests</span>\<span class="hljs-title">CreatesApplication</span>;

uses(
    TestCase::class,
    CreatesApplication::class,
)
    -&gt;beforeEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
        Http::preventStrayRequests();
    })
    -&gt;in(<span class="hljs-string">'Feature'</span>);
</code></pre>
<p>If I run my <code>QueryBooksByTitle</code> test again, I now get a failed test that says:</p>
<pre><code class="lang-bash">RuntimeException: Attempted request to [https://www.googleapis.com/books/v1/volumes?key=XXXXXXXXXXXXX&amp;q=intitle%3AThe%20Lord%20of%20the%20Rings&amp;printType=books] without a matching fake.
</code></pre>
<p>Now, let’s use the <code>Http</code> facade to fake the response.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Actions</span>\<span class="hljs-title">QueryBooksByTitle</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">BooksListData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;

it(<span class="hljs-string">'fetches books by title'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $title = fake()-&gt;sentence();

    <span class="hljs-comment">// Generate a fake response from the Google Books API.</span>
    $responseData = [
        <span class="hljs-string">'kind'</span> =&gt; <span class="hljs-string">'books#volumes'</span>,
        <span class="hljs-string">'totalItems'</span> =&gt; <span class="hljs-number">1</span>,
        <span class="hljs-string">'items'</span> =&gt; [
            [
                <span class="hljs-string">'id'</span> =&gt; fake()-&gt;uuid,
                <span class="hljs-string">'volumeInfo'</span> =&gt; [
                    <span class="hljs-string">'title'</span> =&gt; $title,
                    <span class="hljs-string">'subtitle'</span> =&gt; fake()-&gt;sentence(),
                    <span class="hljs-string">'authors'</span> =&gt; [fake()-&gt;name],
                    <span class="hljs-string">'publisher'</span> =&gt; fake()-&gt;company(),
                    <span class="hljs-string">'publishedDate'</span> =&gt; fake()-&gt;date(),
                    <span class="hljs-string">'description'</span> =&gt; fake()-&gt;paragraphs(asText: <span class="hljs-literal">true</span>),
                    <span class="hljs-string">'pageCount'</span> =&gt; fake()-&gt;numberBetween(<span class="hljs-number">100</span>, <span class="hljs-number">500</span>),
                    <span class="hljs-string">'categories'</span> =&gt; [fake()-&gt;word],
                    <span class="hljs-string">'imageLinks'</span> =&gt; [
                        <span class="hljs-string">'thumbnail'</span> =&gt; fake()-&gt;url(),
                    ],
                ],
            ],
        ],
    ];

    <span class="hljs-comment">// Return the fake response when the client sends a request to the Google Books API.</span>
    Http::fake([<span class="hljs-string">'https://www.googleapis.com/books/v1/*'</span> =&gt; Http::response(
        body: $responseData,
        status: <span class="hljs-number">200</span>
    )]);

    $response = resolve(QueryBooksByTitle::class)($title);

    expect($response)-&gt;toBeInstanceOf(BooksListData::class);
    expect($response-&gt;items[<span class="hljs-number">0</span>][<span class="hljs-string">'volumeInfo'</span>][<span class="hljs-string">'title'</span>])-&gt;toBe($title);
});
</code></pre>
<p>When running the test now, we no longer have the <code>RuntimeException</code> because we are faking the request using the <code>Http::fake()</code> method. The <code>Http::fake()</code> method is very flexible and can accept an array of items and different URLs. Depending on your application, you can just use <code>'*'</code> instead of the full URL or even make it more specific and include query parameters or other dynamic URL data. You can even fake sequences of requests if needed. Refer to the <a target="_blank" href="https://laravel.com/docs/10.x/http-client#faking-response-sequences">Laravel docs</a> for more information.</p>
<p>This test works great but there are still some improvements to be made.</p>
<h2 id="heading-expand-the-dtos">Expand the DTOs</h2>
<p>First, let’s look at the response data again. It’s nice that we map the top level of the response in the <code>BooksListData</code> object, but having <code>items[0]['volumeInfo']['title'])</code> is not very developer-friendly friendly and the IDE cannot provide any type of autocompletion. To fix this, we need to create more DTOs. It’s usually easiest to start with the lowest-level items that need to be mapped. In this case, that would be the <code>imageLinks</code> data from the response. Looking at the response from Google Books, it looks like that could contain a <code>thumbnail</code> and <code>smallThumbnail</code> properties. We’ll create an <code>ImageLinksData</code> object to map this.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Arrayable</span>;

readonly <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImageLinksData</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Arrayable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> ?<span class="hljs-keyword">string</span> $thumbnail = <span class="hljs-literal">null</span>,
        <span class="hljs-keyword">public</span> ?<span class="hljs-keyword">string</span> $smallThumbnail = <span class="hljs-literal">null</span>,
    </span>) </span>{
    }

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fromArray</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $data</span>): <span class="hljs-title">self</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">self</span>(
            thumbnail: data_get($data, <span class="hljs-string">'thumbnail'</span>),
            smallThumbnail: data_get($data, <span class="hljs-string">'smallThumbnail'</span>),
        );
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toArray</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'thumbnail'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;thumbnail,
            <span class="hljs-string">'smallThumbnail'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;smallThumbnail,
        ];
    }
}
</code></pre>
<p>From there, go up a level and we have the <code>VolumeInfoData</code>.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Arrayable</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Collection</span>;

readonly <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">VolumeInfoData</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Arrayable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $title,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $subtitle,
        // Using collections instead of arrays is a personal preference.
        // It makes dealing with the data a little easier.
        <span class="hljs-comment">/** @var Collection&lt;int, string&gt; */</span>
        <span class="hljs-keyword">public</span> Collection $authors,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $publisher,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $publishedDate,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $description,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $pageCount,
        <span class="hljs-comment">/** @var Collection&lt;int, string&gt; */</span>
        <span class="hljs-keyword">public</span> Collection $categories,
        // The image links are mapped by the ImageLinksData <span class="hljs-keyword">object</span>.
        <span class="hljs-keyword">public</span> ImageLinksData $imageLinks,
    </span>) </span>{
    }

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fromArray</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $data</span>): <span class="hljs-title">self</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">self</span>(
            title: data_get($data, <span class="hljs-string">'title'</span>),
            subtitle: data_get($data, <span class="hljs-string">'subtitle'</span>),
            <span class="hljs-comment">// Create collections from the arrays of data.</span>
            authors: collect(data_get($data, <span class="hljs-string">'authors'</span>)),
            publisher: data_get($data, <span class="hljs-string">'publisher'</span>),
            publishedDate: data_get($data, <span class="hljs-string">'publishedDate'</span>),
            description: data_get($data, <span class="hljs-string">'description'</span>),
            pageCount: data_get($data, <span class="hljs-string">'pageCount'</span>),
            <span class="hljs-comment">// Create collections from the arrays of data.</span>
            categories: collect(data_get($data, <span class="hljs-string">'categories'</span>)),
            <span class="hljs-comment">// Map the image links to the ImageLinksData object.</span>
            imageLinks: ImageLinksData::fromArray(data_get($data, <span class="hljs-string">'imageLinks'</span>)),
        );
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toArray</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;title,
            <span class="hljs-string">'subtitle'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;subtitle,
            <span class="hljs-comment">// Convert the collections to arrays since they implement the</span>
            <span class="hljs-comment">// arrayable interface.</span>
            <span class="hljs-string">'authors'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;authors-&gt;toArray(),
            <span class="hljs-string">'publisher'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;publisher,
            <span class="hljs-string">'publishedDate'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;publishedDate,
            <span class="hljs-string">'description'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;description,
            <span class="hljs-string">'pageCount'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;pageCount,
            <span class="hljs-string">'categories'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;categories-&gt;toArray(),
            <span class="hljs-comment">// Since we are using the arrayable interface, we can just call the</span>
            <span class="hljs-comment">// toArray method on the imageLinks object.</span>
            <span class="hljs-string">'imageLinks'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;imageLinks-&gt;toArray(),
        ];
    }
}
</code></pre>
<p>Notice instead of using arrays, I used Laravel’s collections instead. I prefer working with Collections so I make sure anytime I have arrays in my responses, I map to Collections instead. Also, since the <code>VolumeInfoData</code> contains the <code>imageLinks</code> property, we can map it using the <code>ImageLinksData</code> object.</p>
<p>Going up another level, we have the list of items, so we can create the <code>ItemData</code> object.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Arrayable</span>;

readonly <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ItemData</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Arrayable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $id,
        <span class="hljs-keyword">public</span> VolumeInfoData $volumeInfo,
    </span>) </span>{
    }

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fromArray</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $data</span>): <span class="hljs-title">self</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">self</span>(
            id: data_get($data, <span class="hljs-string">'id'</span>),
            volumeInfo: VolumeInfoData::fromArray(data_get($data, <span class="hljs-string">'volumeInfo'</span>)),
        );
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toArray</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'id'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;id,
            <span class="hljs-string">'volumeInfo'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;volumeInfo-&gt;toArray(),
        ];
    }
}
</code></pre>
<p>Finally, we need to go back to the original <code>BooksListData</code> object and instead of mapping an array of data, we want to map a Collection of <code>ItemData</code> objects.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Arrayable</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Collection</span>;

<span class="hljs-comment">/**
 * Stores the top-level data from the Google Books volumes API.
 */</span>
readonly <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BooksListData</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Arrayable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $kind,
        <span class="hljs-comment">/** @var Collection&lt;int, ItemData&gt; */</span>
        <span class="hljs-keyword">public</span> Collection $items,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $totalItems,
    </span>) </span>{
    }

    <span class="hljs-comment">/**
     * Creates a new instance of the class from an array of data.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fromArray</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $data</span>): <span class="hljs-title">BooksListData</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">self</span>(
            data_get($data, <span class="hljs-string">'kind'</span>),
            <span class="hljs-comment">// Map the items to a collection of ItemData objects.</span>
            collect(data_get($data, <span class="hljs-string">'items'</span>, []))-&gt;map(<span class="hljs-function"><span class="hljs-keyword">fn</span> (<span class="hljs-params"><span class="hljs-keyword">array</span> $item</span>) =&gt; <span class="hljs-title">ItemData</span>::<span class="hljs-title">fromArray</span>(<span class="hljs-params">$item</span>)),
            <span class="hljs-title">data_get</span>(<span class="hljs-params">$data, <span class="hljs-string">'totalItems'</span></span>),
        )</span>;
    }

    <span class="hljs-comment">/**
     * Implements Laravel's Arrayable interface to allow the object to be
     * serialized to an array.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toArray</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'kind'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;kind,
            <span class="hljs-string">'items'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;items-&gt;toArray(),
            <span class="hljs-string">'totalItems'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;totalItems,
        ];
    }
}
</code></pre>
<p>With all the new DTOs created, let’s go back to the test and update.</p>
<h2 id="heading-test-the-full-dto">Test the Full DTO</h2>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Actions</span>\<span class="hljs-title">QueryBooksByTitle</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">BooksListData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">ImageLinksData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">ItemData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">VolumeInfoData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;

it(<span class="hljs-string">'fetches books by title'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $title = fake()-&gt;sentence();

    <span class="hljs-comment">// Generate a fake response from the Google Books API.</span>
    $responseData = [
        <span class="hljs-string">'kind'</span> =&gt; <span class="hljs-string">'books#volumes'</span>,
        <span class="hljs-string">'totalItems'</span> =&gt; <span class="hljs-number">1</span>,
        <span class="hljs-string">'items'</span> =&gt; [
            [
                <span class="hljs-string">'id'</span> =&gt; fake()-&gt;uuid,
                <span class="hljs-string">'volumeInfo'</span> =&gt; [
                    <span class="hljs-string">'title'</span> =&gt; $title,
                    <span class="hljs-string">'subtitle'</span> =&gt; fake()-&gt;sentence(),
                    <span class="hljs-string">'authors'</span> =&gt; [fake()-&gt;name],
                    <span class="hljs-string">'publisher'</span> =&gt; fake()-&gt;company(),
                    <span class="hljs-string">'publishedDate'</span> =&gt; fake()-&gt;date(),
                    <span class="hljs-string">'description'</span> =&gt; fake()-&gt;paragraphs(asText: <span class="hljs-literal">true</span>),
                    <span class="hljs-string">'pageCount'</span> =&gt; fake()-&gt;numberBetween(<span class="hljs-number">100</span>, <span class="hljs-number">500</span>),
                    <span class="hljs-string">'categories'</span> =&gt; [fake()-&gt;word],
                    <span class="hljs-string">'imageLinks'</span> =&gt; [
                        <span class="hljs-string">'thumbnail'</span> =&gt; fake()-&gt;url(),
                    ],
                ],
            ],
        ],
    ];

    <span class="hljs-comment">// Return the fake response when the client sends a request to the Google Books API.</span>
    Http::fake([<span class="hljs-string">'https://www.googleapis.com/books/v1/*'</span> =&gt; Http::response(
        body: $responseData,
        status: <span class="hljs-number">200</span>
    )]);

    $response = resolve(QueryBooksByTitle::class)($title);

    expect($response)-&gt;toBeInstanceOf(BooksListData::class)
        -&gt;and($response-&gt;items-&gt;first())-&gt;toBeInstanceOf(ItemData::class)
        -&gt;and($response-&gt;items-&gt;first()-&gt;volumeInfo)-&gt;toBeInstanceOf(VolumeInfoData::class)
        -&gt;imageLinks-&gt;toBeInstanceOf(ImageLinksData::class)
        -&gt;title-&gt;toBe($title);
});
</code></pre>
<p>Now in our expectations, we can see that the response is mapping all the various DTOs and correctly setting the title.</p>
<p>By having the action return the DTO versus the default <code>Illuminate/Http/Client/Response</code>, we now have type safety for the API response and get better autocompletion in the editor which greatly improves the developer experience.</p>
<h2 id="heading-create-test-response-helpers">Create Test Response Helpers</h2>
<p>One other bonus tip for this test that I like to do is create something like a response factory. It is time-consuming to mock out the responses on every single test that you might need for querying books, so I prefer to create a simple trait that helps me mock the responses much quicker.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">Tests</span>\<span class="hljs-title">Helpers</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;

<span class="hljs-keyword">trait</span> GoogleBooksApiResponseHelpers
{
    <span class="hljs-comment">/**
     * Generate a fake response for querying books by title.
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fakeQueryBooksByTitleResponse</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $items = [], <span class="hljs-keyword">int</span> $status = <span class="hljs-number">200</span>, <span class="hljs-keyword">bool</span> $raw = <span class="hljs-literal">false</span></span>): <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-comment">// If raw is true, return the items array as-is. Otherwise, return a</span>
        <span class="hljs-comment">// fake response from the Google Books API.</span>
        $data = $raw ? $items : [
            <span class="hljs-string">'kind'</span> =&gt; <span class="hljs-string">'books#volumes'</span>,
            <span class="hljs-string">'totalItems'</span> =&gt; count($items),
            <span class="hljs-string">'items'</span> =&gt; array_map(<span class="hljs-function"><span class="hljs-keyword">fn</span> (<span class="hljs-params"><span class="hljs-keyword">array</span> $item</span>) =&gt; $<span class="hljs-title">this</span>-&gt;<span class="hljs-title">createItem</span>(<span class="hljs-params">$item</span>), $<span class="hljs-title">items</span>),
        ]</span>;

        Http::fake([<span class="hljs-string">'https://www.googleapis.com/books/v1/*'</span> =&gt; Http::response(
            body: $data,
            status: $status
        )]);
    }

    <span class="hljs-comment">// Create a fake item array.</span>
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createItem</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $data = []</span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'id'</span> =&gt; data_get($data, <span class="hljs-string">'id'</span>, <span class="hljs-string">'123'</span>),
            <span class="hljs-string">'volumeInfo'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;createVolumeInfo(data_get($data, <span class="hljs-string">'volumeInfo'</span>, [])),
        ];
    }

    <span class="hljs-comment">// Create a fake volume info array.</span>
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createVolumeInfo</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $data = []</span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'title'</span> =&gt; data_get($data, <span class="hljs-string">'title'</span>, fake()-&gt;sentence),
            <span class="hljs-string">'subtitle'</span> =&gt; data_get($data, <span class="hljs-string">'subtitle'</span>, <span class="hljs-string">'Book Subtitle'</span>),
            <span class="hljs-string">'authors'</span> =&gt; data_get($data, <span class="hljs-string">'authors'</span>, [<span class="hljs-string">'Author 1'</span>, <span class="hljs-string">'Author 2'</span>]),
            <span class="hljs-string">'publisher'</span> =&gt; data_get($data, <span class="hljs-string">'publisher'</span>, <span class="hljs-string">'Publisher'</span>),
            <span class="hljs-string">'publishedDate'</span> =&gt; data_get($data, <span class="hljs-string">'publishedDate'</span>, <span class="hljs-string">'2021-01-01'</span>),
            <span class="hljs-string">'description'</span> =&gt; data_get($data, <span class="hljs-string">'description'</span>, <span class="hljs-string">'Book description'</span>),
            <span class="hljs-string">'pageCount'</span> =&gt; data_get($data, <span class="hljs-string">'pageCount'</span>, <span class="hljs-number">123</span>),
            <span class="hljs-string">'categories'</span> =&gt; data_get($data, <span class="hljs-string">'categories'</span>, [<span class="hljs-string">'Category 1'</span>, <span class="hljs-string">'Category 2'</span>]),
            <span class="hljs-string">'imageLinks'</span> =&gt; data_get($data, <span class="hljs-string">'imageLinks'</span>, [<span class="hljs-string">'thumbnail'</span> =&gt; <span class="hljs-string">'https://example.com/image.jpg'</span>]),
        ];
    }
}
</code></pre>
<p>To use the trait in a Pest test, we just need to use the <code>uses</code> method.</p>
<pre><code class="lang-php">uses(GoogleBooksApiResponseHelpers::class);
</code></pre>
<p>With that, we can now easily add additional tests without needing to have all the mock data written in each test.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Actions</span>\<span class="hljs-title">QueryBooksByTitle</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">BooksListData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">ImageLinksData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">ItemData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">DataTransferObjects</span>\<span class="hljs-title">VolumeInfoData</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">RequestException</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Tests</span>\<span class="hljs-title">Helpers</span>\<span class="hljs-title">GoogleBooksApiResponseHelpers</span>;

uses(GoogleBooksApiResponseHelpers::class);

it(<span class="hljs-string">'fetches books by title'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $title = fake()-&gt;sentence();

    <span class="hljs-comment">// Generate a fake response from the Google Books API.</span>
    <span class="hljs-keyword">$this</span>-&gt;fakeQueryBooksByTitleResponse([[<span class="hljs-string">'volumeInfo'</span> =&gt; [<span class="hljs-string">'title'</span> =&gt; $title]]]);

    $response = resolve(QueryBooksByTitle::class)($title);

    expect($response)-&gt;toBeInstanceOf(BooksListData::class)
        -&gt;and($response-&gt;items-&gt;first())-&gt;toBeInstanceOf(ItemData::class)
        -&gt;and($response-&gt;items-&gt;first()-&gt;volumeInfo)-&gt;toBeInstanceOf(VolumeInfoData::class)
        -&gt;imageLinks-&gt;toBeInstanceOf(ImageLinksData::class)
        -&gt;title-&gt;toBe($title);
});

it(<span class="hljs-string">'passes the title as a query parameter'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $title = fake()-&gt;sentence();

    <span class="hljs-comment">// Generate a fake response from the Google Books API.</span>
    <span class="hljs-keyword">$this</span>-&gt;fakeQueryBooksByTitleResponse([[<span class="hljs-string">'volumeInfo'</span> =&gt; [<span class="hljs-string">'title'</span> =&gt; $title]]]);

    resolve(QueryBooksByTitle::class)($title);

    Http::assertSent(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Illuminate\Http\Client\Request $request</span>) <span class="hljs-title">use</span> (<span class="hljs-params">$title</span>) </span>{
        expect($request)
            -&gt;method()-&gt;toBe(<span class="hljs-string">'GET'</span>)
            -&gt;data()-&gt;toHaveKey(<span class="hljs-string">'q'</span>, <span class="hljs-string">'intitle:'</span>.$title);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});

it(<span class="hljs-string">'fetches a list of multiple books'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Generate a fake response from the Google Books API.</span>
    <span class="hljs-keyword">$this</span>-&gt;fakeQueryBooksByTitleResponse([
        <span class="hljs-keyword">$this</span>-&gt;createItem(),
        <span class="hljs-keyword">$this</span>-&gt;createItem(),
        <span class="hljs-keyword">$this</span>-&gt;createItem(),
    ]);

    $response = resolve(QueryBooksByTitle::class)(<span class="hljs-string">'Fake Title'</span>);

    expect($response-&gt;items)-&gt;toHaveCount(<span class="hljs-number">3</span>);
});

it(<span class="hljs-string">'throws an exception'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Generate a fake response from the Google Books API.</span>
    <span class="hljs-keyword">$this</span>-&gt;fakeQueryBooksByTitleResponse([
        <span class="hljs-keyword">$this</span>-&gt;createItem(),
    ], <span class="hljs-number">400</span>);

    resolve(QueryBooksByTitle::class)(<span class="hljs-string">'Fake Title'</span>);
})-&gt;throws(RequestException::class);
</code></pre>
<p>With that, we now have cleaner tests, and our API responses are mapped to a DTO. For even more optimizations, you may consider using the <a target="_blank" href="https://spatie.be/docs/laravel-data/v3/introduction">Laravel Data</a> package by Spatie to create the DTOs, it can help reduce some of the boilerplate code for having to create the <code>fromArray</code> and <code>toArray</code> methods.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this post, you've learned how to streamline your process for developing and testing API integrations in Laravel by leveraging DTOs.</p>
<p>We explored the process of creating DTOs, mapping API responses to these DTOs, and developing test response helpers. This not only improved the readability of our code, but also facilitated a more type-safe, efficient, and testable development process.</p>
<p>The techniques discussed here and in my previous <a target="_blank" href="https://seankegel.com/simplifying-api-integration-with-laravels-http-facade">post</a> are useful for all types of API integrations, however, for more advanced solutions, I recommend looking at the <a target="_blank" href="https://docs.saloon.dev/">Saloon</a> PHP library.</p>
<p>I hope this post proves beneficial in your future Laravel projects. Nevertheless, the discussion doesn't have to end here. Do you have extra tips or alternative methods you'd like to share? Or perhaps there are points you'd like to discuss or need clarification on? I'd love to hear your perspective! Feel free to leave a comment.</p>
]]></content:encoded></item><item><title><![CDATA[Simplifying API Integration with Laravel's Http Facade]]></title><description><![CDATA[I’ve been working a lot lately integrating third-party APIs. There are several different approaches to this such as using the third-party provided SDK. However, I feel sticking to Laravel’s Http facade is often a better choice. By using the Http faca...]]></description><link>https://seankegel.com/simplifying-api-integration-with-laravels-http-facade</link><guid isPermaLink="true">https://seankegel.com/simplifying-api-integration-with-laravels-http-facade</guid><category><![CDATA[Laravel]]></category><category><![CDATA[Google]]></category><category><![CDATA[APIs]]></category><category><![CDATA[http]]></category><category><![CDATA[PHP]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Fri, 10 Nov 2023 03:11:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699585660555/4345ca45-14b3-45cd-ac5a-824e7fe8ee60.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve been working a lot lately integrating third-party APIs. There are several different approaches to this such as using the third-party provided SDK. However, I feel sticking to Laravel’s <code>Http</code> facade is often a better choice. By using the <code>Http</code> facade, all third-party integrations can have a similar structure, and testing and mocking becomes a lot easier. Also, your application will have fewer dependencies. You won’t have to worry about keeping the SDK up to date or figuring out what to do if the SDK is no longer supported.</p>
<p>In this post, we will explore integrating the <a target="_blank" href="https://developers.google.com/books/">Google Books API</a>. I will create a reusable client and request class to make using the API very simple. In future posts, I will go into more detail about testing, mocking, as well as creating API resources.</p>
<p>Let’s get started!</p>
<h2 id="heading-add-google-books-configuration-to-laravel">Add Google Books Configuration to Laravel</h2>
<p>Now that we have an API key, we can add it to the <code>.env</code> along with the API URL.</p>
<pre><code class="lang-bash">GOOGLE_BOOKS_API_URL=https://www.googleapis.com/books/v1
GOOGLE_BOOKS_API_KEY=[API KEY FROM GOOGLE]
</code></pre>
<blockquote>
<p>For this example, I am storing an API key that I obtained from the Google Cloud console, though it is not needed for the parts of the API we will be accessing. For more advanced API usage, you would need to integrate with Google’s OAuth 2.0 server and create a client ID and secret that could also be stored in the <code>.env</code> file. This is beyond the scope of this post.</p>
</blockquote>
<p>With the environment variables in place, open the <code>config/services.php</code> file and add a section for Google Books.</p>
<pre><code class="lang-php"><span class="hljs-string">'google_books'</span> =&gt; [
    <span class="hljs-comment">// Base URL for the Google Books API, retrieved from the .env</span>
    <span class="hljs-string">'base_url'</span> =&gt; env(<span class="hljs-string">'GOOGLE_BOOKS_API_URL'</span>),
    <span class="hljs-comment">// API key for the Google Books API, retrieved from the .env</span>
    <span class="hljs-string">'api_key'</span> =&gt; env(<span class="hljs-string">'GOOGLE_BOOKS_API_KEY'</span>),
],
</code></pre>
<h2 id="heading-create-an-apirequest-class">Create an ApiRequest Class</h2>
<p>When making requests to the API, I find it easiest to use a simple class to be able to set any request properties I need.</p>
<p>Below is an example of an <code>ApiRequest</code> class that I use to pass in URL information along with the body, headers, and any query parameters. This class can easily be modified or extended to add additional functionality.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>;

<span class="hljs-comment">/**
 * The ApiRequest class is a utility for building HTTP requests to an API.
 * It provides methods for setting the HTTP method, URI, headers, query
 * parameters, and body of the request.
 * It also provides methods for getting these properties, as well as for
 * clearing the headers, query parameters, and body.
 * Additionally, it provides static methods for creating ApiRequest instances
 * for specific HTTP methods.
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiRequest</span>
</span>{
    <span class="hljs-comment">// Store the headers that will be sent with the API request.</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">array</span> $headers = [];

    <span class="hljs-comment">// Store any query string parameters.</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">array</span> $query = [];

    <span class="hljs-comment">// Store the body of the request.</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">array</span> $body = [];

    <span class="hljs-comment">/**
     * Create an API request for a given HTTP method and URI.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"><span class="hljs-keyword">protected</span> HttpMethod $method = HttpMethod::GET, <span class="hljs-keyword">protected</span> <span class="hljs-keyword">string</span> $uri = <span class="hljs-string">''</span></span>)
    </span>{
    }

    <span class="hljs-comment">/**
     * Set headers for the request.
     * This accepts either a key and value, or an array of key/value pairs.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setHeaders</span>(<span class="hljs-params"><span class="hljs-keyword">array</span>|<span class="hljs-keyword">string</span> $key, <span class="hljs-keyword">string</span> $value = <span class="hljs-literal">null</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">if</span> (is_array($key)) {
            <span class="hljs-keyword">$this</span>-&gt;headers = $key;
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">$this</span>-&gt;headers[$key] = $value;
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>;
    }

    <span class="hljs-comment">/**
     * Clear headers for the request.
     * This method can clear a specific header or all headers in the request if
     * a key is not provided.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">clearHeaders</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $key = <span class="hljs-literal">null</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">if</span> ($key) {
            <span class="hljs-keyword">unset</span>(<span class="hljs-keyword">$this</span>-&gt;headers[$key]);
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">$this</span>-&gt;headers = [];
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>;
    }

    <span class="hljs-comment">/**
     * Set query parameters for the request.
     * This accepts either a key and value, or an array of key/value pairs.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setQuery</span>(<span class="hljs-params"><span class="hljs-keyword">array</span>|<span class="hljs-keyword">string</span> $key, <span class="hljs-keyword">string</span> $value = <span class="hljs-literal">null</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">if</span> (is_array($key)) {
            <span class="hljs-keyword">$this</span>-&gt;query = $key;
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">$this</span>-&gt;query[$key] = $value;
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>;
    }

    <span class="hljs-comment">/**
     * Clear query parameters for the request.
     * This method can clear a specific parameter or all parameters if a key is
     * not provided.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">clearQuery</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $key = <span class="hljs-literal">null</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">if</span> ($key) {
            <span class="hljs-keyword">unset</span>(<span class="hljs-keyword">$this</span>-&gt;query[$key]);
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">$this</span>-&gt;query = [];
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>;
    }

    <span class="hljs-comment">/**
     * Set body data for the request.
     * This accepts either a key and value, or an array of key/value pairs.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setBody</span>(<span class="hljs-params"><span class="hljs-keyword">array</span>|<span class="hljs-keyword">string</span> $key, <span class="hljs-keyword">string</span> $value = <span class="hljs-literal">null</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">if</span> (is_array($key)) {
            <span class="hljs-keyword">$this</span>-&gt;body = $key;
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">$this</span>-&gt;body[$key] = $value;
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>;
    }

    <span class="hljs-comment">/**
     * Clear body data for the request.
     * This method can clear a specific key of data or all data.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">clearBody</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $key = <span class="hljs-literal">null</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">if</span> ($key) {
            <span class="hljs-keyword">unset</span>(<span class="hljs-keyword">$this</span>-&gt;body[$key]);
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">$this</span>-&gt;body = [];
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>;
    }

    <span class="hljs-comment">/**
     * This method returns the headers for the API request.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getHeaders</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;headers;
    }

    <span class="hljs-comment">/**
     * This method returns the query for the API request.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getQuery</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;query;
    }

    <span class="hljs-comment">/**
     * This method returns the body for the API request.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getBody</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;body;
    }

    <span class="hljs-comment">/**
     * This method returns the URI for the API request.
     * If the query is empty, or we have a GET request, the URI can be returned
     * as is.
     * Otherwise, we need to append the query string to the URI.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getUri</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span>
    </span>{
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">empty</span>(<span class="hljs-keyword">$this</span>-&gt;query) || <span class="hljs-keyword">$this</span>-&gt;method === HttpMethod::GET) {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;uri;
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;uri.<span class="hljs-string">'?'</span>.http_build_query(<span class="hljs-keyword">$this</span>-&gt;query);
    }

    <span class="hljs-comment">/**
     * This method returns the HTTP method for the API request.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getMethod</span>(<span class="hljs-params"></span>): <span class="hljs-title">HttpMethod</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;method;
    }

    <span class="hljs-comment">// The following methods are used to create API requests for specific HTTP</span>
    <span class="hljs-comment">// methods.</span>

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $uri = <span class="hljs-string">''</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">static</span>(HttpMethod::GET, $uri);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">post</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $uri = <span class="hljs-string">''</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">static</span>(HttpMethod::POST, $uri);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">put</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $uri = <span class="hljs-string">''</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">static</span>(HttpMethod::PUT, $uri);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">delete</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $uri = <span class="hljs-string">''</span></span>): <span class="hljs-title">static</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">static</span>(HttpMethod::DELETE, $uri);
    }
}
</code></pre>
<p>The class constructor takes an <code>HttpMethod</code>, which is just a simple enum with the various HTTP methods, and a URI.</p>
<pre><code class="lang-php">enum HttpMethod: <span class="hljs-keyword">string</span>
{
    <span class="hljs-keyword">case</span> GET = <span class="hljs-string">'get'</span>;
    <span class="hljs-keyword">case</span> POST = <span class="hljs-string">'post'</span>;
    <span class="hljs-keyword">case</span> PUT = <span class="hljs-string">'put'</span>;
    <span class="hljs-keyword">case</span> DELETE = <span class="hljs-string">'delete'</span>;
}
</code></pre>
<p>There are helper methods to create the request using the HTTP method name and passing a URI. Finally, there are methods to add and clear headers, query parameters, and body data.</p>
<h2 id="heading-create-an-api-client">Create an API Client</h2>
<p>Now that we have the request, we need an API client to send it. This is where we can use the <code>Http</code> facade.</p>
<h3 id="heading-abstract-apiclient">Abstract ApiClient</h3>
<p>First, we’ll create an abstract <code>ApiClient</code> class that will be extended by our various APIs.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">PendingRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Response</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;

<span class="hljs-comment">/**
 * The ApiClient class is an abstract base class for making HTTP requests to an
 * API.
 * It provides a method for sending an ApiRequest and methods for getting and
 * authorizing a base request.
 * Subclasses must implement the baseUrl method to specify the base URL for the
 * API.
 */</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiClient</span>
</span>{
    <span class="hljs-comment">/**
     * Send an ApiRequest to the API and return the response.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params">ApiRequest $request</span>): <span class="hljs-title">Response</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;getBaseRequest()
            -&gt;withHeaders($request-&gt;getHeaders())
            -&gt;{$request-&gt;getMethod()-&gt;value}(
                $request-&gt;getUri(),
                $request-&gt;getMethod() === HttpMethod::GET
                    ? $request-&gt;getQuery()
                    : $request-&gt;getBody()
            );
    }

    <span class="hljs-comment">/**
     * Get a base request for the API.
     * This method has some helpful defaults for API requests.
     * The base request is a PendingRequest with JSON acceptance, a content type
     * of 'application/json', and the base URL for the API.
     * It also throws exceptions for non-successful responses.
     */</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getBaseRequest</span>(<span class="hljs-params"></span>): <span class="hljs-title">PendingRequest</span>
    </span>{
        $request = Http::acceptJson()
            -&gt;contentType(<span class="hljs-string">'application/json'</span>)
            -&gt;throw()
            -&gt;baseUrl(<span class="hljs-keyword">$this</span>-&gt;baseUrl());

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;authorize($request);
    }

    <span class="hljs-comment">/**
     * Authorize a request for the API.
     * This method is intended to be overridden by subclasses to provide
     * API-specific authorization.
     * By default, it simply returns the given request.
     */</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">authorize</span>(<span class="hljs-params">PendingRequest $request</span>): <span class="hljs-title">PendingRequest</span>
    </span>{
        <span class="hljs-keyword">return</span> $request;
    }

    <span class="hljs-comment">/**
     * Get the base URL for the API.
     * This method must be implemented by subclasses to provide the base URL for
     * the API.
     */</span>
    <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">baseUrl</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span></span>;
}
</code></pre>
<p>This class has a <code>getBaseRequest</code> method that sets up some sane defaults using the <code>Http</code> facade to create a <code>PendingRequest</code>. It calls the <code>authorize</code> method which we can override in our Google Books implementation to set our API key.</p>
<p>The <code>baseUrl</code> method is just a simple abstract method that our Google Books class will set to use the Google Books API URL we set earlier.</p>
<p>Finally, the <code>send</code> method is what sends the request to the API. It takes an <code>ApiRequest</code> parameter to build up the request, then returns the response.</p>
<h3 id="heading-googlebooksapiclient">GoogleBooksApiClient</h3>
<p>With the abstract client created, we can now create a <code>GoogleBooksApiClient</code> to extend it.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">PendingRequest</span>;

<span class="hljs-comment">/**
 * The GoogleBooksApiClient class is a concrete implementation of the ApiClient
 * base class for the Google Books API.
 * It provides methods for getting the base URL and authorizing a request for
 * the Google Books API.
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GoogleBooksApiClient</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ApiClient</span>
</span>{
    <span class="hljs-comment">/**
     * Get the base URL for the Google Books API.
     * The base URL is retrieved from the 'services.google_books.base_url'
     * configuration value.
     */</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">baseUrl</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span>
    </span>{
        <span class="hljs-keyword">return</span> config(<span class="hljs-string">'services.google_books.base_url'</span>);
    }

    <span class="hljs-comment">/**
     * Authorize a request for the Google Books API.
     * The Google Books API accepts the API key as a query parameter named
     * 'key'.
     * The API key is retrieved from the 'services.google_books.api_key'
     * configuration value.
     */</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">authorize</span>(<span class="hljs-params">PendingRequest $request</span>): <span class="hljs-title">PendingRequest</span>
    </span>{
        <span class="hljs-keyword">return</span> $request-&gt;withQueryParameters([
            <span class="hljs-string">'key'</span> =&gt; config(<span class="hljs-string">'services.google_books.api_key'</span>),
        ]);
    }
}
</code></pre>
<p>In this class, we just need to set the base URL and configure the authorization. For the Google Books API, that means passing the API key as a URL parameter and setting an empty <code>Authorization</code> header.</p>
<p>If we had an API that used a bearer authorization, we could have an <code>authorize</code> method like the following:</p>
<pre><code class="lang-php"><span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">authorize</span>(<span class="hljs-params">PendingRequest $request</span>): <span class="hljs-title">PendingRequest</span>
</span>{
    <span class="hljs-keyword">return</span> $request-&gt;withToken(config(services.someApi.token));
}
</code></pre>
<p>The nice part about having this <code>authorize</code> method is the flexibility it offers to support a variety of API authorization methods.</p>
<h2 id="heading-query-books-by-title">Query Books By Title</h2>
<p>Now that we have our <code>ApiRequest</code> class and <code>GoogleBooksApiClient</code>, we can create an action to query books by title. It would look something like this:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Actions</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">GoogleBooksApiClient</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Response</span>;

<span class="hljs-comment">/**
 * The QueryBooksByTitle class is an action for querying books by title from the
 * Google Books API.
 * It provides an __invoke method that takes a title and returns the response
 * from the API.
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">QueryBooksByTitle</span>
</span>{
    <span class="hljs-comment">/**
     * Query books by title from the Google Books API and return the response.
     * This method creates a GoogleBooksApiClient and an ApiRequest for the
     * 'volumes' endpoint
     * with the given title as the 'q' query parameter and 'books' as the
     * 'printType' query parameter.
     * It then sends the request using the client and returns the response.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__invoke</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $title</span>): <span class="hljs-title">Response</span>
    </span>{
        $client = app(GoogleBooksApiClient::class);

        $request = ApiRequest::get(<span class="hljs-string">'volumes'</span>)
            -&gt;setQuery(<span class="hljs-string">'q'</span>, <span class="hljs-string">'intitle:'</span>.$title)
            -&gt;setQuery(<span class="hljs-string">'printType'</span>, <span class="hljs-string">'books'</span>);

        <span class="hljs-keyword">return</span> $client-&gt;send($request);
    }
}
</code></pre>
<p>Then, to call the action, if I wanted to find information about the book <em>The Ferryman</em>, which I just read and highly recommend, use the following snippet:</p>
<pre><code class="lang-php"><span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Actions</span>\<span class="hljs-title">QueryBooksByTitle</span>;

$response = app(QueryBooksByTitle::class)(<span class="hljs-string">"The Ferryman"</span>);

$response-&gt;json();
</code></pre>
<h2 id="heading-bonus-tests">Bonus: Tests</h2>
<p>Below, I added some examples for testing the request and client classes. For the tests, I am using <a target="_blank" href="https://pestphp.com/">Pest PHP</a> which provides a clean syntax and additional features on top of PHPUnit.</p>
<h3 id="heading-apirequest">ApiRequest</h3>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">HttpMethod</span>;

it(<span class="hljs-string">'sets request data properly'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = (<span class="hljs-keyword">new</span> ApiRequest(HttpMethod::GET, <span class="hljs-string">'/'</span>))
        -&gt;setHeaders([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
        -&gt;setQuery([<span class="hljs-string">'baz'</span> =&gt; <span class="hljs-string">'qux'</span>])
        -&gt;setBody([<span class="hljs-string">'quux'</span> =&gt; <span class="hljs-string">'quuz'</span>]);

    expect($request)
        -&gt;getHeaders()-&gt;toBe([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
        -&gt;getQuery()-&gt;toBe([<span class="hljs-string">'baz'</span> =&gt; <span class="hljs-string">'qux'</span>])
        -&gt;getBody()-&gt;toBe([<span class="hljs-string">'quux'</span> =&gt; <span class="hljs-string">'quuz'</span>])
        -&gt;getMethod()-&gt;toBe(HttpMethod::GET)
        -&gt;getUri()-&gt;toBe(<span class="hljs-string">'/'</span>);
});

it(<span class="hljs-string">'sets request data properly with a key-&gt;value'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = (<span class="hljs-keyword">new</span> ApiRequest(HttpMethod::GET, <span class="hljs-string">'/'</span>))
        -&gt;setHeaders(<span class="hljs-string">'foo'</span>, <span class="hljs-string">'bar'</span>)
        -&gt;setQuery(<span class="hljs-string">'baz'</span>, <span class="hljs-string">'qux'</span>)
        -&gt;setBody(<span class="hljs-string">'quux'</span>, <span class="hljs-string">'quuz'</span>);

    expect($request)
        -&gt;getHeaders()-&gt;toBe([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
        -&gt;getQuery()-&gt;toBe([<span class="hljs-string">'baz'</span> =&gt; <span class="hljs-string">'qux'</span>])
        -&gt;getBody()-&gt;toBe([<span class="hljs-string">'quux'</span> =&gt; <span class="hljs-string">'quuz'</span>])
        -&gt;getMethod()-&gt;toBe(HttpMethod::GET)
        -&gt;getUri()-&gt;toBe(<span class="hljs-string">'/'</span>);
});

it(<span class="hljs-string">'clears request data properly'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = (<span class="hljs-keyword">new</span> ApiRequest(HttpMethod::GET, <span class="hljs-string">'/'</span>))
        -&gt;setHeaders([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
        -&gt;setQuery([<span class="hljs-string">'baz'</span> =&gt; <span class="hljs-string">'qux'</span>])
        -&gt;setBody([<span class="hljs-string">'quux'</span> =&gt; <span class="hljs-string">'quuz'</span>]);

    $request-&gt;clearHeaders()
        -&gt;clearQuery()
        -&gt;clearBody();

    expect($request)
        -&gt;getHeaders()-&gt;toBe([])
        -&gt;getQuery()-&gt;toBe([])
        -&gt;getBody()-&gt;toBe([])
        -&gt;getUri()-&gt;toBe(<span class="hljs-string">'/'</span>);
});

it(<span class="hljs-string">'clears request data properly with a key'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = (<span class="hljs-keyword">new</span> ApiRequest(HttpMethod::GET, <span class="hljs-string">'/'</span>))
        -&gt;setHeaders(<span class="hljs-string">'foo'</span>, <span class="hljs-string">'bar'</span>)
        -&gt;setQuery(<span class="hljs-string">'baz'</span>, <span class="hljs-string">'qux'</span>)
        -&gt;setBody(<span class="hljs-string">'quux'</span>, <span class="hljs-string">'quuz'</span>);

    $request-&gt;clearHeaders(<span class="hljs-string">'foo'</span>)
        -&gt;clearQuery(<span class="hljs-string">'baz'</span>)
        -&gt;clearBody(<span class="hljs-string">'quux'</span>);

    expect($request)
        -&gt;getHeaders()-&gt;toBe([])
        -&gt;getQuery()-&gt;toBe([])
        -&gt;getBody()-&gt;toBe([])
        -&gt;getUri()-&gt;toBe(<span class="hljs-string">'/'</span>);
});

it(<span class="hljs-string">'creates instance with correct method'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">HttpMethod $method</span>) </span>{
    $request = ApiRequest::{$method-&gt;value}(<span class="hljs-string">'/'</span>);

    expect($request-&gt;getMethod())-&gt;toBe($method);
})-&gt;with([
    [HttpMethod::GET],
    [HttpMethod::POST],
    [HttpMethod::PUT],
    [HttpMethod::DELETE],
]);
</code></pre>
<p>The <code>ApiRequest</code> tests check that the correct request data is being set and the correct methods are being used.</p>
<h3 id="heading-apiclient">ApiClient</h3>
<p>Testing for the <code>ApiClient</code> will be a little more complex. Since it is an abstract class, we will use an anonymous class in the <code>beforeEach</code> function to create a client to use that extends <code>ApiClient</code>.</p>
<p>Notice, that we also use the <code>Http::fake()</code> method. This creates mocks on the <code>Http</code> facade that we can make assertions against and prevent making API requests in the tests.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiClient</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">HttpMethod</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">PendingRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Request</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;

beforeEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    Http::fake();

    <span class="hljs-keyword">$this</span>-&gt;client = <span class="hljs-keyword">new</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ApiClient</span>
    </span>{
        <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">baseUrl</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span>
        </span>{
            <span class="hljs-keyword">return</span> <span class="hljs-string">'https://example.com'</span>;
        }
    };
});

it(<span class="hljs-string">'sends a get request'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = ApiRequest::get(<span class="hljs-string">'foo'</span>)
        -&gt;setHeaders([<span class="hljs-string">'X-Foo'</span> =&gt; <span class="hljs-string">'Bar'</span>])
        -&gt;setQuery([<span class="hljs-string">'baz'</span> =&gt; <span class="hljs-string">'qux'</span>]);

    <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    Http::assertSent(<span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)
            -&gt;url()-&gt;toBe(<span class="hljs-string">'https://example.com/foo?baz=qux'</span>)
            -&gt;method()-&gt;toBe(HttpMethod::GET-&gt;name)
            -&gt;header(<span class="hljs-string">'X-Foo'</span>)-&gt;toBe([<span class="hljs-string">'Bar'</span>]);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});

it(<span class="hljs-string">'sends a post request'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = ApiRequest::post(<span class="hljs-string">'foo'</span>)
        -&gt;setBody([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
        -&gt;setHeaders([<span class="hljs-string">'X-Foo'</span> =&gt; <span class="hljs-string">'Bar'</span>])
        -&gt;setQuery([<span class="hljs-string">'baz'</span> =&gt; <span class="hljs-string">'qux'</span>]);

    <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    Http::assertSent(<span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)
            -&gt;url()-&gt;toBe(<span class="hljs-string">'https://example.com/foo?baz=qux'</span>)
            -&gt;method()-&gt;toBe(HttpMethod::POST-&gt;name)
            -&gt;data()-&gt;toBe([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
            -&gt;header(<span class="hljs-string">'X-Foo'</span>)-&gt;toBe([<span class="hljs-string">'Bar'</span>]);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});

it(<span class="hljs-string">'sends a put request'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = ApiRequest::put(<span class="hljs-string">'foo'</span>)
        -&gt;setBody([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
        -&gt;setHeaders([<span class="hljs-string">'X-Foo'</span> =&gt; <span class="hljs-string">'Bar'</span>])
        -&gt;setQuery([<span class="hljs-string">'baz'</span> =&gt; <span class="hljs-string">'qux'</span>]);

    <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    Http::assertSent(<span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)
            -&gt;url()-&gt;toBe(<span class="hljs-string">'https://example.com/foo?baz=qux'</span>)
            -&gt;method()-&gt;toBe(HttpMethod::PUT-&gt;name)
            -&gt;data()-&gt;toBe([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
            -&gt;header(<span class="hljs-string">'X-Foo'</span>)-&gt;toBe([<span class="hljs-string">'Bar'</span>]);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});

it(<span class="hljs-string">'sends a delete request'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = ApiRequest::delete(<span class="hljs-string">'foo'</span>)
        -&gt;setBody([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
        -&gt;setHeaders([<span class="hljs-string">'X-Foo'</span> =&gt; <span class="hljs-string">'Bar'</span>])
        -&gt;setQuery([<span class="hljs-string">'baz'</span> =&gt; <span class="hljs-string">'qux'</span>]);

    <span class="hljs-keyword">$this</span>-&gt;client-&gt;send($request);

    Http::assertSent(<span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)
            -&gt;url()-&gt;toBe(<span class="hljs-string">'https://example.com/foo?baz=qux'</span>)
            -&gt;method()-&gt;toBe(HttpMethod::DELETE-&gt;name)
            -&gt;data()-&gt;toBe([<span class="hljs-string">'foo'</span> =&gt; <span class="hljs-string">'bar'</span>])
            -&gt;header(<span class="hljs-string">'X-Foo'</span>)-&gt;toBe([<span class="hljs-string">'Bar'</span>]);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});

it(<span class="hljs-string">'handles authorization'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $client = <span class="hljs-keyword">new</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ApiClient</span>
    </span>{
        <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">baseUrl</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span>
        </span>{
            <span class="hljs-keyword">return</span> <span class="hljs-string">'https://example.com'</span>;
        }

        <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">authorize</span>(<span class="hljs-params">PendingRequest $request</span>): <span class="hljs-title">PendingRequest</span>
        </span>{
            <span class="hljs-keyword">return</span> $request-&gt;withHeaders([<span class="hljs-string">'Authorization'</span> =&gt; <span class="hljs-string">'Bearer foo'</span>]);
        }
    };

    $request = ApiRequest::get(<span class="hljs-string">'foo'</span>);

    $client-&gt;send($request);

    Http::assertSent(<span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)-&gt;header(<span class="hljs-string">'Authorization'</span>)-&gt;toBe([<span class="hljs-string">'Bearer foo'</span>]);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});
</code></pre>
<p>For the tests, we are confirming that the request properties are being set correctly on the various request methods. We also confirm the <code>baseUrl</code> and <code>authorize</code> methods are being called correctly. To make these assertions, we are using the <code>Http::assertSent</code> method which expects a callback with a <code>$request</code> that we can test against. Notice that I am using the PestPHP expectations and then returning <code>true</code>. We could just use a normal comparison and return that, but by using the expectations, we get much cleaner error messages when the tests fail. Read this excellent <a target="_blank" href="https://chrisgmyr.dev/improve-your-productivity-while-utilizing-laravel-mocks">article</a> for more information.</p>
<h3 id="heading-googlebooksapiclienttest">GoogleBooksApiClientTest</h3>
<p>The test for the <code>GoogleBooksApiClient</code> is similar to the <code>ApiClient</code> test where we just want to make sure our custom implementation details are being handled properly, like setting the base URL and adding a query parameter with the API key.</p>
<p>Also, not the <code>config</code> helper in the <code>beforeEach</code> method. By using the helper, we can set test values for the Google Books service config that will be used in each of our tests.</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ApiRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">GoogleBooksApiClient</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Http</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Client</span>\<span class="hljs-title">Request</span>;

beforeEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    Http::fake();
    config([
        <span class="hljs-string">'services.google_books.base_url'</span> =&gt; <span class="hljs-string">'https://example.com'</span>,
        <span class="hljs-string">'services.google_books.api_key'</span> =&gt; <span class="hljs-string">'foo'</span>,
    ]);
});

it(<span class="hljs-string">'sets the base url'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = ApiRequest::get(<span class="hljs-string">'foo'</span>);

    app(GoogleBooksApiClient::class)-&gt;send($request);

    Http::assertSent(<span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)-&gt;url()-&gt;toStartWith(<span class="hljs-string">'https://example.com/foo'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});

it(<span class="hljs-string">'sets the api key as a query parameter'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    $request = ApiRequest::get(<span class="hljs-string">'foo'</span>);

    app(GoogleBooksApiClient::class)-&gt;send($request);

    Http::assertSent(<span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">Request $request</span>) </span>{
        expect($request)-&gt;url()-&gt;toContain(<span class="hljs-string">'key=foo'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    });
});
</code></pre>
<h2 id="heading-summary">Summary</h2>
<p>In this article, we covered some helpful steps for integrating third-party APIs in Laravel. By using these simple custom classes, along with the <code>Http</code> facade, we can ensure all integrations function similarly, are easier to test, and don’t require any project dependencies. In a later post, I will expand on these integration tips by covering DTOs, testing with mock responses, and using API resources.</p>
<p>Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Git Quick Tip: Using a Global Gitignore]]></title><description><![CDATA[Did you know Git supports a global .gitignore file that can be used across all of your local repositories?
Getting Started
To get started, let's create a file named ignore in the ~/.config/git folder. You may need to create this folder if it does not...]]></description><link>https://seankegel.com/git-quick-tip-using-a-global-gitignore</link><guid isPermaLink="true">https://seankegel.com/git-quick-tip-using-a-global-gitignore</guid><category><![CDATA[GitHub]]></category><category><![CDATA[Git]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Fri, 20 Oct 2023 14:14:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697811156624/61b9b416-0ca7-45de-aa0a-a71480e84a2f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Did you know Git supports a global <code>.gitignore</code> file that can be used across all of your local repositories?</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>To get started, let's create a file named <code>ignore</code> in the <code>~/.config/git</code> folder. You may need to create this folder if it does not already exist.</p>
<pre><code class="lang-bash">mkdir -p ~/.config/git &amp;&amp; touch ~/.config/git/ignore
</code></pre>
<p>In the file, we can add some patterns to ignore across all files.</p>
<pre><code class="lang-plaintext"># Ignore files generated by operating systems
.DS_Store
Thumbs.db
</code></pre>
<p>Git should automatically load this file. The patterns in the global file are merged with any other <code>.gitignore</code> files in your repositories.</p>
<p>If you wanted to put the file in another location, you would need to update the Git configuration to point to the file. For example, if I had a <code>~/Code/</code> folder where I store all of my projects, I could create my file there: <code>~/Code/.gitignore_global</code>. Then, I can run the following command to update my Git configuration.</p>
<pre><code class="lang-bash">git config --global core.excludesfile ~/Code/.gitignore_global
</code></pre>
<h2 id="heading-what-should-i-add-to-my-global-gitignore">What Should I Add to My Global Gitignore?</h2>
<p>What other types of files can be ignored globally? A lot of developers like to ignore the default editor/IDE folders like <code>.vscode</code> and <code>.idea</code>. This would depend on your project though, some projects commit some of the files in these folders.</p>
<p>You can also ignore package folders like <code>node_modules</code> and <code>vendor</code> for PHP Composer.</p>
<p>Generated files can also usually be ignored globally. These files usually exist in the <code>build/</code> or <code>dist/</code> folders.</p>
<p>Another great ignore to add is <code>.env</code>. These are used in projects of all types and usually contain sensitive information like API keys and passwords, so it's nice to have some extra protection to make sure you don't accidentally commit the file.</p>
<p>The global <code>.gitignore</code> can also support things specific to your workflow. For example, when working in JetBrains IDEs, I love using scratch files to store notes and pseudo code I might have when coding. If I am using something like Vim or VSCode though, then my scratch files aren't supported. So instead, I add <code>.scratch/</code> to me global <code>.gitignore</code> and then I can create markdown files or whatever else I might want there without it being committed. I also use <code>.http</code> to store JetBrains HTTP requests if I don't want them committed to the project. Read my post about the <a target="_blank" href="https://seankegel.com/simplify-api-testing-with-phpstorm-http-requests">PhpStorm HTTP client and requests</a> to learn more.</p>
<h2 id="heading-gitignore-example">Gitignore Example</h2>
<p>Below is my current global <code>.gitignore</code>:</p>
<pre><code class="lang-plaintext"># OS files
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
ehthumbs.db
Desktop.ini

# Editor Files
*.swp
*.swo
*.swn
*.swm
*.swl
*.swk
*.bak
*.backup
*.orig
*.ref
*~

# Logs
*.log
log.txt

# Generated files
build/
dist/

# Dependencies
node_modules/
vendor/

# Sensitive files
*.env
*.key
*.pem
*.credentials
*.password
*.secret

# Scratch files
.scratch/

# PhpStorm HTTP Requests
.http/
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Using a global <code>.gitignore</code> is helpful to make sure you aren't committing files that shouldn't be committed. Most of these items should already be in the project's <code>.gitignore</code>, however, that might not always be the case.</p>
<p>Also, I don't like adding items to a project's <code>.gitignore</code> that don't apply to all developers, like my <code>.http/</code> or <code>.scratch</code>. This is where a global <code>.gitignore</code> shines as it lets you add items relevant to your workflows.</p>
<p>For more information, you can refer to the GitHub documentation which has some great content including templates and links to the Git documentation.</p>
<ul>
<li><a target="_blank" href="https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files#configuring-ignored-files-for-all-repositories-on-your-computer">Ignoring files - GitHub Docs</a></li>
</ul>
<p>Let me know in the comments if you have any other helpful items to add to the global <code>.gitignore</code>. I would love to hear about any custom workflows you might have that having the global ignore helps support. Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Simplify API Testing with PhpStorm HTTP Requests]]></title><description><![CDATA[API testing is a critical aspect of modern software development, ensuring that the services we build and integrate work seamlessly. While developers often rely on dedicated tools like Postman and Insomnia, what many aren't aware of is that JetBrains'...]]></description><link>https://seankegel.com/simplify-api-testing-with-phpstorm-http-requests</link><guid isPermaLink="true">https://seankegel.com/simplify-api-testing-with-phpstorm-http-requests</guid><category><![CDATA[PHP]]></category><category><![CDATA[PhpStorm]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Postman]]></category><category><![CDATA[APIs]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sat, 14 Oct 2023 19:27:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309826635/b87e2785-6aa8-4e44-ab8d-9f336ea315b0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>API testing is a critical aspect of modern software development, ensuring that the services we build and integrate work seamlessly. While developers often rely on dedicated tools like Postman and Insomnia, what many aren't aware of is that JetBrains' IDEs, including PhpStorm, offer powerful built-in features for handling HTTP requests.</p>
<p>In this guide, I'll explore how PhpStorm's HTTP Client streamlines API testing and improves my development workflow right within the IDE. I'll delve into organizing requests, utilizing variables, and even importing and exporting requests. By the end, you'll have a solid grasp of how to leverage PhpStorm's capabilities for effective API testing.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>To keep things organized, I prefer to create a folder in my project called ".http". This allows me to store all my HTTP requests in a single place. To ensure that these requests don't get accidentally committed, I will add the ".http" folder to my global <code>.gitignore</code> file. Read my article about <a target="_blank" href="https://seankegel.com/git-quick-tip-using-a-global-gitignore">using a global gitignore</a> for more information. However, if I ever need to share these requests with my team, I can simply store them in a folder that is committed to the repository.</p>
<p>For this article, I will use the <a target="_blank" href="https://jsonplaceholder.typicode.com/">JSONPlaceholder - Free Fake REST API</a> website, which provides various API endpoints that can be used for testing purposes.</p>
<p>Once I have the ".http" folder in my project, I can easily create a new request file called "Posts.http". This allows me to organize all my Post-related HTTP requests in one place.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697308075005/7ec1051c-566e-484d-9632-4313542321aa.png" alt="Create an HTTP Request" class="image--center mx-auto" /></p>
<h2 id="heading-posts-requests">Posts Requests</h2>
<h3 id="heading-get-posts-list">Get Posts List</h3>
<p>In my "Posts.http" file, I create a request to list all posts. I can either type this request manually or use the UI to add it. With the UI, I will click on the plus <code>+</code> icon and select the request type.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697312556501/afbac4d8-ebcb-4188-b1a6-f716982a3f04.png" alt="Create a GET Request" class="image--center mx-auto" /></p>
<p>For listing posts, I will create a <code>GET</code> request. This will add the following content to the file:</p>
<pre><code class="lang-http"><span class="hljs-attribute">GET http://localhost:80/api/item?id=99
Accept</span>: application/json

<span class="python"><span class="hljs-comment">###</span></span>
</code></pre>
<p>I will update it to use the <a target="_blank" href="https://jsonplaceholder.typicode.com/">JSONPlaceholder - Free Fake REST API</a>. I will remove the <code>Accept</code> header for now.</p>
<pre><code class="lang-http"><span class="hljs-attribute">GET https://jsonplaceholder.typicode.com/posts

###</span>
</code></pre>
<p>Once the request is ready, it's time to run it. I click the "Play" button, and the results appear in the console below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697308783644/70a0378e-900e-412e-8e00-c4637192c667.png" alt="Run a Request" class="image--center mx-auto" /></p>
<h3 id="heading-get-post">Get Post</h3>
<p>To get a specific post, I can add a new request to the file. I can even add comments to describe the requests, either using <code>#</code> or <code>//</code>.</p>
<pre><code class="lang-http"># List Posts
<span class="hljs-attribute">GET https://jsonplaceholder.typicode.com/posts

###

# Get Post
GET https://jsonplaceholder.typicode.com/posts/1</span>
</code></pre>
<p>Also, I make sure to separate the requests with <code>###</code> which is required by the <code>.http</code> syntax.</p>
<h3 id="heading-create-post">Create Post</h3>
<p>To create a new post, I’ll add a <code>POST</code> request and a JSON body.</p>
<pre><code class="lang-http"># List Posts
<span class="hljs-attribute">GET https://jsonplaceholder.typicode.com/posts

###

# Get Post
GET https://jsonplaceholder.typicode.com/posts/1

###

# Create Post
POST https://jsonplaceholder.typicode.com/posts
Content-Type</span>: application/json

<span class="json">{
  <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Post title"</span>,
  <span class="hljs-attr">"body"</span>: <span class="hljs-string">"Post body"</span>,
  <span class="hljs-attr">"userId"</span>: <span class="hljs-number">1</span>
}</span>
</code></pre>
<p>It can also be helpful to add the <code>Content-Type</code> header to specify that JSON is being passed to the server.</p>
<h3 id="heading-update-post">Update Post</h3>
<p>Updating a post is similar to creating a post, but I’ll use a <code>PUT</code> request.</p>
<pre><code class="lang-http"># List Posts
<span class="hljs-attribute">GET https://jsonplaceholder.typicode.com/posts

###

# Get Post
GET https://jsonplaceholder.typicode.com/posts/1

###

# Create Post
POST https://jsonplaceholder.typicode.com/posts
Content-Type</span>: application/json

<span class="rego"><span class="hljs-punctuation">{
</span>  <span class="hljs-string">"title"</span>: <span class="hljs-string">"Post title"</span><span class="hljs-punctuation">,
</span>  <span class="hljs-string">"body"</span>: <span class="hljs-string">"Post body"</span><span class="hljs-punctuation">,
</span>  <span class="hljs-string">"userId"</span>: <span class="hljs-number">1</span>
<span class="hljs-punctuation">}
</span>
<span class="hljs-comment">###</span>

<span class="hljs-comment"># Update Post</span>
PUT https://<span class="hljs-variable">jsonplaceholder</span>.<span class="hljs-variable">typicode</span>.com/posts/<span class="hljs-number">1</span>
Content-Type: application/json

<span class="hljs-punctuation">{
</span>  <span class="hljs-string">"title"</span>: <span class="hljs-string">"New title"</span>
<span class="hljs-punctuation">}</span></span>
</code></pre>
<h3 id="heading-delete-post">Delete Post</h3>
<p>Finally, I can delete a post using a <code>DELETE</code> request.</p>
<pre><code class="lang-http"># List Posts
<span class="hljs-attribute">GET https://jsonplaceholder.typicode.com/posts

###

# Get Post
GET https://jsonplaceholder.typicode.com/posts/1

###

# Create Post
POST https://jsonplaceholder.typicode.com/posts
Content-Type</span>: application/json

<span class="rego"><span class="hljs-punctuation">{
</span>  <span class="hljs-string">"title"</span>: <span class="hljs-string">"Post title"</span><span class="hljs-punctuation">,
</span>  <span class="hljs-string">"body"</span>: <span class="hljs-string">"Post body"</span><span class="hljs-punctuation">,
</span>  <span class="hljs-string">"userId"</span>: <span class="hljs-number">1</span>
<span class="hljs-punctuation">}
</span>
<span class="hljs-comment">###</span>

<span class="hljs-comment"># Update Post</span>
PUT https://<span class="hljs-variable">jsonplaceholder</span>.<span class="hljs-variable">typicode</span>.com/posts/<span class="hljs-number">1</span>
Content-Type: application/json

<span class="hljs-punctuation">{
</span>    <span class="hljs-string">"title"</span>: <span class="hljs-string">"New title"</span>
<span class="hljs-punctuation">}
</span>
<span class="hljs-comment">###</span>

<span class="hljs-comment"># Delete Post</span>
DELETE https://<span class="hljs-variable">jsonplaceholder</span>.<span class="hljs-variable">typicode</span>.com/posts/<span class="hljs-number">1</span></span>
</code></pre>
<h2 id="heading-variables">Variables</h2>
<p>Variables are a powerful tool when it comes to managing complex API testing scenarios. PhpStorm offers various types of variables, starting with Environment Variables.</p>
<h3 id="heading-environment-variables">Environment Variables</h3>
<p>Environment variables can be used to store information such as usernames, passwords, authentication tokens, and more. They can also be used to support multiple environments. PhpStorm provides both public and private variables, which are merged before a request is made. To begin, create a public environment file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697308944939/5fcea05e-d021-4a08-bbc7-9e723f08c237.png" alt="Create a Public Environment File" class="image--center mx-auto" /></p>
<p>This creates the file below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697308960142/ee40791f-f1ca-45b4-9f3d-f6d1123eebbe.png" alt="Public Environment File" class="image--center mx-auto" /></p>
<p>I will update the file to store a dev and production environment with a host and authentication token.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"dev"</span>: {
        <span class="hljs-attr">"host"</span>: <span class="hljs-string">"&lt;https://jsonplaceholder.typicode.com&gt;"</span>,
        <span class="hljs-attr">"token"</span>: <span class="hljs-string">""</span>
    },
    <span class="hljs-attr">"prod"</span>: {
        <span class="hljs-attr">"host"</span>: <span class="hljs-string">"&lt;https://jsonplaceholder.typicode.com&gt;"</span>,
        <span class="hljs-attr">"token"</span>: <span class="hljs-string">""</span>
    }
}
</code></pre>
<p>I will leave the token empty for now and add it to the private environment file. I’ll create that now.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697308980325/1b67701a-19a8-4422-bea7-a9c48290e32f.png" alt="Create a Private Environment File" class="image--center mx-auto" /></p>
<p>In this file, I will add the authentication tokens. It can also store usernames and passwords depending on the server authentication.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"dev"</span>: {
        <span class="hljs-attr">"token"</span>: <span class="hljs-string">"DEVELOPMENT_TOKEN"</span>
    },
    <span class="hljs-attr">"prod"</span>: {
        <span class="hljs-attr">"token"</span>: <span class="hljs-string">"PRODUCTION_TOKEN"</span>
    }
}
</code></pre>
<p>Now, I can add these variables to the requests.</p>
<pre><code class="lang-http"># List Posts
<span class="hljs-attribute">GET {{host}}/posts
Authorization</span>: Bearer {{token}}

<span class="rego"><span class="hljs-comment">###</span>

<span class="hljs-comment"># Get Post</span>
GET <span class="hljs-punctuation">{{</span>host<span class="hljs-punctuation">}}</span>/posts/<span class="hljs-number">1</span>
Authorization: Bearer <span class="hljs-punctuation">{{</span>token<span class="hljs-punctuation">}}</span>

<span class="hljs-comment">###</span>

<span class="hljs-comment"># Create Post</span>
POST <span class="hljs-punctuation">{{</span>host<span class="hljs-punctuation">}}</span>/posts
Authorization: Bearer <span class="hljs-punctuation">{{</span>token<span class="hljs-punctuation">}}</span>
Content-Type: application/json

<span class="hljs-punctuation">{
</span>  <span class="hljs-string">"title"</span>: <span class="hljs-string">"Post title"</span><span class="hljs-punctuation">,
</span>  <span class="hljs-string">"body"</span>: <span class="hljs-string">"Post body"</span><span class="hljs-punctuation">,
</span>  <span class="hljs-string">"userId"</span>: <span class="hljs-number">1</span>
<span class="hljs-punctuation">}
</span>
<span class="hljs-comment">###</span>

<span class="hljs-comment"># Update Post</span>
PUT <span class="hljs-punctuation">{{</span>host<span class="hljs-punctuation">}}</span>/posts/<span class="hljs-number">1</span>
Authorization: Bearer <span class="hljs-punctuation">{{</span>token<span class="hljs-punctuation">}}</span>
Content-Type: application/json

<span class="hljs-punctuation">{
</span>    <span class="hljs-string">"title"</span>: <span class="hljs-string">"New title"</span>
<span class="hljs-punctuation">}
</span>
<span class="hljs-comment">###</span>

<span class="hljs-comment"># Delete Post</span>
DELETE <span class="hljs-punctuation">{{</span>host<span class="hljs-punctuation">}}</span>/posts/<span class="hljs-number">1</span>
Authorization: Bearer <span class="hljs-punctuation">{{</span>token<span class="hljs-punctuation">}}</span></span>
</code></pre>
<p>This approach saves me from having to repeat the full URL everywhere and works exceptionally well in scenarios where different environments require different hosts or tokens.</p>
<p>For APIs that have authorization, I can add an <code>Authorization</code> header with a value of <code>Bearer {{token}}</code>.</p>
<p>Before making a request, I will set the environment I want to use to replace my variables.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309067494/274055c6-f616-4e73-b223-23365362a180.png" alt="Select Environment" class="image--center mx-auto" /></p>
<h3 id="heading-per-request-variables">Per-Request Variables</h3>
<p>Per-request variables allow me to set variables before specific requests, offering flexibility in customization. Here's how I set a per-request variable for the "Get Post" request:</p>
<pre><code class="lang-http"># Get Post
&lt; {%
    request.variables.set("postId", "10")
%}
<span class="hljs-attribute">GET {{host}}/posts/{{postId}}
Authorization</span>: Bearer {{token}}
</code></pre>
<p>Now, when running the request, it will automatically replace <code>postId</code> in the URL with “10” from the <code>postId</code> variable. Note that request variables must be a string value.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309100591/75a70c8a-25ab-4347-92fb-65c88b370ccd.png" alt="Making a request with a pre-request variable" class="image--center mx-auto" /></p>
<p>This method is particularly useful when I need to adjust variables for specific requests without affecting global environment variables.</p>
<h3 id="heading-response-variables">Response Variables</h3>
<p>Response variables can be really powerful. They allow me to set a new variable based on the response of a request. For example, after fetching the list of posts, I can create a new <code>postId</code> variable that grabs the first <code>id</code> from the lists of posts.</p>
<pre><code class="lang-http"># List Posts
<span class="hljs-attribute">GET {{host}}/posts
Authorization</span>: Bearer {{token}}

<span class="solidity"><span class="hljs-operator">&gt;</span> {<span class="hljs-operator">%</span>
    client.global.set(<span class="hljs-string">"postId"</span>, response.body[<span class="hljs-number">0</span>].id);
<span class="hljs-operator">%</span>}</span>
</code></pre>
<p>Now, if I wanted to test updating the first post from the list, I can do the following:</p>
<pre><code class="lang-http"># Update Post
<span class="hljs-attribute">PUT {{host}}/posts/{{postId}}
Authorization</span>: Bearer {{token}}
<span class="hljs-attribute">Content-Type</span>: application/json

<span class="json">{
    <span class="hljs-attr">"title"</span>: <span class="hljs-string">"New title"</span>
}</span>
</code></pre>
<p>The <code>postId</code> is set from previously calling the “List Posts” request, so it will be set to “1”. I can test by running the “Update Post” request.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309141688/e6a87210-6c88-462c-b8bb-852a10b4d836.png" alt="Using a response variable" class="image--center mx-auto" /></p>
<p>When running the request, I can see in the console that the <code>postId</code> was replaced with “1” in the URL.</p>
<h3 id="heading-dynamic-variables">Dynamic Variables</h3>
<p>Dynamic variables are kind of like helper variables. They allow automatically generating common data on the fly, like UUIDs, random integers, timestamps, and more.</p>
<p>As an example, if I want to delete a random post, I can use the <code>$random.integer(1, 100)</code> dynamic variable. This will generate a random number between 1 and 100.</p>
<pre><code class="lang-http"># Delete Post
<span class="hljs-attribute">DELETE {{host}}/posts/{{$random.integer(1, 100)}}
Authorization</span>: Bearer {{token}}
</code></pre>
<p>When running the request, I see the following output, where the random number is 96.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309184012/6900eee1-e884-4fc8-804b-ece1ccbc2e31.png" alt="Using a dynamic variable" class="image--center mx-auto" /></p>
<p>For a complete list of available dynamic variables, refer to the <a target="_blank" href="https://www.jetbrains.com/help/phpstorm/exploring-http-syntax.html#dynamic-variables">documentation</a>.</p>
<h2 id="heading-importing-and-exporting-requests">Importing and Exporting Requests</h2>
<p>PhpStorm's built-in HTTP client simplifies importing and exporting requests, making it easy to transition my workflow. It handles importing cURL requests and even Postman collections.</p>
<h3 id="heading-curl">cURL</h3>
<p>With the following cURL code, I can import it right into a request file.</p>
<pre><code class="lang-bash">curl -X POST --location <span class="hljs-string">"&lt;https://jsonplaceholder.typicode.com/posts&gt;"</span> \\ 
-H <span class="hljs-string">"Authorization: Bearer DEVELOPMENT_TOKEN"</span> \\ 
-H <span class="hljs-string">"Content-Type: application/json"</span> \\ 
-d <span class="hljs-string">"{ \\"</span>title\\": \\"Post title\\", \\"body\\": \\"Post body\\", \\"userId\\": 1 }<span class="hljs-string">"</span>
</code></pre>
<p>To import, I click the import button and select the import type, cURL for this example.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309234404/7e9f9fff-2bff-4ea9-949e-8b05eabd1d81.png" alt="Import cURL Command" class="image--center mx-auto" /></p>
<p>This will bring up a new window to paste in the cURL code.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309257208/5ec75e99-f0f3-4799-9a2c-fabb93c2c181.png" alt="Convert cURL to HTTP Request" class="image--center mx-auto" /></p>
<p>After clicking convert, the cURL request is now added to the file as an HTTP request.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309271692/5080317d-6ec5-486d-b83f-78dccf0e78f8.png" alt="cURL Request added to HTTP Request File" class="image--center mx-auto" /></p>
<p>For exporting requests, I simply click the export button, select the environment to use for variable conversion, and the cURL code for the request is copied automatically.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309288169/0e800fd0-7b54-4bd9-beaf-4a53fc227a0a.png" alt="Export to cURL" class="image--center mx-auto" /></p>
<h3 id="heading-postman">Postman</h3>
<p>If I'm transitioning from Postman, JetBrains offers a plugin that allows me to import Postman collections into PhpStorm's HTTP requests. This plugin can ease the migration process.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697309306174/927f3c32-569a-49ad-9679-a936f9412f11.png" alt="Import from Postman" class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>PhpStorm's built-in HTTP client and requests functionality offers an array of features that have significantly enhanced my API testing process. Beyond simplifying basic requests, I can execute JavaScript logic within <code>&lt; {% %}</code> blocks, handle cookies, and even accommodate GraphQL requests. This all-in-one approach saves me the hassle of switching between different applications like Postman or Insomnia.</p>
<p>For more in-depth information and additional features, consult the PhpStorm documentation:</p>
<ul>
<li><p><a target="_blank" href="https://www.jetbrains.com/help/phpstorm/http-client-in-product-code-editor.html">HTTP Client | PhpStorm</a></p>
</li>
<li><p><a target="_blank" href="https://www.jetbrains.com/help/phpstorm/exploring-http-syntax.html">Exploring the HTTP request syntax | PhpStorm</a></p>
</li>
</ul>
<p>Thank you for joining me on this journey exploring PhpStorm’s HTTP client and requests!</p>
]]></content:encoded></item><item><title><![CDATA[PhpStorm with Docker]]></title><description><![CDATA[I have come across developers on social media and at various jobs having trouble setting up their editors when Docker is involved. In this article, I will go over PhpStorm and how to use it properly with Docker. When using Docker, it is usually best ...]]></description><link>https://seankegel.com/phpstorm-with-docker</link><guid isPermaLink="true">https://seankegel.com/phpstorm-with-docker</guid><category><![CDATA[Laravel]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Docker]]></category><category><![CDATA[PhpStorm]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Mon, 09 Oct 2023 01:07:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696812240387/a0cc8374-3bf3-43dc-bbbd-8a641069bdd1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have come across developers on social media and at various jobs having trouble setting up their editors when Docker is involved. In this article, I will go over <a target="_blank" href="https://www.jetbrains.com/phpstorm/">PhpStorm</a> and how to use it properly with Docker. When using Docker, it is usually best to run various scripts and binaries from the Docker container versus running locally. This includes linters, formatters, and even tests.</p>
<p>In this article, I will use a <a target="_blank" href="https://laravel.com">Laravel</a> application with <a target="_blank" href="https://laravel.com/docs/10.x/sail">Laravel Sail</a> as an example for configuration PhpStorm.</p>
<h2 id="heading-laravel-sail-installation">Laravel Sail Installation</h2>
<p>Use the following command to spin up a new Laravel application with Sail:</p>
<pre><code class="lang-bash">curl -s <span class="hljs-string">"https://laravel.build/phpstorm-docker"</span> | bash
</code></pre>
<p>This can take several minutes to complete. Once it is done, run <code>sail up</code> to start the application.</p>
<pre><code class="lang-bash">/vendor/bin/sail up
</code></pre>
<blockquote>
<p><strong>Quick Tip:</strong> Add a relative folder to your PATH to enable calling binaries from Composer and Node by using the name of the binary instead of the whole path. In your <code>.zshrc</code> or <code>.bashrc</code>, add the following:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> PATH=<span class="hljs-string">"./vendor/bin:<span class="hljs-variable">$PATH</span>"</span>
<span class="hljs-built_in">export</span> PATH=<span class="hljs-string">"./node_modules/.bin:<span class="hljs-variable">$PATH</span>"</span>
</code></pre>
<p>Then, instead of calling <code>/vendor/bin/sail up</code>, it is now possible to call <code>sail up</code>!</p>
</blockquote>
<h2 id="heading-set-the-phpstorm-php-interpreter">Set the PhpStorm PHP Interpreter</h2>
<p>Open the settings window and go to the PHP section. From there, click the <code>...</code> next to the CLI Interpreter box.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696774879746/9806bb79-1d61-4dff-956c-e4163c988ae3.png" alt="Add new interpreter" class="image--center mx-auto" /></p>
<p>You may have a local PHP interpreter set up. Now you’ll want to add the Docker interpreter. Use the <code>+</code> in the top left and select the “From Docker, Vagrant, VM, WSL, Remote…” option.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696775010440/29e7720a-4763-438f-b5fb-663fe291a668.png" alt="Add new interpreter" class="image--center mx-auto" /></p>
<p>In the next popup, select the “Docker Compose” option. This shows a list of available services included in the <code>docker-compose.yml</code> file. Select the <code>laravel.test</code> service, which is the PHP container for Sail.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696775053596/6c9f8137-b85a-4734-8dcf-9c750278ba50.png" alt="Select service from Docker Compose file" class="image--center mx-auto" /></p>
<p>Now, the new PHP interpreter should be set as the default for the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696775102245/e028817c-e013-4f2d-931f-2dc9c341cf78.png" alt="New interpreter set" class="image--center mx-auto" /></p>
<p>With the interpreter setup, let’s look at running <a target="_blank" href="https://laravel.com/docs/10.x/pint">Laravel Pint</a> to format files.</p>
<h2 id="heading-running-linters-and-formatters-in-docker-container">Running Linters and Formatters in Docker Container</h2>
<p>For this article, I will look at running Laravel Pint. However, this same method can be used for PHP CS Fixer, PHPStan, and other quality tools. Later, I will also go through how to do this with Node and tools like Prettier.</p>
<h3 id="heading-laravel-pint">Laravel Pint</h3>
<p>To start:</p>
<ol>
<li><p>Go to Settings &gt; PHP &gt; Quality Tools.</p>
</li>
<li><p>Expand the Laravel Pint section and turn on the Pint.</p>
</li>
<li><p>Set the configuration to the interpreter pointing to the Docker container, in my case, it is <code>laravel.test</code>.</p>
</li>
<li><p>Click the three dots <code>...</code> to set up Pint for the interpreter.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696776066029/746333f8-d191-4495-85e1-1f86f7b08800.png" alt="Set up Laravel Pint" class="image--center mx-auto" /></p>
<p>After clicking the three dots <code>...</code>, a new window opens to allow you to add a new Pint configuration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696776238014/ff68af35-b02d-4108-b685-1f0961330e6f.png" alt="Create a new Laravel Pint configuration" class="image--center mx-auto" /></p>
<ol>
<li><p>Click the plus icon <code>+</code> in the top left.</p>
</li>
<li><p>Select the interpreter pointing to the Docker container.</p>
</li>
</ol>
<p>After selecting the interpreter and hitting "OK", the configuration should be created. The next step is to set the path and validate.</p>
<ol>
<li><p>In the "Laravel Pint path" field, add <code>vendor/bin/pint</code> to point to the Pint binary in the Composer vendor directory.</p>
</li>
<li><p>Click the "Validate" button.</p>
</li>
<li><p>If everything is correct, the Pint version should be shown at the bottom.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696776365332/35d2624c-20b9-4995-ae90-ce2f5c04d0e4.png" alt="Set Laravel Pint path" class="image--center mx-auto" /></p>
<p>With this set, Pint needs to be set as the external formatter. Back in Settings &gt; PHP &gt; Quality Tools, scroll to the bottom and click the radio for "Laravel Pint" in the "External formatters" section.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696776592747/4a4428a4-7cb4-40d1-a140-bcae085d6db6.png" alt="Set Pint as the external formatter" class="image--center mx-auto" /></p>
<p>Now, the code can be reformatted using Laravel Pint from the Docker container.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696777137595/0026f298-f7e7-4784-9031-3172e6cc26b0.gif" alt="Reformat code with Docker Laravel Pint" class="image--center mx-auto" /></p>
<p>In the example above, the incorrect code is underlined with a squiggly line because it does not follow the formatting set by Laravel Pint. After formatting, the class and method braces are fixed, braces are added to the conditional, and a blank line is added above the return statement.</p>
<p>To make this even easier, enable formatting on save. To accomplish this, go into Settings &gt; Tools &gt; Actions on Save and check the box for "Reformat Code".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696801057836/73d22701-933f-4498-abee-7b4a82c77688.png" alt="Reformat code on save" class="image--center mx-auto" /></p>
<p>Now, anytime a file is saved, it will automatically be formatted by Laravel Pint, running on the Docker container.</p>
<h3 id="heading-prettier">Prettier</h3>
<p>First, install Prettier using the following command:</p>
<pre><code class="lang-bash">sail npm install --save-dev --save-exact prettier
</code></pre>
<p>Notice the command was prefixed with <code>sail</code>, which installs Prettier using the version of Node and npm on the Docker container versus the local version.</p>
<p>Next, similar to PHP, create a Node interpreter that points at the Docker version. Go to Settings &gt; Languages &amp; Frameworks &gt; Node.js. From there, click the three dots <code>...</code> next to "Node interpreter".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696801557390/96af3951-75d3-4898-8f9a-e471b6535888.png" alt="Create a new Node interpreter" class="image--center mx-auto" /></p>
<p>In the new window, click the plus <code>+</code> button in the top left. Then select "Add Remote...".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696801687370/72cc36d3-1f82-4942-8f09-560b5f66d356.png" alt="Add remote Node interpreter" class="image--center mx-auto" /></p>
<p>Just like before with PHP, select the "Docker Compose" option and select the <code>laravel.test</code> service.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696801769008/54b70de0-7051-4dc0-b3d4-a2c91e832398.png" alt="Configure the Node.js Remote Interpreter" class="image--center mx-auto" /></p>
<p>Node is now pointing at the version in the Docker container. To continue with Prettier, go to Settings &gt; Languages &amp; Frameworks &gt; JavaScript &gt; Prettier.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696802342441/91ba5efe-9a6d-4e60-812f-4076d2e13a37.png" alt="Set Prettier configuration" class="image--center mx-auto" /></p>
<p>In the Prettier settings:</p>
<ol>
<li><p>Select "Automatic Prettier configuration".</p>
</li>
<li><p>Check "Run on save" for automatic formatting when saving a file.</p>
</li>
</ol>
<p>Now, Prettier is all setup and running right from the Docker container. These same steps can be followed to run ESLint.</p>
<h2 id="heading-running-tests-from-docker-container">Running Tests from Docker Container</h2>
<p>For this article, I will be using PHPUnit. However, the same steps will also apply to Pest and JavaScript test runners like Jest.</p>
<p>Since <code>laravel.test</code> is already set as the default interpreter for the project, the tests should automatically run using PHPUnit from the Docker container. This can be seen by running a test:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696804796986/f1f35826-d0f3-42b7-a548-982c2c2da50e.png" alt class="image--center mx-auto" /></p>
<p>After running the test, the test output shows the command. You can see it was run using <code>docker-compose</code> pointing at the <code>laravel.test</code> service of the local <code>docker-compose.yml</code> file.</p>
<p>If, for some reason, this is not working, maybe an existing project with other configurations, or an alternate test framework, you can continue with the next steps.</p>
<p>Go into Settings &gt; PHP &gt; Test Frameworks. Then, click the plus <code>+</code> button to add a new test framework.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696803590527/eec22fdf-a032-4600-aea9-a3c93d766d49.png" alt="Create a remote test framework" class="image--center mx-auto" /></p>
<p>Select the <code>laravel.test</code> interpreter from the new window:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696803641688/0058af7f-f531-44df-9202-7ef6297f1b60.png" alt="PHPUnit remote interpreter" class="image--center mx-auto" /></p>
<p>Now, a new PHPUnit Test Framework is available.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696803723916/13b5936b-9003-439e-b407-8b98e54ad531.png" alt="Remote PHPUnit test framework" class="image--center mx-auto" /></p>
<p>Next, modify the run configurations by going to the Run menu and selecting "Edit Configurations...".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696803951324/957e9597-fe4e-46f6-9d47-4b579ce699a9.png" alt="Edit run configurations" class="image--center mx-auto" /></p>
<p>From the new window, click the gear icon next to the "Use alternative configuration file" field.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696804147917/2bbf0a8b-b37a-419f-a4c2-8a5909b14535.png" alt="Set PHPUnit test framework configuration" class="image--center mx-auto" /></p>
<p>This opens a new window to select the new remote PHPUnit configuration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696804235573/fe43f1a3-4515-4242-8bc6-b33e91cab544.png" alt="Set remote interpreter for tests" class="image--center mx-auto" /></p>
<p>Finally, make sure the configuration is pointing to the <code>laravel.test</code> interpreter or the default interpreter if it is set as <code>laravel.test</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696805186731/558c429e-3012-4c0f-87a3-71a9ddc420fd.png" alt="Set remote interpreter for tests" class="image--center mx-auto" /></p>
<p>That's it! Everything should now be set up to run PHPUnit from the Docker container.</p>
<h2 id="heading-package-managers">Package Managers</h2>
<p>You can even run your package managers like Composer and npm from the Docker container.</p>
<h3 id="heading-composer">Composer</h3>
<p>Go to Settings &gt; PHP &gt; Composer, then:</p>
<ol>
<li><p>Select the radio for "Remote Interpreter".</p>
</li>
<li><p>Select <code>laravel.test</code> as the "CLI Interpreter".</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696810721973/96ce63fc-5b33-4753-acec-d431cb1d4e35.png" alt="Run Composer from Remote Interpreter" class="image--center mx-auto" /></p>
<p>With Composer set, go to Tools &gt; Composer, and select a command that will be run from the Docker container.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696810872956/d01a15ed-8e00-4281-b0ca-1eef50ca135e.png" alt="Composer commands in PhpStorm" class="image--center mx-auto" /></p>
<h3 id="heading-npm">npm</h3>
<p>For npm, the remote Node interpreter should have already been set up from the Prettier steps above. Now, open the npm tool window.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696811020500/7f0789bc-dacb-4edd-a0b2-59f16091b93f.png" alt="npm Tool window" class="image--center mx-auto" /></p>
<p>The npm tool window will show all the scripts configured in the <code>package.json</code> file. Right-click on the <code>dev</code> script and click "Edit 'dev' Settings...".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696811146605/664d386c-75f4-4628-ba2e-a266477463a9.png" alt="Edit npm configurations" class="image--center mx-auto" /></p>
<p>With the new window open:</p>
<ol>
<li><p>Set the "Node interpreter" to the Docker compose <code>laravel.test</code> interpreter if it is not already set.</p>
</li>
<li><p>Add a new environment variable: <code>WWWUSER=sail</code>. This prevents PhpStorm from trying to run the npm commands as the <code>npm</code> user.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696811259162/5478493f-0c36-4211-8e58-f7656ebb80f4.png" alt="npm run configuration" class="image--center mx-auto" /></p>
<p>Now, the <code>dev</code> script can be run from the npm tool window and the output is shown in PhpStorm.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696811486201/26d25e91-60de-424b-82b6-1890f3cfcc65.png" alt="npm script running in PhpStorm" class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope this is helpful for anyone working on a project with a Docker configuration. Though running these scripts and binaries from Docker can be slower than running locally, it does have the benefit of making sure all the developers on the project are running the same versions. This should help prevent the infamous "it runs on my computer" type issues. By using Docker, these various languages, scripts, and binaries also do not need to be installed locally. I can have all of this running without even having Node installed on my local machine.</p>
<p>Thanks for reading! Please let me know if you have any questions in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[Laravel Data and Value Objects]]></title><description><![CDATA[Recently, I was presented with a problem using value objects with the Laravel Data package by Spatie. I have been trying to use value objects a lot more in my code for things like money, emails, phone numbers, etc. When I am working with data from an...]]></description><link>https://seankegel.com/laravel-data-and-value-objects</link><guid isPermaLink="true">https://seankegel.com/laravel-data-and-value-objects</guid><category><![CDATA[Laravel]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[value objects]]></category><category><![CDATA[laravel-data]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sat, 30 Sep 2023 20:07:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696642038148/9aacbc06-5906-49b3-88bf-c91fcd40bfd6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377795511/45b4b0d5-56dc-4336-a1ac-50f151e3ee0e.png" alt="Laravel Data and Value Objects" /></p>
<p>Recently, I was presented with a problem using value objects with the <a target="_blank" href="https://spatie.be/docs/laravel-data/v3/introduction">Laravel Data</a> package by Spatie. I have been trying to use value objects a lot more in my code for things like money, emails, phone numbers, etc. When I am working with data from an external API, it is very helpful to convert this data to value objects when I can.</p>
<p>If you haven’t been using value objects or data transfer objects, here are some helpful articles to learn more:</p>
<ul>
<li><p><a target="_blank" href="https://martinjoo.dev/value-objects-everywhere">Value Objects Everywhere — Martin Joo</a></p>
</li>
<li><p><a target="_blank" href="https://matthiasnoback.nl/2022/09/is-it-a-dto-or-a-value-object/">Is it a DTO or a Value Object — Matthias Noback</a></p>
</li>
<li><p><a target="_blank" href="https://www.conroyp.com/articles/building-resilient-code-harnessing-the-power-of-value-objects">Building Resilient Code: Harnessing the Power of Value Objects</a></p>
</li>
</ul>
<p>When using my own custom data transfer objects, I can create <code>fromArray</code> and <code>toArray</code> methods to automatically instantiate these value objects. However, Laravel Data provides a lot of nice features out of the box that can help reduce some of the boilerplate code in my data transfer objects. The problem is, I didn’t know the best ways to use Laravel Data to instantiate my value objects. I knew of some of the various features of Laravel Data, like casts and transformers, but had never used them, until now.</p>
<p>In my project, I receive order data from an API. The order data that comes into the application might look something like the following:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"id"</span>: <span class="hljs-number">123</span>,
    <span class="hljs-attr">"user_id"</span>: <span class="hljs-number">345</span>,
    <span class="hljs-attr">"product_id"</span>: <span class="hljs-number">678</span>,
    <span class="hljs-attr">"amount"</span>: <span class="hljs-string">"10.99"</span>,
    <span class="hljs-attr">"status"</span>: <span class="hljs-string">"success"</span>,
    <span class="hljs-attr">"processed_at"</span>: <span class="hljs-string">"2023-09-30T10:00:00+00:00"</span>,
    <span class="hljs-attr">"created_at"</span>: <span class="hljs-string">"2023-09-28T10:00:00+00:00"</span>,
    <span class="hljs-attr">"updated_at"</span>: <span class="hljs-string">"2023-09-30T10:00:00+00:00"</span>,
}
</code></pre>
<p>To model this in Laravel Data, I could have a class like the following:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $user_id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $product_id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $amount,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $status,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $processed_at,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $created_at,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $updated_at,
    </span>) </span>{}
}
</code></pre>
<p>This will map the data from the API fine, but it can be a lot better. The first thing that jumps out to me is the <code>amount</code> comes in as a string. In my application, I typically prefer to deal with monetary values as cents using integers. However, maybe I have another external service that is expecting monetary values to be passed as a float. This is a great case for using a value object so I am not constantly doing these conversions all over the application.</p>
<p>Here’s a simple example of what my <code>Currency</code> value object might look:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Currency</span>
</span>{
    <span class="hljs-keyword">public</span> readonly <span class="hljs-keyword">string</span> $display;
    <span class="hljs-keyword">public</span> readonly <span class="hljs-keyword">int</span> $cents;
    <span class="hljs-keyword">public</span> readonly <span class="hljs-keyword">float</span> $dollars;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> readonly mixed $value,
    </span>)
    </span>{
        match (<span class="hljs-literal">true</span>) {
            is_int($value) =&gt; <span class="hljs-keyword">$this</span>-&gt;cents = $value,
            is_float($value) =&gt; <span class="hljs-keyword">$this</span>-&gt;cents = <span class="hljs-keyword">$this</span>-&gt;floatToCents($value),
            is_string($value) =&gt; <span class="hljs-keyword">$this</span>-&gt;cents = <span class="hljs-keyword">$this</span>-&gt;stringToCents($value),
            <span class="hljs-keyword">default</span> =&gt; <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">InvalidArgumentException</span>(<span class="hljs-string">'Invalid value for Currency'</span>),
        };

        <span class="hljs-keyword">$this</span>-&gt;dollars = <span class="hljs-keyword">$this</span>-&gt;cents / <span class="hljs-number">100</span>;
        <span class="hljs-keyword">$this</span>-&gt;display = number_format(<span class="hljs-keyword">$this</span>-&gt;dollars, <span class="hljs-number">2</span>);
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">floatToCents</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $value</span>): <span class="hljs-title">int</span>
    </span>{
        <span class="hljs-keyword">return</span> (<span class="hljs-keyword">int</span>) (round($value, <span class="hljs-number">2</span>) * <span class="hljs-number">100</span>);
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">stringToCents</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $value</span>): <span class="hljs-title">int</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;floatToCents((<span class="hljs-keyword">float</span>) $value);
    }
}
</code></pre>
<p>The <code>Currency</code> class can accept an integer, string, or float value, and convert as needed into a cents integer. However, it also gives me the option to get a dollar float value or even a display string. It also has some built-in validation to make sure any other value that might be passed into this class will throw an exception. This is just a simple example and in a normal application, you might also be tracking the type of currency or need some additional validation, but this will work for my purposes right now.</p>
<p>So now, I can update my <code>OrderData</code> class to the following:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $user_id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $product_id,
        <span class="hljs-keyword">public</span> Currency $amount,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $status,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $processed_at,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $created_at,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $updated_at,
    </span>) </span>{}
}
</code></pre>
<p>Now the <code>$amount</code> is a <code>Currency</code> type. However, Laravel Data does not know how to instantiate this object. This is where a cast comes into play. A cast in Laravel Data is used to convert simple API data into a complex object. To create this in Laravel Data, I need a class that implements the <code>Spatie\LaravelData\Casts\Cast</code> interface, which looks like the following:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Cast</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">cast</span>(<span class="hljs-params">DataProperty $property, mixed $value, <span class="hljs-keyword">array</span> $context</span>): <span class="hljs-title">mixed</span></span>;
}
</code></pre>
<p>The <code>$property</code> parameter is an object that represents the property on the Laravel Data object and stores various information about the property. You can read more <a target="_blank" href="https://spatie.be/docs/laravel-data/v3/advanced-usage/internal-structures#content-dataproperty">here</a>. The <code>$value</code> parameter is the value that is being passed into the Laravel data object for the property, in my case, this will be the money string <code>"10.99"</code>. Finally, the <code>$context</code> array is an array of the rest of the data being passed into the data object.</p>
<p>A cast implementation for my <code>Currency</code> object looks like the following:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CurrencyCast</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Cast</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">cast</span>(<span class="hljs-params">DataProperty $property, mixed $value, <span class="hljs-keyword">array</span> $context</span>): <span class="hljs-title">Currency</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Currency($value);
    }
}
</code></pre>
<p>Pretty simple right? I just need to return a new Currency object by passing the $value to it. To make this work with my data object, I can use a property attribute:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $user_id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $product_id,
        #[WithCast(<span class="hljs-params">CurrencyCast::<span class="hljs-keyword">class</span></span>)]
        <span class="hljs-keyword">public</span> Currency $amount,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $status,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $processed_at,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $created_at,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $updated_at,
    </span>) </span>{}
}
</code></pre>
<p>Now, any time my <code>OrderData</code> object is created, instead of just having a string value for <code>$amount</code>, I now have a much more helpful <code>Currency</code> object.</p>
<p>This can still be improved though! This data object has three different date strings and I’d prefer to use those as a <code>Carbon</code> object in Laravel. You can think of a <code>Carbon</code> date as a value object and I want to cast my various dates to that. The good news, this comes out of the box in Laravel Data, all I need to do is update the types in my object.</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $user_id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $product_id,
        #[WithCast(<span class="hljs-params">CurrencyCast::<span class="hljs-keyword">class</span></span>)]
        <span class="hljs-keyword">public</span> Currency $amount,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $status,
        <span class="hljs-keyword">public</span> Carbon $processed_at,
        <span class="hljs-keyword">public</span> Carbon $created_at,
        <span class="hljs-keyword">public</span> Carbon $updated_at,
    </span>) </span>{}
}
</code></pre>
<p>Now, if I create a new data object, I have <code>Carbon</code> instances instead of strings.</p>
<pre><code class="lang-php">$data = OrderData::from([
    <span class="hljs-string">'id'</span> =&gt; <span class="hljs-number">123</span>,
    <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-number">345</span>,
    <span class="hljs-string">'product_id'</span> =&gt; <span class="hljs-number">678</span>,
    <span class="hljs-string">'amount'</span> =&gt; <span class="hljs-string">"10.99"</span>,
    <span class="hljs-string">'status'</span> =&gt; <span class="hljs-string">"success"</span>,
    <span class="hljs-string">'processed_at'</span> =&gt; <span class="hljs-string">'2023-09-30T10:00:00+00:00'</span>,
    <span class="hljs-string">'created_at'</span> =&gt; <span class="hljs-string">'2023-09-28T10:00:00+00:00'</span>,
    <span class="hljs-string">'updated_at'</span> =&gt; <span class="hljs-string">'2023-09-30T10:00:00+00:00'</span>,
]);

$data-&gt;processed_at::class;
<span class="hljs-comment">// "Carbon\Carbon"</span>
</code></pre>
<p>You might be wondering how this works since I didn’t use a cast anywhere. As I mentioned, this is built-in with Laravel Data and it is handled in the configuration file using a global cast.</p>
<pre><code class="lang-php"><span class="hljs-comment">// /app/config/data.php</span>

<span class="hljs-keyword">return</span> [
    ...
    <span class="hljs-comment">/*
     * Global casts will cast values into complex types when creating a data
     * object from simple types.
     */</span>
    <span class="hljs-string">'casts'</span> =&gt; [
        DateTimeInterface::class =&gt; Spatie\LaravelData\Casts\DateTimeInterfaceCast::class,
        BackedEnum::class =&gt; Spatie\LaravelData\Casts\EnumCast::class,
    ],
    ...
];
</code></pre>
<p>When Laravel Data runs across a complex type, it will first check if a <code>Cast</code> has been configured in the object definition, and if not, it will attempt to fall back to the global casts. For <code>Carbon</code>, this is the <code>DateTimeInterfaceCast</code>. If Laravel Data sees a property that has a type that implements the <code>DateTimeInterface</code>, which <code>Carbon</code> does, it will attempt to cast the value of that property to the type specified.</p>
<p>Now, imagine I have many other data transfer objects that might contain monetary values, which could be integers, strings, or floats. Instead of explicitly adding the cast attribute in each data transfer object, it can instead be added to the global casts array.</p>
<pre><code class="lang-php"><span class="hljs-keyword">return</span> [
  ...
  <span class="hljs-comment">/*
   * Global casts will cast values into complex types when creating a data
   * object from simple types.
   */</span>
  <span class="hljs-string">'casts'</span> =&gt; [
      DateTimeInterface::class =&gt; Spatie\LaravelData\Casts\DateTimeInterfaceCast::class,
      BackedEnum::class =&gt; Spatie\LaravelData\Casts\EnumCast::class,
      \App\ValueObjects\Currency::class =&gt; \App\Data\Casts\CurrencyCast::class,
  ],
  ...
];
</code></pre>
<p>With the global cast set, the <code>OrderData</code> object no longer needs the cast attribute:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $user_id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $product_id,
        <span class="hljs-keyword">public</span> Currency $amount,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $status,
        <span class="hljs-keyword">public</span> Carbon $processed_at,
        <span class="hljs-keyword">public</span> Carbon $created_at,
        <span class="hljs-keyword">public</span> Carbon $updated_at,
    </span>) </span>{}
}
</code></pre>
<p>Though not necessarily a value object, the <code>$status</code> can also be improved here. Let’s say status can be one of three values, “pending”, “success”, or “failed”. This is a perfect case for an enum in PHP which could look like the following:</p>
<pre><code class="lang-php">enum OrderStatus: <span class="hljs-keyword">string</span>
{
    <span class="hljs-keyword">case</span> PENDING = <span class="hljs-string">'pending'</span>;
    <span class="hljs-keyword">case</span> SUCCESS = <span class="hljs-string">'success'</span>;
    <span class="hljs-keyword">case</span> FAILED = <span class="hljs-string">'failed'</span>;
}
</code></pre>
<p>To handle this in the Laravel Data object, I just need to update the type:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $user_id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $product_id,
        <span class="hljs-keyword">public</span> Currency $amount,
        <span class="hljs-keyword">public</span> OrderStatus $status,
        <span class="hljs-keyword">public</span> Carbon $processed_at,
        <span class="hljs-keyword">public</span> Carbon $created_at,
        <span class="hljs-keyword">public</span> Carbon $updated_at,
    </span>) </span>{}
}
</code></pre>
<p>Similar to the <code>Carbon</code> casts, Laravel Data has built-in support for casting to enums using <code>Spatie\LaravelData\Casts\EnumCast::class</code>.</p>
<p>I’ve covered using casts in Laravel Data, now I will move on to transformers. A transformer is essentially the opposite of a cast. A transformer takes a complex object and converts it to simple values to pass to JSON.</p>
<p>In my <code>OrderData</code> example, if I wanted to pass the data to another API, I probably don’t want to pass <code>Currency</code> or <code>Carbon</code> objects. When I convert my <code>OrderData</code> instance to JSON, I get something like the following:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"id"</span>: <span class="hljs-number">123</span>,
    <span class="hljs-attr">"user_id"</span>: <span class="hljs-number">345</span>,
    <span class="hljs-attr">"product_id"</span>: <span class="hljs-number">678</span>,
    <span class="hljs-attr">"amount"</span>: {
        <span class="hljs-attr">"display"</span>: <span class="hljs-string">"$10.99"</span>,
        <span class="hljs-attr">"cents"</span>: <span class="hljs-number">1099</span>,
        <span class="hljs-attr">"dollars"</span>: <span class="hljs-number">10.99</span>,
        <span class="hljs-attr">"value"</span>: <span class="hljs-string">"10.99"</span>
    },
    <span class="hljs-attr">"status"</span>: <span class="hljs-string">"success"</span>,
    <span class="hljs-attr">"processed_at"</span>: <span class="hljs-string">"2023-09-30T10:00:00+00:00"</span>,
    <span class="hljs-attr">"created_at"</span>: <span class="hljs-string">"2023-09-28T10:00:00+00:00"</span>,
    <span class="hljs-attr">"updated_at"</span>: <span class="hljs-string">"2023-09-30T10:00:00+00:00"</span>
}
</code></pre>
<p>Some good news and bad news. Like the built-in casts, Laravel Data has built-in transformers for <code>BackedEnum</code> and <code>DateTimeInterface</code> objects, so my <code>$status</code> field and various date fields have been converted to strings. However, my <code>$amount</code> field is incompatible with the API I am calling. I need that data back into a string, so I need a custom transformer class.</p>
<p>To create the transformer, I need to use the <code>Spatie\LaravelData\Transformers\Transformer</code> interface:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Transformer</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transform</span>(<span class="hljs-params">DataProperty $property, mixed $value</span>): <span class="hljs-title">mixed</span></span>;
}
</code></pre>
<p>So, for my <code>Currency</code> object, a transformer could look like the following:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CurrencyTransformer</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Transformer</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transform</span>(<span class="hljs-params">DataProperty $property, mixed $value</span>): <span class="hljs-title">string</span>
    </span>{
        <span class="hljs-keyword">return</span> $value-&gt;display;
    }
}
</code></pre>
<p>With that in place, I can add an attribute to my <code>OrderData</code> class.</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Data</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $user_id,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> $product_id,
        #[WithTransformer(<span class="hljs-params">CurrencyTransformer::<span class="hljs-keyword">class</span></span>)]
        <span class="hljs-keyword">public</span> Currency $amount,
        <span class="hljs-keyword">public</span> OrderStatus $status,
        <span class="hljs-keyword">public</span> Carbon $processed_at,
        <span class="hljs-keyword">public</span> Carbon $created_at,
        <span class="hljs-keyword">public</span> Carbon $updated_at,
    </span>) </span>{}
}
</code></pre>
<p>Now, when converting to JSON, my output looks like the following:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"id"</span>: <span class="hljs-number">123</span>,
    <span class="hljs-attr">"user_id"</span>: <span class="hljs-number">345</span>,
    <span class="hljs-attr">"product_id"</span>: <span class="hljs-number">678</span>,
    <span class="hljs-attr">"amount"</span>: <span class="hljs-string">"10.99"</span>,
    <span class="hljs-attr">"status"</span>: <span class="hljs-string">"success"</span>,
    <span class="hljs-attr">"processed_at"</span>: <span class="hljs-string">"2023-09-30T10:00:00+00:00"</span>,
    <span class="hljs-attr">"created_at"</span>: <span class="hljs-string">"2023-09-28T10:00:00+00:00"</span>,
    <span class="hljs-attr">"updated_at"</span>: <span class="hljs-string">"2023-09-30T10:00:00+00:00"</span>
}
</code></pre>
<p>Just like global casts, global transformers can be configured as well.</p>
<p>I hope this article for learning how to use casts and transformers in Laravel Data to work with value objects. Refer to the documentation for more information:</p>
<ul>
<li><p><a target="_blank" href="https://spatie.be/docs/laravel-data/v3/advanced-usage/creating-a-transformer">Creating a transformer | laravel-data</a></p>
</li>
<li><p><a target="_blank" href="https://spatie.be/docs/laravel-data/v3/advanced-usage/creating-a-cast">Creating a cast | laravel-data</a></p>
</li>
</ul>
<p>Laravel Data is an extremely useful package and is very flexible to support whatever needs may arise. To learn more, I recommend looking into <a target="_blank" href="https://spatie.be/docs/laravel-data/v3/advanced-usage/pipeline">pipelines</a> for Laravel Data as a next step.</p>
<p>Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Quick Tip: Use Node to Create Random Strings]]></title><description><![CDATA[How many times have you worked on an application and you needed a random alpha-numeric string? Did you try to provide a list of numbers and characters in a string and pass in a length to randomly generate a string with a loop?
Using Node.js, this can...]]></description><link>https://seankegel.com/quick-tip-use-node-to-create-random-strings</link><guid isPermaLink="true">https://seankegel.com/quick-tip-use-node-to-create-random-strings</guid><category><![CDATA[Node.js]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[cli]]></category><category><![CDATA[terminal]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sat, 02 Sep 2023 01:28:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696643595225/daa4309d-e046-4758-b885-442e5fc2f3fd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377799233/08bf1ab3-22ad-4d1d-a538-87fec1f5f5ce.png" alt="Generate random string with Node" /></p>
<p>How many times have you worked on an application and you needed a random alpha-numeric string? Did you try to provide a list of numbers and characters in a string and pass in a length to randomly generate a string with a loop?</p>
<p>Using Node.js, this can be a lot simpler. You just need to use the crypto library which is built-in.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> { randomBytes } = <span class="hljs-built_in">require</span>(“node:crypto”);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">randomString</span>(<span class="hljs-params">length</span>) </span>{
  <span class="hljs-keyword">if</span> (length % <span class="hljs-number">2</span> !== <span class="hljs-number">0</span>) {
    length++;
  }

  <span class="hljs-keyword">return</span> randomBytes(length / <span class="hljs-number">2</span>).toString(<span class="hljs-string">"hex"</span>);
}

<span class="hljs-keyword">const</span> string = randomString(<span class="hljs-number">8</span>);
<span class="hljs-comment">// string = "df5ec630"</span>
</code></pre>
<p>A few issues with this is the length passed to the <code>randomBytes</code> method needs to be an even number so it can be divided in half. Otherwise, the string returned will be twice the length of the length passed in. This is why the condition is part of the function.</p>
<p>As a bonus, if you just quickly need a random string to use outside of an application, you can just go into your terminal, type <code>node</code> to go into the REPL, then add <code>require(‘node:crypto’).randomBytes(8).toString(‘hex’)</code>.</p>
<pre><code class="lang-bash">node
Welcome to Node.js v20.4.0.
Type <span class="hljs-string">".help"</span> <span class="hljs-keyword">for</span> more information.
&gt; require(<span class="hljs-string">'node:crypto'</span>).randomBytes(8).toString(<span class="hljs-string">'hex'</span>)
<span class="hljs-string">'568b6620639fdf54'</span>
</code></pre>
<p>If you want to generate a random string without even going into the REPL, you could use the following:</p>
<pre><code class="lang-bash">node -e “console.log(require(‘node:crypto’).randomBytes(8).toString(‘hex’))”
</code></pre>
<p>On a Mac, you can even add <code>| pbcopy</code> to the command to have the string copy right to your clipboard.</p>
<pre><code class="lang-bash">node -e “console.log(require(‘node:crypto’).randomBytes(8).toString(‘hex’))” | pbcopy
</code></pre>
<p>Just remember, when using the command from the terminal or REPL, the length passed to <code>randomBytes</code> will be half the length of what is outputted. So if you need a random string of 8 characters, pass 4 as the length.</p>
<p>I hope this quick tip was helpful. Let me know if the comments if you’ve seen any optimizations to the code above or if there’s an easier way to generate random strings.</p>
<p>Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Improve Git CLI Efficiency with Oh My Zsh]]></title><description><![CDATA[Do you use Git in the CLI or GUI? I was a heavy GUI user earlier in my career and I still do some quick operations using VSCode, PhpStorm, or Fork. At a previous company, I was forced to use GitKraken because it helped prevent developers from making ...]]></description><link>https://seankegel.com/improve-git-cli-efficiency-with-oh-my-zsh</link><guid isPermaLink="true">https://seankegel.com/improve-git-cli-efficiency-with-oh-my-zsh</guid><category><![CDATA[GitHub]]></category><category><![CDATA[Git]]></category><category><![CDATA[oh-my-zsh]]></category><category><![CDATA[zsh]]></category><category><![CDATA[terminal]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Sat, 26 Aug 2023 01:44:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696643637564/52cbe1d8-35c2-4855-ae01-156b76224446.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377802373/f7ce110b-a50c-4f6e-bb2f-6f2975b82247.png" alt="Git aliases" /></p>
<p>Do you use Git in the CLI or GUI? I was a heavy GUI user earlier in my career and I still do some quick operations using VSCode, PhpStorm, or Fork. At a previous company, I was forced to use GitKraken because it helped prevent developers from making Git errors. GitKraken is a great product and using the GUI tools definitely helped me become more comfortable using Git and learning the various commands. It really helps you visualize the changes being made to your repository.</p>
<p>However, as you start to progress in your career, you may find yourself gravitating more to the terminal to do a quick <code>git pull</code>, <code>git push</code>, or <code>git commit</code>. Or maybe you’re already an experienced Git user and do all your work in the CLI.</p>
<p>Using Git aliases in the CLI can make you even more efficient. That’s where <a target="_blank" href="https://ohmyz.sh/">Oh My Zsh</a> comes in. Oh My Zsh is a framework for managing your Zsh configuration. Zsh is a shell similar to Bash but with some nice additional features. You can navigate to directories without typing <code>cd</code>. It offers spelling correction, plugins, themes, etc. One of my favorite features is the previous command search. I can start typing a command, then push “up” to see previous commands I’ve used starting with the text I already typed. If you are using a Mac, you’ve likely already have Zsh installed and running as the default. Otherwise, you can actually refer to the Oh My Zsh <a target="_blank" href="https://github.com/ohmyzsh/ohmyzsh/wiki/Installing-ZSH">docs</a> to install it.</p>
<p>Once you are running Zsh, the next step is to install Oh My Zsh. This can be done using a simple <code>curl</code> or <code>wget</code> command:</p>
<pre><code class="lang-bash">sh -c “$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)<span class="hljs-string">"

sh -c "</span>$(wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)<span class="hljs-string">"</span>
</code></pre>
<p>Once installed, you’ll want to edit your <code>.zshrc</code> file, and look for the <code>plugins</code> array and make sure <code>git</code> is added. It should look something like the following:</p>
<pre><code class="lang-bash">plugins=(
  git
)
</code></pre>
<p>Once that is in place go ahead and source your <code>.zshrc</code> file, or just restart your terminal.</p>
<p>Now, we have a ton of aliases for common Git commands.</p>
<p>Here’s some of my favorites:</p>
<p><strong>Commits</strong></p>
<pre><code class="lang-bash">gcmsg ‘Commit messsage’ <span class="hljs-comment"># Equivalent to git commit -m ‘Commit message’</span>
</code></pre>
<p><strong>Amend all files to previous commit</strong></p>
<p>Sometimes I add a commit and realize I forgot to modify a file, so I make the change and then I want to amend the change into the previous commit.</p>
<pre><code class="lang-bash">gcan! <span class="hljs-comment"># Equivalent to git commit — verbose — all — no-edit — amend</span>
</code></pre>
<p><strong>Work in progress</strong></p>
<p>This creates a commit with the message <code>--wip-- [skip ci]</code> with all your currently modified files.</p>
<pre><code class="lang-bash">gwip
</code></pre>
<p><strong>Pushing and Pulling</strong></p>
<pre><code class="lang-bash">gl <span class="hljs-comment"># Equivalent to git pull</span>

gp <span class="hljs-comment"># Equivalent to git push</span>
</code></pre>
<p><strong>Git force push with lease</strong></p>
<p>You always need to be careful with force pushing a branch. One of the best things to do is force push with lease. This means the force push will fail if someone else has added additional commits to the remote branch.</p>
<pre><code class="lang-bash">ggfl <span class="hljs-comment"># Equivalent to git push — force-with-lease</span>
</code></pre>
<p><strong>Branches</strong></p>
<p>The aliases for creating and checking out branches are some of my most used.</p>
<pre><code class="lang-bash">gcb my-new-branch <span class="hljs-comment"># Equivalent to git checkout -b my-new-branch</span>

gco - <span class="hljs-comment"># Equivalent to git checkout — which checks out the previous branch</span>

gcm <span class="hljs-comment"># Equivalent to git checkout main</span>
</code></pre>
<p><strong>Rebasing</strong></p>
<p>I use tend to use <code>git rebase</code> pretty often. It’s definitely something that using a GUI really helped me understand what it was doing. I use <code>git rebase</code> to add my commits onto another branch. I also use <code>git rebase -- interactive HEAD~3</code> to reorder and modify commits, in this case, <code>HEAD~3</code> refers to the last three commits.</p>
<pre><code class="lang-bash">grb {branch} <span class="hljs-comment"># Equivalent to git rebase {branch}</span>

grbi HEAD~3 <span class="hljs-comment"># Equivalent to git rebase — interactive HEAD~3</span>
</code></pre>
<p><strong>Diff and Log</strong></p>
<pre><code class="lang-bash">gd <span class="hljs-comment"># Equivalent to git diff</span>
</code></pre>
<p><strong>Git log</strong></p>
<p>When I want to quickly see a one line version of all the previous commits, I use the <code>glog</code> alias.</p>
<pre><code class="lang-bash">glog <span class="hljs-comment"># Equivalent to git log — oneline — decorate — graph</span>
</code></pre>
<p>For a complete list of aliases available in the plugin, <a target="_blank" href="https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/git/README.md">click here</a>.</p>
<p>If you are using a different shell, like <code>bash</code> or <code>fsh</code>, there may be similar plugins available, but if not, you can still easily create you own aliases in your config file, like <code>.bashrc</code>. Or maybe there is a common Git commands (or CLI commands in general) that you use often enough to warrant an alias.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">alias</span> gc=’git commit -m’
</code></pre>
<p>Thanks for reading! I hope this will help you improve you Git CLI skills. Let me know if you have any other common Git aliases you use, either from the plugin or custom aliases you have created.</p>
]]></content:encoded></item><item><title><![CDATA[VSCode for PHP and Laravel]]></title><description><![CDATA[This post should help you set up Visual Studio Code to use for PHP and Laravel development. It is a solid base configuration that can be expanded upon using additional workspace-specific configurations. I will cover the best extensions to use as well...]]></description><link>https://seankegel.com/vscode-for-php-and-laravel</link><guid isPermaLink="true">https://seankegel.com/vscode-for-php-and-laravel</guid><category><![CDATA[Visual Studio Code]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[PHP]]></category><category><![CDATA[PhpStorm]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Sean Kegel]]></dc:creator><pubDate>Mon, 21 Aug 2023 02:18:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696641658737/8c5f991e-70a1-47d9-847a-72ddf6ab7995.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377805258/139f06d7-da90-4aeb-aaf8-a4fbe3d90916.png" alt="VSCode+PHP+Laravel" /></p>
<p>This post should help you set up Visual Studio Code to use for PHP and Laravel development. It is a solid base configuration that can be expanded upon using additional workspace-specific configurations. I will cover the best extensions to use as well as some helpful configuration settings and external tools.</p>
<p>Let’s get started with the most important extensions!</p>
<h2 id="heading-intelephense">Intelephense</h2>
<ul>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client">PHP Intelephense - Visual Studio Marketplace</a></p>
</li>
<li><p><a target="_blank" href="https://intelephense.com/">Intelephense</a></p>
</li>
</ul>
<p>This is the most important extension to install for PHP support. It provides a fast language server that adds code completion, go-to definition, formatting, and more. You can also purchase a license at <a target="_blank" href="https://intelephense.com/">Intelephense</a>, which I highly recommend. It adds some additional features like renaming symbols and other code actions.</p>
<p>Once installed, disable the built-in PHP features so Intelephense is used instead:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377806756/d9d28617-6d33-4c30-b85d-c58db11b42ef.png" alt="Disable Built-in PHP Support" /></p>
<p>If a license was purchased, use <code>cmd+shift+p</code> to bring up the Command Palette and search for “Enter licence key”.</p>
<p>For the most part, the default settings are fine for Intelephense. At a workspace level, setting the document path and PHP version can be helpful if working on multiple PHP projects and frameworks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377808409/974444c3-014e-4d89-858b-632c308db124.png" alt="Setup Intelephense Document Root and PHP Version" /></p>
<p>Finally, if Intelephense will be used for formatting (versus an external tool like PHP CS Fixer or Pint), then set it as the default formatter for PHP files. This is done by pulling up the JSON version of the settings.</p>
<pre><code class="lang-json"><span class="hljs-string">"[php]"</span>: {
    <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"bmewburn.vscode-intelephense-client"</span>
},
</code></pre>
<h2 id="heading-phpactor">Phpactor</h2>
<ul>
<li><p><a target="_blank" href="https://github.com/phpactor/phpactor">phpactor/phpactor: Mainly a PHP Language Server with more features than you can shake a stick at</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/phpactor/vscode-phpactor">phpactor/vscode-phpactor: Phpactor VS Code Extension</a></p>
</li>
</ul>
<p>This is both an external tool and an extension. However, the extension is not available in the VSCode extension marketplace and needs to be installed manually. The extension provides a lot more helpful code actions like replacing a qualifier with an import and adding return types.</p>
<p>First, install Phpactor by using the <a target="_blank" href="https://phpactor.readthedocs.io/en/master/usage/standalone.html">instructions</a> or following the steps below:</p>
<pre><code class="lang-shell"># Download phpactor.phar
curl -Lo phpactor.phar https://github.com/phpactor/phpactor/releases/latest/download/phpactor.phar
</code></pre>
<pre><code class="lang-shell">chmod a+x phpactor.phar # update permissions
mv phpactor.phar ~/.local/bin/phpactor # move file into path
</code></pre>
<pre><code class="lang-shell"># Check the installation
phpactor status
</code></pre>
<p>With Phpactor installed on the system, the next step is to install the plugin. Download the latest <a target="_blank" href="https://github.com/phpactor/vscode-phpactor/releases/latest">release</a> (<code>phpactor.vsix</code>). Then install in VSCode either by importing the plugin or using the CLI.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377809940/bee10a1a-7f77-4732-9b64-de94ff4778d5.png" alt="Install VSCode Plugin from VSIX" /></p>
<pre><code class="lang-shell">code --install-extension /path/to/phpactor.vsix
</code></pre>
<p>With the plugin installed, Phpactor will index the workspace and then add new code actions. The screenshots below show replacing a qualifier with a namespace and adding return types.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377811344/98b7ee76-9a35-49ac-ac9b-1511b3ab336f.png" alt="Replace qualifier with import" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377812653/1e83afbc-445f-4568-937b-3b9ab6db72d9.png" alt="Add missing return types" /></p>
<h2 id="heading-laravel-extra-intellisense">Laravel Extra Intellisense</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=amiralizadeh9480.laravel-extra-intellisense#views-and-variables">Laravel Extra Intellisense - Visual Studio Marketplace</a></li>
</ul>
<p>This plugin adds additional autocompletion for things like views and routes. For example, in the <code>routes/web.php</code> file, a list of available views displays in the autocompletion popup:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377814250/7efca9a1-d34d-431f-9723-2a7d1d3259c2.png" alt="View Autocompletion" /></p>
<p>It also adds autocompletion for Laravel validation rules and configuration.</p>
<h2 id="heading-laravel-goto">Laravel Goto</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=absszero.vscode-laravel-goto">Laravel Goto - Visual Studio Marketplace</a></li>
</ul>
<p>This plugin is used to quickly navigate to view, config, and other files in a Laravel application just by hovering over the string. For example, in the <code>routes/web.php</code> file again, when hovering over “welcome” and popup appears to navigate directly to the <code>welcome.blade.php</code> file. The <code>opt+;</code> shortcut can also be used to navigate to the file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377815597/ce47e910-c9be-4241-aac6-3fe9abf2819b.png" alt="Go to view file" /></p>
<h2 id="heading-laravel-blade-snippets">Laravel Blade Snippets</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=onecentlin.laravel-blade">Laravel Blade Snippets - Visual Studio Marketplace</a></li>
</ul>
<p>This plugin adds syntax highlighting for <code>.blade.php</code> files as well as helpful snippets. It can even be used to format Blade files if desired. However, later in this post, Prettier will be set up to format Blade files.</p>
<h2 id="heading-better-pest-better-phpunit">Better Pest / Better PHPUnit</h2>
<ul>
<li><p><a target="_blank" href="https://github.com/m1guelpf/better-pest">m1guelpf/better-pest: A better Pest test runner for VS Code</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/calebporzio/better-phpunit">calebporzio/better-phpunit: A better PHPUnit test runner for VS Code</a></p>
</li>
</ul>
<p>These plugins provide easy keybindings for running tests. If the project uses Pest, use the Better Pest extension, otherwise, use Better PHPUnit. These plugins can also be enabled/disabled specifically for workspaces as well if using multiple projects.</p>
<p>The following keybindings are provided by default to run a specific test depending on the cursor location or run all the tests in the file, or the previously run test(s).</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"key"</span>: <span class="hljs-string">"cmd+k cmd+r"</span>,
    <span class="hljs-attr">"command"</span>: <span class="hljs-string">"better-pest.run"</span>
},
{
    <span class="hljs-attr">"key"</span>: <span class="hljs-string">"cmd+k cmd+f"</span>,
    <span class="hljs-attr">"command"</span>: <span class="hljs-string">"better-pest.run-file"</span>
},
{
    <span class="hljs-attr">"key"</span>: <span class="hljs-string">"cmd+k cmd+p"</span>,
    <span class="hljs-attr">"command"</span>: <span class="hljs-string">"better-pest.run-previous"</span>
}
</code></pre>
<p>Refer to the docs for additional settings to set up with Docker or other more advanced configurations.</p>
<h2 id="heading-prettier">Prettier</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode">Prettier - Code formatter - Visual Studio Marketplace</a></li>
</ul>
<p>Prettier is an opinionated framework for formatting JavaScript/TypeScript files. It is highly recommended for formatting React and Vue files if using something like Inertia. It can also format other file types, like JSON and Markdown. After installing the extension, also install Prettier into the project:</p>
<pre><code class="lang-shell">npm install --save-dev --save-exact prettier
</code></pre>
<p>Once installed, the command palette (<code>cmd+shift+p</code>) can be opened and use the <code>Prettier: Create Configuration File</code> to create a <code>.prettierrc</code> file in the project. See the following for a list of configurable rules: <a target="_blank" href="https://prettier.io/docs/en/options">Options · Prettier</a>.</p>
<p>Additional plugins can be added to support more features and languages for Prettier as well, such as Blade support. Install the <a target="_blank" href="https://github.com/shufo/prettier-plugin-blade">shufo/prettier-plugin-blade</a> plugin and add it to the config file:</p>
<pre><code class="lang-shell">npm install --save-dev @shufo/prettier-plugin-blade prettier
</code></pre>
<pre><code class="lang-json"><span class="hljs-comment">// .prettierrc</span>
{
  <span class="hljs-attr">"plugins"</span>: [<span class="hljs-string">"@shufo/prettier-plugin-blade"</span>],
  <span class="hljs-attr">"overrides"</span>: [
    {
      <span class="hljs-attr">"files"</span>: [<span class="hljs-string">"*.blade.php"</span>],
      <span class="hljs-attr">"options"</span>: {
        <span class="hljs-attr">"parser"</span>: <span class="hljs-string">"blade"</span>,
        <span class="hljs-attr">"tabWidth"</span>: <span class="hljs-number">4</span>
      }
    }
  ]
}
</code></pre>
<p>It can even sort Tailwind CSS classes in Blade files by using the <code>sortTailwindcssClasses</code> option. Look at the <a target="_blank" href="https://github.com/shufo/prettier-plugin-blade">Github repo</a> for additional settings.</p>
<p>To set the Prettier as the default formatter for various file types, use the following settings in VSCode:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"[javascript]"</span>: {
    <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
    <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"[javascriptreact]"</span>: {
    <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
    <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"[typescript]"</span>: {
    <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
    <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"[typescriptreact]"</span>: {
    <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
    <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"[svelte]"</span>: {
    <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
    <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"[vue]"</span>: {
    <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
    <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"[blade]"</span>: {
    <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
    <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
  }
}
</code></pre>
<h2 id="heading-laravel-ide-helper">Laravel IDE Helper</h2>
<ul>
<li><a target="_blank" href="https://github.com/barryvdh/laravel-ide-helper">barryvdh/laravel-ide-helper: IDE Helper for Laravel</a></li>
</ul>
<p>Though this is not an extension for VSCode, the Laravel IDE Helper greatly improves the development experience in the editor. It generates helper files for Laravel applications to have better autocompletion support for various magic methods in Laravel that Intelephense cannot resolve. It can analyze models, migrations, facades, and more. Install it with composer.</p>
<pre><code class="lang-shell">composer require --dev barryvdh/laravel-ide-helper
</code></pre>
<p>Once installed, new Artisan commands are available to generate the helper files. The following are the most useful.</p>
<pre><code class="lang-shell">php artisan ide-helper:generate # Generate PHPDocs for facades
php artisan ide-helper:meta # Generate meta file to add support for the factory pattern
php artisan ide-helper:models # Generate PHPDocs for models
</code></pre>
<p>For the models generation, it is possible to add PHPDocs directly to the model files or use an external file. The former tends to be more accurate but adds a lot of comments to the files. The latter is cleaner but sometimes go-to definition goes to the generated file versus the actual model. Decide what works best for the project.</p>
<p>When the commands are run, VSCode can now autocomplete fields on models, like the <code>email</code> field on the <code>User</code> model below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696377817007/d5e082f3-7207-4e89-bf71-14ab01b806b8.png" alt="Autocompletion for model properties" /></p>
<p>This can be made even simpler by adding some scripts to the <code>composer.json</code> file. First, create an <code>ide-helper</code> script to run the various commands. The script below writes the PHPDocs externally for the models versus on the models themselves.</p>
<pre><code class="lang-json"><span class="hljs-string">"ide-helper"</span>: [
    <span class="hljs-string">"@php artisan ide-helper:generate"</span>,
    <span class="hljs-string">"@php artisan ide-helper:meta"</span>,
    <span class="hljs-string">"@php artisan ide-helper:models -N --reset"</span>
]
</code></pre>
<p>Next, update the <code>post-update-cmd</code> script. This will automatically run the <code>ide-helper</code> script when running <code>composer update</code>.</p>
<pre><code class="lang-json"><span class="hljs-string">"post-update-cmd"</span>: [
    <span class="hljs-string">"@php artisan vendor:publish --tag=laravel-assets --ansi --force"</span>,
    <span class="hljs-string">"Illuminate\\Foundation\\ComposerScripts::postUpdate"</span>,
    <span class="hljs-string">"@composer ide-helper"</span>
]
</code></pre>
<p>To take this one step further, VSCode tasks can be configured to run this command from within VSCode and even assign it to a shortcut.</p>
<p>To create the <code>tasks.json</code> file, use <code>cmd+shift+p</code> to open the command palette, the search for <code>Tasks: Configure Task</code>. Inside the menu select the <code>Create tasks.json file from template</code>, then select <code>Others</code>. This creates a <code>tasks.json</code> file inside the <code>.vscode</code> directory in the project. In the <code>tasks.json</code> file, add the following:</p>
<pre><code class="lang-json"><span class="hljs-string">"tasks"</span>: [
    {
        <span class="hljs-attr">"label"</span>: <span class="hljs-string">"Laravel IDE Helper"</span>,
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"shell"</span>,
        <span class="hljs-attr">"command"</span>: <span class="hljs-string">"composer ide-helper"</span>
    }
]
</code></pre>
<p>Now this command can be run from the command palette by going to <code>Tasks: Run Task</code>.</p>
<p>Finally, to add a keyboard shortcut for the tasks, add the following to the <code>keybindings.json</code>:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"key"</span>: <span class="hljs-string">"cmd+shift+."</span>,
    <span class="hljs-attr">"command"</span>: <span class="hljs-string">"workbench.action.tasks.runTask"</span>,
    <span class="hljs-attr">"args"</span>: <span class="hljs-string">"Laravel IDE Helper"</span>
}
</code></pre>
<p>Now clicking <code>cmd+shift+.</code> will quickly run the IDE helper.</p>
<hr />
<h2 id="heading-other-helpful-extensions">Other Helpful Extensions</h2>
<ul>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss">Tailwind CSS IntelliSense - Visual Studio Marketplace</a> Provides better autocompletion and previews for Tailwind CSS classes.</p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">GitLens — Git supercharged - Visual Studio Marketplace</a> Provides useful Git information right in the editor, like the current line blame.</p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=austenc.laravel-docs">Laravel Docs - Visual Studio Marketplace</a> Search and open Laravel documentation from the command palette.</p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ryannaddy.laravel-artisan">Laravel Artisan - Visual Studio Marketplace</a> Run Artisan commands from the command palette.</p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug">PHP Debug - Visual Studio Marketplace</a> An extension to use Xdebug from within VSCode.</p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=DEVSENSE.composer-php-vscode">Composer - Visual Studio Marketplace</a> Run Composer commands and code actions from within VSCode.</p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=junstyle.php-cs-fixer">php cs fixer - Visual Studio Marketplace</a> Adds PHP CS Fixer as an available formatter for PHP files.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope this guide will help make you a much more efficient Laravel developer. Visual Studio Code is a powerful editor and with the right extensions and configuration is an excellent editor for PHP and Laravel development.</p>
<p>It is not quite as powerful as PhpStorm (especially with the Laravel Idea package) and requires more work to configure. However, for a free-to-use editor, it is tough to beat. Though I highly recommend PhpStorm, I know a lot of developers have experience with VSCode and may be doing more than just PHP development so the change may not be worth it.</p>
<p>Please let me know in the comments if there’s anything else you would like me to cover or expand on, like Laravel Sail support or additional tools like Phpstan/Larastan. Also, let me know if there are any other helpful extensions or configurations I might have missed.</p>
<p>Finally, below I provided an example of the settings I use in VSCode.</p>
<hr />
<h2 id="heading-settings">Settings</h2>
<p>Here’s an example of the settings I use in VSCode for PHP and Laravel development. I also make heavy use of workspace-specific settings to further customize my setup for individual projects.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"files.autoSave"</span>: <span class="hljs-string">"onFocusChange"</span>,
    <span class="hljs-attr">"files.defaultLanguage"</span>: <span class="hljs-string">"markdown"</span>,
    <span class="hljs-attr">"files.encoding"</span>: <span class="hljs-string">"utf8"</span>,
    <span class="hljs-attr">"files.eol"</span>: <span class="hljs-string">"\n"</span>,
    <span class="hljs-attr">"files.insertFinalNewline"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"files.trimFinalNewlines"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"files.trimTrailingWhitespace"</span>: <span class="hljs-literal">true</span>,


    <span class="hljs-comment">// ********************</span>
    <span class="hljs-comment">// Formatting</span>
    <span class="hljs-comment">// ********************</span>
    <span class="hljs-attr">"prettier.endOfLine"</span>: <span class="hljs-string">"lf"</span>,


    <span class="hljs-comment">// ********************</span>
    <span class="hljs-comment">// Language Settings</span>
    <span class="hljs-comment">// ********************</span>

    <span class="hljs-comment">// JavaScript/TypeScript</span>
    <span class="hljs-attr">"javascript.preferences.quoteStyle"</span>: <span class="hljs-string">"single"</span>,
    <span class="hljs-attr">"typescript.preferences.quoteStyle"</span>: <span class="hljs-string">"single"</span>,
    <span class="hljs-attr">"typescript.updateImportsOnFileMove.enabled"</span>: <span class="hljs-string">"always"</span>,
    <span class="hljs-attr">"javascript.updateImportsOnFileMove.enabled"</span>: <span class="hljs-string">"always"</span>,
    <span class="hljs-attr">"javascript.preferences.importModuleSpecifier"</span>: <span class="hljs-string">"relative"</span>,
    <span class="hljs-attr">"typescript.preferences.importModuleSpecifier"</span>: <span class="hljs-string">"relative"</span>,
    <span class="hljs-attr">"[javascript]"</span>: {
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
      <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-attr">"[javascriptreact]"</span>: {
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
      <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-attr">"[typescript]"</span>: {
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
      <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-attr">"[typescriptreact]"</span>: {
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
      <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-attr">"[svelte]"</span>: {
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
      <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-attr">"[vue]"</span>: {
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
      <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
    },

    <span class="hljs-comment">// CSS</span>
    <span class="hljs-attr">"tailwindCSS.emmetCompletions"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"tailwindCSS.includeLanguages"</span>: {
      <span class="hljs-attr">"Typescript"</span>: <span class="hljs-string">"typescriptreact"</span>
    },

    <span class="hljs-comment">// PHP</span>
    <span class="hljs-attr">"php.validate.enable"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">"php.suggest.basic"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">"[php]"</span>: {
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"bmewburn.vscode-intelephense-client"</span>
    },
    <span class="hljs-attr">"[blade]"</span>: {
      <span class="hljs-attr">"editor.autoClosingBrackets"</span>: <span class="hljs-string">"always"</span>,
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
      <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-attr">"intelephense.telemetry.enabled"</span>: <span class="hljs-literal">false</span>,

    <span class="hljs-comment">// JSON</span>
    <span class="hljs-attr">"[json]"</span>: {
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
      <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-attr">"[jsonc]"</span>: {
      <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>
    }
}
</code></pre>
]]></content:encoded></item></channel></rss>