<?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[Tony Joe's Blog]]></title><description><![CDATA[**Clean code lover** and therefore lover of **Laravel**.

Best friend of **deep work & focusing**.
Interested in critical thinking, self improvement, personal f]]></description><link>https://tonyjoe.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 18:29:24 GMT</lastBuildDate><atom:link href="https://tonyjoe.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Easiest Passwordless Login in Laravel without external packages]]></title><description><![CDATA[In this fast tutorial, we will create the easiest Passwordless Login in Laravel, using Signed URLs.

Signed URLs are available in Laravel since version 5.6, but in my experience they aren’t known enough.

📌 We assume you have the login view with a f...]]></description><link>https://tonyjoe.dev/easiest-passwordless-login-in-laravel-without-external-packages</link><guid isPermaLink="true">https://tonyjoe.dev/easiest-passwordless-login-in-laravel-without-external-packages</guid><category><![CDATA[Laravel]]></category><category><![CDATA[LaravelTutorials]]></category><category><![CDATA[Passwordless]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Tony Joe]]></dc:creator><pubDate>Sat, 23 Mar 2024 18:31:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1711215707384/77b5c363-d65f-4a68-9f6c-3f5171bb00f1.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this fast tutorial, we will create the easiest <strong>Passwordless Login</strong> in Laravel, using <a target="_blank" href="https://laravel.com/docs/10.x/urls#signed-urls">Signed URLs</a>.</p>
<blockquote>
<p>Signed URLs are available in Laravel since version 5.6, but in my experience they aren’t known enough.</p>
</blockquote>
<p><em>📌 We assume you have the login view with a form with only the email field.</em></p>
<p>We need just 2 routes, that is…</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711218333563/16c7518f-3d24-48a0-b72d-04e452169e0c.png" alt="Passwordless Login steps" class="image--center mx-auto" /></p>
<h2 id="heading-route-1-post-user-email">Route 1: Post user email</h2>
<p>This route:</p>
<ul>
<li><p>receive the user email</p>
</li>
<li><p>create a Signed URL</p>
</li>
<li><p>and send it to user via email (or other channel).</p>
</li>
</ul>
<pre><code class="lang-php"><span class="hljs-comment">// routes/web.php</span>

Route::post(<span class="hljs-string">'/passwordless/login'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">Request $request</span>) </span>{
    <span class="hljs-comment">// please, move me to a Controller ;)</span>

    $request-&gt;validate([
        <span class="hljs-string">'email'</span> =&gt; <span class="hljs-string">'required|email'</span>
    ]);

    $user = User::query()
        -&gt;where(<span class="hljs-string">'email'</span>, $request-&gt;email)
        -&gt;first();

    <span class="hljs-keyword">if</span> ($user) {
        $passwordlessUrl = \URL::temporarySignedRoute(
            <span class="hljs-string">'passwordless.login'</span>,
            now()-&gt;addMinutes(<span class="hljs-number">10</span>),
            [<span class="hljs-string">'user'</span> =&gt; $user-&gt;id]
        );

        <span class="hljs-comment">// notify user via email or other channel...</span>
        $user-&gt;notify(<span class="hljs-keyword">new</span> PasswordlessNotification($passwordlessUrl));
    }
    <span class="hljs-comment">// else... we send always a success message to avoid any "info extraction"</span>

    <span class="hljs-keyword">return</span> back()-&gt;with(<span class="hljs-string">'success'</span>, <span class="hljs-string">'You have an email!'</span>);
});
</code></pre>
<h2 id="heading-route-2-check-signature-and-login">Route 2: check signature and login</h2>
<p>Here, we have the route that login the user:</p>
<ul>
<li><p>it receive the user id (the model is loaded automatically by <em>Model Binding</em>)</p>
</li>
<li><p>it validate signature (🎯 it’s really important! 😎)</p>
</li>
<li><p>and finally login the user.</p>
</li>
</ul>
<pre><code class="lang-php"><span class="hljs-comment">// routes/web.php</span>

Route::get(<span class="hljs-string">'/passwordless/login/{user}'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">Request $request, User $user</span>) </span>{
    <span class="hljs-comment">// please, move me to a Controller ;)</span>

    <span class="hljs-keyword">if</span> (! $request-&gt;hasValidSignature()) {
        abort(<span class="hljs-number">401</span>);
    }

    \Auth::login($user);

    <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">'/'</span>);

})-&gt;name(<span class="hljs-string">'passwordless.login'</span>);
</code></pre>
<p>…and that’s it!</p>
<h2 id="heading-the-passwordlessnotification-class">The <code>PasswordlessNotification</code> class</h2>
<p>In the <em>Route 1</em>, we assumed that you have a <code>PasswordNotification</code> class.</p>
<p>For simply do that:</p>
<pre><code class="lang-bash">php artisan make:notification PasswordlessNotification
</code></pre>
<p>And then:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Notifications/PasswordlessNotification.php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordlessNotification</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Notification</span>
</span>{
    <span class="hljs-keyword">use</span> <span class="hljs-title">Queueable</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> $passwordlessUrl
    </span>) </span>{}

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">via</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> $notifiable</span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [<span class="hljs-string">'mail'</span>];
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toMail</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> $notifiable</span>): <span class="hljs-title">MailMessage</span>
    </span>{
        <span class="hljs-keyword">return</span> (<span class="hljs-keyword">new</span> MailMessage)
            -&gt;subject(<span class="hljs-string">'Your magic link to login'</span>)

            -&gt;line(<span class="hljs-string">"Hi <span class="hljs-subst">{$notifiable-&gt;firstname}</span>"</span>)
            -&gt;line(<span class="hljs-string">'you can login by the link below:'</span>)
            -&gt;action(<span class="hljs-string">'Login'</span>, <span class="hljs-keyword">$this</span>-&gt;passwordlessUrl)

            -&gt;line(<span class="hljs-string">'Thank you for using our application!'</span>);
    }
}
</code></pre>
<hr />
<h2 id="heading-some-important-considerations">Some important considerations</h2>
<h3 id="heading-first-of-all-is-it-secure">First of all: is it secure?</h3>
<p>The short answer is: <strong>it depends</strong>.</p>
<p>I'll state the obvious, but it's important to be clear: the tutorial above doesn't cover every situation!</p>
<p>In some simple situations this may be enough, but in many others it is not!</p>
<p><em>On the other hand, it says "<strong><strong>Easiest</strong></strong>" in the title!</em></p>
<p>Some question that you need to ask yourself:</p>
<ul>
<li><p>What is the context?</p>
</li>
<li><p>What is the level of risk to manage?</p>
</li>
<li><p>What additional security mechanisms are needed?</p>
</li>
</ul>
<h3 id="heading-do-you-hate-passwordless-login-in-general">Do you hate Passwordless Login in general?</h3>
<p>If the answer is <em>true</em>, this tutorial is not for you.</p>
<p>After years of development, I have met many developers (even good ones!) who don't even want to consider systems of this type.</p>
<p>I think that everything needs to be put into context: <strong>booking on the barbershop app is not the same as accessing home banking!</strong></p>
<p>If the barbershop app asked me for 2FA with an external app... I would actually laugh. But worse, if the barbershop app asked me to enter a password, I'd be worried. What is the probability that it would store my password securely? I've seen so many DBs with passwords saved in MD5 or so.</p>
<h3 id="heading-how-can-we-improve-the-tutorial-above">How can we improve the tutorial above?</h3>
<ol>
<li><p>Add <em>nonce</em> to URL</p>
</li>
<li><p>One-time use of URL <em>(for example, using Cache (or DB) to invalidate used URL)</em></p>
</li>
<li><p>Switch to generated token, instead of Signed URLs <em>(again, using Cache or DB)</em></p>
</li>
<li><p><strong>And many other… add your own in the comments!</strong></p>
</li>
</ol>
<hr />
<p>Now, let’s see improvement 1, 2 and 3.</p>
<h4 id="heading-1-add-nonce-to-url">1. Add <em>nonce</em> to URL</h4>
<p>In “Route 1”, let’s add a parameter (<code>hello</code> in this example) containing a random string.</p>
<pre><code class="lang-php">$passwordlessUrl = \URL::temporarySignedRoute(
    <span class="hljs-string">'passwordless.login'</span>,
    now()-&gt;addMinutes(<span class="hljs-number">10</span>),
    [<span class="hljs-string">'user'</span> =&gt; $user-&gt;id, <span class="hljs-string">'hello'</span> =&gt; \Str::random(<span class="hljs-number">64</span>)]
);
</code></pre>
<h4 id="heading-2-one-time-use-of-url">2. One-time use of URL</h4>
<p>In “Route 2”, let’s add full URL in Cache (just 10 minutes, equals to the Signed URL duration) and check if it is already in Cache, before login.</p>
<pre><code class="lang-php"><span class="hljs-comment">// routes/web.php</span>

Route::get(<span class="hljs-string">'/passwordless/login/{user}'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">Request $request, User $user</span>) </span>{
    <span class="hljs-comment">// please, move me to a Controller ;)</span>

    <span class="hljs-keyword">if</span> (! $request-&gt;hasValidSignature()) {
        abort(<span class="hljs-number">401</span>);
    }

    $onetimeCacheKey = <span class="hljs-string">"pwl.url.<span class="hljs-subst">{$request-&gt;fullUrl()}</span>"</span>;

    <span class="hljs-keyword">if</span> (\Cache::has($onetimeCacheKey)) {
        abort(<span class="hljs-number">401</span>);
    }

    \Auth::login($user);

    \Cache::put($onetimeCacheKey, <span class="hljs-number">1</span>, <span class="hljs-number">10</span> * <span class="hljs-number">60</span>);

    <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">'/'</span>);

})-&gt;name(<span class="hljs-string">'passwordless.login'</span>);
</code></pre>
<h4 id="heading-3-switch-to-generated-token">3. Switch to generated token</h4>
<p>In this case, <strong>we change the approach</strong>: we move to a generated token, instead of Signed URL.</p>
<p>And then, “<strong>Route 1</strong>":</p>
<pre><code class="lang-php">Route::post(<span class="hljs-string">'/passwordless/login'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">Request $request</span>) </span>{
    <span class="hljs-comment">// please, move me to a Controller ;)</span>

    $request-&gt;validate([
        <span class="hljs-string">'email'</span> =&gt; <span class="hljs-string">'required|email'</span>
    ]);

    $user = User::query()
        -&gt;where(<span class="hljs-string">'email'</span>, $request-&gt;email)
        -&gt;first();

    <span class="hljs-keyword">if</span> ($user) {
        $token = \Str::random(<span class="hljs-number">64</span>);

        \Cache::put(<span class="hljs-string">"pwl.tkn.<span class="hljs-subst">{$token}</span>"</span>, $user-&gt;id, <span class="hljs-number">10</span> * <span class="hljs-number">60</span>);

        $passwordlessUrl = route(<span class="hljs-string">'passwordless.login'</span>, [
            <span class="hljs-string">'token'</span> =&gt; $token
        ]);

        <span class="hljs-comment">// notify user via email or other channel...</span>
        $user-&gt;notify(<span class="hljs-keyword">new</span> PasswordlessNotification($passwordlessUrl));
    }
    <span class="hljs-comment">// else... we send always a success message to avoid any "info extraction"</span>

    <span class="hljs-keyword">return</span> back()-&gt;with(<span class="hljs-string">'success'</span>, <span class="hljs-string">'You have an email!'</span>);
});
</code></pre>
<p>Finally, “<strong>Route 2</strong>":</p>
<pre><code class="lang-php">Route::get(<span class="hljs-string">'/passwordless/login/{token}'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">Request $request, <span class="hljs-keyword">string</span> $token</span>) </span>{
    <span class="hljs-comment">// please, move me to a Controller ;)</span>

    $tokenCacheKey = <span class="hljs-string">"pwl.tkn.<span class="hljs-subst">{$token}</span>"</span>;

    $userId = \Cache::get($tokenCacheKey);

    abort_if($userId == <span class="hljs-literal">null</span>, <span class="hljs-number">401</span>);

    $user = User::findOrFail($userId);

    \Auth::login($user);

    \Cache::forget($tokenCacheKey);

    <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">'/'</span>);

})-&gt;name(<span class="hljs-string">'passwordless.login'</span>);
</code></pre>
<hr />
<p>✸ Enjoy your coding!</p>
<p><em>If you liked this post, don't forget to</em> <strong><em>Subscribe</em></strong> <em>to my</em> <strong><em>newsletter</em></strong>!</p>
]]></content:encoded></item><item><title><![CDATA[data_get(): Warning with array keys with dots - Laravel Tips]]></title><description><![CDATA[Ingredients
data_get()
data_get() is a useful helper function in Laravel that retrieves values from a nested array or an object using dot notation.
The strength of data_get() is also that it accepts wildcards.

If you don't know or you don't use it, ...]]></description><link>https://tonyjoe.dev/dataget-warning-with-array-keys-with-dots-laravel-tips</link><guid isPermaLink="true">https://tonyjoe.dev/dataget-warning-with-array-keys-with-dots-laravel-tips</guid><category><![CDATA[Laravel]]></category><category><![CDATA[Tips for Developers]]></category><category><![CDATA[tips]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Tony Joe]]></dc:creator><pubDate>Thu, 04 Jan 2024 15:42:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704382862111/d4b25ee8-1062-491b-be84-54e26aa0374c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-ingredients">Ingredients</h1>
<h2 id="heading-dataget"><code>data_get()</code></h2>
<p><code>data_get()</code> is a useful helper function in Laravel that retrieves values from a nested array or an object using <em>dot notation</em>.
The strength of <code>data_get()</code> is also that it accepts wildcards.</p>
<blockquote>
<p>If you don't know or you don't use it, I suggest you to discover it (<a target="_blank" href="https://laravel.com/docs/10.x/helpers#method-data-get">here the docs</a>), with <em>brothers</em> and <em>sisters</em>: <a target="_blank" href="https://laravel.com/docs/10.x/helpers#method-data-set"><code>data_set()</code></a>, <a target="_blank" href="https://laravel.com/docs/10.x/helpers#method-data-fill"><code>data_fill()</code></a>, <a target="_blank" href="https://laravel.com/docs/10.x/helpers#method-data-forget"><code>data_forget()</code></a>! ;)</p>
</blockquote>
<h2 id="heading-arrget"><code>Arr::get()</code></h2>
<p>Unlike <code>data_get()</code>, <a target="_blank" href="https://laravel.com/docs/10.x/helpers#method-array-get"><code>Arr::get()</code></a> works only on arrays and it doesn't accept wildcards.</p>
<hr />
<h1 id="heading-the-tip">The Tip</h1>
<p>Let’s show this example:</p>
<pre><code class="lang-php">$array = [
    <span class="hljs-string">'key.sub'</span> =&gt; <span class="hljs-string">'a-value'</span>
];

data_get($array, <span class="hljs-string">'key.sub'</span>);  <span class="hljs-comment">// --&gt; null ❗</span>

<span class="hljs-comment">// instead...</span>
\Arr::get($array, <span class="hljs-string">'key.sub'</span>); <span class="hljs-comment">// --&gt; "a-value"</span>
</code></pre>
<p>Instead of this:</p>
<pre><code class="lang-php">$array = [
    <span class="hljs-string">'key'</span> =&gt; [
        <span class="hljs-string">'sub'</span> =&gt; <span class="hljs-string">'a-value'</span>
    ]
];

data_get($array, <span class="hljs-string">'key.sub'</span>);  <span class="hljs-comment">// --&gt; "a-value"</span>

<span class="hljs-comment">// and also...</span>
\Arr::get($array, <span class="hljs-string">'key.sub'</span>); <span class="hljs-comment">// --&gt; "a-value"</span>
</code></pre>
<hr />
<h1 id="heading-and-you">And you?</h1>
<ul>
<li>Did you use already <code>data_get()</code>?</li>
<li>And <code>Arr::get()</code>?</li>
<li>Did you know that difference?</li>
</ul>
<p><strong>Leave your comment!</strong></p>
<hr />
<p>✸ Enjoy your coding!</p>
<h4 id="heading-if-you-liked-this-post-dont-forget-to-subscribe-to-my-newsletter">If you liked this post, don't forget to <strong>Subscribe</strong> to my newsletter!</h4>
]]></content:encoded></item><item><title><![CDATA[wasChanged() vs wasRecentlyCreated - Laravel Tips]]></title><description><![CDATA[Let's start with a simple model like this:
class MyModel extends Model
{
    protected $fillable = [
        'name'
    ];

    protected static function booted(): void
    {
        static::created(function ($model) {
            $model->dumpEvent('...]]></description><link>https://tonyjoe.dev/waschanged-vs-wasrecentlycreated-laravel-tips</link><guid isPermaLink="true">https://tonyjoe.dev/waschanged-vs-wasrecentlycreated-laravel-tips</guid><category><![CDATA[Laravel]]></category><category><![CDATA[tips]]></category><category><![CDATA[tips and tricks]]></category><category><![CDATA[Tips for Developers]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Tony Joe]]></dc:creator><pubDate>Tue, 02 Jan 2024 10:24:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704190866507/02a1039d-9b22-4c5e-bc13-1129b6f34b03.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's start with a simple model like this:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyModel</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span>
</span>{
    <span class="hljs-keyword">protected</span> $fillable = [
        <span class="hljs-string">'name'</span>
    ];

    <span class="hljs-keyword">protected</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">booted</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-built_in">static</span>::created(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">$model</span>) </span>{
            $model-&gt;dumpEvent(<span class="hljs-string">'CREATED'</span>);
        });

        <span class="hljs-built_in">static</span>::updated(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">$model</span>) </span>{
            $model-&gt;dumpEvent(<span class="hljs-string">'UPDATED'</span>);
        });

        <span class="hljs-built_in">static</span>::saved(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">$model</span>) </span>{
            $model-&gt;dumpEvent(<span class="hljs-string">'SAVED'</span>);
        });
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">dumpEvent</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $event</span>): <span class="hljs-title">void</span>
    </span>{
        dump([
            <span class="hljs-string">'event'</span> =&gt; $event,
            <span class="hljs-string">'wasChanged()'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;wasChanged(),
            <span class="hljs-string">'wasRecentlyCreated'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;wasRecentlyCreated
        ]);
    }
}
</code></pre>
<p>Let’s create a new model:</p>
<pre><code class="lang-php">$model = MyModel::create([<span class="hljs-string">'name'</span> =&gt; <span class="hljs-string">'ABC'</span>]);
</code></pre>
<p>We get:</p>
<pre><code class="lang-php"><span class="hljs-keyword">array</span>:<span class="hljs-number">3</span> [ <span class="hljs-comment">// app/Models/MyModel.php:30</span>
  <span class="hljs-string">"event"</span> =&gt; <span class="hljs-string">"CREATED"</span>
  <span class="hljs-string">"wasChanged()"</span> =&gt; <span class="hljs-literal">false</span>      <span class="hljs-comment">// &lt;-- ❗</span>
  <span class="hljs-string">"wasRecentlyCreated"</span> =&gt; <span class="hljs-literal">true</span>
]
<span class="hljs-keyword">array</span>:<span class="hljs-number">3</span> [ <span class="hljs-comment">// app/Models/MyModel.php:30</span>
  <span class="hljs-string">"event"</span> =&gt; <span class="hljs-string">"SAVED"</span>
  <span class="hljs-string">"wasChanged()"</span> =&gt; <span class="hljs-literal">false</span>      <span class="hljs-comment">// &lt;-- ❗</span>
  <span class="hljs-string">"wasRecentlyCreated"</span> =&gt; <span class="hljs-literal">true</span>
]
</code></pre>
<p>NOTE:</p>
<ul>
<li>❗<code>wasChanged()</code> is <code>false</code></li>
</ul>
<hr />
<p>If we call <code>save()</code> without changes on the same model instance:</p>
<pre><code class="lang-php"><span class="hljs-comment">// $model = MyModel::create(['name' =&gt; 'ABC']);</span>
<span class="hljs-comment">// ...</span>
<span class="hljs-comment">// ... no changes on `$model` fields</span>

$model-&gt;save();
</code></pre>
<p>We get:</p>
<pre><code class="lang-php"><span class="hljs-keyword">array</span>:<span class="hljs-number">3</span> [ <span class="hljs-comment">// app/Models/MyModel.php:30</span>
  <span class="hljs-string">"event"</span> =&gt; <span class="hljs-string">"SAVED"</span>
  <span class="hljs-string">"wasChanged()"</span> =&gt; <span class="hljs-literal">false</span>
  <span class="hljs-string">"wasRecentlyCreated"</span> =&gt; <span class="hljs-literal">true</span>
]
</code></pre>
<p>NOTE: </p>
<ul>
<li>it will be triggered the <code>saved()</code> event</li>
<li>but it will not be triggered the <code>updated()</code> event</li>
<li><code>wasRecentlyCreated</code> is still <code>true</code> (remember: it’s the same <code>$model</code> instance)</li>
</ul>
<hr />
<p>Now, let’s make some changes, always on the same model instance:</p>
<pre><code class="lang-php"><span class="hljs-comment">// $model = MyModel::create(['name' =&gt; 'ABC']);</span>
<span class="hljs-comment">// ...</span>

$model-&gt;name = <span class="hljs-string">'ABC new'</span>;
$model-&gt;save();
</code></pre>
<p>We get:</p>
<pre><code class="lang-php"><span class="hljs-keyword">array</span>:<span class="hljs-number">3</span> [ <span class="hljs-comment">// app/Models/MyModel.php:30</span>
  <span class="hljs-string">"event"</span> =&gt; <span class="hljs-string">"UPDATED"</span>
  <span class="hljs-string">"wasChanged()"</span> =&gt; <span class="hljs-literal">true</span>
  <span class="hljs-string">"wasRecentlyCreated"</span> =&gt; <span class="hljs-literal">true</span> <span class="hljs-comment">// &lt;-- ❗</span>
]
<span class="hljs-keyword">array</span>:<span class="hljs-number">3</span> [ <span class="hljs-comment">// app/Models/MyModel.php:30</span>
  <span class="hljs-string">"event"</span> =&gt; <span class="hljs-string">"SAVED"</span>
  <span class="hljs-string">"wasChanged()"</span> =&gt; <span class="hljs-literal">true</span>
  <span class="hljs-string">"wasRecentlyCreated"</span> =&gt; <span class="hljs-literal">true</span> <span class="hljs-comment">// &lt;-- ❗</span>
]
</code></pre>
<p>NOTE:</p>
<ul>
<li><code>wasChanged()</code> is <code>true</code> (OK, we expected it to be)</li>
<li>❗<code>wasRecentlyCreated</code> is still <code>true</code> (remember: it’s the same <code>$model</code> instance)</li>
</ul>
<hr />
<p>What happens after a <code>refresh()</code>?</p>
<pre><code class="lang-php"><span class="hljs-comment">// $model = MyModel::create(['name' =&gt; 'ABC']);</span>
<span class="hljs-comment">// ...</span>

<span class="hljs-comment">// $model-&gt;name = 'ABC new';</span>
<span class="hljs-comment">// $model-&gt;save();</span>

$model-&gt;refresh();
$model-&gt;save();
</code></pre>
<p>We get again:</p>
<pre><code class="lang-php"><span class="hljs-keyword">array</span>:<span class="hljs-number">3</span> [ <span class="hljs-comment">// app/Models/MyModel.php:30</span>
  <span class="hljs-string">"event"</span> =&gt; <span class="hljs-string">"SAVED"</span>
  <span class="hljs-string">"wasChanged()"</span> =&gt; <span class="hljs-literal">true</span>       <span class="hljs-comment">// &lt;-- ❗</span>
  <span class="hljs-string">"wasRecentlyCreated"</span> =&gt; <span class="hljs-literal">true</span> <span class="hljs-comment">// &lt;-- ❗</span>
]
</code></pre>
<p>So even after a <code>refresh()</code>, it doesn’t change the internal status for the <code>wasChanged()</code> or <code>wasRecentlyCreated</code>!</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>In general, between different requests, reloading a model from the DB does not generate problems. But the behavior of <code>wasChanged()</code> and <code>wasRecentlyCreated</code> can become bizarre in the same request, i.e. on the same instance of the newly created model, especially if the model is configured to trigger events in the <code>saved()</code> method, when something has actually changed, for example:</p>
<pre><code class="lang-php"><span class="hljs-keyword">protected</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">booted</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-built_in">static</span>::saved(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">$model</span>) </span>{
        <span class="hljs-keyword">if</span> (! $model-&gt;wasChanged() &amp;&amp; ! $model-&gt;wasRecentlyCreated) {
            <span class="hljs-keyword">return</span>; <span class="hljs-comment">// no changes and no new record</span>
        }

        event(<span class="hljs-keyword">new</span> MyCustomEvent(
            $model-&gt;wasRecentlyCreated ? <span class="hljs-string">'mymodel.new'</span> : <span class="hljs-string">'mymodel.updated'</span>),
            $model
        );
    });
}
</code></pre>
<hr />
<p>✸ Enjoy your coding!</p>
<h6 id="heading-if-you-liked-this-post-dont-forget-to-add-your-subscribe-to-my-newsletter"><em>If you liked this post, don't forget to add your</em> <strong><em>Subscribe</em></strong> <em>to my newsletter!</em></h6>
]]></content:encoded></item><item><title><![CDATA[How to easily export in Excel or CSV with Laravel]]></title><description><![CDATA["Hey Tony 🙏, I need to export those results to Excel by tomorrow morning otherwise I get fired! 🤯"

Has something like this ever happened to you?
Ok, keep calm and don't reinvent the wheel.

Fortunately, the Laravel ecosystem is wonderful and provi...]]></description><link>https://tonyjoe.dev/how-to-easily-export-in-excel-or-csv-with-laravel</link><guid isPermaLink="true">https://tonyjoe.dev/how-to-easily-export-in-excel-or-csv-with-laravel</guid><category><![CDATA[Laravel]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Export data]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Tony Joe]]></dc:creator><pubDate>Sun, 19 Nov 2023 12:18:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700395831323/98bff7e7-262e-425f-abe5-9cc691b960b5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>"<em>Hey Tony</em> 🙏, <em>I need to export those results to Excel by tomorrow morning otherwise I get fired!</em> 🤯"</p>
</blockquote>
<p>Has something like this ever happened to you?</p>
<p><em>Ok, keep calm and don't reinvent the wheel.</em></p>
<hr />
<p>Fortunately, the Laravel ecosystem is wonderful and provides us with truly great tools. This is the case with the package we are going to rely on now:<br /><strong>👉 Laravel Excel</strong> (<a target="_blank" href="https://laravel-excel.com/">laravel-excel.com</a>).</p>
<p>This package can be used to manage many aspects of both data export and import.</p>
<p><strong>Here we will focus on data export</strong>, in a very common situation, that is, when the data source is a <strong>Model</strong> and therefore, presumably, the corresponding <strong>table in the DB</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700395667063/0fdb3167-25ef-4d7d-a3d7-b2043dd2e461.jpeg" alt="Export Excel or CSV with Laravel" class="image--center mx-auto" /></p>
<h2 id="heading-steps">Steps</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-1-before-we-go-what-is-the-model">Before we go, what is the Model?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-install-the-laravel-excel-package">Install the Laravel Excel package</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-create-the-export-class">Create the export class</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-create-the-controller-and-open-a-route">Create the controller and open a route</a></p>
</li>
</ol>
<h2 id="heading-1-before-we-go-what-is-the-model">1. Before we go, what is the Model?</h2>
<p>Let's assume we have <em>Orders</em>, each of which is connected to a <em>Customer</em>.</p>
<p>The <code>Order</code> Model:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Order</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span>
</span>{
    <span class="hljs-keyword">protected</span> $fillable = [
        <span class="hljs-string">'code'</span>,
        <span class="hljs-string">'status'</span>,
        <span class="hljs-string">'amount'</span>,
        <span class="hljs-string">'notes'</span>,
    ];

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">customer</span>(<span class="hljs-params"></span>): <span class="hljs-title">BelongsTo</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;belongsTo(Customer::class);
    }
}
</code></pre>
<p>The <code>Customer</code> Model:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Customer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span>
</span>{
    <span class="hljs-keyword">protected</span> $fillable = [
        <span class="hljs-string">'business_name'</span>,
        <span class="hljs-string">'vat'</span>,
        <span class="hljs-string">'email'</span>,
    ];

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">orders</span>(<span class="hljs-params"></span>): <span class="hljs-title">HasMany</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;hasMany(Order::class);
    }
}
</code></pre>
<hr />
<h2 id="heading-2-install-the-laravel-excel-package">2. Install the Laravel Excel package</h2>
<p><em>Let's start!</em></p>
<p>Install the package:</p>
<pre><code class="lang-bash">composer require maatwebsite/excel:^3.1
</code></pre>
<p>Publish the config file <code>config/excel.php</code>:</p>
<pre><code class="lang-bash">php artisan vendor:publish --provider=<span class="hljs-string">"Maatwebsite\Excel\ExcelServiceProvider"</span> --tag=config
</code></pre>
<p><em>You can find many default parameters in the configuration file that you can customize if necessary. But right now you can just move on.</em></p>
<hr />
<h2 id="heading-3-create-the-export-class">3. Create the export class</h2>
<p>Once the package has been installed, we have the <code>make:export</code> generator available.</p>
<p>We use it now:</p>
<pre><code class="lang-bash">php artisan make:<span class="hljs-built_in">export</span> OrdersExport --model=Order
</code></pre>
<p>Ok, now let's open the newly created class:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Exports/OrdersExport.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Models</span>\<span class="hljs-title">Order</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Maatwebsite</span>\<span class="hljs-title">Excel</span>\<span class="hljs-title">Concerns</span>\<span class="hljs-title">FromCollection</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrdersExport</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">FromCollection</span>
</span>{
    <span class="hljs-comment">/**
    * <span class="hljs-doctag">@return</span> \Illuminate\Support\Collection
    */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">collection</span>(<span class="hljs-params"></span>)
    </span>{
        <span class="hljs-keyword">return</span> Order::all();
    }
}
</code></pre>
<p>This is a really basic version and we will almost certainly need to modify it.</p>
<p>First of all, let's remove the implementation of the <code>FromCollection</code> interface and replace it with the <code>FromQuery</code> interface. In this way, the query will be executed in <em>chunks</em>.</p>
<p>Furthermore, we add:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrdersExport</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">FromQuery</span>, <span class="hljs-title">WithHeadings</span>, <span class="hljs-title">WithMapping</span>, <span class="hljs-title">WithCustomCsvSettings</span>
</span>{
    <span class="hljs-keyword">use</span> <span class="hljs-title">Exportable</span>;

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

    <span class="hljs-comment">/**
     * Prepare the query for data export
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">query</span>(<span class="hljs-params"></span>)
    </span>{
        <span class="hljs-comment">// ...</span>
    }

    <span class="hljs-comment">/**
     * Customize the csv header (first row)
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">headings</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-comment">// ...</span>
    }

    <span class="hljs-comment">/**
     * Get and (eventually) customize single row
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">map</span>(<span class="hljs-params">$order</span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-comment">// ...</span>
    }

    <span class="hljs-comment">/**
     * Customize CSV seettings
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCsvSettings</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-comment">// ...</span>
    }
}
</code></pre>
<p>Finally, here is the <strong>complete version</strong> of the <code>OrdersExport</code> class, in which we also manage 2 very simple filters, on <em>Customer</em> and on the reference <em>year</em>:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Exports/OrdersExport.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Models</span>\<span class="hljs-title">Order</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Maatwebsite</span>\<span class="hljs-title">Excel</span>\<span class="hljs-title">Concerns</span>\<span class="hljs-title">Exportable</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Maatwebsite</span>\<span class="hljs-title">Excel</span>\<span class="hljs-title">Concerns</span>\<span class="hljs-title">FromQuery</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Maatwebsite</span>\<span class="hljs-title">Excel</span>\<span class="hljs-title">Concerns</span>\<span class="hljs-title">WithCustomCsvSettings</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Maatwebsite</span>\<span class="hljs-title">Excel</span>\<span class="hljs-title">Concerns</span>\<span class="hljs-title">WithHeadings</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Maatwebsite</span>\<span class="hljs-title">Excel</span>\<span class="hljs-title">Concerns</span>\<span class="hljs-title">WithMapping</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrdersExport</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">FromQuery</span>, <span class="hljs-title">WithHeadings</span>, <span class="hljs-title">WithMapping</span>, <span class="hljs-title">WithCustomCsvSettings</span>
</span>{
    <span class="hljs-keyword">use</span> <span class="hljs-title">Exportable</span>;

    <span class="hljs-keyword">private</span> ?Customer $customer;
    <span class="hljs-keyword">private</span> ?<span class="hljs-keyword">int</span> $year;

    <span class="hljs-comment">/**
     * Filter orders by specific Customer
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">forCustomer</span>(<span class="hljs-params">?Customer $customer</span>): <span class="hljs-title">self</span>
    </span>{
        <span class="hljs-keyword">$this</span>-&gt;customer = $customer;

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

    <span class="hljs-comment">/**
     * Filter orders by specific year
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">forYear</span>(<span class="hljs-params">?<span class="hljs-keyword">int</span> $year</span>): <span class="hljs-title">self</span>
    </span>{
        <span class="hljs-keyword">$this</span>-&gt;year = $year;

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

    <span class="hljs-comment">/**
     * Prepare the query for data export
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">query</span>(<span class="hljs-params"></span>)
    </span>{
        $q = Order::query()-&gt;with([<span class="hljs-string">'customer'</span>]);

        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>-&gt;customer != <span class="hljs-literal">null</span>) {
            $q-&gt;where(<span class="hljs-string">'customer_id'</span>, <span class="hljs-keyword">$this</span>-&gt;customer-&gt;id);
        }

        <span class="hljs-keyword">if</span> (filled(<span class="hljs-keyword">$this</span>-&gt;year) &amp;&amp; <span class="hljs-keyword">$this</span>-&gt;year &gt; <span class="hljs-number">1970</span>) {
            $q-&gt;whereYear(<span class="hljs-string">'created_at'</span>, <span class="hljs-keyword">$this</span>-&gt;year);
        }

        <span class="hljs-keyword">return</span> $q-&gt;latest();
    }

    <span class="hljs-comment">/**
     * Customize the csv header (first row)
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">headings</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'Order ID'</span>,
            <span class="hljs-string">'Order Code'</span>,
            <span class="hljs-string">'Order Status'</span>,
            <span class="hljs-string">'Order Amount'</span>,

            <span class="hljs-string">'Customer Business Name'</span>,
            <span class="hljs-string">'Customer VAT'</span>,
            <span class="hljs-string">'Customer Email'</span>,

            <span class="hljs-string">'Order Notes'</span>,
            <span class="hljs-string">'Created At'</span>,
            <span class="hljs-string">'Last Updated At'</span>,
        ];
    }

    <span class="hljs-comment">/**
     * Get and (eventually) customize single row
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">map</span>(<span class="hljs-params">$order</span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            $order-&gt;id,
            $order-&gt;code,
            $order-&gt;status,
            $order-&gt;amount,

            $order-&gt;customer?-&gt;business_name ?? <span class="hljs-string">'(Unknown)'</span>,
            $order-&gt;customer?-&gt;vat,
            $order-&gt;customer?-&gt;email,

            $order-&gt;notes ?? <span class="hljs-string">'(No notes)'</span>,
            $order-&gt;created_at,
            $order-&gt;updated_at,
        ];
    }

    <span class="hljs-comment">/**
     * Customize CSV seettings
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCsvSettings</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'delimiter'</span> =&gt; <span class="hljs-string">','</span>,
            <span class="hljs-string">'use_bom'</span> =&gt; <span class="hljs-literal">false</span>,
            <span class="hljs-string">'output_encoding'</span> =&gt; <span class="hljs-string">'UTF-8'</span>,
        ];
    }
}
</code></pre>
<hr />
<h2 id="heading-4-create-the-controller-and-open-a-route">4. Create the controller and open a route</h2>
<p>Now that the <code>OrdersExport</code> class is ready, we are almost done. All we have to do is use it in a controller and then open a specific route.</p>
<p>Here is an example controller:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Http/Controllers/OrdersController.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">Exports</span>\<span class="hljs-title">OrdersExport</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrdersController</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">export</span>(<span class="hljs-params">?Customer $customer = <span class="hljs-literal">null</span>, ?<span class="hljs-keyword">int</span> $year = <span class="hljs-literal">null</span></span>)
    </span>{
        $filename = <span class="hljs-keyword">$this</span>-&gt;buildFilename(<span class="hljs-string">'orders'</span>, $customer, $year);

        <span class="hljs-keyword">return</span> (<span class="hljs-keyword">new</span> OrdersExport)
            -&gt;forCustomer($customer)
            -&gt;forYear($year)
            -&gt;download($filename);
    }

    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">buildFilename</span>(<span class="hljs-params">$basename, ?Customer $customer = <span class="hljs-literal">null</span>, ?<span class="hljs-keyword">int</span> $year = <span class="hljs-literal">null</span></span>)
    </span>{
        $customerfmt = ($customer)
            ? \Str::slug($customer-&gt;business_name)
            : <span class="hljs-string">'anycustomer'</span>;

        $yearfmt = filled($year) ? $year : <span class="hljs-string">'anytime'</span>;
        $today = date(<span class="hljs-string">'Ymd'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-string">"<span class="hljs-subst">{$basename}</span>-<span class="hljs-subst">{$customerfmt}</span>-<span class="hljs-subst">{$yearfmt}</span>-<span class="hljs-subst">{$today}</span>.csv"</span>;
    }
}
</code></pre>
<p>And finally, the route:</p>
<pre><code class="lang-php"><span class="hljs-comment">// routes/web.php</span>

Route::get(<span class="hljs-string">'/orders/export/{customer?}/{year?}'</span>, [OrdersController::class, <span class="hljs-string">'export'</span>])
    -&gt;name(<span class="hljs-string">'orders.export'</span>);
</code></pre>
<hr />
<p>✸ Enjoy your coding!</p>
<p><em>If you liked this post, don't forget to</em> <strong><em>Subscribe</em></strong> <em>to my</em> <strong><em>newsletter</em></strong>!</p>
]]></content:encoded></item><item><title><![CDATA[How to handle a Private Beta with access code for your new app in Laravel]]></title><description><![CDATA[Do you have an app that needs testing before the big launch? Before opening a public beta, it is best to keep the doors ajar and start with a private beta.
In this tutorial, we will see how to make our Laravel application capable of requesting an acc...]]></description><link>https://tonyjoe.dev/how-to-handle-a-private-beta-with-access-code-for-your-new-app-in-laravel</link><guid isPermaLink="true">https://tonyjoe.dev/how-to-handle-a-private-beta-with-access-code-for-your-new-app-in-laravel</guid><category><![CDATA[Laravel]]></category><category><![CDATA[LaravelTutorials]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[beta]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Tony Joe]]></dc:creator><pubDate>Sun, 24 Sep 2023 09:29:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694968246751/4814c7d8-f4e2-43bc-9816-5762fb1ded64.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Do you have an app that needs testing before the big launch?</strong> Before opening a <em>public beta</em>, it is best to keep the <em>doors ajar</em> and start with a <em>private beta</em>.</p>
<p>In this tutorial, we will see how to make our <strong>Laravel</strong> application capable of requesting an <strong>access code</strong> before starting to use it.</p>
<hr />
<h2 id="heading-first-the-basics-what-is-a-private-beta">First, the basics: What is a Private Beta?</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694968359634/130ece8f-6b91-4e55-b73c-4b7f294e9810.jpeg" alt="A Private Beta with Access Code" class="image--center mx-auto" /></p>
<p>A <strong>private</strong> (or <em>closed</em>) <strong>beta</strong> is an app accessible <em>by invitation</em> to a small group of testers. In contrast, a <strong>public</strong> (or <em>open</em>) <strong>beta</strong> is accessible to anyone interested.</p>
<p>Generally, the <strong>private beta</strong> is not ready to be used by everyone for various reasons: perhaps some features are still missing or there are potential scalability problems or the app simply needs to be shown to <em>potential investors</em> before the actual launch on the market.</p>
<hr />
<h2 id="heading-steps">Steps</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-1-make-the-model-and-migration-for-invitations">Make the Model and Migration for Invitations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-add-some-config-parameters">Add some Config parameters</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-make-a-service-class">Make a Service class</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-make-the-routes-the-view-and-the-controller">Make the Routes, the View and the Controller</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-make-the-middleware-this-is-where-the-work-happens">Make the Middleware: This is where the work happens</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-check-that-everything-works">Check that everything works!</a></p>
</li>
</ol>
<hr />
<h2 id="heading-1-make-the-model-and-migration-for-invitations">1. Make the Model and Migration for Invitations</h2>
<p>Let's create a Model named <code>PrivateBetaInvitation</code> and its migration:</p>
<pre><code class="lang-bash">php artisan make:model PrivateBetaInvitation --migration
</code></pre>
<p>In the migration, let's add the following fields to the table <code>private_beta_invitations</code>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Field</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><code>status</code></td><td><em>values:</em> <strong>pending</strong>, <strong>waiting</strong>, <strong>active</strong>, <strong>archived</strong></td></tr>
<tr>
<td><code>access_code</code></td><td>the code that users (testers) will receive and must enter to access the app</td></tr>
<tr>
<td><code>expire_at</code></td><td><em>optional</em></td></tr>
<tr>
<td><code>num_requests</code></td><td>basic stats</td></tr>
<tr>
<td><code>last_access_at</code></td><td>-</td></tr>
</tbody>
</table>
</div><p>The migration:</p>
<pre><code class="lang-php"><span class="hljs-comment">// database/migrations/XXXX_create_private_beta_invitations_table.php</span>

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Database</span>\<span class="hljs-title">Migrations</span>\<span class="hljs-title">Migration</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Database</span>\<span class="hljs-title">Schema</span>\<span class="hljs-title">Blueprint</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">Schema</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">extends</span> <span class="hljs-title">Migration</span>
</span>{
    <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">'private_beta_invitations'</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">'email'</span>)-&gt;index();
            $table-&gt;string(<span class="hljs-string">'status'</span>, <span class="hljs-number">16</span>)-&gt;index()-&gt;default(<span class="hljs-string">'pending'</span>)
                -&gt;comment(<span class="hljs-string">'pending|waiting|active|archived'</span>);
            $table-&gt;string(<span class="hljs-string">'access_code'</span>, <span class="hljs-number">32</span>)-&gt;index()
                -&gt;comment(<span class="hljs-string">'the code that users (testers) will receive and must enter to access the app'</span>);
            $table-&gt;timestamp(<span class="hljs-string">'expire_at'</span>)-&gt;nullable();
            $table-&gt;unsignedInteger(<span class="hljs-string">'num_requests'</span>)-&gt;default(<span class="hljs-number">0</span>);
            $table-&gt;timestamp(<span class="hljs-string">'last_access_at'</span>)-&gt;nullable();

            $table-&gt;timestamps();
        });
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">down</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
    </span>{
        Schema::dropIfExists(<span class="hljs-string">'private_beta_invitations'</span>);
    }
};
</code></pre>
<p>Migrate:</p>
<pre><code class="lang-bash">php artisan migrate
</code></pre>
<p>The Model <code>PrivateBetaInvitation</code>:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Models/PrivateBetaInvitation.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Database</span>\<span class="hljs-title">Eloquent</span>\<span class="hljs-title">Model</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PrivateBetaInvitation</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span>
</span>{
    <span class="hljs-keyword">protected</span> $fillable = [
        <span class="hljs-string">'email'</span>,
        <span class="hljs-string">'status'</span>,
        <span class="hljs-string">'access_code'</span>,
        <span class="hljs-string">'expire_at'</span>,
    ];

    <span class="hljs-keyword">protected</span> $casts = [
        <span class="hljs-string">'expire_at'</span> =&gt; <span class="hljs-string">'datetime'</span>,
        <span class="hljs-string">'num_requests'</span> =&gt; <span class="hljs-string">'integer'</span>,
        <span class="hljs-string">'last_access_at'</span> =&gt; <span class="hljs-string">'datetime'</span>,
    ];
}
</code></pre>
<hr />
<h2 id="heading-2-add-some-config-parameters">2. Add some Config parameters</h2>
<p>Add variables in <code>.env</code> file:</p>
<pre><code class="lang-ini"><span class="hljs-attr">PRIVATE_BETA_ENABLED</span>=<span class="hljs-literal">true</span>

<span class="hljs-comment"># whitelist IPs example: &lt;my-home-ip&gt;,&lt;my-office-ip&gt;</span>
<span class="hljs-attr">PRIVATE_BETA_WHITELIST_IPS</span>=<span class="hljs-string">""</span>
</code></pre>
<p>Then, refer to these variables in the <code>config/app.php</code> file:</p>
<pre><code class="lang-php"><span class="hljs-comment">// config/app.php</span>

<span class="hljs-keyword">return</span> [
    <span class="hljs-comment">// ...</span>

    <span class="hljs-string">'private-beta'</span> =&gt; [
        <span class="hljs-comment">/*
        | When `true`, the app can't be browsed without an access code!
        */</span>
        <span class="hljs-string">'enabled'</span> =&gt; env(<span class="hljs-string">'PRIVATE_BETA_ENABLED'</span>, <span class="hljs-literal">false</span>),

        <span class="hljs-comment">/*
        | Comma separated IP list for exclude the access code required when Private Beta is `enabled`.
        | You can put here YOUR IP.
        */</span>
        <span class="hljs-string">'whitelist_ips'</span> =&gt; env(<span class="hljs-string">'PRIVATE_BETA_WHITELIST_IPS'</span>, <span class="hljs-string">''</span>),
    ],

];
</code></pre>
<blockquote>
<p><strong><em>💡 SMALL TIP: In case, remember to launch</em></strong> <code>php artisan config:clear</code> or <code>php artisan config:cache</code></p>
</blockquote>
<hr />
<h2 id="heading-3-make-a-service-class">3. Make a Service class</h2>
<p>Let's create a Service class <code>PrivateBetaService</code> where we put all the logic.</p>
<p>Since there is no "<em>make:service</em>" command in Laravel (yet?) we can simply create an empty file in the <code>app/Services</code> folder by running:</p>
<pre><code class="lang-bash">mkdir app/Services &amp;&amp; touch app/Services/PrivateBetaService.php
</code></pre>
<p>Before implementation, here is an overview of the <strong>method interfaces</strong> of the Service class:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Services/PrivateBetaService.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Models</span>\<span class="hljs-title">PrivateBetaInvitation</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PrivateBetaService</span>
</span>{

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isEnabled</span>(<span class="hljs-params"></span>): <span class="hljs-title">bool</span>
    </span>{
        <span class="hljs-comment">// ...</span>
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isIpWhitelisted</span>(<span class="hljs-params">?<span class="hljs-keyword">string</span> $ip</span>): <span class="hljs-title">bool</span>
    </span>{
        <span class="hljs-comment">// ...</span>
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkAccessCode</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $accessCode</span>): <span class="hljs-title">PrivateBetaInvitation</span>
    </span>{
        <span class="hljs-comment">// ...</span>
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">access</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $accessCode</span>): <span class="hljs-title">PrivateBetaInvitation</span>
    </span>{
        <span class="hljs-comment">// ...</span>
    }

}
</code></pre>
<p>Finally, the full implementation of the Service class:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Services/PrivateBeteService.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Models</span>\<span class="hljs-title">PrivateBetaInvitation</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PrivateBetaService</span>
</span>{

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isEnabled</span>(<span class="hljs-params"></span>): <span class="hljs-title">bool</span>
    </span>{
        <span class="hljs-keyword">return</span> config(<span class="hljs-string">'app.private-beta.enabled'</span>, <span class="hljs-literal">false</span>);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isIpWhitelisted</span>(<span class="hljs-params">?<span class="hljs-keyword">string</span> $ip</span>): <span class="hljs-title">bool</span>
    </span>{
        <span class="hljs-keyword">if</span> (blank($ip)) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
        }

        $whitelistIps = config(<span class="hljs-string">'app.private-beta.whitelist_ips'</span>);
        <span class="hljs-keyword">if</span> (blank($whitelistIps)) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
        }

        <span class="hljs-keyword">return</span> \Str::of($whitelistIps)
            -&gt;split(<span class="hljs-string">'/[\s,]+/'</span>)
            -&gt;contains($ip);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkAccessCode</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $accessCode</span>): <span class="hljs-title">PrivateBetaInvitation</span>
    </span>{
        <span class="hljs-keyword">if</span> (! <span class="hljs-keyword">$this</span>-&gt;isEnabled()) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> \<span class="hljs-built_in">Exception</span>(__(<span class="hljs-string">'Private Beta is not enabled!'</span>));
        }

        $invitation = PrivateBetaInvitation::query()
            -&gt;where(<span class="hljs-string">'access_code'</span>, $accessCode)
            -&gt;latest()
            -&gt;first();

        <span class="hljs-keyword">if</span> (! $invitation) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> \<span class="hljs-built_in">Exception</span>(__(<span class="hljs-string">'The access code you sent is invalid'</span>));
        }

        <span class="hljs-keyword">if</span> ($invitation-&gt;status != <span class="hljs-string">'active'</span>) {
            $msg = match($invitation-&gt;status) {
                <span class="hljs-string">'pending'</span>  =&gt; __(<span class="hljs-string">'Your access code is currenty pending.'</span>),
                <span class="hljs-string">'waiting'</span>  =&gt; __(<span class="hljs-string">'Your access code is currenty waiting for the staff.'</span>),
                <span class="hljs-string">'archived'</span> =&gt; __(<span class="hljs-string">'Your access code has expired.'</span>),
                <span class="hljs-keyword">default</span>    =&gt; __(<span class="hljs-string">'The access code you sent is invalid!'</span>)
            };

            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> \<span class="hljs-built_in">Exception</span>($msg);
        }

        <span class="hljs-keyword">if</span> ($invitation-&gt;expire_at &amp;&amp; now()-&gt;gt($invitation-&gt;expire_at)) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> \<span class="hljs-built_in">Exception</span>(__(<span class="hljs-string">'Your access code has expired.'</span>));
        }

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

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">access</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $accessCode</span>): <span class="hljs-title">PrivateBetaInvitation</span>
    </span>{
        $invitation = <span class="hljs-keyword">$this</span>-&gt;checkAccessCode($accessCode);

        $invitation-&gt;num_requests += <span class="hljs-number">1</span>;
        $invitation-&gt;last_access_at = now();
        $invitation-&gt;save();

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

}
</code></pre>
<hr />
<h2 id="heading-4-make-the-routes-the-view-and-the-controller">4. Make the Routes, the View and the Controller</h2>
<h3 id="heading-the-routes">The Routes</h3>
<p>Let's add 2 routes:</p>
<ol>
<li><p><strong>GET</strong> <code>/private-beta</code> (named: <code>private-beta.index</code>)</p>
</li>
<li><p><strong>POST</strong> <code>/private-beta/access</code> (named: <code>private-beta.access</code>)</p>
</li>
</ol>
<pre><code class="lang-php"><span class="hljs-comment">// routes/web.php</span>

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

<span class="hljs-comment">// ...</span>
Route::get(<span class="hljs-string">'/private-beta'</span>, [PrivateBetaController::class, <span class="hljs-string">'index'</span>])
    -&gt;name(<span class="hljs-string">'private-beta.index'</span>);

Route::post(<span class="hljs-string">'/private-beta/access'</span>, [PrivateBetaController::class, <span class="hljs-string">'access'</span>])
    -&gt;middleware(<span class="hljs-string">'throttle:5,1'</span>)
    -&gt;name(<span class="hljs-string">'private-beta.access'</span>);
</code></pre>
<blockquote>
<p>Note the <code>'throttle:5,1'</code>: you cannot attempt to access more than 5 times per minute.</p>
</blockquote>
<h3 id="heading-the-view">The View</h3>
<p>Make the View <code>private-beta.index</code>:</p>
<pre><code class="lang-bash">php artisan make:view private-beta.index
</code></pre>
<p>Then the content with a form pointing to the route named <code>private-beta.access</code>:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- resources/views/private-beta/index.blade.php --&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">x-guest-layout</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">x-auth-session-status</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mb-4"</span> <span class="hljs-attr">:status</span>=<span class="hljs-string">"session('status')"</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-center text-4xl font-extrabold leading-none tracking-tight text-gray-600 dark:text-white"</span>&gt;</span>
        @lang('Private Beta')
    <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-3 text-center text-gray-700 dark:text-white"</span>&gt;</span>
        @lang('This app is only open to users with an access code.')
    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">hr</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"my-6"</span>&gt;</span>

    @if (! $isIpWhitelisted)
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"POST"</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"{{ route('private-beta.access') }}"</span>&gt;</span>
        @csrf

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">x-input-label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"access_code"</span> <span class="hljs-attr">:value</span>=<span class="hljs-string">"__('Access Code') . ':'"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sm:text-lg sm:text-blue-700"</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">x-text-input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"access_code"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"block mt-1 w-full"</span>
                <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"access_code"</span> <span class="hljs-attr">:value</span>=<span class="hljs-string">"old('access_code', $currentAccessCode ?? null)"</span>
                <span class="hljs-attr">required</span> <span class="hljs-attr">autofocus</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">x-input-error</span> <span class="hljs-attr">:messages</span>=<span class="hljs-string">"$errors-&gt;get('access_code')"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-2"</span> /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-4"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">x-primary-button</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-center w-full sm:block sm:text-lg"</span>&gt;</span>
                @lang('Enter Private Beta')
            <span class="hljs-tag">&lt;/<span class="hljs-name">x-primary-button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
    @else
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-center text-green-600 font-bold"</span>&gt;</span>
        @lang('Your IP is Whitelisted!')
    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    @endif
<span class="hljs-tag">&lt;/<span class="hljs-name">x-guest-layout</span>&gt;</span>
</code></pre>
<blockquote>
<p>Here, we will use <strong>Blade</strong> and assume that the <a target="_blank" href="https://laravel.com/docs/10.x/starter-kits#laravel-breeze"><strong>Breeze</strong></a> Starter Kit is installed, but you can create the view however you like.</p>
</blockquote>
<hr />
<h3 id="heading-the-controller">The Controller</h3>
<p>Now, let's add the Controller <code>PrivateBetaController</code> with 2 methods:</p>
<ol>
<li><p><code>index()</code> renders the view</p>
</li>
<li><p><code>access()</code> receives and validates the <em>access code</em> and, when it is correct, sends a cookie to the client for the next requests.</p>
</li>
</ol>
<pre><code class="lang-bash">php artisan make:controller PrivateBetaController
</code></pre>
<p>The Controller:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Http/Controllers/PrivateBetaController.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">Services</span>\<span class="hljs-title">PrivateBetaService</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">PrivateBetaController</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">protected</span> PrivateBetaService $privateBetaService
    </span>) </span>{}

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">index</span>(<span class="hljs-params">Request $request</span>)
    </span>{
        <span class="hljs-keyword">if</span> (! <span class="hljs-keyword">$this</span>-&gt;privateBetaService-&gt;isEnabled()) {
            <span class="hljs-keyword">return</span> back();
        }

        $isIpWhitelisted = <span class="hljs-keyword">$this</span>-&gt;privateBetaService-&gt;isIpWhitelisted($request-&gt;ip());
        $currentAccessCode = $request-&gt;cookie(<span class="hljs-string">'private_beta_access_code'</span>);

        <span class="hljs-keyword">return</span> view(<span class="hljs-string">'private-beta.index'</span>, compact(<span class="hljs-string">'isIpWhitelisted'</span>, <span class="hljs-string">'currentAccessCode'</span>));
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">access</span>(<span class="hljs-params">Request $request</span>)
    </span>{
        <span class="hljs-keyword">if</span> (! <span class="hljs-keyword">$this</span>-&gt;privateBetaService-&gt;isEnabled()) {
            <span class="hljs-keyword">return</span> back();
        }

        $request-&gt;validate([
            <span class="hljs-string">'access_code'</span> =&gt; <span class="hljs-string">'required'</span>,
        ]);

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">$this</span>-&gt;privateBetaService-&gt;checkAccessCode($request-&gt;access_code);
        } <span class="hljs-keyword">catch</span> (\<span class="hljs-built_in">Exception</span> $e) {
            <span class="hljs-keyword">return</span> back()-&gt;withErrors([<span class="hljs-string">'access_code'</span> =&gt; $e-&gt;getMessage()]);
        }

        $cookie = cookie(<span class="hljs-string">'private_beta_access_code'</span>, $request-&gt;access_code);

        <span class="hljs-keyword">return</span> redirect()
            -&gt;intended() <span class="hljs-comment">// &lt;-- this is where the user originally wanted to go</span>
            -&gt;withCookie($cookie);
    }
}
</code></pre>
<hr />
<h2 id="heading-5-make-the-middleware-this-is-where-the-work-happens">5. Make the Middleware: This is where the work happens</h2>
<p>Let's make the Middleware with the name <code>PrivateBetaMiddleware</code>:</p>
<pre><code class="lang-bash">php artisan make:middleware PrivateBetaMiddleware
</code></pre>
<p>The Service class implementation:</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">Middleware</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Services</span>\<span class="hljs-title">PrivateBetaService</span>;
<span class="hljs-comment">// use ...</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PrivateBetaMiddleware</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">protected</span> PrivateBetaService $privateBetaService
    </span>) </span>{}

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handle</span>(<span class="hljs-params">Request $request, <span class="hljs-built_in">Closure</span> $next</span>): <span class="hljs-title">Response</span>
    </span>{
        <span class="hljs-keyword">if</span> (
            ! <span class="hljs-keyword">$this</span>-&gt;isCheckPassed($request)
            &amp;&amp; ! $request-&gt;route()?-&gt;named(<span class="hljs-string">'private-beta.*'</span>)
        ) {
            <span class="hljs-keyword">return</span> redirect()
                -&gt;setIntendedUrl(url()-&gt;current()) 
                <span class="hljs-comment">// ^ here is where the user will be redirect back</span>
                -&gt;route(<span class="hljs-string">'private-beta.index'</span>);
        }

        <span class="hljs-keyword">return</span> $next($request);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isCheckPassed</span>(<span class="hljs-params">Request $request</span>): <span class="hljs-title">bool</span>
    </span>{
        <span class="hljs-keyword">if</span> (! <span class="hljs-keyword">$this</span>-&gt;privateBetaService-&gt;isEnabled()) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
        }

        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>-&gt;privateBetaService-&gt;isIpWhitelisted($request-&gt;ip())) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
        }

        $cookieAccessCode = $request-&gt;cookie(<span class="hljs-string">'private_beta_access_code'</span>);
        <span class="hljs-keyword">if</span> (blank($cookieAccessCode)) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
        }

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">$this</span>-&gt;privateBetaService-&gt;access($cookieAccessCode);
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
        } <span class="hljs-keyword">catch</span> (\<span class="hljs-built_in">Exception</span> $e) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
        }
    }
}
</code></pre>
<p>Now, we add the middleware to the <code>'web'</code> group of <code>App\Http\Kernel</code> class. In this way, it will be applied to each <em>web request</em>.</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Http/Kernel.php</span>

<span class="hljs-comment">// ...</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Kernel</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpKernel</span>
</span>{
    <span class="hljs-comment">// ...</span>

    <span class="hljs-keyword">protected</span> $middlewareGroups = [
        <span class="hljs-string">'web'</span> =&gt; [
            <span class="hljs-comment">// ...</span>

            \App\Http\Middleware\PrivateBetaMiddleware::class,
        ],
    ];

    <span class="hljs-comment">// ...</span>
}
</code></pre>
<hr />
<h2 id="heading-6-check-that-everything-works">6. Check that everything works!</h2>
<p>For simplicity, let's create a <code>PrivateBetaInvitation</code> Model record directly via <em>Tinker</em>.</p>
<blockquote>
<p><strong>IMPORTANT</strong>: <code>status</code> must be set to <code>'active'</code></p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695417267400/4675c858-4150-45bd-981d-ed1345b41de4.png" alt="Create record of Model PrivateBetaInvitation" class="image--center mx-auto" /></p>
<p>Now, let's open the app in the browser. If everything is ok, you will be redirected to the <code>/private-beta</code> page.</p>
<p>In a first test, we enter the incorrect code "WRONGCODE" and verify that the error message is displayed as we expect:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695417351134/12554049-d61e-4b80-8e65-ce3a14499387.png" alt="Test: Put a wrong Access Code" class="image--center mx-auto" /></p>
<p>Finally, it's time to put the correct code “JUSTATEST":</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695418102964/4b342050-ee73-4f70-9a47-aa277be4f166.png" alt="Test: Put a correct Access Code" class="image--center mx-auto" /></p>
<p><strong>Boom! It works!</strong></p>
<p>Let's open browser <em>DevTools</em> and check that the cookie is set as we expect:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695418057278/97385bd9-a2ce-44a2-9d16-35c38674f686.png" alt="Check in browser DevTools" class="image--center mx-auto" /></p>
<p>Back to Tinker, fetch again the invitation record and check if <code>num_requests</code> and <code>last_access_at</code> are changed:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695417528281/d017cd29-39f5-4a4d-81ae-90a5875f1a4d.png" alt="Check if data is changed in the Model" class="image--center mx-auto" /></p>
<p><em>Great!</em></p>
<hr />
<h2 id="heading-next-steps">Next Steps</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695547609136/0ecedc21-9b19-4f24-8f1a-33ea49d17ffb.jpeg" alt="Next Steps" class="image--center mx-auto" /></p>
<h3 id="heading-what-is-missing">What is missing?</h3>
<p>Obviously, to manage a <strong>private beta</strong> it is not enough to have an access point but, at a minimum, you need to be able to manage requests from new testers and send them access codes via email, in order to simplify access as much as possible.</p>
<p>All these operations <em>can be automated</em>. This makes sense to do when the numbers are not manageable manually. Perhaps we will address these aspects in a future specific tutorial.</p>
<hr />
<p>✸ Enjoy your coding!</p>
<h6 id="heading-if-you-liked-this-post-dont-forget-to-add-your-subscribe-to-my-newsletter"><em>If you liked this post, don't forget to add your</em> <strong><em>Subscribe</em></strong> <em>to my</em> <strong><em>newsletter</em></strong>!</h6>
]]></content:encoded></item><item><title><![CDATA[How to robustly expose a webhook in Laravel]]></title><description><![CDATA[In this tutorial, we will see how to make our Laravel application capable of receiving notifications from a server app, using a simple webhook.
The app that exposes the webhook is a client app. The client part is generally much simpler than the serve...]]></description><link>https://tonyjoe.dev/how-to-robustly-expose-a-webhook-in-laravel</link><guid isPermaLink="true">https://tonyjoe.dev/how-to-robustly-expose-a-webhook-in-laravel</guid><category><![CDATA[Laravel]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[webhooks]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Tony Joe]]></dc:creator><pubDate>Wed, 06 Sep 2023 14:27:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693865231684/438b2d04-87e7-452f-a0d4-2a5f2e0e8fbc.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this tutorial, we will see how to make our <strong>Laravel</strong> application capable of receiving notifications from a server app, using a simple <strong>webhook</strong>.</p>
<p>The app that exposes the <strong>webhook</strong> is a <em>client app</em>. The client part is generally much simpler than the server part. In fact, the client basically has to worry about <em>exposing an endpoint</em> (i.e. the <strong>webhook</strong>) and managing incoming calls.</p>
<p>In reality, in a robust and efficient system, the client should respond <em>immediately</em> and without delay. For this purpose, it is necessary <em>to queue</em> the received notifications and subsequently manage them with a <em>Job</em>. By doing so, it is also possible to perform complex and/or time-consuming tasks, without blocking the caller unnecessarily.</p>
<blockquote>
<p>Thankfully, to manage all of this we don't have to rewrite everything from scratch. We will use the package: 👉 <a target="_blank" href="https://github.com/spatie/laravel-webhook-client">Laravel Webhook Client</a></p>
<p><em>The package is developed and maintained by</em> <a target="_blank" href="https://spatie.be/"><strong><em>Spatie</em></strong></a> <em>and we all know how much this is a guarantee!</em></p>
</blockquote>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>Note: Here we won't worry about what the server app has to do, but if you are interested you can refer to the post </em><a target="_blank" href="https://tonyjoe.dev/how-to-manage-subscribed-webhooks-in-laravel"><em>How to manage subscribed webhooks in Laravel</em></a><em>.</em></div>
</div>

<hr />
<h3 id="heading-can-i-trust-the-caller">Can I trust the caller?</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693950272339/795e0355-1b07-4b4e-807c-7ad75868f61a.jpeg" alt="Can I trust the caller?" class="image--center mx-auto" /></p>
<p>One of the main issues to be addressed on the client side concerns the <strong>reliability of the caller</strong>, namely: is the application that contacts our webhook exactly the one <em>authorized</em> to do so or is there some <em>funny guy</em> pretending to be it?</p>
<p>The method that we will use in this tutorial is to verify that <strong>a signature is present among the headers of the requests received</strong> and that this has been affixed using a <strong>secret key</strong>. The package will do the checking for us. It will be enough for us to configure the correct <em>Signature Secret Key</em>.</p>
<p>By default, <a target="_blank" href="https://github.com/spatie/laravel-webhook-client">Laravel Webhook Client package</a> uses the class <a target="_blank" href="https://github.com/spatie/laravel-webhook-client/blob/main/src/SignatureValidator/DefaultSignatureValidator.php"><code>DefaultSignatureValidator</code></a> to validate signatures. This is how that class will compute the signature:</p>
<pre><code class="lang-php">hash_hmac(<span class="hljs-string">'sha256'</span>, $request-&gt;getContent(), $signatureSecretKey);
</code></pre>
<p><em>As you can see, the validation is very easy but, at the same time, very strong.</em></p>
<hr />
<h2 id="heading-steps">Steps</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-1-install-the-laravel-webhook-client-package">Install the Laravel Webhook Client package</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-open-the-route-and-handle-the-job">Open the route and handle the job</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-make-some-tests-from-server-app">Make some tests from server app</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-prune-webhookcall-models">Prune <code>WebhookCall</code> models</a></p>
</li>
</ol>
<hr />
<h3 id="heading-1-install-the-laravel-webhook-client-package">1. Install the Laravel Webhook Client package</h3>
<p>Install the package:</p>
<pre><code class="lang-bash">composer require spatie/laravel-webhook-client
</code></pre>
<p>Publish the config file <code>config/webhook-client.php</code>:</p>
<pre><code class="lang-bash">php artisan vendor:publish --provider=<span class="hljs-string">"Spatie\WebhookClient\WebhookClientServiceProvider"</span> --tag=<span class="hljs-string">"webhook-client-config"</span>
</code></pre>
<p>Publish the migration:</p>
<pre><code class="lang-bash">php artisan vendor:publish --provider=<span class="hljs-string">"Spatie\WebhookClient\WebhookClientServiceProvider"</span> --tag=<span class="hljs-string">"webhook-client-migrations"</span>
</code></pre>
<p>Migrate:</p>
<pre><code class="lang-bash">php artisan migrate
</code></pre>
<hr />
<h3 id="heading-2-open-the-route-and-handle-the-job">2. Open the route and handle the job</h3>
<p>Add a route in <code>routes/web.php</code>.</p>
<pre><code class="lang-php"><span class="hljs-comment">// routes/web.php</span>

Route::webhooks(<span class="hljs-string">'/webhook'</span>);
<span class="hljs-comment">// this will open a POST route `/webhook`</span>
</code></pre>
<p><strong>IMPORTANT</strong>: Of course, you can change <code>/webhook</code> with others, but the correct URL must be shared with the server app.</p>
<blockquote>
<p>Now, if you run <code>php artisan route:list</code> you can check if there is the POST entry:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693948397757/f2277519-53f7-4421-91f4-64faca4f6b07.png" alt="Route list" class="image--center mx-auto" /></p>
</blockquote>
<p>Because the app that sends webhooks to you has no way of getting a csrf-token, you must add that route to the <code>except</code> array of the <code>VerifyCsrfToken</code> middleware:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Http/Middleware/VerifyCsrfToken.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Foundation</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Middleware</span>\<span class="hljs-title">VerifyCsrfToken</span> <span class="hljs-title">as</span> <span class="hljs-title">Middleware</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">VerifyCsrfToken</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Middleware</span>
</span>{
    <span class="hljs-keyword">protected</span> $except = [
        <span class="hljs-string">'/webhook'</span>, <span class="hljs-comment">// &lt;-- add this line</span>
    ];
}
</code></pre>
<p>Now, it's time to set in the <code>.env</code> file the <em>Signature Secret Key</em> that your server application shared with you:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># .env</span>
<span class="hljs-comment"># ...</span>

WEBHOOK_CLIENT_SECRET=<span class="hljs-string">"paste-here-the-signature-secret-key-shared-with-server-app"</span>
</code></pre>
<p>If you make a test request now, you get an Exception by the package informing you that there is no valid <em>process webhook job class</em>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693948787831/dd77eda3-5903-478a-af88-ac9d6d8e8e7e.png" alt="Test request to webhook" class="image--center mx-auto" /></p>
<p><em>And that's what we're going to do now!</em></p>
<p>Make the job to handle calls:</p>
<pre><code class="lang-bash">php artisan make:job ProcessWebhookJob
</code></pre>
<p>For the purpose of the tutorial, we simply log the received data. We can find it in <code>$this-&gt;webhookCall</code>:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Jobs/ProcessWebhookJob.php</span>

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

<span class="hljs-comment">/* use ... */</span>
<span class="hljs-keyword">use</span> <span class="hljs-title">Spatie</span>\<span class="hljs-title">WebhookClient</span>\<span class="hljs-title">Jobs</span>\<span class="hljs-title">ProcessWebhookJob</span> <span class="hljs-title">as</span> <span class="hljs-title">SpatieProcessWebhookJob</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProcessWebhookJob</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">SpatieProcessWebhookJob</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">ShouldQueue</span>
</span>{
    <span class="hljs-keyword">use</span> <span class="hljs-title">Dispatchable</span>, <span class="hljs-title">InteractsWithQueue</span>, <span class="hljs-title">Queueable</span>, <span class="hljs-title">SerializesModels</span>;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handle</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-comment">// `$this-&gt;webhookCall` contains an instance of:</span>
        <span class="hljs-comment">// `\Spatie\WebhookClient\Models\WebhookCall`</span>

        $event = \Arr::get(<span class="hljs-keyword">$this</span>-&gt;webhookCall-&gt;payload, <span class="hljs-string">'event'</span>);
        $data = \Arr::get(<span class="hljs-keyword">$this</span>-&gt;webhookCall-&gt;payload, <span class="hljs-string">'data'</span>, []);

        <span class="hljs-comment">// ...</span>
        <span class="hljs-comment">// [perform the work here] ...</span>

        <span class="hljs-comment">// simply log `event` and `data`:</span>
        logger(<span class="hljs-string">'ProcessWebhookJob'</span>, [
            <span class="hljs-string">'event'</span> =&gt; $event,
            <span class="hljs-string">'data'</span>  =&gt; $data,
        ]);

        <span class="hljs-comment">// ...</span>
    }
}
</code></pre>
<p>Finally, we set the <code>process_webhook_job</code> field to indicate the <em>Job</em> class we just created, in the config file at <code>config/webhook-client.php</code>:</p>
<pre><code class="lang-php"><span class="hljs-comment">// config/webhook-client.php</span>
<span class="hljs-comment">// ...</span>
<span class="hljs-string">'process_webhook_job'</span> =&gt; \App\Jobs\ProcessWebhookJob::class,
</code></pre>
<p>Now, if you re-launch the test request from earlier, you successfully get an invalid signature error:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693949314345/0427094c-d5f8-4d04-a729-cd4d40c27807.png" alt="Test request to webhook" class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-3-make-some-tests-from-server-app">3. Make some tests from server app</h3>
<p>👉 From the <strong>server app</strong> we trigger a test event:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693952596013/2c3e85f3-b749-4eb5-a479-6636228c18a1.png" alt="Trigger event from server app" class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>Note: Here we are using the server app that we created with the post </em><a target="_blank" href="https://tonyjoe.dev/how-to-manage-subscribed-webhooks-in-laravel"><em>How to manage subscribed webhooks in Laravel</em></a><em>.</em></div>
</div>

<p>🎯 In the log of <strong>client app</strong> we have the correct data!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693952732245/aa8920a9-fcb9-48fd-b2e7-d02ff1a3e40e.png" alt="Logs in client app" class="image--center mx-auto" /></p>
<hr />
<p>👉 Another event triggered from <strong>server app</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693953292771/d2794e10-205c-4383-8161-8b13f91b7663.png" alt="Another event from server app" class="image--center mx-auto" /></p>
<p>🎯 And in the <strong>client app</strong> log:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693953465928/8009e99c-2335-4efb-8ea0-dc23f6e2d5bb.png" alt="Logs again in client app" class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-4-prune-webhookcall-models">4. Prune <code>WebhookCall</code> models</h3>
<p>With each call to our webhook, the package will add a record of the <code>WebhookCall</code> model. You can simply prune old records, by calling <code>model:prune</code>.</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Console/Kernel.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Foundation</span>\<span class="hljs-title">Console</span>\<span class="hljs-title">Kernel</span> <span class="hljs-title">as</span> <span class="hljs-title">ConsoleKernel</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Spatie</span>\<span class="hljs-title">WebhookClient</span>\<span class="hljs-title">Models</span>\<span class="hljs-title">WebhookCall</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Kernel</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ConsoleKernel</span>
</span>{
    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">schedule</span>(<span class="hljs-params">Schedule $schedule</span>)
    </span>{
        $schedule-&gt;command(<span class="hljs-string">'model:prune'</span>, [
            <span class="hljs-string">'--model'</span> =&gt; [WebhookCall::class],
        ])-&gt;daily();

        <span class="hljs-comment">// This will not work, as models in a package are not used by default</span>
        <span class="hljs-comment">// $schedule-&gt;command('model:prune')-&gt;daily();</span>
    }
}
</code></pre>
<p>By default, records created more than 30 days ago will be removed.</p>
<p>You can customize this limit using the parameter <code>delete_after_days</code> in file <code>config/webhook-client.php</code>:</p>
<pre><code class="lang-php"><span class="hljs-comment">// config/webhook-client.php</span>

<span class="hljs-keyword">return</span> [
    <span class="hljs-string">'configs'</span> =&gt; [
        <span class="hljs-comment">// ...</span>
    ],

    <span class="hljs-string">'delete_after_days'</span> =&gt; <span class="hljs-number">30</span>,
];
</code></pre>
<hr />
<p>✸ Enjoy your coding!</p>
<p><em>If you liked this post, don't forget to</em> <strong><em>Subscribe</em></strong> <em>to my</em> <strong><em>newsletter</em></strong>!</p>
]]></content:encoded></item><item><title><![CDATA[How to manage subscribed webhooks in Laravel]]></title><description><![CDATA[Web applications are increasingly interconnected: data is continuously exchanged and notifications are sent when certain events occur. Until a few years ago, developing solid connections between different systems was no mean feat and required conside...]]></description><link>https://tonyjoe.dev/how-to-manage-subscribed-webhooks-in-laravel</link><guid isPermaLink="true">https://tonyjoe.dev/how-to-manage-subscribed-webhooks-in-laravel</guid><category><![CDATA[Laravel]]></category><category><![CDATA[LaravelTutorials]]></category><category><![CDATA[webhooks]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Tony Joe]]></dc:creator><pubDate>Sun, 27 Aug 2023 21:17:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693170743928/ccc5ed6f-78a8-462e-afac-16821803dd52.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Web applications are increasingly <strong>interconnected</strong>: data is continuously exchanged and notifications are sent when certain events occur. Until a few years ago, developing <em>solid</em> connections between different systems was no mean feat and required considerable effort.</p>
<p>In this tutorial, we will see how to make our <strong>Laravel</strong> application capable of sending notifications to external <em>listening</em> applications, using <strong>webhooks</strong>.</p>
<hr />
<h2 id="heading-first-the-basics-what-is-a-webhook">First, the basics: What is a Webhook?</h2>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0dktpz0k2ja6fz2vy2ii.png" alt="What is a Webhook" /></p>
<p><strong>Webhooks</strong> are HTTP callbacks that an external application (client) can <em>subscribe</em> to receive notifications from a main application (server).</p>
<p>Callbacks are therefore generally activated when an event occurs in the main application (server).</p>
<h4 id="heading-why-not-just-use-api-calls">Why not just use API calls?</h4>
<p>Our aim is to <em>standardize outgoing calls</em>. In other words, by exploiting the webhook mechanism, the server application does not need to adapt to the interface of each client, but it is the clients that must adapt and activate themselves based on the content of the <em>standardized payload</em> they will receive.</p>
<blockquote>
<p>For example, think about <em>Stripe</em>: client applications sign up for new payment events. It is the clients that fit the <em>Stripe</em> specification and not the other way around.</p>
</blockquote>
<hr />
<h2 id="heading-the-goal-of-the-tutorial-in-short">The goal of the tutorial, in short</h2>
<p>We will create a <strong>Laravel server app</strong> capable of storing client subscriptions in DB. Each client will have a different <em>Signature Secret Key</em>.</p>
<p><em>For simplicity, subscriptions will be handled with a</em> console command.</p>
<blockquote>
<p>To handle the calls, we will use a very nice package: 👉 <a target="_blank" href="https://github.com/spatie/laravel-webhook-server">Laravel Webhook Server</a></p>
<p><em>The package is developed and maintained by</em> <a target="_blank" href="https://spatie.be/"><em>Spatie</em></a> <em>and we all know how much this is a guarantee!</em></p>
</blockquote>
<hr />
<h2 id="heading-steps">Steps</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-1-install-the-laravel-webhook-server-package">Install the Laravel Webhook Server package</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-make-the-model-and-migration-for-subscriptions">Make the model and migration for subscriptions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-create-a-console-command-to-subscribe-to-webhooks">Create a console command to subscribe to webhooks</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-add-a-general-webhook-event">Add a general webhook event</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-add-the-event-listener-that-makes-the-calls">Add the event listener that makes the calls</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-example-how-to-dispatch-the-webhook-event">Example: how to dispatch the webhook event</a></p>
</li>
</ol>
<hr />
<h3 id="heading-1-install-the-laravel-webhook-server-package">1. Install the Laravel Webhook Server package</h3>
<p>Install the package:</p>
<pre><code class="lang-sh">composer require spatie/laravel-webhook-server
</code></pre>
<p>Publish the config file in <code>config/webhook-server.php</code>:</p>
<pre><code class="lang-sh">php artisan vendor:publish --provider=<span class="hljs-string">"Spatie\WebhookServer\WebhookServerServiceProvider"</span>
</code></pre>
<p>Let's extend the timeout for the calls to:</p>
<pre><code class="lang-php"><span class="hljs-comment">// config/webhook-server.php</span>

<span class="hljs-comment">/*
 * If a call to a webhook takes longer that
 * this amount of seconds
 * the attempt will be considered failed.
 */</span>
<span class="hljs-string">'timeout_in_seconds'</span> =&gt; <span class="hljs-number">10</span>,
</code></pre>
<p>You can customize many parameters. To do that, you can follow the <a target="_blank" href="https://github.com/spatie/laravel-webhook-server">documentation of the package</a>.</p>
<hr />
<h3 id="heading-2-make-the-model-and-migration-for-subscriptions">2. Make the model and migration for subscriptions</h3>
<p>Make the model <code>WebhookSubscription</code> with <code>--migration</code> option:</p>
<pre><code class="lang-sh">php artisan make:model WebhookSubscription --migration
</code></pre>
<p>In the migration, let's add the following fields to the table <code>webhook_subscriptions</code>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Field</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><code>url</code></td><td>the URL of the webhook (external app)</td></tr>
<tr>
<td><code>signature_secret_key</code></td><td>the secret key used to sign each call (to share with external app)</td></tr>
<tr>
<td><code>is_active</code></td><td></td></tr>
<tr>
<td><code>listen_events</code></td><td><code>*</code> for any event or comma-separated values - for example: <code>order:new,order:updated</code></td></tr>
</tbody>
</table>
</div><pre><code class="lang-php"><span class="hljs-comment">// database/migrations/XXXX_create_webhook_subscriptions_table</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">extends</span> <span class="hljs-title">Migration</span>
</span>{
    <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">'webhook_subscriptions'</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">'url'</span>, <span class="hljs-number">180</span>)-&gt;unique()
                -&gt;comment(<span class="hljs-string">'the url of the webhook (external app)'</span>);

            $table-&gt;string(<span class="hljs-string">'signature_secret_key'</span>, <span class="hljs-number">128</span>)
                -&gt;comment(<span class="hljs-string">'the secret key used to sign each call (to share with external app)'</span>);

            $table-&gt;boolean(<span class="hljs-string">'is_active'</span>)
                -&gt;index()
                -&gt;default(<span class="hljs-number">1</span>);

            $table-&gt;text(<span class="hljs-string">'listen_events'</span>)
                -&gt;default(<span class="hljs-string">'*'</span>)
                -&gt;comment(<span class="hljs-string">'`*` for any event or comma separated values - for example: `order:new,order:updated`'</span>);

            $table-&gt;timestamps();
        });
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">down</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
    </span>{
        Schema::dropIfExists(<span class="hljs-string">'webhook_subscriptions'</span>);
    }
};
</code></pre>
<pre><code class="lang-sh">php artisan migrate
</code></pre>
<blockquote>
<p>💡 <strong>SMALL TIP</strong> <em>For this tutorial, we just need to use a</em> <strong><em>SQLite DB</em></strong>*, but of course you can use any other type of DB already used in your app.*</p>
<p><strong>To use a SQLite DB</strong>:</p>
<pre><code class="lang-ini"><span class="hljs-comment"># .env</span>
<span class="hljs-comment"># ...</span>

<span class="hljs-attr">DB_CONNECTION</span>=sqlite
<span class="hljs-attr">DB_HOST</span>=
<span class="hljs-attr">DB_PORT</span>=
<span class="hljs-attr">DB_DATABASE</span>=tony_webhook.sqlite
</code></pre>
</blockquote>
<p>The model:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Models/WebhookSubscription.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Database</span>\<span class="hljs-title">Eloquent</span>\<span class="hljs-title">Casts</span>\<span class="hljs-title">Attribute</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Database</span>\<span class="hljs-title">Eloquent</span>\<span class="hljs-title">Factories</span>\<span class="hljs-title">HasFactory</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Database</span>\<span class="hljs-title">Eloquent</span>\<span class="hljs-title">Model</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebhookSubscription</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> $fillable = [
        <span class="hljs-string">'url'</span>,
        <span class="hljs-string">'signature_secret_key'</span>,
        <span class="hljs-string">'is_active'</span>,
        <span class="hljs-string">'listen_events'</span>,
    ];

    <span class="hljs-keyword">protected</span> $casts = [
        <span class="hljs-string">'is_active'</span> =&gt; <span class="hljs-string">'boolean'</span>,
    ];

    <span class="hljs-keyword">protected</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">booted</span>(<span class="hljs-params"></span>)
    </span>{
        <span class="hljs-built_in">self</span>::saving(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">$model</span>) </span>{
            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">empty</span>($model-&gt;signature_secret_key)) {
                $model-&gt;signature_secret_key = \Str::random(<span class="hljs-number">64</span>);
            }
        });
    }

    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">listenEvents</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">fn</span> (<span class="hljs-params">?<span class="hljs-keyword">string</span> $value</span>) =&gt; <span class="hljs-title">is_string</span>(<span class="hljs-params">$value</span>)
                ? <span class="hljs-title">array_map</span>(<span class="hljs-params"><span class="hljs-string">'trim'</span>, explode(<span class="hljs-params"><span class="hljs-string">','</span>, $value</span>)</span>)
                : [],
            <span class="hljs-title">set</span>: <span class="hljs-title">fn</span> (<span class="hljs-params"><span class="hljs-keyword">string</span>|<span class="hljs-keyword">array</span>|<span class="hljs-literal">null</span> $value</span>) =&gt; <span class="hljs-title">is_array</span>(<span class="hljs-params">$value</span>)
                ? <span class="hljs-title">implode</span>(<span class="hljs-params"><span class="hljs-string">','</span>, $value</span>)
                : $<span class="hljs-title">value</span>,
        )</span>;
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isListenFor</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $event</span>): <span class="hljs-title">bool</span>
    </span>{
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>-&gt;isListenForAnyEvent()) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
        }

        <span class="hljs-keyword">return</span> in_array($event, <span class="hljs-keyword">$this</span>-&gt;listen_events);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isListenForAnyEvent</span>(<span class="hljs-params"></span>): <span class="hljs-title">bool</span>
    </span>{
        <span class="hljs-keyword">return</span> in_array(<span class="hljs-string">'*'</span>, <span class="hljs-keyword">$this</span>-&gt;listen_events);
    }

}
</code></pre>
<hr />
<h3 id="heading-3-create-a-console-command-to-subscribe-to-webhooks">3. Create a console command to subscribe to webhooks</h3>
<p><em>This is just a utility to add subscriptions via console, but you can add records via</em> <code>artisan tinker</code> or build the UI for that.</p>
<pre><code class="lang-sh">php artisan make:<span class="hljs-built_in">command</span> WebhookSubscribe
</code></pre>
<pre><code class="lang-php"><span class="hljs-comment">// app/Console/Commands/WebhookSubscribe.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Models</span>\<span class="hljs-title">WebhookSubscription</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Console</span>\<span class="hljs-title">Command</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">Console</span>\<span class="hljs-title">PromptsForMissingInput</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebhookSubscribe</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Command</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">PromptsForMissingInput</span>
</span>{

    <span class="hljs-keyword">protected</span> $signature = <span class="hljs-string">'app:webhook-subscribe
        {url : The URL of the webhook (external app)}
        {events : The events that webhook may listen ("*" for any event or comma separated values - for example: "order:new,order:updated")}'</span>;

    <span class="hljs-keyword">protected</span> $description = <span class="hljs-string">'Add a subscribed external app to webhook'</span>;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handle</span>(<span class="hljs-params"></span>)
    </span>{
        $url = trim(<span class="hljs-keyword">$this</span>-&gt;argument(<span class="hljs-string">'url'</span>));
        $events = trim(<span class="hljs-keyword">$this</span>-&gt;argument(<span class="hljs-string">'events'</span>));

        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">empty</span>($url)) {
            <span class="hljs-keyword">$this</span>-&gt;error(<span class="hljs-string">'The URL is required!'</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
        }
        <span class="hljs-keyword">if</span> (! \Str::of($url)-&gt;startsWith(<span class="hljs-string">'https://'</span>)) {
            <span class="hljs-keyword">$this</span>-&gt;error(<span class="hljs-string">'The URL must start with `https://`!'</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
        }
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">empty</span>($events)) {
            $events = <span class="hljs-string">'*'</span>;
        }

        $alreadyExists = WebhookSubscription::where(<span class="hljs-string">'url'</span>, $url)-&gt;exists();
        <span class="hljs-keyword">if</span> ($alreadyExists) {
            <span class="hljs-keyword">$this</span>-&gt;error(<span class="hljs-string">"The URL [<span class="hljs-subst">{$url}</span>] is already subscribed!"</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
        }

        $WebhookSubscription = WebhookSubscription::create([
            <span class="hljs-string">'url'</span> =&gt; $url,
            <span class="hljs-string">'listen_events'</span> =&gt; $events,
            <span class="hljs-string">'is_active'</span> =&gt; <span class="hljs-literal">true</span>,
        ]);

        <span class="hljs-keyword">$this</span>-&gt;info(<span class="hljs-string">'OK, Webhook Subscription created.'</span>);
        <span class="hljs-keyword">$this</span>-&gt;newLine();
        <span class="hljs-keyword">$this</span>-&gt;line(<span class="hljs-string">'&gt; The signature secret key is:'</span>);
        <span class="hljs-keyword">$this</span>-&gt;line(<span class="hljs-string">"&gt; &lt;bg=magenta;fg=black&gt;<span class="hljs-subst">{$WebhookSubscription-&gt;signature_secret_key}</span>&lt;/&gt;"</span>);
        <span class="hljs-keyword">$this</span>-&gt;newLine();
        <span class="hljs-keyword">$this</span>-&gt;warn(<span class="hljs-string">'(You have to pass this key to external app for checking signature)'</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    }
}
</code></pre>
<p>Now, we have a new command. If we call the <code>artisan list</code>, we get:</p>
<pre><code class="lang-sh">php artisan list
</code></pre>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zgivy4lvi3x3o1ml1o70.png" alt="php artisan list" /></p>
<p>If we call the <code>artisan help</code>, we get:</p>
<pre><code class="lang-sh">php artisan <span class="hljs-built_in">help</span> app:webhook-subscribe
</code></pre>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bl1xmvmsfwbh36xi4ali.png" alt="php artisan help" /></p>
<h4 id="heading-create-some-subscriptions">Create some subscriptions</h4>
<p>Now, let's create a subscription example:</p>
<pre><code class="lang-sh">php artisan app:webhook-subscribe https://tony-webhook-client-1.test/webhook <span class="hljs-string">"*"</span>
</code></pre>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ydc1v5k4wgn5rr2ygj4.png" alt="Webhook subscribe" /></p>
<p>(<em>NOTE the signature secret key that you must copy and send or use in client app</em>)</p>
<hr />
<h3 id="heading-4-add-a-general-webhook-event">4. Add a general webhook event</h3>
<p>Now, let's make a general event that we will use in our app to notify all webhook subscribers.</p>
<pre><code class="lang-sh">php artisan make:event WebhookEvent
</code></pre>
<pre><code class="lang-php"><span class="hljs-comment">// app/Events/WebhookEvent.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Foundation</span>\<span class="hljs-title">Events</span>\<span class="hljs-title">Dispatchable</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Queue</span>\<span class="hljs-title">SerializesModels</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebhookEvent</span>
</span>{
    <span class="hljs-keyword">use</span> <span class="hljs-title">Dispatchable</span>, <span class="hljs-title">SerializesModels</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> $name,
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">array</span> $data,
    </span>) </span>{}

}
</code></pre>
<hr />
<h3 id="heading-5-add-the-event-listener-that-makes-the-calls">5. Add the event listener that makes the calls</h3>
<p>We have the <code>WebhookEvent</code>: now let's create the related listener:</p>
<pre><code class="lang-sh">php artisan MakeWebhookCalls --event=WebhookEvent
</code></pre>
<pre><code class="lang-php"><span class="hljs-comment">// app/Listeners/MakeWebhookCalls.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Events</span>\<span class="hljs-title">WebhookEvent</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Models</span>\<span class="hljs-title">WebhookSubscription</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Spatie</span>\<span class="hljs-title">WebhookServer</span>\<span class="hljs-title">WebhookCall</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MakeWebhookCalls</span>
</span>{

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handle</span>(<span class="hljs-params">WebhookEvent $event</span>): <span class="hljs-title">void</span>
    </span>{
        $subscriptions = WebhookSubscription::query()
            -&gt;where(<span class="hljs-string">'is_active'</span>, <span class="hljs-literal">true</span>)
            -&gt;oldest()
            -&gt;get();

        <span class="hljs-keyword">foreach</span> ($subscriptions <span class="hljs-keyword">as</span> $subscription) {
            <span class="hljs-keyword">if</span> (! $subscription-&gt;isListenFor($event-&gt;name)) {
                <span class="hljs-keyword">continue</span>;
            }

            WebhookCall::create()
                -&gt;url($subscription-&gt;url)
                -&gt;payload([
                    <span class="hljs-string">'event'</span> =&gt; $event-&gt;name,
                    <span class="hljs-string">'data'</span> =&gt; $event-&gt;data
                ])
                -&gt;useSecret($subscription-&gt;signature_secret_key)
                -&gt;dispatch();
        }
    }
}
</code></pre>
<p>Don't forget to bind the event and the listener in the <code>EventServiceProvider</code> class:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Providers/EventServiceProvider.php</span>

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

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Events</span>\<span class="hljs-title">WebhookEvent</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Listeners</span>\<span class="hljs-title">MakeWebhookCalls</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Foundation</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Providers</span>\<span class="hljs-title">EventServiceProvider</span> <span class="hljs-title">as</span> <span class="hljs-title">ServiceProvider</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EventServiceProvider</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ServiceProvider</span>
</span>{
    <span class="hljs-keyword">protected</span> $listen = [
        <span class="hljs-comment">// ...</span>

        WebhookEvent::class =&gt; [
            MakeWebhookCalls::class,
        ],
    ];
</code></pre>
<hr />
<h3 id="heading-6-example-how-to-dispatch-the-webhook-event">6. Example: how to dispatch the webhook event</h3>
<h4 id="heading-example-1-customer-newupdated">Example 1: Customer new/updated</h4>
<p>An example <code>CustomerController</code> with 2 event dispatches (<code>customer:new</code> and <code>customer:updated</code>):</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Http/Controllers/CustomerController.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">Events</span>\<span class="hljs-title">WebhookEvent</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">CustomerController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span>
</span>{
    <span class="hljs-comment">// ...</span>

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">store</span>(<span class="hljs-params">Request $request</span>)
    </span>{
        <span class="hljs-comment">// customer store logic...</span>

        event(<span class="hljs-keyword">new</span> WebhookEvent(<span class="hljs-string">'customer:new'</span>, [
            <span class="hljs-comment">// any payload data</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">Request $request</span>)
    </span>{
        <span class="hljs-comment">// customer update logic...</span>

        event(<span class="hljs-keyword">new</span> WebhookEvent(<span class="hljs-string">'customer:updated'</span>, [
            <span class="hljs-comment">// any payload data</span>
        ]));
    }
}
</code></pre>
<h4 id="heading-example-2-order-newupdated">Example 2: Order new/updated</h4>
<p>Another example <code>OrderController</code> with 2 event dispatches (<code>order:new</code> and <code>order:updated</code>):</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Http/Controllers/CustomerController.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">Events</span>\<span class="hljs-title">WebhookEvent</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">OrderController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span>
</span>{
    <span class="hljs-comment">// ...</span>

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">store</span>(<span class="hljs-params">Request $request</span>)
    </span>{
        <span class="hljs-comment">// order store logic...</span>

        event(<span class="hljs-keyword">new</span> WebhookEvent(<span class="hljs-string">'order:new'</span>, [
            <span class="hljs-comment">// any payload data</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">Request $request</span>)
    </span>{
        <span class="hljs-comment">// order update logic...</span>

        event(<span class="hljs-keyword">new</span> WebhookEvent(<span class="hljs-string">'order:updated'</span>, [
            <span class="hljs-comment">// any payload data</span>
        ]));
    }
}
</code></pre>
<hr />
<p>✸ Enjoy your coding!</p>
<p><em>If you liked this post, don't forget to</em> <strong><em>Subscribe</em></strong> <em>to my</em> <strong><em>newsletter</em></strong>!</p>
]]></content:encoded></item><item><title><![CDATA[How to simply expose an endpoint with API Key in Laravel]]></title><description><![CDATA[We all need, sooner or later, to expose an endpoint quickly and with the greatest possible security. Laravel provides advanced methods to manage authentication, whether username/password or API token.
But here we are talking about an agile method tha...]]></description><link>https://tonyjoe.dev/how-to-simply-expose-an-endpoint-with-api-key-in-laravel</link><guid isPermaLink="true">https://tonyjoe.dev/how-to-simply-expose-an-endpoint-with-api-key-in-laravel</guid><category><![CDATA[Laravel]]></category><category><![CDATA[LaravelTutorials]]></category><category><![CDATA[APIs]]></category><category><![CDATA[apikey]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Tony Joe]]></dc:creator><pubDate>Sun, 27 Aug 2023 20:57:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693169023960/477a8b76-5dd7-48d3-b8a5-325d3b7acc76.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We all need, sooner or later, to expose an endpoint quickly and with the greatest possible security. <strong>Laravel</strong> provides advanced methods to manage authentication, whether username/password or API token.</p>
<p>But here we are talking about an agile method that we can use in a project where, for example, we do not intend to use users or use <a target="_blank" href="https://laravel.com/docs/10.x/sanctum">Laravel Sanctum</a>.</p>
<hr />
<h2 id="heading-first-the-basics-what-is-an-api-key">First, the basics: What is an API Key?</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695764790098/51dd5dca-a922-47fd-90a8-4171718270b0.jpeg" alt /></p>
<p>An <strong>API Key</strong> is a <em>key</em> or <em>token</em> to be used for <strong>authenticating</strong> one or more <strong>server-to-server</strong> calls.</p>
<p>In other words, it is a <em>secret key</em> and therefore must never be exposed in the frontend code (such as that of a <em>SPA</em>).</p>
<hr />
<h2 id="heading-steps">Steps</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-1-create-a-key">Create a key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-create-a-middleware-that-checks-the-api-key">Create a middleware that checks the API Key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-create-an-example-route-and-controller">Create an example route and controller</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-test-with-postman">Test with Postman</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-create-a-test-class">Create a test class</a></p>
</li>
</ol>
<hr />
<h3 id="heading-1-create-a-key">1. Create a key</h3>
<p>Add a variable in the <code>.env</code> file:</p>
<pre><code class="lang-ini"><span class="hljs-comment"># .env</span>
<span class="hljs-comment"># ...</span>

<span class="hljs-attr">APP_FAST_API_KEY</span>=paste_here_a_generated_api_key
</code></pre>
<p>The key must be in the <code>.env</code> file, because we don't want to keep it in repository.</p>
<blockquote>
<p>💡 <strong>SMALL TIP</strong> to generate a key on the fly: Launch <code>php artisan tinker</code> and then simply <code>\Str::random(64)</code></p>
</blockquote>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tod7fcrmd82jd0j9jqrc.png" alt="Generate an API Key on-fly" /></p>
<hr />
<p>Then, add an array key that refer the variable in the <code>.env</code> file:</p>
<pre><code class="lang-php"><span class="hljs-comment">// config/app.php</span>

<span class="hljs-keyword">return</span> [
    <span class="hljs-comment">// ...</span>

    <span class="hljs-string">'fast_api_key'</span> =&gt; env(<span class="hljs-string">'APP_FAST_API_KEY'</span>),

];
</code></pre>
<blockquote>
<p>💡 <strong>SMALL TIP</strong>: In case, remember to launch <code>php artisan cache:config</code></p>
</blockquote>
<hr />
<h3 id="heading-2-create-a-middleware-that-checks-the-api-key">2. Create a middleware that checks the API Key</h3>
<pre><code class="lang-sh">php artisan make:middleware VerifyFastApiKey
</code></pre>
<pre><code class="lang-php"><span class="hljs-comment">// app/Http/Middleware/VerifyFastApiKey.php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">VerifyFastApiKey</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handle</span>(<span class="hljs-params">Request $request, <span class="hljs-built_in">Closure</span> $next</span>): <span class="hljs-title">Response</span>
    </span>{
        $apiKey = config(<span class="hljs-string">'app.fast_api_key'</span>);

        $apiKeyIsValid = (
            filled($apiKey)
            &amp;&amp; $request-&gt;header(<span class="hljs-string">'x-api-key'</span>) === $apiKey
        );

        abort_if (! $apiKeyIsValid, <span class="hljs-number">403</span>, <span class="hljs-string">'Access denied'</span>);

        <span class="hljs-keyword">return</span> $next($request);
    }
}
</code></pre>
<p>Create an alias for this middleware:</p>
<pre><code class="lang-php"><span class="hljs-comment">// app/Http/Kernel.php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Kernel</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpKernel</span>
</span>{
    <span class="hljs-comment">// ...</span>

    <span class="hljs-keyword">protected</span> $middlewareAliases = [
        <span class="hljs-comment">// ...</span>

        <span class="hljs-string">'with_fast_api_key'</span> =&gt; \App\Http\Middleware\VerifyFastApiKey::class,
    ];

}
</code></pre>
<hr />
<h3 id="heading-3-create-an-example-route-and-controller">3. Create an example route and controller</h3>
<p>Add some routes in <code>routes/api.php</code> with the newly created <code>with_fast_api_key</code> middleware:</p>
<pre><code class="lang-php"><span class="hljs-comment">// routes/api.php</span>

Route::group([
    <span class="hljs-string">'prefix'</span> =&gt; <span class="hljs-string">'v1'</span>,
    <span class="hljs-string">'middleware'</span> =&gt; <span class="hljs-string">'with_fast_api_key'</span>
], <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{

    Route::post(<span class="hljs-string">'/just/an/example'</span>, [SomethingController::class, <span class="hljs-string">'justAnExample'</span>]);

    <span class="hljs-comment">// ...</span>
});
</code></pre>
<hr />
<p>Create an example controller:</p>
<pre><code class="lang-sh">php artisan make:controller SomethingController
</code></pre>
<pre><code class="lang-php"><span class="hljs-comment">// app/Http/Controllers/SomethingController.php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SomethingController</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">justAnExample</span>(<span class="hljs-params"></span>)
    </span>{
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">'msg'</span> =&gt; <span class="hljs-string">'It works!'</span>
        ];
    }
}
</code></pre>
<hr />
<h3 id="heading-4-test-with-postman">4. Test with Postman</h3>
<p>First, call the endpoint <code>/just/an/example</code> without an API Key set in Headers, and check if it fails as expected:</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9qa07640lyqa42m6xdze.jpg" alt="Test the endpoint - 403" /></p>
<hr />
<p>Finally, call the endpoint <code>/just/an/example</code> with the correct API Key set in the Header <code>X-API-Key</code>, and check if it works as expected:</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/frye97orx3kzd8vdrzlz.jpg" alt="Test the endpoint - OK 200" /></p>
<hr />
<h3 id="heading-5-create-a-test-class">5. Create a test class</h3>
<p>Make test class:</p>
<pre><code class="lang-sh">php artisan make:<span class="hljs-built_in">test</span> FastApiKeyTest
</code></pre>
<p>Add some test methods:</p>
<pre><code class="lang-php"><span class="hljs-comment">// tests/Feature/FastApiKeyTest.php</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FastApiKeyTest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">TestCase</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_fail_without_api_key</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
    </span>{
        $response = <span class="hljs-keyword">$this</span>-&gt;postJson(<span class="hljs-string">'/api/v1/just/an/example'</span>);

        $response-&gt;assertStatus(<span class="hljs-number">403</span>);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_fail_with_wrong_api_key</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
    </span>{
        $response = <span class="hljs-keyword">$this</span>-&gt;postJson(<span class="hljs-string">'/api/v1/just/an/example'</span>, [], [
            <span class="hljs-string">'X-API-Key'</span> =&gt; <span class="hljs-string">'a-wrong-key'</span>
        ]);

        $response-&gt;assertStatus(<span class="hljs-number">403</span>);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_success_with_corrent_api_key</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
    </span>{
        $response = <span class="hljs-keyword">$this</span>-&gt;postJson(<span class="hljs-string">'/api/v1/just/an/example'</span>, [], [
            <span class="hljs-string">'X-API-Key'</span> =&gt; config(<span class="hljs-string">'app.fast_api_key'</span>)
        ]);

        $response-&gt;assertStatus(<span class="hljs-number">200</span>);
    }
}
</code></pre>
<p>Finally, launch the tests:</p>
<pre><code class="lang-sh">php artisan <span class="hljs-built_in">test</span>
</code></pre>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zu39iw0ck9wg3e7gt56p.png" alt="Launch the tests" /></p>
<hr />
<p>✸ Enjoy your coding!</p>
<h6 id="heading-if-you-liked-this-post-dont-forget-to-add-your-subscribe-to-my-newsletter"><em>If you liked this post, don't forget to add your</em> <strong><em>Subscribe</em></strong> <em>to my newsletter!</em></h6>
]]></content:encoded></item></channel></rss>