<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Freek Van der Herten</title>
    <description>The latest articles on DEV Community by Freek Van der Herten (@freekmurze).</description>
    <link>https://dev.to/freekmurze</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F306794%2F01ca4d6d-752b-4d40-bd08-48b1dc172cc4.jpeg</url>
      <title>DEV Community: Freek Van der Herten</title>
      <link>https://dev.to/freekmurze</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/freekmurze"/>
    <language>en</language>
    <item>
      <title>A walkthrough of Laravel Backup Server</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Mon, 04 May 2020 12:17:25 +0000</pubDate>
      <link>https://dev.to/freekmurze/a-walkthrough-of-laravel-backup-server-1mp9</link>
      <guid>https://dev.to/freekmurze/a-walkthrough-of-laravel-backup-server-1mp9</guid>
      <description>&lt;p&gt;In this stream (which is the first I've ever done) I'll show you what you can do with our upcoming &lt;a href="https://docs.spatie.be/laravel-backup-server"&gt;laravel-backup-server&lt;/a&gt; package and how it works under the hood.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/ejS84jFFUUE"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Building a realtime dashboard powered by Laravel, Livewire and Tailwind (2020 edition)</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Mon, 04 May 2020 12:16:29 +0000</pubDate>
      <link>https://dev.to/freekmurze/building-a-realtime-dashboard-powered-by-laravel-livewire-and-tailwind-2020-edition-2lpp</link>
      <guid>https://dev.to/freekmurze/building-a-realtime-dashboard-powered-by-laravel-livewire-and-tailwind-2020-edition-2lpp</guid>
      <description>&lt;p&gt;At &lt;a href="https://spatie.be"&gt;Spatie&lt;/a&gt; we have a TV screen against the wall that displays a dashboard. This dashboard displays the tasks our team should be working on, important events in the near future, which tasks each of our team members should be working on, what music they are listening to, and so on. Here's what it looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w9CgmpSG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/dashboard.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w9CgmpSG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/dashboard.png" alt="dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This dashboard is built using our &lt;a href="https://docs.spatie.be/laravel-dashboard"&gt;laravel-dashboard&lt;/a&gt; package. It will allow you to built a similar dashboard in no time.&lt;/p&gt;

&lt;p&gt;In this blogpost I'd like to walk you through both the dashboard and the package.&lt;/p&gt;

&lt;h2&gt;
  
  
  History
&lt;/h2&gt;

&lt;p&gt;We've had a dashboard at Spatie for quite some time now. Before our current Laravel-based one we used &lt;a href="http://dashing.io"&gt;Dashing&lt;/a&gt;, a framework to quickly build dashboards. The framework was created by the folks at &lt;a href="https://www.shopify.com/"&gt;Shopify&lt;/a&gt; and uses Ruby under the hood.&lt;/p&gt;

&lt;p&gt;When I first built our dashboard, a few years ago, we were at a crossroads with &lt;a href="https://spatie.be"&gt;our company&lt;/a&gt;. There wasn't much momentum in the PHP world and we were toying with the idea of switching over to Ruby. The idea was that by playing with Dashing we would get some experience with the language. Then Composer and Laravel happened and we decided to stick with PHP (and given the current state of PHP ecosystem we don't regret that choice at all).&lt;/p&gt;

&lt;p&gt;When &lt;a href="http://dashing.io"&gt;support for Dashing had officially stopped&lt;/a&gt; in 2016, I thought it was a good time to completely &lt;a href="https://freek.dev/490-building-a-dashboard-using-laravel-vuejs-and-pusher"&gt;rebuild&lt;/a&gt; the dashboard using Laravel and Vue. &lt;/p&gt;

&lt;p&gt;Every year, my team I iterated further on the dashboard. It gained some more tiles, &lt;a href="https://freek.dev/558-our-dashboard-has-been-updated-to-make-use-of-laravel-echo"&gt;support for Laravel Echo was added&lt;/a&gt;. a &lt;a href="https://freek.dev/1212-building-a-realtime-dashboard-powered-by-laravel-vue-pusher-and-tailwind-2018-edition"&gt;cleaner layout powered by Tailwind&lt;/a&gt;,...&lt;/p&gt;

&lt;p&gt;The dashboard always made as &lt;a href="https://github.com/spatie/dashboard.spatie.be"&gt;a complete Laravel app&lt;/a&gt;. In the back of my mind I always had the plan to make the core dashboard and individual tiles available as packages. The big hurdle to take was that the stack is kinda complicated: in order to create a tile we'd need PHP classes to fetch data, create events, and have Vue component to display stuff. &lt;/p&gt;

&lt;p&gt;This year we moved away from WebSocket and Vue in favour of &lt;a href="https://laravel-livewire.com"&gt;Laravel Livewire&lt;/a&gt;. Using Livewire has some drawbacks, but a big advantage is that packaging up tiles now is &lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/creating-your-own-tile/"&gt;much simpler&lt;/a&gt;. Instead of one big Laravel app, the dashboard no consist of &lt;a href="https://github.com/spatie/laravel-dashboard"&gt;a core package&lt;/a&gt; and &lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/overview/"&gt;a collection a tile packages&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tiles
&lt;/h2&gt;

&lt;p&gt;Let's take a closer look at what the dashboard is displaying. The configured dashboard from the above screenshot has the following tiles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Twitter tile that shows all mentions of quotes of &lt;code&gt;@spatie_be&lt;/code&gt;. Under the hood this is powered by &lt;a href="https://github.com/spatie/twitter-labs"&gt;our Twitter Labs package&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;There's a dedicated tile for each member of our team. Each tile displays the tasks for that member for the current week. Those tasks are fetched from a few markdown files in a private repo on GitHub. There's a little bit more to this tile. More on that later.&lt;/li&gt;
&lt;li&gt;Some statistics of &lt;a href="https://github.com/spatie"&gt;our numerous public repositories&lt;/a&gt; on GitHub. This data is coming from the GitHub and the &lt;a href="https://packagist.org/"&gt;Packagist API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A team calendar that pulls in events from a Google Calendar.&lt;/li&gt;
&lt;li&gt;A clock with the current date. and some weather conditions, &lt;a href="https://github.com/spatie/laravel-dashboard-time-weather-tile/blob/5c2381308c2f9bbb051cb1649cdaeb85e9648a26/src/Services/OpenWeatherMap.php#L11-L13"&gt;retrieved&lt;/a&gt; from the &lt;a href="https://openweathermap.org"&gt;Open Weather Map API&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;In our home city, Antwerp, there is a shared biking system called &lt;a href="https://www.velo-antwerpen.be/en"&gt;Velo&lt;/a&gt;. The bike tile shows how many bikes there are available in the nearest bike points near our office. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To help everyone to stay "in the zone" we bought the entire team &lt;a href="https://www.bose.com/en_us/products/headphones/over_ear_headphones/quietcomfort-35-wireless-ii.html"&gt;Bose QuietComfort headphones&lt;/a&gt;. The dashboard displays the current track for each team member on his team member tile. The avatar will be replaced by the artwork of the album. We leverage the API of &lt;a href="https://www.last.fm"&gt;last.fm&lt;/a&gt; to get this info. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w4AZrmto--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/music.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w4AZrmto--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/music.png" width="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The team member tile will also display a little crown when it's someone's birthday?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--W9Bo7M46--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/birthday.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--W9Bo7M46--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/birthday.png" width="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the ongoing Corona pandemic our team works 100% remotely. In normal times team members also regularly work from home. When not working in the office for a day we have the habit of setting our status in Slack to "Working from home". When a team member sets that status in Slack we'll display a nice little tent emoji.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NCKuDk86--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/remote.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NCKuDk86--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/remote.png" width="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  High level overview
&lt;/h2&gt;

&lt;p&gt;After the browser displays the dashboard for the first time we'll never refresh the page again. &lt;a href="https://laravel-livewire.com"&gt;Livewire&lt;/a&gt; is used to refresh the tiles. Doing it this way will avoid having to refresh the page and in turn, avoid flashing screens.&lt;/p&gt;

&lt;p&gt;Most tiles consist of these functionalities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an artisan command that fetches data and stores it somewhere.  The artisan commands &lt;a href="https://github.com/spatie/dashboard.spatie.be/blob/f989a4c23270192d97a88305e8841696ee089086/app/Console/Kernel.php#L22-L31"&gt;should be scheduled&lt;/a&gt; so that data is fetched periodically.&lt;/li&gt;
&lt;li&gt;a Livewire component: this class accepts various props that can be used to configure the tile. It should at least accept a   &lt;code&gt;position&lt;/code&gt; prop. Livewire components can re-render themselves. Livewire does this by re-rendering the html on the server and update the changed parts in the DOM using &lt;a href="https://github.com/patrick-steele-idem/morphdom"&gt;morphdom&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;a Blade view to render the component. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The base &lt;a href="https://github.com/spatie/laravel-dashboard"&gt;laravel-dashboard&lt;/a&gt; package provides &lt;a href="https://github.com/spatie/laravel-dashboard/blob/81c92321764cdc0817cd3d49869ecca013ff8b55/src/Models/Tile.php#L8"&gt;a &lt;code&gt;Tile&lt;/code&gt; model&lt;/a&gt; that tiles can use to store data. Using the &lt;code&gt;Tile&lt;/code&gt; model isn't required, tiles can choose themselves how they store and retrieve data.&lt;/p&gt;

&lt;p&gt;A tile is entirely written in PHP. This makes it easy to create new tiles. It's also easy to &lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/creating-your-own-tile/"&gt;package up tiles&lt;/a&gt;. Nearly all our tiles are available as packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/google-calendar"&gt;Time and Weather&lt;/a&gt;: displays the current time and weather at your location&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/google-calendar"&gt;Calendar&lt;/a&gt;: displays events that are on a Google Calendar&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/twitter"&gt;Twitter&lt;/a&gt;: displays mentions on Twitter&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/oh-dear-uptime"&gt;Oh Dear Uptime&lt;/a&gt;: displays sites that are detected as down by &lt;a href="https://ohdear.app"&gt;Oh Dear&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/belgian-trains"&gt;Belgian Trains&lt;/a&gt;: displays real-time info on Belgian trains&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/velo"&gt;Velo&lt;/a&gt;: displays the status of &lt;a href="https://www.velo-antwerpen.be/en"&gt;Velo&lt;/a&gt;, the Antwerp bike sharing system.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Creating a dashboard and positioning tiles
&lt;/h2&gt;

&lt;p&gt;Our company dashboard, which you can find in &lt;a href="https://github.com/spatie/dashboard.spatie.be"&gt;this repo&lt;/a&gt;, is a Laravel app which makes use of laravel-dashboard package. A dashboard is simply &lt;a href="https://github.com/spatie/dashboard.spatie.be/blob/e5b4dfdcd3f2047b44b4096f5fe09ca8665a0ba1/routes/web.php#L9"&gt;a route&lt;/a&gt; that points to a view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in routes/web.php&lt;/span&gt;

&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'dashboard'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here's the content of the dashboard, slightly redacted for brevity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;x-dashboard&amp;gt;
    &amp;lt;livewire:twitter-tile position="a1:a14" /&amp;gt;

    &amp;lt;livewire:team-member-tile
        position="b1:b8"
        name="adriaan"
        :avatar="gravatar('adriaan@spatie.be')"
        birthday="1995-10-22"
    /&amp;gt;

    &amp;lt;livewire:team-member-tile
        name="freek"
        :avatar="gravatar('freek@spatie.be')"
        birthday="1979-09-22"
        position="b9:b16"
    /&amp;gt;

    {{-- other tile members removed for brevity --}}

    &amp;lt;livewire:calendar-tile position="e7:e16" :calendar-id="config('google-calendar.calendar_id')" /&amp;gt;

    &amp;lt;livewire:velo-tile position="e17:e24" /&amp;gt;

    &amp;lt;livewire:statistics-tile position="a15:a24" /&amp;gt;

    &amp;lt;livewire:belgian-trains-tile position="a1:a24"/&amp;gt;

    &amp;lt;livewire:oh-dear-uptime-tile position="e7:e16" /&amp;gt;

    &amp;lt;livewire:time-weather-tile position="e1:e6" /&amp;gt;
&amp;lt;/x-dashboard&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This view is quite simple. You have to use the &lt;code&gt;x-dashboard&lt;/code&gt; &lt;a href="https://laravel.com/docs/master/blade#components"&gt;Blade component&lt;/a&gt;. Behind the scenes, this component will pull in all necessary css and JavaScript used by Livewire.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;x-dashboard&lt;/code&gt; you can use &lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/overview/"&gt;a livewire tile component&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A tile can be positioned by passing coordinates to the &lt;code&gt;position&lt;/code&gt; prop.&lt;/p&gt;

&lt;p&gt;You should image the dashboard as an excel like layout. The columns are represented by letters, the rows by number. You can pass a single position like &lt;code&gt;a1&lt;/code&gt;. The first letter, &lt;code&gt;a&lt;/code&gt;, represent the first column. The &lt;code&gt;1&lt;/code&gt; represents the first row. You an also pass ranges. Here are a few examples.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;a1&lt;/code&gt;: display the tile in the top left corner&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;b2&lt;/code&gt;: display a tile in the second row of the second column&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;a1:a3&lt;/code&gt;: display a tile over the three first rows of the first column&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;b1:c2&lt;/code&gt;: display the tile as a square starting at the first row of the second column to the second row of the third column&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dashboard is being rendered using css grid. Behind the scenes, these coordinates will be converted to grid classes. The grid will grow automatically. If a &lt;code&gt;c&lt;/code&gt; is the "highest" letter used on the dashboard, it will have 3 columns, if a &lt;code&gt;e&lt;/code&gt; is used on any tile, the dashboard will have 5 columns. The same applies with the rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The calendar tile
&lt;/h2&gt;

&lt;p&gt;Now that you know how the dashboard works in broad lines, let's go in details how some of the tiles work. &lt;/p&gt;

&lt;p&gt;Here is how the calendar tile looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---P97GAuL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/calendar.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---P97GAuL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/calendar.png" width="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To fetch data, this tile uses &lt;a href="https://github.com/spatie/laravel-google-calendar"&gt;our google-calendar package&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is &lt;a href="https://github.com/spatie/laravel-dashboard-calendar-tile/blob/7d046ecf738dbe60587af0a33b915eca44428320/src/FetchCalendarEventsCommand.php#L21"&gt;the command&lt;/a&gt; that fetches the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Spatie\CalendarTile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Carbon\Carbon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Illuminate\Console\Command&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Spatie\GoogleCalendar\Event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FetchCalendarEventsCommand&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Command&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'dashboard:fetch-calendar-events'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Fetch events from a Google Calendar'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Fetching calendar events...'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard.tiles.calendar.ids'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$calendarId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nv"&gt;$calendarId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nv"&gt;$sortDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;getSortDate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'date'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;createFromFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m-d H:i:s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$sortDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;ATOM&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="nx"&gt;CalendarStore&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;setEventsForCalendarId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'google-calendar.calendar_id'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'All done!'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Most of the tiles that we create also have &lt;a href="https://github.com/spatie/laravel-dashboard-calendar-tile/blob/7d046ecf738dbe60587af0a33b915eca44428320/src/CalendarStore.php"&gt;a &lt;code&gt;Store&lt;/code&gt; class&lt;/a&gt;. This store class is responsible for storing and retrieving data. When retrieving the data, it also formats it so it's easily consumable for the component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace Spatie\CalendarTile;

use Carbon\Carbon;
use Illuminate\Support\Collection;
use Spatie\Dashboard\Models\Tile;

class CalendarStore
{
    private Tile $tile;

    public static function make()
    {
        return new static();
    }

    public function __construct()
    {
        $this-&amp;gt;tile = Tile::firstOrCreateForName('calendar');
    }

    public function setEventsForCalendarId(array $events, string $calendarId): self
    {
        $this-&amp;gt;tile-&amp;gt;putData('events_' . $calendarId, $events);

        return $this;
    }

    public function eventsForCalendarId(string $calendarId): Collection
    {
        return collect($this-&amp;gt;tile-&amp;gt;getData('events_' . $calendarId) ?? [])
            -&amp;gt;map(function (array $event) {
                $carbon = Carbon::createFromTimeString($event['date']);

                $event['date'] = $carbon;
                $event['withinWeek'] = $event['date']-&amp;gt;diffInDays() &amp;lt; 7;
                $event['presentableDate'] = $this-&amp;gt;getPresentableDate($carbon);

                return $event;
            });
    }

    public function getPresentableDate(Carbon $carbon): string
    {
        if ($carbon-&amp;gt;isToday()) {
            return 'Today';
        }

        if ($carbon-&amp;gt;isTomorrow()) {
            return 'Tomorrow';
        }

        if ($carbon-&amp;gt;diffInDays() &amp;lt; 8) {
            return "In {$carbon-&amp;gt;diffInDays()} days";
        }

        if ($carbon-&amp;gt;diffInDays() &amp;lt;= 14) {
            return "Next week";
        }

        $dateFormat = config('dashboard.tiles.calendar.date_format') ?? 'd.m.Y';

        return $carbon-&amp;gt;format($dateFormat);
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;If you are not as pragmatic as I am, add are a devout follower of the Single Responsibility principle, you can move the responsibilities to separate classes.&lt;/p&gt;

&lt;p&gt;The events in the store are retrieved by the &lt;code&gt;CalendarTileComponent&lt;/code&gt; which is a Livewire component&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Spatie\CalendarTile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Livewire\Component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CalendarTileComponent&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @var string */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$calendarId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var string */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var string|null */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var int */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$refreshInSeconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$calendarId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$refreshInSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;calendarId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$calendarId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;refreshInSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$refreshInSeconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard-calendar-tile::tile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'events'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;CalendarStore&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;eventsForCalendarId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;calendarId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'refreshIntervalInSeconds'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard.tiles.calendar.refresh_interval_in_seconds'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This component accepts the calendar id of which the events should be displayed. It also accepts the position where the title should be displayed, a title, and a frequency at which the component should be re-rendered.&lt;/p&gt;

&lt;p&gt;Next, let's take a look at the Blade view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;x-dashboard-tile :position="$position" :refresh-interval="$refreshIntervalInSeconds"&amp;gt;
    &amp;lt;div class="grid {{ isset($title) ? 'grid-rows-auto-auto gap-2' : '' }} h-full"&amp;gt;
        @isset($title)
            &amp;lt;h1 class="uppercase font-bold"&amp;gt;
                {{ $title }}
            &amp;lt;/h1&amp;gt;
        @endisset

        &amp;lt;ul class="self-center divide-y-2 divide-canvas"&amp;gt;
            @foreach($events as $event)
                &amp;lt;li class="py-1"&amp;gt;
                    &amp;lt;div class="my-2"&amp;gt;
                        &amp;lt;div class="{{ $event['withinWeek'] ? 'font-bold' : '' }}"&amp;gt;{{ $event['name'] }}&amp;lt;/div&amp;gt;
                        &amp;lt;div class="text-sm text-dimmed"&amp;gt;
                            {{ $event['presentableDate'] }}
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/li&amp;gt;
            @endforeach
        &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/x-dashboard-tile&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This view simply renders the passed events to HTML. You probably noticed that a &lt;code&gt;x-dashboard-tile&lt;/code&gt; &lt;a href="https://laravel.com/docs/master/blade#components"&gt;Blade component&lt;/a&gt; is used here. That &lt;code&gt;DashboardTileComponent&lt;/code&gt;  is &lt;a href="https://github.com/spatie/laravel-dashboard/blob/81c92321764cdc0817cd3d49869ecca013ff8b55/src/Http/Components/DashboardTileComponent.php"&gt;part of&lt;/a&gt; the base laravel-dashboard package. That class will, amongst other things, convert an the excel like position notation like &lt;code&gt;a1:a14&lt;/code&gt; to a css grid style like: &lt;code&gt;grid-area: 1 / 1 / 15 / 2;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Let's take a look at &lt;a href="https://github.com/spatie/laravel-dashboard/blob/81c92321764cdc0817cd3d49869ecca013ff8b55/resources/views/tile.blade.php"&gt;the Blade view&lt;/a&gt; of &lt;code&gt;Dashboard&lt;/code&gt; tile component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div
    style="grid-area: {{ $gridArea }};{{ $show ? '' : 'display:none' }}"
    class="overflow-hidden rounded relative bg-tile"
    {{ $refreshIntervalInSeconds ? "wire:poll.{$refreshIntervalInSeconds}s" : ''  }}
&amp;gt;
    &amp;lt;div
        class="absolute inset-0 overflow-hidden p-4"
        @if($fade)
            style="-webkit-mask-image: linear-gradient(black, black calc(100% - 1rem), transparent)"
        @endif
    &amp;gt;
        {{ $slot }}
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here you can see that &lt;code&gt;$refreshIntervalInSeconds&lt;/code&gt; is use to add &lt;a href="https://laravel-livewire.com/docs/polling"&gt;Livewire's &lt;code&gt;wire:poll&lt;/code&gt; directive&lt;/a&gt;. Adding this directive will result in the automatic re-rendering of the component.&lt;/p&gt;

&lt;p&gt;In case you're not familiar with Blade components, that &lt;code&gt;$slot&lt;/code&gt; variable contains all html that's between the &lt;code&gt;x-dashboard-tile&lt;/code&gt; tags. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Twitter tile
&lt;/h2&gt;

&lt;p&gt;The Twitter tile is quite fun. Here is how it looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kKMCOEpH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/twitter.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kKMCOEpH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/twitter.png" width="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It displays tweets in (near) real-time that contain a certain string. Under the hood this tile is powered by &lt;a href="https://github.com/spatie/twitter-labs"&gt;our Twitter Labs package&lt;/a&gt;. It uses &lt;a href="https://developer.twitter.com/en/docs/labs/filtered-stream/overview"&gt;the filtered stream Twitter API&lt;/a&gt; and &lt;a href="https://reactphp.org"&gt;ReactPHP&lt;/a&gt; to listen for tweets.&lt;/p&gt;

&lt;p&gt;To starting listening for incoming tweets you must execute this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan dashboard:listen-twitter-mentions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This command will never end. In production should probably want to use something like Supervisord to keep this this task running and to automatically start it when your system restarts.&lt;/p&gt;

&lt;p&gt;The command in the config of the tile in the &lt;code&gt;dashboard&lt;/code&gt; config file, you can specify to which tweets it should listen for. In our dashboard we are &lt;a href="https://github.com/spatie/dashboard.spatie.be/blob/e5b4dfdcd3f2047b44b4096f5fe09ca8665a0ba1/config/dashboard.php#L18-L32"&gt;listening for &lt;code&gt;@spatie_be&lt;/code&gt;, &lt;code&gt;spatie.be&lt;/code&gt; and &lt;code&gt;github.com/spatie&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Whenever a tweet containing any of those strings comes in, we store the tweet in the &lt;code&gt;Tile&lt;/code&gt; model. The &lt;code&gt;TwitterComponent&lt;/code&gt; will read the tweets stored in that tile model. The component is re-rendered every five seconds, so we have a (near) real time feed.&lt;/p&gt;

&lt;p&gt;You can even use this tile multiple times to create a tweet wall in no time. Here's a video in which I demonstrate how you can do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Oh Dear uptime tile
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ohdear.app"&gt;Oh Dear&lt;/a&gt; is an app I've built to monitor the uptime of sites. Most competitors only monitor the homepage. Oh Dear crawls your sites and can also alert you when your site contains a broken link or mixed content. &lt;/p&gt;

&lt;p&gt;Oh Dear has &lt;a href="https://ohdear.app/docs/integrations/api/introduction"&gt;a beautiful API&lt;/a&gt; to work with. It also provides &lt;a href="https://ohdear.app/docs/integrations/webhooks/laravel-package"&gt;a package to easily consume web hooks sent by Oh Dear&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;All sites built and managed by our team are monitored by Oh Dear. &lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/oh-dear-uptime/"&gt;The Oh Dear Uptime Tile&lt;/a&gt; displays all sites that are down. Here's how it looks like when sites are down.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kWbjDGQQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/ohdear.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kWbjDGQQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/ohdear.png" width="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This tile is only displayed when sites are down. The tile component class accepts a callable which is used to determine if it should be rendered. In our dashboard, that callable is registered &lt;a href="https://github.com/spatie/dashboard.spatie.be/blob/master/app/Providers/AppServiceProvider.php#L13"&gt;in the &lt;code&gt;boot&lt;/code&gt; method of &lt;code&gt;AppServiceProvider&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nx"&gt;OhDearUptimeTileComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;showTile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$downSites&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$downSites&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This is how that callable gets used &lt;a href="https://github.com/spatie/laravel-dashboard-oh-dear-uptime-tile/blob/e1c4df4bcaaabb73933bb49021293e91df7d400b/src/OhDearUptimeTileComponent.php"&gt;in the &lt;code&gt;OhDearUptimeTileComponent&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$downSites&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;OhDearUptimeStore&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;downSites&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$showTile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$showTile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$showTile&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nv"&gt;$downSites&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$refreshIntervalInSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard.tiles.oh_dear_uptime.refresh_interval_in_seconds'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard-oh-dear-uptime-tile::tile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'downSites'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'showTile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refreshIntervalInSeconds'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showTile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$callable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$showTile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$callable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The contents of &lt;code&gt;showTile&lt;/code&gt; will eventually be passed on the the &lt;code&gt;$show&lt;/code&gt; prop of the &lt;code&gt;x-dashboard-tile&lt;/code&gt;. In the Blade view of &lt;code&gt;x-dashboard-tile&lt;/code&gt; it &lt;a href="https://github.com/spatie/laravel-dashboard/blob/66aacdeaa820c385022b11b7cb626ec2561290b8/resources/views/tile.blade.php#L2"&gt;gets used&lt;/a&gt; to determine if the tile should be displayed or not with &lt;code&gt;{{ $show ? '' : 'display:none' }}&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring themes
&lt;/h2&gt;

&lt;p&gt;The dashboard has a dark mode. This is is how that looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JZObfe3T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/darkmode.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JZObfe3T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/darkmode.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;dashboard&lt;/code&gt; config file there are &lt;a href="https://github.com/spatie/dashboard.spatie.be/blob/2e254e4d89f0ae3f54d6fb4f0355d1dfb1afae11/config/dashboard.php#L4-L21"&gt;a couple of options available&lt;/a&gt; that determine if the dashboard should use light or dark mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return [
    /*
     * The dashboard supports these themes:
     *
     * - light: always use light mode
     * - dark: always use dark mode
     * - device: follow the OS preference for determining light or dark mode
     * - auto: use light mode when the sun is up, dark mode when the sun is down
     */
    'theme' =&amp;gt; 'auto',

    /*
     * When the dashboard uses the `auto` theme, these coordinates will be used
     * to determine whether the sun is up or down
     */
    'auto_theme_location' =&amp;gt; [
        'lat' =&amp;gt; 51.260197, // coordinates of Antwerp, Belgium
        'lng' =&amp;gt; 4.402771,
    ],
];
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Our own dashboard used auto mode. This means that light mode is used when the sun is up, dark mode when the sun is down. To determine the position of the sun &lt;a href="https://github.com/spatie/sun"&gt;the spatie/sun package&lt;/a&gt; is used.&lt;/p&gt;

&lt;p&gt;My colleague &lt;a href="https://sebastiandedeyne.com"&gt;Sebastian&lt;/a&gt; added a nice option: you can force the dashboard in a specific mode by adding a URL parameter: &lt;code&gt;?theme=dark&lt;/code&gt;. This is handy if you want to display a dashboard on a second screen and always want it to be dark, no matter how the dashboard is configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating your own tile
&lt;/h2&gt;

&lt;p&gt;If you have knowledge of Laravel, creating a new component is a straightforward process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a minimal tile
&lt;/h3&gt;

&lt;p&gt;At the minimum a tile consist of a Livewire component class and a view. If you have never worked with Livewire before, we recommend to first read &lt;a href="https://laravel-livewire.com/docs/quickstart"&gt;the documentation of Livewire&lt;/a&gt;, especially the part on &lt;a href="https://laravel-livewire.com/docs/making-components"&gt;making components&lt;/a&gt;. Are you a visual learner? Then you'll be happy to know there's also &lt;a href="https://laravel-livewire.com/screencasts/installation"&gt;a free video course&lt;/a&gt; to get you started.&lt;/p&gt;

&lt;p&gt;This is the most minimal Tile component you can create.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Tiles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Livewire\Component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DummyComponent&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @var string */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tiles.dummy'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;You should always accept a &lt;code&gt;position&lt;/code&gt; via the mount function. This position will used &lt;a href="https://dev.to/laravel-dashboard/v1/basic-usage/positioning-tiles"&gt;to position tiles on the dashboard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's how that &lt;code&gt;tiles.dummy&lt;/code&gt; view could look like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;x-dashboard-tile&lt;/span&gt; &lt;span class="na"&gt;:position=&lt;/span&gt;&lt;span class="s"&gt;"$position"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Dummy&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/x-dashboard-tile&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In your view you should always use &lt;code&gt;x-dashboard-tile&lt;/code&gt; and pass it the &lt;code&gt;position&lt;/code&gt; your component accepted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fetching and storing data
&lt;/h3&gt;

&lt;p&gt;The most common way to feed a component data is via a scheduled command. Inside that scheduled command you can perform any API request you want. You can also store fetched data and retrieve in your component however you want. To help you with this, the dashboard provides a &lt;code&gt;Tile&lt;/code&gt; model that can be used to store and retrieve data.&lt;/p&gt;

&lt;p&gt;Let's take a look at a simple example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Commands&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Illuminate\Console\Command&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Spatie\Dashboard\Models\Tile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FetchDataForDummyComponentCommand&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Command&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'dashboard:fetch-data-for-dummy-component'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Fetch data for dummy component'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;some&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;fancy&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// this will store your data in the database&lt;/span&gt;
            &lt;span class="nx"&gt;Tile&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;firstOrCreateForName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dummy'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;putData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_data'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This command could &lt;a href="https://laravel.com/docs/master/scheduling#scheduling-artisan-commands"&gt;be scheduled&lt;/a&gt; to run at any frequency you want.&lt;/p&gt;

&lt;p&gt;In your component class, you can fetch the stored data using the &lt;code&gt;getData&lt;/code&gt; function on the &lt;code&gt;Tile&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// inside your component&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tiles.dummy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
       &lt;span class="s1"&gt;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Spatie\Dashboard\Models\Tile&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;firstOrCreateForName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dummy'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_data'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In your view, you can do with the data whatever you want.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refreshing the component
&lt;/h3&gt;

&lt;p&gt;To refresh a tile, you should pass an amount of seconds to the &lt;code&gt;refresh-interval&lt;/code&gt; prop of &lt;code&gt;x-dashboard-tile&lt;/code&gt;.  In this example the component will be refreshed every 60 seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;x-dashboard-tile&lt;/span&gt; &lt;span class="na"&gt;:position=&lt;/span&gt;&lt;span class="s"&gt;"$position"&lt;/span&gt; &lt;span class="na"&gt;refresh-interval=&lt;/span&gt;&lt;span class="s"&gt;"60"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Dummy&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

    {{-- display the $data --}}
&lt;span class="nt"&gt;&amp;lt;/x-dashboard-tile&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;If your component only needs to be refreshed partials, you can add &lt;code&gt;wire:poll&lt;/code&gt; to your view (instead of using the &lt;code&gt;refresh-interval&lt;/code&gt; prop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;x-dashboard-tile&lt;/span&gt; &lt;span class="na"&gt;:position=&lt;/span&gt;&lt;span class="s"&gt;"$position"&lt;/span&gt; &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Dummy&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

     &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;wire:poll&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="na"&gt;60s&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Only this part will be refreshed
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/x-dashboard-tile&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Styling your component
&lt;/h3&gt;

&lt;p&gt;The dashboard is styled using &lt;a href="https://tailwindcss.com"&gt;Tailwind&lt;/a&gt;. In your component you can use any of the classes Tailwind provides.&lt;/p&gt;

&lt;p&gt;In addition to Tailwind, the dashboard defines these extra colors for you to use: &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;invers&lt;/code&gt;, &lt;code&gt;dimmed&lt;/code&gt;, &lt;code&gt;accent&lt;/code&gt;, &lt;code&gt;canvas&lt;/code&gt;, &lt;code&gt;tile&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;By default, these colors are automatically shared by the &lt;code&gt;textColor&lt;/code&gt;, &lt;code&gt;borderColor&lt;/code&gt;, and &lt;code&gt;backgroundColor&lt;/code&gt; utilities, so you can use utility classes like &lt;code&gt;text-canvas&lt;/code&gt;, &lt;code&gt;border-error&lt;/code&gt;, and &lt;code&gt;bg-dimmed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These colors have a separate value for light and dark mode, so your component will also look beautiful in dark mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding extra JavaScript / CSS
&lt;/h3&gt;

&lt;p&gt;If your tile needs to load extra JavaScript or CSS, you can do so using &lt;code&gt;Spatie\Dashboard\Facades\Dashboard&lt;/code&gt; facade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nx"&gt;Dashboard&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urlToScript&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;Dashboard&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$extraJavaScript&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;Dashboard&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;stylesheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urlToStyleSheet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;Dashboard&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;inlineStylesheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$extraCss&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Packaging up your component
&lt;/h3&gt;

&lt;p&gt;If you have created a tile that could be beneficial to others, consider sharing your awesome tile with the community by packaging it up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/spatie/laravel-dashboard-skeleton-tile"&gt;This repo&lt;/a&gt; contains a skeleton that can help you kick start your tile package.&lt;/p&gt;

&lt;p&gt;When you have published you package, let me know by sending a mail to &lt;a href="mailto:info@spatie.be"&gt;info@spatie.be&lt;/a&gt;, and we'll mention your tile &lt;a href="https://docs.spatie.be/laravel-dashboard/v1/adding-tiles/overview/"&gt;in our docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Displaying the dashboard on a TV
&lt;/h2&gt;

&lt;p&gt;Behind our tv there is a &lt;a href="https://www.raspberrypi.org/products/raspberry-pi-2-model-b/"&gt;Raspberry Pi 2&lt;/a&gt; that displays the dashboard. It is powered by a USB port in the tv and it has &lt;a href="https://thepihut.com/products/usb-wifi-adapter-for-the-raspberry-pi"&gt;a small Wifi&lt;/a&gt; dongle to connect to the internet, so cables aren't necessary at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---zomes0Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/2016/06/IMG_0938-1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---zomes0Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/2016/06/IMG_0938-1.jpg" alt="Photo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Pi used the default &lt;a href="https://www.raspberrypi.org/downloads/raspbian/"&gt;Raspian OS&lt;/a&gt;. When it is powered on it'll automatically launch &lt;a href="https://www.raspberrypi.org/forums/viewtopic.php?t=121195"&gt;Chromium 56&lt;/a&gt; and display the contents of &lt;a href="https://dashboard.spatie.be"&gt;https://dashboard.spatie.be&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AvqZjqTR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/2016/06/IMG_0939-1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AvqZjqTR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/2016/06/IMG_0939-1.jpg" alt="Raspberry Pi that runs our dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Previous iterations
&lt;/h2&gt;

&lt;p&gt;We created our dashboard a couple of years ago. Every year we iterate on it. Here are some screenshots from the very first version up until the most current one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ohMShjmw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20160531.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ohMShjmw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20160531.png" alt="Picture of the dashboard in 2016"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3VmRN2Nr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20169714.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3VmRN2Nr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20169714.jpg" alt="Picture of the dashboard in 2016"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EtnaLD6H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20170127.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EtnaLD6H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20170127.jpg" alt="Picture of the dashboard in 2017"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--E95B4Fe6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20170620.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--E95B4Fe6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20170620.jpg" alt="Picture of the dashboard in 2017"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7a9l29EO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20181118.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7a9l29EO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2018/history/screenshot20181118.png" alt="Picture of the dashboard in 2018"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w9CgmpSG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/dashboard.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w9CgmpSG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/dashboard-2020/dashboard.png" alt="Picture of the dashboard in 2020"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing Vue/WebSockets to Livewire
&lt;/h2&gt;

&lt;p&gt;Some people consider WebSockets to always be superior to polling. To me that is silly. I think, like a lot of things in programming, it depends on the context.&lt;/p&gt;

&lt;p&gt;In the previous iterations of our dashboard we used WebSockets. This brought two nice benefits to the table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the dashboard got updated as soon as new data comes in&lt;/li&gt;
&lt;li&gt;in theory, keeping the dashboard open on multiple screens (even thousands) wouldn't result in any meaningful extra load. New event data was broadcasted out to all connected clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There were also some drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tech stack is more complicated: you need to have a WebSocket server running at all times&lt;/li&gt;
&lt;li&gt;when opening the dashboard old data was potentially displayed. The dashboard would only be up to date after a few minutes, when all tiles got new data via newly broadcasted events. We could fix that by also storing and loading data from the db, but that would make the Vue components more complicated.&lt;/li&gt;
&lt;li&gt;because of the Vue components, there should be a build process to compile the tiles. This makes it, not impossible, but more difficult to package tiles up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Switching to Livewire brings a new set of benefits and drawbacks. Let's start with the benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tech stack is extremely simple. Because we use server rendered partials (powered by Livewire), you don't need to write any JavaScript yourself to achieve near real time updates.&lt;/li&gt;
&lt;li&gt;it's now very easy to package up and share tiles. Only knowledge of PHP is needed&lt;/li&gt;
&lt;li&gt;because the data is stored in the database and also loaded on the initial load of the dashboard, the displayed data is immediately up to date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I see these drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;because every browser triggers re-renders, the load on the server increases with every open instance of the dashboard&lt;/li&gt;
&lt;li&gt;the dashboard isn't 100% realtime anymore as even the Twitter tile only polls for new data every 5 seconds&lt;/li&gt;
&lt;li&gt;Livewire isn't an industry standard like WebSockets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choosing between Vue/WebSockets and Livewire is a trade off. For our use case, I prefer the benefits/drawbacks that Livewire brings. The simplicity is appealing to me. Our dashboard is never going to opened more than in a couple of places. Even if every member of our team has the dashboard open, our server doesn't break a sweat. &lt;/p&gt;

&lt;p&gt;One cool thing to note is that should you want to use Livewire in combination with WebSockets, &lt;a href="https://laravel-livewire.com/docs/events"&gt;you still have that option&lt;/a&gt; (scroll down to the Laravel Echo section).&lt;/p&gt;

&lt;p&gt;If you would prefer a dashboard that is powered by Vue/WebSockets, you can fork &lt;a href="https://github.com/spatie/dashboard.spatie.be/tree/vue-websockets"&gt;the &lt;code&gt;vue-websockets&lt;/code&gt; branch&lt;/a&gt; of the dashboard.spatie.be repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing off
&lt;/h2&gt;

&lt;p&gt;The dashboard is a labour of love. I'm pretty happy that in our latest iteration we could make it much simpler, thanks to Livewire. One of the things that always bothered me was the tiles couldn't be easily packaged up. That problem has now been solved. I'm pretty curious what kind of tiles people will come up with. &lt;/p&gt;

&lt;p&gt;If you create a custom tile package, do let me know. I'll add it to our docs. Should you create a dashboard of your own using our package, feel free to send me a screenshot.&lt;/p&gt;

&lt;p&gt;In a next iteration of the dashboard, I'm thinking of adding an admin section where people can add tiles and customise the dashboard. That might be a good addition for next year!&lt;/p&gt;

&lt;p&gt;Want to get started using the dashboard, head over to &lt;a href="https://docs.spatie.be/laravel-dashboard/v1/introduction/"&gt;the documentation&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;If you like the dashboard, please consider &lt;a href="https://github.com/sponsors/spatie"&gt;sponsoring us on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're still here reading this, you probably are pretty excited about the dashboard. Here's the recording of a stream where I first showed off the Livewire powered dashboard.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/C8iNnIEJC2w"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>livewire</category>
    </item>
    <item>
      <title>Mixing event sourcing in a traditional Laravel app</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Mon, 04 May 2020 12:15:02 +0000</pubDate>
      <link>https://dev.to/freekmurze/mixing-event-sourcing-in-a-traditional-laravel-app-2m7b</link>
      <guid>https://dev.to/freekmurze/mixing-event-sourcing-in-a-traditional-laravel-app-2m7b</guid>
      <description>&lt;p&gt;Together with my colleague &lt;a href="https://twitter.com/brendt_gd"&gt;Brent&lt;/a&gt;, I'm working on designing the architecture of a massive Laravel application. In that application, we'll have traditional parts and event sourced parts. In this blog post, I'd like to give a practical example of how we think to achieve this.&lt;br&gt;
This post is the second one in a two-part series. I highly encourage &lt;a href="https://stitcher.io/blog/combining-event-sourcing-and-stateful-systems"&gt;to read the first part&lt;/a&gt; before continuing to read this post. You also should have a good understanding of event sourcing, the role of aggregates, and projectors. Get up to speed by going through &lt;a href="https://docs.spatie.be/laravel-event-sourcing/v3/introduction/"&gt;this introduction&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the stage
&lt;/h2&gt;

&lt;p&gt;First, let's do a quick recap of &lt;a href="https://stitcher.io/blog/combining-event-sourcing-and-stateful-systems"&gt;Brent's post&lt;/a&gt;. We need event sourcing in our application. In certain parts, we need to make decisions based on the past and, as time progresses, we need to be able to generate reports without knowing upfront what should be in those reports. &lt;/p&gt;

&lt;p&gt;These problems can be solved by using event sourcing. We just store all events that happen in the app, so we can have aggregate roots that can make decisions based on the past when they are reconstituted from the events. We can generate new reports by creating projectors and replaying all recorded events to them. &lt;/p&gt;

&lt;p&gt;A wise man, by the name of Frank de Jonge, once said: "Event sourcing makes the hard parts easy, and the easy parts hard". And it's true. Setting up event sourcing takes some time, and it's not as straightforward. Some overhead gets in the way when you want to do simple things.&lt;/p&gt;

&lt;p&gt;In our application, we also have parts that are rather cruddy and don't need the power of event sourcing. Wouldn't it be nice to be able to have the best of both worlds in a single app? Brent and I researched this subject and pair programmed on some experiments. We'd like to share one of these experiments with you.&lt;/p&gt;

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

&lt;p&gt;We're going to implement some of the parts mentioned in &lt;a href="https://stitcher.io/blog/combining-event-sourcing-and-stateful-systems"&gt;Brent's post&lt;/a&gt;: products and orders. &lt;code&gt;Product&lt;/code&gt;  is a boring cruddy part. We don't need history nor reports here. For the orders part, we'll use event sourcing. &lt;/p&gt;

&lt;p&gt;You can find the source code of the example in &lt;a href="https://github.com/spatie/laravel-context-demo"&gt;this repo on GitHub&lt;/a&gt;.  Keep in mind that this is a very simple example. In a real-world app the logic involved would probably be more complicated. In this experiment, we're just concentrating on how such to part should/would communicate with each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  The non-event-sourced part
&lt;/h3&gt;

&lt;p&gt;If you take a look in the &lt;code&gt;Context&lt;/code&gt; directory, you'll see the two parts: &lt;code&gt;Order&lt;/code&gt; and &lt;code&gt;Product&lt;/code&gt;. Let's take a look at `Product first:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DfXIR-bv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/context/product.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DfXIR-bv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/context/product.png" alt="screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, this part is very simple: we just have models and events. The idea here is that we can just build it as we're used to, with not too much overhead. The only thing that we need to do is to fire off specific events when something changes, so the other parts of the app can hook into that. &lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;Product&lt;/code&gt; model &lt;a href="https://github.com/spatie/laravel-context-demo/blob/dc52265fe620bc709dc0acb71ab1536acc53e10b/app/Context/Product/Models/Product.php"&gt;you'll see this code&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
protected $dispatchesEvents = [&lt;br&gt;
    'created' =&amp;gt; ProductCreatedEvent::class,&lt;br&gt;
    'updated' =&amp;gt; ProductUpdatedEvent::class,&lt;br&gt;
    'deleting' =&amp;gt; DeletingProductEvent::class,&lt;br&gt;
    'deleted' =&amp;gt; ProductDeletedEvent::class,&lt;br&gt;
];&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;$dispatchesEvents&lt;/code&gt;, you can define which events should be fired when certain things happen to the model. This is standard Laravel functionality. Sure, we could rely on the events that eloquent natively fires, but we want to make this very explicit by using event classes of our own.&lt;/p&gt;

&lt;p&gt;Let's take a look at &lt;a href="https://github.com/spatie/laravel-context-demo/blob/dc52265fe620bc709dc0acb71ab1536acc53e10b/app/Context/Product/Events/ProductCreatedEvent.php"&gt;one of these events&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
namespace App\Context\Product\Events;&lt;/p&gt;

&lt;p&gt;use App\Context\Product\Models\Product;&lt;/p&gt;

&lt;p&gt;class ProductCreatedEvent&lt;br&gt;
{&lt;br&gt;
    public Product $product;&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function __construct(Product $product)
{
    $this-&amp;gt;product = $product;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;As you can see, there's nothing special going on here. We're just using the model as an event property.&lt;/p&gt;

&lt;h3&gt;
  
  
  The event sourced part
&lt;/h3&gt;

&lt;p&gt;Let's now turn our eye to the &lt;code&gt;Order&lt;/code&gt; part of the app, which is event sourced. This is how that structure looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BCnUKOwh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/context/order.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BCnUKOwh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/context/order.png" alt="screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You see projectors and an aggregate root here, so it's clear that this part uses event sourcing. We're using &lt;a href="https://docs.spatie.be/laravel-event-sourcing"&gt;our homegrown event sourcing package&lt;/a&gt; to handle the logic around storing events, projectors, aggregate roots, ...&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;Product&lt;/code&gt; part of the app, we fired off events. These events were just regular Laravel events, we didn't store them. The &lt;code&gt;ProductEventSubscriber&lt;/code&gt; is listening for these events. Let's take a look at the code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
namespace App\Context\Order\Subscribers;&lt;/p&gt;

&lt;p&gt;use App\Context\Product\Events\DeletingProductEvent as AdminDeletingProductEvent;&lt;br&gt;
use App\Context\Product\Events\ProductCreatedEvent as AdminProductCreatedEvent;&lt;br&gt;
use App\Context\Order\Events\ProductCreatedEvent;&lt;br&gt;
use App\Context\Order\Events\ProductDeletedEvent;&lt;br&gt;
use App\Support\Events\EventSubscriber as BaseEventSubscriber;&lt;br&gt;
use App\Support\Events\SubscribesToEvents;&lt;/p&gt;

&lt;p&gt;class ProductEventSubscriber implements BaseEventSubscriber&lt;br&gt;
{&lt;br&gt;
    use SubscribesToEvents;&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;protected array $handlesEvents = [
    AdminProductCreatedEvent::class =&amp;gt; 'onProductCreated',
    AdminDeletingProductEvent::class =&amp;gt; 'onDeletingProduct',
];

public function onProductCreated(AdminProductCreatedEvent $event): void
{
    $event = $event-&amp;gt;product;

    event(new ProductCreatedEvent(
        $event-&amp;gt;getUuid(),
        $event-&amp;gt;stock,
    ));
}

public function onDeletingProduct(AdminDeletingProductEvent $event): void
{
    $event = $event-&amp;gt;product;

    event(new ProductDeletedEvent(
        $event-&amp;gt;getUuid(),
    ));
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What happens here is quite interesting. We are going to listen for the events that are coming from the non event sourced part. When an event from the &lt;code&gt;Product&lt;/code&gt; context, that is interesting to the &lt;code&gt;Order&lt;/code&gt; context, comes in, we are going to fire off another event. That other event is part of the &lt;code&gt;Product&lt;/code&gt; context. &lt;/p&gt;

&lt;p&gt;So when &lt;code&gt;App\Context\Product\Events\ProductCreatedEvent&lt;/code&gt; comes in (and it is aliased to &lt;code&gt;AdminProductCreatedEvent&lt;/code&gt; because probably an action by an admin on the UI will have caused that action), we are going to fire off &lt;code&gt;App\Context\Order\Events\ProductCreatedEvent&lt;/code&gt; (from the &lt;code&gt;Order&lt;/code&gt; context).&lt;/p&gt;

&lt;p&gt;We are going to take all the properties that are interesting to &lt;code&gt;Order&lt;/code&gt; context and put them in the &lt;code&gt;App\Context\Order\Events\ProductCreatedEvent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
public function onProductCreated(AdminProductCreatedEvent $event): void&lt;br&gt;
{&lt;br&gt;
    $event = $event-&amp;gt;product;&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event(new ProductCreatedEvent(
    $event-&amp;gt;getUuid(),
    $event-&amp;gt;stock,
));
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Let's take a look at that event itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
namespace App\Context\Order\Events;&lt;/p&gt;

&lt;p&gt;use App\Support\ValueObjects\ProductUuid;&lt;br&gt;
use Spatie\EventSourcing\ShouldBeStored;&lt;/p&gt;

&lt;p&gt;class ProductCreatedEvent extends ShouldBeStored&lt;br&gt;
{&lt;br&gt;
    public ProductUuid $productUuid;&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public int $stock;

public function __construct(ProductUuid $productUuid, int $stock)
{
    $this-&amp;gt;productUuid = $productUuid;

    $this-&amp;gt;stock = $stock;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can see that this event extends &lt;code&gt;ShouldBeStored&lt;/code&gt;. That base class is part of our event sourcing package. This will cause the event to be stored.&lt;/p&gt;

&lt;p&gt;We immediately use that stored event to build up a projection that holds the stock. Let's take a look at &lt;code&gt;ProductStockProjector&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
namespace App\Context\Order\Projectors;&lt;/p&gt;

&lt;p&gt;use App\Context\Order\Events\OrderCancelledEvent;&lt;br&gt;
use App\Context\Order\Events\ProductCreatedEvent;&lt;br&gt;
use App\Context\Order\Events\ProductDeletedEvent;&lt;br&gt;
use App\Context\Order\Events\OrderCreatedEvent;&lt;br&gt;
use App\Context\Order\Models\Order;&lt;br&gt;
use App\Context\Order\Models\ProductStock;&lt;br&gt;
use App\Support\ValueObjects\ProductStockUuid;&lt;br&gt;
use Spatie\EventSourcing\Projectors\Projector;&lt;br&gt;
use Spatie\EventSourcing\Projectors\ProjectsEvents;&lt;/p&gt;

&lt;p&gt;class ProductStockProjector implements Projector&lt;br&gt;
{&lt;br&gt;
    use ProjectsEvents;&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;protected array $handlesEvents = [
    ProductCreatedEvent::class =&amp;gt; 'onProductCreated',
    ProductDeletedEvent::class =&amp;gt; 'onProductDeleted',
    OrderCreatedEvent::class =&amp;gt; 'onOrderCreated',
    OrderCancelledEvent::class =&amp;gt; 'onOrderCancelled',
];

public function onProductCreated(ProductCreatedEvent $event): void
{
    ProductStock::create([
        'uuid' =&amp;gt; ProductStockUuid::create(),
        'product_uuid' =&amp;gt; $event-&amp;gt;productUuid,
        'stock' =&amp;gt; $event-&amp;gt;stock,
    ]);
}

public function onProductDeleted(ProductDeletedEvent $event): void
{
    ProductStock::forProduct($event-&amp;gt;productUuid)-&amp;gt;delete();
}

public function onOrderCreated(OrderCreatedEvent $event): void
{
    $productStock = ProductStock::forProduct($event-&amp;gt;productUuid);

    $productStock-&amp;gt;update([
        'stock' =&amp;gt; $productStock-&amp;gt;stock - $event-&amp;gt;quantity,
    ]);
}

public function onOrderCancelled(OrderCancelledEvent $event): void
{
    $order = Order::findByUuid($event-&amp;gt;aggregateRootUuid());

    $productStock = ProductStock::forProduct($order-&amp;gt;product-&amp;gt;uuid);

    $productStock-&amp;gt;update([
        'stock' =&amp;gt; $productStock-&amp;gt;stock + $order-&amp;gt;quantity,
    ]);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;ProductCreatedEvent&lt;/code&gt; is fired, we will call &lt;code&gt;ProductStock::create&lt;/code&gt;. &lt;code&gt;ProductStock&lt;/code&gt; is a regular Eloquent model.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
namespace App\Context\Order\Models;&lt;/p&gt;

&lt;p&gt;use App\Context\Product\Models\Product;&lt;br&gt;
use Illuminate\Database\Eloquent\Model;&lt;/p&gt;

&lt;p&gt;class ProductStock extends Model&lt;br&gt;
{&lt;br&gt;
    /**&lt;br&gt;
     * &lt;a class="comment-mentioned-user" href="https://dev.to/param"&gt;@param&lt;/a&gt;
 \App\Context\Product\Models\Product|\App\Support\ValueObjects\ProductUuid $productUuid&lt;br&gt;
     *&lt;br&gt;
     * @return \App\Context\Order\Models\ProductStock&lt;br&gt;
     */&lt;br&gt;
    public static function forProduct($productUuid): ProductStock&lt;br&gt;
    {&lt;br&gt;
        if ($productUuid instanceof Product) {&lt;br&gt;
            $productUuid = $productUuid-&amp;gt;getUuid();&lt;br&gt;
        }&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    return static::query()
        -&amp;gt;where('product_uuid', $productUuid)
        -&amp;gt;first();
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;ProductStockProjector&lt;/code&gt; the &lt;code&gt;ProductStock&lt;/code&gt; model will be updated when orders come in, when they get canceled, or when an order is deleted.&lt;/p&gt;

&lt;p&gt;Currently, we have an agreement with every member working on the project that it is not allowed to write to model from other contexts. The &lt;code&gt;Order&lt;/code&gt; context may write and read from this model, but the &lt;code&gt;Product&lt;/code&gt; context may only read it. Because this is such an important rule, we will probably technically enforce it soon.&lt;/p&gt;

&lt;p&gt;Finally, let's take a look at the &lt;code&gt;OrderAggregateRoot&lt;/code&gt; where the &lt;code&gt;ProductStock&lt;/code&gt; model is being used to make a decision.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
namespace App\Context\Order;&lt;/p&gt;

&lt;p&gt;use App\Context\Product\Models\Product;&lt;br&gt;
use App\Context\Order\Events\CouldNotCreateOrderBecauseInsufficientStock;&lt;br&gt;
use App\Context\Order\Events\OrderCreatedEvent;&lt;br&gt;
use App\Context\Order\Events\OrderCancelledEvent;&lt;br&gt;
use App\Context\Order\Exceptions\CannotCreateOrderBecauseInsufficientStock;&lt;br&gt;
use App\Context\Order\Models\ProductStock;&lt;br&gt;
use Spatie\EventSourcing\AggregateRoot;&lt;/p&gt;

&lt;p&gt;class OrderAggregateRoot extends AggregateRoot&lt;br&gt;
{&lt;br&gt;
    public function createOrder(Product $product, int $quantity): self&lt;br&gt;
    {&lt;br&gt;
        $eventAvailability = ProductStock::forProduct($product);&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    if ($eventAvailability-&amp;gt;availability &amp;lt; $quantity) {
        $this-&amp;gt;recordThat(new CouldNotCreateOrderBecauseInsufficientStock(
            $product-&amp;gt;getUuid(),
            $quantity
        ));

        throw CannotCreateOrderBecauseInsufficientStock::make();
    }

    $unitPrice = $product-&amp;gt;unit_price;

    $totalPrice = $unitPrice * $quantity;

    $this-&amp;gt;recordThat(new OrderCreatedEvent(
        $product-&amp;gt;getUuid(),
        $quantity,
        $unitPrice,
        $totalPrice,
    ));

    return $this;
}

public function cancelOrder(): self
{
    $this-&amp;gt;recordThat(new OrderCancelledEvent());

    return $this;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
`&lt;code&gt;&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;In a complete app, the &lt;code&gt;createOrder&lt;/code&gt; function would probably be triggered by an action that is performed in the UI. A product can be ordered for a certain amount. Using the &lt;code&gt;ProductStock,&lt;/code&gt; the aggregate root will decide if there is enough stock for the order to be created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;A non event sourced part of the app fires of regular events. The event sourced parts listen for these events and record data interesting for them in events of their own. These events, own to the context, will be stored. These stored events are being used to feed projectors. These projectors create models that can be read by the non-event-sourced parts. The non-events-sourced parts may only read these models. Aggregate roots can use the events and projection to make decisions.&lt;/p&gt;

&lt;p&gt;This approach brings a lot of benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a non-event-sourced part can be built like we're used to. We just have to take care that events are being fired&lt;/li&gt;
&lt;li&gt;an event sourced part can record its own events to act on&lt;/li&gt;
&lt;li&gt;we could create new projectors and replay all recorded events to build up new state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I know this all is a lot to take in. This way of doing things is by no means set in stone for us. We're in a discovery phase, but we have a good feeling about what we've already discovered. Brent and/or I will probably write some follow-up posts soon.&lt;/p&gt;

</description>
      <category>php</category>
      <category>eventsourcing</category>
    </item>
    <item>
      <title>Replacing web sockets with Livewire</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Mon, 04 May 2020 12:13:56 +0000</pubDate>
      <link>https://dev.to/freekmurze/replacing-web-sockets-with-livewire-484l</link>
      <guid>https://dev.to/freekmurze/replacing-web-sockets-with-livewire-484l</guid>
      <description>&lt;p&gt;Up until a few days ago, the real-time UI of &lt;a href="https://ohdear.app"&gt;Oh Dear&lt;/a&gt; (an uptime monitoring SaaS I run) was powered with web sockets. We recently replaced this with &lt;a href="https://laravel-livewire.com"&gt;Livewire&lt;/a&gt; components.&lt;/p&gt;

&lt;p&gt;In this blog post, I'd like to explain why and how we did that.&lt;/p&gt;

&lt;p&gt;I'm going to assume that you already know what Livewire is. If not, head over to &lt;a href="https://laravel-livewire.com"&gt;the Livewire docs&lt;/a&gt;. There's even &lt;a href="https://laravel-livewire.com/screencasts/installation"&gt;a short video course&lt;/a&gt; on there. In short, Livewire enables you to create dynamic UIs using server-rendered partials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ditch web sockets
&lt;/h2&gt;

&lt;p&gt;There are hundreds of uptime checking services, but most of them look pretty crappy. When my buddy Mattias and I created Oh Dear, we set out the goal to create a beautiful service that is easy to use. We believe that our service doesn't necessarily has to be unique. By providing a good design, UX, and docs, you can get ahead of a large part of the competition.&lt;/p&gt;

&lt;p&gt;One of the things we decided on very early on was that our UI needed to be real-time. We don't want users to refresh to see the latest results of their checks. When a new user adds their first site to our service, we wanted the whole process to be snappy and display results fast.&lt;/p&gt;

&lt;p&gt;This is what that looks like in Oh Dear. After signing up, users can fill out this form. When pressing enter, it gets added to the list, and results are immediately displayed as they come in.&lt;/p&gt;

&lt;p&gt;This behavior used to be powered by web sockets. Whenever Oh Dear started and completed a check, it would broadcast events to the front end.  The broadcasting itself would be done using Laravel's &lt;a href="https://laravel.com/docs/7.x/broadcasting"&gt;native broadcasting features&lt;/a&gt; and the Laravel WebSockets package that Marcel and I created. On the front end, we used &lt;a href="https://laravel.com/docs/7.x/broadcasting#installing-laravel-echo"&gt;Laravel Echo&lt;/a&gt; to receive the events and a couple of Vue components.&lt;/p&gt;

&lt;p&gt;It all worked fine, but it sure added some complexity to our stack. Also, most of the time, once users have set up their sites, they won't visit our website often again. But we'd still keep broadcasting events. We could avoid this by tracking presence in some way, but this would complicate things even further.&lt;/p&gt;

&lt;p&gt;By moving to Livewire, we can ditch all of the things mentioned above entirely. There's no need for a web sockets server, Echo, and Vue to create the experience in the movie above. In fact, in that movie, Livewire is already being used.&lt;/p&gt;

&lt;p&gt;Let's take a look at these Livewire components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Displaying the list of sites
&lt;/h2&gt;

&lt;p&gt;This is what the list of sites looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MF0cS0vk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/site-list/site-list.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MF0cS0vk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/site-list/site-list.png" alt="screenshot of site list"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the Blade view that is used to render that screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;x-app-layout title="Dashboard" :breadcrumbs="Breadcrumbs::render('sites')"&amp;gt;
    &amp;lt;div class="wrap"&amp;gt;
        &amp;lt;section class="card pb-8 cloak-fade" v-cloak&amp;gt;
            @include('app.sites.list.partials.notificationsNotConfigured')

            &amp;lt;livewire:site-list /&amp;gt;

            &amp;lt;div class="flex items-center mt-8" id="site-adder"&amp;gt;
                &amp;lt;livewire:site-adder /&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/section&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/x-app-layout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;So we have two Livewire components going on: &lt;code&gt;site-list&lt;/code&gt; and &lt;code&gt;site-adder&lt;/code&gt;. In the &lt;code&gt;LivewireServiceProvider&lt;/code&gt; you can see&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Providers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Livewire\Sites\SiteListComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Livewire\SiteSettings\SiteAdderComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Illuminate\Support\ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Livewire\Livewire&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LivewireServiceProvider&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;ServiceProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Livewire&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'site-list'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SiteListComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;Livewire&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'site-adder'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SiteAdderComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// other components&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Next, let's take a look at the &lt;code&gt;SiteListComponent&lt;/code&gt; class and its underlying view. You don't have to understand it all now. After the source code, I'm going to explain some parts, but I think it's good that you already take a look at the component as a whole, so you have some context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Livewire\Sites&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Livewire\Component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Livewire\WithPagination&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SiteListComponent&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;WithPagination&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$listeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'siteAdded'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'$refresh'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$perPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$onlySitesWithIssues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$showSiteAdder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;onlySitesWithIssues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showOnlyWithIssues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;onlySitesWithIssues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showSiteAdder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;showSiteAdder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hideSiteAdder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;showSiteAdder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.sites.list.components.siteList'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'sites'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;sites&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'sitesWithIssuesCount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;sitesWithIssuesCount&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentTeam&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;sites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;onlySitesWithIssues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;hasIssues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;orderByRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'(SUBSTRING(url, LOCATE("://", url)))'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sitesWithIssuesCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;currentTeam&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;sites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;hasIssues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And this is the content of the view that is being rendered in that &lt;code&gt;render&lt;/code&gt; function: &lt;code&gt;app.sites.list.components.siteList&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div&amp;gt;
    &amp;lt;div wire:poll.5000ms&amp;gt;
        &amp;lt;section class="flex items-center justify-between w-full mb-8"&amp;gt;
            &amp;lt;div class="text-xs"&amp;gt;
                &amp;lt;div class="switcher"&amp;gt;
                    &amp;lt;button wire:click="showAll"
                            class="switcher-button {{ ! $onlySitesWithIssues ? 'is-active' : '' }}"&amp;gt;Display all sites
                    &amp;lt;/button&amp;gt;
                    &amp;lt;button wire:click="showOnlyWithIssues"
                            class="switcher-button {{ $onlySitesWithIssues ? 'is-active' : '' }}"&amp;gt;
                        Display {{ $sitesWithIssuesCount }} {{ \Illuminate\Support\Str::plural('site', $sitesWithIssuesCount) }} with issues
                    &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;div class="flex items-center justify-end"&amp;gt;
                &amp;lt;a href="#site-adder" wire:click="$emit('showSiteAdder')" class="button is-secondary mr-4"&amp;gt;Add another site&amp;lt;/a&amp;gt;
                &amp;lt;input wire:model="search"
                       class="form-input is-small w-48 focus:w-64"
                       type="text"
                       placeholder="Filter sites..."/&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/section&amp;gt;


        &amp;lt;table class="site-list-table w-full"&amp;gt;
            &amp;lt;thead&amp;gt;
            &amp;lt;th style="width: 27%;"&amp;gt;Site&amp;lt;/th&amp;gt;
            &amp;lt;th style="width: 14%;"&amp;gt;Uptime&amp;lt;/th&amp;gt;
            &amp;lt;th style="width: 14%;"&amp;gt;Broken links&amp;lt;/th&amp;gt;
            &amp;lt;th style="width: 14%;"&amp;gt;Mixed Content&amp;lt;/th&amp;gt;
            &amp;lt;th style="width: 14%;"&amp;gt;Certificate Health&amp;lt;/th&amp;gt;
            &amp;lt;th style="width: 17%;"&amp;gt;Last check&amp;lt;/th&amp;gt;
            &amp;lt;/thead&amp;gt;
            &amp;lt;tbody&amp;gt;
            @forelse($sites as $site)
                &amp;lt;tr&amp;gt;
                    &amp;lt;td class="pr-2"&amp;gt;
                        &amp;lt;div class="flex"&amp;gt;
                            &amp;lt;span class="w-6"&amp;gt;&amp;lt;i
                                    class="fad text-gray-700 {{ $site-&amp;gt;uses_https ? 'fa-lock' : '' }}"&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;a
                                href="{{ route('site', $site) }}"
                                class="flex-1 underline truncate"&amp;gt;{{ $site-&amp;gt;label }}&amp;lt;/a&amp;gt;
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/td&amp;gt;
                    &amp;lt;td class="pr-2"&amp;gt;
                        &amp;lt;x-check-result :site="$site" check-type="uptime"/&amp;gt;
                    &amp;lt;/td&amp;gt;
                    &amp;lt;td class="pr-2"&amp;gt;
                        &amp;lt;x-check-result :site="$site" check-type="broken_links"/&amp;gt;
                    &amp;lt;/td&amp;gt;
                    &amp;lt;td class="pr-2"&amp;gt;
                        &amp;lt;x-check-result :site="$site" check-type="mixed_content"/&amp;gt;
                    &amp;lt;/td&amp;gt;
                    &amp;lt;td class="pr-2"&amp;gt;
                        &amp;lt;x-check-result :site="$site" check-type="certificate_health"/&amp;gt;
                    &amp;lt;/td&amp;gt;
                    &amp;lt;td class="text-sm text-gray-700"&amp;gt;
                        @if($site-&amp;gt;latest_run_date)
                            &amp;lt;a href="{{ route('site', $site) }}"&amp;gt;
                                &amp;lt;time datetime="{{ $site-&amp;gt;latest_run_date-&amp;gt;format('Y-m-d H:i:s') }}"
                                      title="{{ $site-&amp;gt;latest_run_date-&amp;gt;format('Y-m-d H:i:s') }}"&amp;gt;{{ $site-&amp;gt;latest_run_date-&amp;gt;diffInSeconds() &amp;lt; 60 ? 'Less than a minute ago' : $site-&amp;gt;latest_run_date-&amp;gt;diffForHumans() }}&amp;lt;/time&amp;gt;
                            &amp;lt;/a&amp;gt;
                        @else
                            No runs yet
                        @endif
                    &amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
            @empty
                &amp;lt;tr&amp;gt;&amp;lt;td class="text-center" colspan="6"&amp;gt;There are no sites that match your search...&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
            @endforelse
            &amp;lt;/tbody&amp;gt;
        &amp;lt;/table&amp;gt;

        @if ($sites-&amp;gt;total() &amp;gt; $perPage)
            &amp;lt;div class="flex justify-between mt-4"&amp;gt;
                &amp;lt;div class="flex-1 w-1/2 mt-4"&amp;gt;
                    {{ $sites-&amp;gt;links() }}
                &amp;lt;/div&amp;gt;

                &amp;lt;div class="flex w-1/2 text-right text-muted text-sm text-gray-700 mt-4"&amp;gt;
                    &amp;lt;div class="w-full block"&amp;gt;
                        Showing {{ $sites-&amp;gt;firstItem() }} to {{ $sites-&amp;gt;lastItem() }} out of {{ $sites-&amp;gt;total() }} sites
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        @endif
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;render&lt;/code&gt; function, you probably have noticed that the component itself is responsible for getting the sites.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.sites.list.components.siteList'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'sites'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;sites&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'sitesWithIssuesCount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;sitesWithIssuesCount&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentTeam&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;sites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;onlySitesWithIssues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;hasIssues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;orderByRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'(SUBSTRING(url, LOCATE("://", url)))'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Filtering sites
&lt;/h3&gt;

&lt;p&gt;Notice that &lt;code&gt;$this-&amp;gt;onlySitesWithIssues&lt;/code&gt; check? Let's see how that variable is being set. If you take a look at the screenshot of the site list, you'll notice a little filter on the top with "Display all sites" and "Display sites with issues".&lt;/p&gt;

&lt;p&gt;This is the part of the view that renders that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="switcher"&amp;gt;
    &amp;lt;button wire:click="showAll"
            class="switcher-button {{ ! $onlySitesWithIssues ? 'is-active' : '' }}"&amp;gt;Display all sites
    &amp;lt;/button&amp;gt;
    &amp;lt;button wire:click="showOnlyWithIssues"
            class="switcher-button {{ $onlySitesWithIssues ? 'is-active' : '' }}"&amp;gt;
        Display {{ $sitesWithIssuesCount }} {{ \Illuminate\Support\Str::plural('site', $sitesWithIssuesCount) }} with issues
    &amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;wire:click="showOnlyWithIssues"&lt;/code&gt;. When the user clicks an element with &lt;code&gt;wire:click&lt;/code&gt; the method whose name is in the attribute value will be executed, and the component will be re-rendered. So, in this case, &lt;code&gt;showOnlyWithIssues&lt;/code&gt; is executed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showOnlyWithIssues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;onlySitesWithIssues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;So, here our &lt;code&gt;onlySitesWithIssues&lt;/code&gt; instance variable is changed, causing our component to re-render. Because &lt;code&gt;onlySitesWithIssues&lt;/code&gt; is now set to &lt;code&gt;true&lt;/code&gt;, the &lt;code&gt;sites()&lt;/code&gt; method will now filter on sites having issues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if ($this-&amp;gt;onlySitesWithIssues) {
    $query = $query-&amp;gt;hasIssues();
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;When the component gets rendered, only sites with issues are displayed. Under the hood, Livewire takes care of a lot of things, most notably calling the method on the component when that &lt;code&gt;wire:click&lt;/code&gt; element is called, and replacing the HTML of the component on the page when the HTML of the re-rendered version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching sites
&lt;/h2&gt;

&lt;p&gt;In the top right corner of the site list, you see a search box. When you type something there, you will only see sites who's name contain your query. Here's how that works.&lt;/p&gt;

&lt;p&gt;In the Blade view, this is the relevant HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;input 
   wire:model="search"
   class="form-input is-small w-48 focus:w-64"
   type="text"
   placeholder="Filter sites..."
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here, another Livewire directive is used: &lt;code&gt;wire:model&lt;/code&gt;. This directive will make sure that each time you type something in that element, Livewire will update the instance variable with the same name in the component and re-render the component. By default, this behavior is debounced, meaning that when you type fast, only one request will be made every 150ms.&lt;/p&gt;

&lt;p&gt;So, because the &lt;code&gt;search&lt;/code&gt; instance variable is changed on the component, it will re-render. In the first line of the &lt;code&gt;sites()&lt;/code&gt; method, the value is being used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$query = currentTeam()-&amp;gt;sites()-&amp;gt;search($this-&amp;gt;search);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And with this in place, searching sites works. I think it's kinda amazing that you can have this behavior by merely adding  &lt;code&gt;livewire:model&lt;/code&gt; in your view, and using that value to scope your query. Sure, you could do this with Vue, but this is way simpler.&lt;/p&gt;

&lt;p&gt;For completeness, here's the &lt;code&gt;scopeSearch&lt;/code&gt; that's on the &lt;code&gt;Site&lt;/code&gt; model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scopeSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$searchFor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'like'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$searchFor&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Paginating sites
&lt;/h2&gt;

&lt;p&gt;Laravel natively &lt;a href="https://laravel.com/docs/master/pagination#displaying-pagination-results"&gt;has excellent support for paginating query results&lt;/a&gt;. You only have to call &lt;code&gt;paginate&lt;/code&gt; on your query, which we have done in the &lt;code&gt;sites()&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in the sites() method&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;orderByRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'(SUBSTRING(url, LOCATE("://", url)))'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;$sites-&amp;gt;links()&lt;/code&gt; in the view will render URLs with a &lt;code&gt;page&lt;/code&gt; parameter. Under the hood of the  &lt;code&gt;paginate()&lt;/code&gt; method, that query parameter is used to fetch the results of that page. That's in short how pagination works in Laravel.&lt;/p&gt;

&lt;p&gt;Adding pagination support to a Livewire component is super easy with Livewire. You just have to use the &lt;code&gt;Livewire\WithPagination&lt;/code&gt; trait on your component. This trait does some magic so the render links don't contain a URL to your page, but to an internal Livewire route. This is the output of &lt;code&gt;$sites()-&amp;gt;links()&lt;/code&gt; in the view of Livewire component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-item active"&lt;/span&gt; &lt;span class="na"&gt;aria-current=&lt;/span&gt;&lt;span class="s"&gt;"page"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-link"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://ohdear.app/livewire/message/site-list?page=2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-link"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://ohdear.app/livewire/message/site-list?page=3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-link"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://ohdear.app/livewire/message/site-list?page=4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;4&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"page-link"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://ohdear.app/livewire/message/site-list?page=2"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"next"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Next »"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;›&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;When clicking such a link, Livewire will set the &lt;code&gt;page&lt;/code&gt; query parameter in the URL and re-render the component. It's an elegant solution, and I like that it hooks in so nicely with Laravel's default pagination. &lt;/p&gt;

&lt;p&gt;As a Livewire user, the only thing you needed to do extra was to apply that &lt;code&gt;WithPagination&lt;/code&gt; trait. Beautiful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating the list
&lt;/h2&gt;

&lt;p&gt;As stated earlier, want to have our UI to display the current state, without users having to refresh. If one of the sites displayed in the list goes down, the list should get updated automatically.&lt;/p&gt;

&lt;p&gt;The solution for this is straightforward: we're just going to poll for changes. Livewire comes with support for polling out of the box. At the top of the view you might have noticed this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div wire:poll.5000ms&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This will make Livewire refresh the component automatically every five seconds. To re-render the component in the browser, Livewire makes use of &lt;a href="https://github.com/patrick-steele-idem/morphdom"&gt;morphdom&lt;/a&gt;. This will compare the HTML of the component with the HTML of the re-rendered version. Only the changes will get updated in the DOM, making the cost for re-rendering in the browser very low.&lt;/p&gt;

&lt;p&gt;While typing this blog post, I can already feel the screams of some readers: "Freek, what are you doing!?!? This is highly inefficient: it will perform queries and make requests the whole time...". And yes, that is true. &lt;/p&gt;

&lt;p&gt;Using Livewire with polling is a trade-off. One the one hand, we have some more requests/queries, but this only happens when users are looking at their site lists. This won't happen often. To know the state of their sites, the vast majority of our users rely on the alerts we sent out (via Slack, Mail, Telegram, ...), and they won't be staring at the site list the whole time.&lt;/p&gt;

&lt;p&gt;It's also very nice that Livewire only polls if the browser is displaying the page. When a browser tab is in the background, polling is paused. &lt;/p&gt;

&lt;p&gt;By using a little bit of polling, we don't need to set up web sockets or Vue, and we don't need to broadcast events the whole time, ... It vastly simplifies our tech stack. I think in this case, the trade-off that Livewire / polling brings to the table is worth it. &lt;/p&gt;

&lt;h2&gt;
  
  
  In closing
&lt;/h2&gt;

&lt;p&gt;I hope you enjoyed this dissection of our site list Livewire component. Other place in our UI, where realtime info is being displayed, was refactored in a similar fashion.&lt;/p&gt;

&lt;p&gt;Working with &lt;a href="https://laravel-livewire.com/docs/making-components"&gt;Livewire&lt;/a&gt; was a pleasure. The API makes sense, the docs are well written,  and it's easy to create powerful components in a short amount of time.&lt;/p&gt;

&lt;p&gt;If you want to see the Livewire components discussed in this blogpost in action, head over to &lt;a href="https://ohdear.app"&gt;Oh Dear&lt;/a&gt; and &lt;a href="https://ohdear.app/register"&gt;start your ten-day trial&lt;/a&gt; (no credit card needed).&lt;/p&gt;

&lt;p&gt;After ten days, you might want to get a subscription. What sets Oh Dear apart from the competition is that we not only monitor your homepage, but your entire site. We can alert you when a broken link, or mixed content, is found on any pages of your website. And we also monitor certificate health and provide beautiful status pages &lt;a href="https://status.flareapp.io"&gt;like this one&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Should you have questions about these Livewire components, or Oh Dear in general, let me know in the comments below.&lt;/p&gt;

</description>
      <category>php</category>
    </item>
    <item>
      <title>Building complex forms with Laravel Livewire in Oh Dear</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Mon, 04 May 2020 12:12:54 +0000</pubDate>
      <link>https://dev.to/freekmurze/building-complex-forms-with-laravel-livewire-in-oh-dear-39nj</link>
      <guid>https://dev.to/freekmurze/building-complex-forms-with-laravel-livewire-in-oh-dear-39nj</guid>
      <description>&lt;p&gt;Together with my buddy &lt;a href="https://twitter.com/mattiasgeniar"&gt;Mattias Geniar&lt;/a&gt;, I run &lt;a href="https://ohdear.app"&gt;Oh Dear&lt;/a&gt;, an uptime checker service on steroids. &lt;/p&gt;

&lt;p&gt;Unlike most uptime trackers, Oh Dear doesn't only check your homepage, but every single page of your site. When we detect a broken link or some mixed content, we send a notification. Oh, and we provide status pages, like this one from &lt;a href="https://status.laravel.com"&gt;Laravel&lt;/a&gt; and &lt;a href="https://status.flareapp.io"&gt;Flare&lt;/a&gt; too.&lt;/p&gt;

&lt;p&gt;In this blog post, I'd like to show you how we use Livewire to render some complex forms in the UI of Oh Dear.&lt;/p&gt;

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

&lt;p&gt;Livewire is an impressive library written by &lt;a href="https://calebporzio.com"&gt;Caleb Porzio&lt;/a&gt; that aims to make building dynamic interfaces simple, without needing to resort to JavaScript for most cases). &lt;/p&gt;

&lt;p&gt;This blog post isn't an introduction to Livewire, if you need that head over to &lt;a href="https://laravel-livewire.com/screencasts/installation"&gt;this video series on the Livewire docs&lt;/a&gt;. Before continuing reading my post, you should have a grasp of what Livewire can do.&lt;/p&gt;

&lt;p&gt;Oh Dear was initially built a couple of years ago (oh how time flies), and our best option for creating the UI we wanted was, at the time, Vue.  Both Mattias and I don't see ourselves as JavaScript experts. If we can solve something on the server-side, that will probably be our preferred route. That's why we're now slowly replacing our custom Vue components with Livewire.&lt;/p&gt;

&lt;p&gt;I still love Vue, but in some instances, we don't want the complexity that it brings. In this blog post, I'll show you two examples of Vue components that were replaced by Livewire counterparts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Livewire component to add headers
&lt;/h2&gt;

&lt;p&gt;Oh Dear sends requests to sites to check their health. Users can specify extra headers that should be used when making those requests. &lt;/p&gt;

&lt;p&gt;This is how the form in the site settings screen looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vtClIA7a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/header.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vtClIA7a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/header.png" alt="screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When clicking "Add custom header" fields are displayed to enter the name and value of the header.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yd7b6kdH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/header-one.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yd7b6kdH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/header-one.png" alt="screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Users can specify up to five headers. If there are five headers on screen, the "Add custom header" button will be hidden.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TI1-Lw8u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/header-five.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TI1-Lw8u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/header-five.png" alt="screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, when you press "Remove", that line will get removed. Any changes you make will get saved when clicking "Update".&lt;/p&gt;

&lt;p&gt;Let's take a look at the needed HTML when rendering two headers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form mb-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"_token"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-group is-row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex mb-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mr-2 flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Header name"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                                       &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"http_client_headers[0][name]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mr-2 flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Header value"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                                       &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"http_client_headers[0][value]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button is-secondary items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xs text-gray-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                Remove
                            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex mb-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mr-2 flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Header name"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                                       &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"http_client_headers[1][name]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mr-2 flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Header value"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                                       &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"http_client_headers[1][value]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button is-secondary items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xs text-gray-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                Remove
                            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button is-secondary mr-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    Add custom header
                &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"help"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            When performing the uptime, mixed content and broken links checks we'll add these headers to each request we
            make to BackupPC.
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-group has-button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Update&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The cool thing to note here is that you can use &lt;code&gt;[]&lt;/code&gt; when you want to submit an array of data. This is standard HTML. Here are all the names of the form elements in the snippet above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http_client_headers[0][value]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http_client_headers[0][name]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http_client_headers[1][value]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http_client_headers[1][name]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the server-side, this is how that data gets validated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Requests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Domain\Site\Rules\HttpHeaderNameRule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Illuminate\Foundation\Http\FormRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateHeaderSettingsRequest&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;FormRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'http_client_headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array|max:5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'http_client_headers.*.name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'max:500'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;HttpHeaderNameRule&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
            &lt;span class="s1"&gt;'http_client_headers.*.value'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|max:500'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;I'll sidestep a little and, for completeness, show the code of that &lt;code&gt;HttpHeaderRule&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Domain\Site\Rules&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Illuminate\Contracts\Validation\Rule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HttpHeaderNameRule&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attribute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'accept-encoding'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'connection'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'user-agent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'host'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'referer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'pragma'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'This header is not allowed.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now that you know which HTML is rendered and how it is validated, let's take a look at our Vue components that would handle rendering the HTML.&lt;/p&gt;

&lt;p&gt;Here's the content of &lt;code&gt;HttpHeaders.vue&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;div v-for="(httpHeader, index) in httpHeaders"&amp;gt;
            &amp;lt;http-header
                :http-header="httpHeader"
                :index="index"
                :error="getError(index)"
                :removable="removable"
                @remove="removeHeader(httpHeader)"
            &amp;gt;&amp;lt;/http-header&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div v-if="httpHeaders.length &amp;lt; 10" @click="addHeader" class="button is-secondary mr-4"&amp;gt;
            Add custom header
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import httpHeader from './httpHeader';

export default {
    props: ['initHttpHeaders', 'initErrors'],

    components: {
        httpHeader,
    },

    computed: {
        removable() {
            return true;
        },
    },

    data() {
        return {
            httpHeaders: [],
            errors: {},
        };
    },

    created() {
        this.initHttpHeaders = this.inithttpHeaders;
        this.errors = this.initErrors;
    },

    methods: {
        addHeader() {
            this.httpHeaders.push(this.emptyHeader());
        },

        removeHeader(httpHeader) {
            this.httpHeaders.splice(this.httpHeaders.indexOf(httpHeader), 1);

            this.errors = {};
        },

        emptyHeader() {
            return { name: '', value: '' };
        },

        getError(index) {
            return {
                name: this.errors[`http_headers.${index}.name`],
                value: this.errors[`http_headers.${index}.value`],
            };
        },
    },
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And here is the content of &lt;code&gt;HttpHeader.vue&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex mb-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mr-2 flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
                &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Header name"&lt;/span&gt;
                &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"httpHeader.name"&lt;/span&gt;
                &lt;span class="na"&gt;:name=&lt;/span&gt;&lt;span class="s"&gt;"`http_headers[${index}][name]`"&lt;/span&gt;
            &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-error"&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"hasError('name')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                {{ getError('name') }}
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mr-2 flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
                &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Header value"&lt;/span&gt;
                &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"httpHeader.value"&lt;/span&gt;
                &lt;span class="na"&gt;:name=&lt;/span&gt;&lt;span class="s"&gt;"`http_headers[${index}][value]`"&lt;/span&gt;
            &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-error"&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"hasError('value')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                {{ getError('value') }}
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button is-secondary items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"removable"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xs text-gray-500"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"$emit('remove')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                Remove
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;httpHeader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;removable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;

        &lt;span class="nx"&gt;getError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;I'm not going to go over all this code; it should be pretty straightforward if you know your way around Vue.&lt;/p&gt;

&lt;p&gt;Something that has always bothered me is that in these Vue components, I can't make use of any partials or form helpers that are available server-side. Also, getting the errors from Laravel displayed into the component takes some effort. None of this is rocket science to solve, but still...&lt;/p&gt;

&lt;p&gt;We've replaced these two Vue components with a single Livewire component. Let's take a look.&lt;/p&gt;

&lt;p&gt;Here is the &lt;code&gt;HttpHeaders&lt;/code&gt; component itself. I've added some comments to make it more clear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Livewire&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Livewire\Component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HttpHeaders&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * This array contains an array with a name and value for
     * each header we display.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * This function will get called when ever the component
     * is rendered for the first time.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="cd"&gt;/**
         * We first check if there are any old values for
         * the form elements we want to render.
         *
         * When a user has submitted a form with values that
         * didn't pass validation, we display those old values.
         */&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'http_client_headers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * This function will add an empty header value pair
     * causing an extra row to be rendered.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addHeader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;canAddMoreHeaders&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'value'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Here we'll remove the item with the given key
     * from the headers array, so a rendered row will
     * disappear.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;removeHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * This check is used both in this component class
     * and in the view.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canAddMoreHeaders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'livewire.http-headers'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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



&lt;p&gt;And here is that &lt;code&gt;http-headers&lt;/code&gt; view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
        @foreach($headers as $i =&amp;gt; $header)
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex mb-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mr-2 flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
                            &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{{ $header['name'] }}"&lt;/span&gt;
                            &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Header name"&lt;/span&gt;
                            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                            &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"http_client_headers[{{ $i }}][name]"&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    @error("http_client_headers.{$i}.name")
                    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-error"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            {{ $errors-&amp;gt;first("http_client_headers.{$i}.name") }}
                        &lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    @enderror
                &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mr-2 flex-grow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
                            &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{{ $header['value'] }}"&lt;/span&gt;
                            &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Header value"&lt;/span&gt;
                            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                            &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"http_client_headers[{{ $i }}][value]"&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    @error("http_client_headers.{$i}.value")
                    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-error"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            {{ $errors-&amp;gt;first("http_client_headers.{$i}.value") }}
                        &lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    @enderror
                &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button is-secondary items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;wire:click.prevent=&lt;/span&gt;&lt;span class="s"&gt;"removeHeader({{ $i }})"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xs text-gray-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        Remove
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        @endforeach
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    @if ($this-&amp;gt;canAddMoreHeaders())
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;wire:click.prevent=&lt;/span&gt;&lt;span class="s"&gt;"addHeader"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button is-secondary mr-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            Add custom header
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    @endif
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Again, not rocket science, but it's pretty beautiful. I didn't need to write a single line of JavaScript. I can make use of any Blade directive. I don't need to do anything special to handle errors. &lt;/p&gt;

&lt;p&gt;This solution feels much lighter to me than resorting to Vue. &lt;/p&gt;

&lt;p&gt;There's also a small drawback. Each time a user wants to add an extra header, "Add custom header" is going to get clicked, and the server will need to build up the additional HTML. When using Vue, there's no extra trip. In Oh Dear, however, the load that we get from the UI pales in comparison to the load we get from performing checks and crawling sites. So, I gladly accept this small drawback. The benefits that Livewire brings to the table outweigh the drawbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Displaying notification preferences
&lt;/h2&gt;

&lt;p&gt;Let's take a look at a more complex form. The core business of Oh Dear is to notify users when something is wrong. We support my notification channels, such as mail, SMS, Slack, Webhooks, Pushover, ... In our UI, users can specify which notifications should be sent to which channels.&lt;/p&gt;

&lt;p&gt;Here's what it looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--N83Nv3Gg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/slack.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--N83Nv3Gg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/slack.png" alt="screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Much like the headers component, you can add multiple values.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7s-jpEa1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/slack-multiple.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7s-jpEa1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/slack-multiple.png" alt="screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Something to keep in mind is that, for some channels, multiple form fields need to be rendered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--chWJM_Hu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/nexmo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--chWJM_Hu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/ohdear-livewire/nexmo.png" alt="screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's different compared to the headers component is that, by default, some form fields are shown. We don't display any form element by default for the http headers component, because most users don't need to set custom headers. When you're on the notification screen, you probably do want to set up or view your notification preference.&lt;/p&gt;

&lt;p&gt;This form is a bit more complicated than the HTTP headers one. We have different form fields per channel, and we have a bunch of toggles.&lt;/p&gt;

&lt;p&gt;Let's first take a look at the Vue components that we've ditched.&lt;/p&gt;

&lt;p&gt;Here's &lt;code&gt;NotificationDestinations.vue&lt;/code&gt;, which handles the entire collection of forms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;div class="mb-12"&amp;gt;
        &amp;lt;h2 class="h2 mb-2"&amp;gt;{{ title }}&amp;lt;/h2&amp;gt;
        &amp;lt;p v-if="$slots.description" class="help mb-6"&amp;gt;
            &amp;lt;slot name="description" /&amp;gt;
        &amp;lt;/p&amp;gt;
        &amp;lt;div class="alert is-success mb-4" v-if="form.successful"&amp;gt;
            The notification settings have been updated!
        &amp;lt;/div&amp;gt;
        &amp;lt;notification-destination
            v-for="(notificationDestination, index) in form.notification_destinations"
            :key="notificationDestination.id"
            :init-notification-destination="notificationDestination"
            :input-label="inputLabel + ' #' + (index + 1)"
            :validation-error="getValidationError(index)"
            :removable="removable"
            @remove="removeNotificationDestination(notificationDestination.id)"
        &amp;gt;&amp;lt;/notification-destination&amp;gt;
        &amp;lt;div class="flex items-center justify-end mb-4"&amp;gt;
            &amp;lt;button
                v-if="form.notification_destinations.length &amp;lt; 5"
                @click="addNotificationDestination"
                class="button is-secondary mr-4"
            &amp;gt;
                Add another {{ inputLabel.toLowerCase() }}
            &amp;lt;/button&amp;gt;
            &amp;lt;button class="button" type="submit" @click.prevent="update" :disabled="form.busy"&amp;gt;
                Update
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import NotificationDestination from './NotificationDestination';

export default {
    props: ['title', 'notificationDestinations', 'channel', 'inputLabel', 'updateEndpoint'],

    components: {
        NotificationDestination,
    },

    computed: {
        removable() {
            if (this.form.notification_destinations.length &amp;gt; 1) {
                return true;
            }

            return this.form.notification_destinations[0].destination != '';
        },
    },

    data() {
        return {
            form: new SparkForm({
                notification_destinations: [],
                channel: this.channel,
            }),
        };
    },

    mounted() {
        this.form.notification_destinations = this.notificationDestinations.map(
            (notificationDestination, index) =&amp;gt; {
                notificationDestination.id = index;

                return notificationDestination;
            }
        );

        if (!this.form.notification_destinations.length) {
            this.form.notification_destinations.push(this.newDestination());
        }
    },

    methods: {
        async update() {
            await Spark.put(this.updateEndpoint, this.form);
        },

        addNotificationDestination() {
            this.form.notification_destinations.push(this.newDestination());
        },

        removeNotificationDestination(id) {
            const destinations = this.form.notification_destinations.filter(
                destination =&amp;gt; destination.id !== id
            );

            if (!destinations.length) {
                destinations.push(this.newDestination());
            }

            this.$set(this.form, 'notification_destinations', destinations);
        },

        getValidationError(index) {
            return this.form.errors.get(`notification_destinations.${index}`);
        },

        newDestination() {
            return {
                id: this.form.notification_destinations.length,
                channel: this.channel,
                destination: {},
                accepts_notification_classes: [
                    'CertificateExpiresSoon',
                    'CertificateFixed',
                    'CertificateHasChanged',
                    'CertificateUnhealthy',
                    'CertificateIssued',
                    'MixedContentFixed',
                    'MixedContentFound',
                    'BrokenLinksFixed',
                    'BrokenLinksFound',
                    'UptimeCheckRecovered',
                    'UptimeCheckFailed',
                    'SiteAdded',
                ],
            };
        },
    },
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This is the code of &lt;code&gt;NotificationDestination.vue&lt;/code&gt;, which handles a single form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;p class="alert is-danger is-small mb-2" v-show="validationError"&amp;gt;
            {{ validationError }}
        &amp;lt;/p&amp;gt;

        &amp;lt;div
            :is="this.channelComponentName"
            :input-label="inputLabel"
            :notification-destination="notificationDestination"
        &amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class="notification-toggles" :class="{ 'is-removable': removable }"&amp;gt;
            &amp;lt;div class="notification-toggles-left"&amp;gt;
                &amp;lt;button v-if="removable" class="text-xs text-gray-500" @click="$emit('remove')"&amp;gt;
                    Remove
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class="notification-toggles-right"&amp;gt;
                &amp;lt;div v-for="toggle in toggles" :key="toggle.type" class="inline-block w-full"&amp;gt;
                    &amp;lt;notification-toggle
                        :label="toggle.label"
                        :type="toggle.type"
                        :checked="acceptsNotification(toggle.type)"
                        @change="updateAcceptsNotification"
                    &amp;gt;&amp;lt;/notification-toggle&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import NotificationToggle from './NotificationToggle';
import HipChat from './Channels/HipChat';
import Slack from './Channels/Slack';
import Discord from './Channels/Discord';
import Mail from './Channels/Mail';
import Pushover from './Channels/Pushover';
import Nexmo from './Channels/Nexmo';
import Webhook from './Channels/Webhook';

export default {
    props: ['initNotificationDestination', 'inputLabel', 'validationError', 'removable'],

    components: {
        NotificationToggle,
        Slack,
        Discord,
        HipChat,
        Mail,
        Pushover,
        Nexmo,
        Webhook,
    },

    computed: {
        channelComponentName() {
            return (
                this.notificationDestination.channel.charAt(0).toUpperCase() +
                this.notificationDestination.channel.slice(1)
            );
        },
    },

    data() {
        return {
            notificationDestination: {},
            toggles: _.sortBy(
                [
                    { type: 'UptimeCheckFailed', label: 'Site down' },
                    { type: 'UptimeCheckRecovered', label: 'Site recovered' },
                    { type: 'MixedContentFound', label: 'Mixed content found' },
                    { type: 'MixedContentFixed', label: 'Mixed content fixed' },
                    { type: 'BrokenLinksFound', label: 'Broken links found' },
                    { type: 'BrokenLinksFixed', label: 'Broken links fixed' },
                    { type: 'CertificateExpiresSoon', label: 'Certificate expires soon' },
                    { type: 'CertificateUnhealthy', label: 'Certificate not ok' },
                    { type: 'CertificateFixed', label: 'Certificate fixed' },
                    { type: 'CertificateIssued', label: 'Certificate issued' },
                    { type: 'CertificateHasChanged', label: 'Certificate changed' },
                    { type: 'SiteAdded', label: 'Site added to account' },
                ],
                'label'
            ),
        };
    },

    created() {
        this.notificationDestination = this.initNotificationDestination;
    },

    methods: {
        acceptsNotification(notificationType) {
            return this.notificationDestination.accepts_notification_classes.includes(
                notificationType
            );
        },

        updateAcceptsNotification(notificationClass, accepts) {
            if (accepts) {
                this.notificationDestination.accepts_notification_classes.push(notificationClass);

                return;
            }

            const newNotifificationClasses = this.notificationDestination.accepts_notification_classes.filter(
                existingNotificationClass =&amp;gt; existingNotificationClass !== notificationClass
            );

            return (this.notificationDestination.accepts_notification_classes = newNotifificationClasses);
        },
    },
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here is the &lt;code&gt;Slack.vue&lt;/code&gt; component which was responsible for rendering the Slack specific fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;label class="form-group is-row"&amp;gt;
        &amp;lt;span class="label"&amp;gt;{{ inputLabel }}&amp;lt;/span&amp;gt;
        &amp;lt;input
            placeholder="https://hooks.slack.com/services/..."
            type="text"
            v-model="notificationDestination.destination.url"
            name="webhook_url"
        /&amp;gt;
    &amp;lt;/label&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
    props: ['notificationDestination', 'inputLabel'],
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;You don't need to go through the entire Vue code. I just want to stress that quite some code was required to make this work.&lt;/p&gt;

&lt;p&gt;Now let's look at the Livewire component that replaces the Vue components above.&lt;/p&gt;

&lt;p&gt;First, let's take a look at the controller that will render the view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Controllers\Settings\Team\Notifications\Channels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Controllers\Settings\Team\Notifications\Channels\Concerns\HandlesTeamNotificationDestinations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Requests\Notifications\UpdateSlackNotificationsRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SlackTeamNotificationsController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;HandlesTeamNotificationDestinations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'slack'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UpdateSlackNotificationsRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;handleUpdateRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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



&lt;p&gt;We have a controller per channel. The real meat of the functionality is in the &lt;code&gt;BuildsTeamNotificationsTestView&lt;/code&gt; trait. When working with controllers, I generally prefer moving common functionality to traits and not to (abstract) base classes because you can apply multiple traits to a class, but you can only have one class where you can extend from.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;update&lt;/code&gt; method is overridden so I can use a specific form request for validation.&lt;/p&gt;

&lt;p&gt;Here's that &lt;code&gt;UpdateSlackNotificationsRequest&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Requests\Notifications&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Domain\Notification\Rules\NotificationDestinationTypeRule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Domain\Site\Rules\UrlRule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Illuminate\Foundation\Http\FormRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateSlackNotificationsRequest&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;FormRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'notification_destinations'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array|max:5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'notification_destinations.*.destination.url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;UrlRule&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
            &lt;span class="s1"&gt;'notification_destinations.*.accepts_notification_classes.*'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;NotificationDestinationTypeRule&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The request makes it clear that we accept two arrays back &lt;br&gt;
&lt;code&gt;notification_destinations.*.destination&lt;/code&gt; and &lt;code&gt;notification_destinations.*.accepts_notification_classes&lt;/code&gt;. In &lt;code&gt;destination&lt;/code&gt; all specific configuration values of the channel are expected.&lt;code&gt;accepts_notification_classes&lt;/code&gt; should hold all types of notification that should be sent to this notification destination.&lt;/p&gt;

&lt;p&gt;It surprises exactly no-one that the controller for handling Nexmo notifications looks quite similar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Controllers\Settings\Team\Notifications\Channels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Controllers\Settings\Team\Notifications\Channels\Concerns\HandlesTeamNotificationDestinations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Requests\Notifications\UpdateNexmoNotificationsRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NexmoTeamNotificationsController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;HandlesTeamNotificationDestinations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'nexmo'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UpdateNexmoNotificationsRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;handleUpdateRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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



&lt;p&gt;Let's take a look at the &lt;code&gt;HandlesTeamNotificationDestinations&lt;/code&gt; trait.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\App\Controllers\Settings\Team\Notifications\Channels\Concerns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;trait&lt;/span&gt; &lt;span class="nc"&gt;HandlesTeamNotificationDestinations&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$notificationDestinations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentTeam&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;notificationDestinationsForChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app.settings.team.notifications.channels.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'notificationDestinations'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'channel'&lt;/span&gt;
        &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleUpdateRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;currentTeam&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;setNotificationDestinationsForChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$channelLabel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ucfirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nx"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"The &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$channelLabel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; notification preferences have been updated."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;back&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here you can see that the actual &lt;code&gt;handleUpdateRequest&lt;/code&gt; logic is the same for all notification channels.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;edit&lt;/code&gt; you see that there is a specific view per channel. Let's take a look at the &lt;code&gt;app.settings.team.notifications.channels.slack&lt;/code&gt; view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@component('app.sites.components.page', [
    'site' =&amp;gt; $site,
    'breadcrumbs' =&amp;gt; ['site-notifications', $site],
])
    &amp;lt;div class="card"&amp;gt;
        &amp;lt;x-site-notification-header channel="slack" :site="$site" /&amp;gt;

            &amp;lt;p class="help mb-4"&amp;gt;
                Oh Dear can notify you via Slack. You can learn how to set up a Slack webhook at &amp;lt;a href="/docs/notifications/slack"&amp;gt;our Slack documentation&amp;lt;/a&amp;gt;.
            &amp;lt;/p&amp;gt;

        &amp;lt;livewire:slack-notification-destinations :notificationDestinations="$notificationDestinations"/&amp;gt;
    &amp;lt;/div&amp;gt;
@endcomponent
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The view above uses the old &lt;code&gt;@component&lt;/code&gt; notation to extend from &lt;code&gt;app.sites.components.page&lt;/code&gt;, which contains the general application layout. In a future refactor, we'll probably refactor this to &lt;a href="https://laravel.com/docs/master/blade#components"&gt;Laravel 7 style Blade components&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;To render the header of the card (which contains the title and the tabs), a blade component is already used. Last, you can see that we render a Livewire component &lt;code&gt;slack-notification-destinations&lt;/code&gt;. In a future refactor, I might wrap this up in a Blade component.&lt;/p&gt;

&lt;p&gt;Here is the class component that backs &lt;code&gt;slack-notification-destinations&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Livewire&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SlackNotificationDestinations&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;NotificationDestinations&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'url'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'label'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Webhook URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'placeholder'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://hooks.slack.com/services/...'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;So this one only holds the field definitions for the channel.&lt;/p&gt;

&lt;p&gt;When looking at the Nexmo specific one, you'll see other fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Livewire&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NexmoNotificationDestinations&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;NotificationDestinations&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'apiKey'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'label'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'API Key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'placeholder'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'API Key'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'apiSecret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'label'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'API Secret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'placeholder'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'API Secret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'from'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'label'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'From'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'placeholder'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Telephone number'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'to'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'label'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'To'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'placeholder'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Telephone number'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The meat functionality of this component is the &lt;code&gt;NotificationDestinations&lt;/code&gt; where the channel-specific components extend from. I've added some comments to make it more clear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Livewire&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;App\Domain\Notification\Models\NotificationDestination&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;NotificationDestinationModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Illuminate\View\View&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Livewire\Component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationDestinations&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$notificationDestinations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$listeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'removeNotificationDestination'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     *  Here the notification destinations from the server
     * are passed to the components.
     *
     * If there are none defined, we are going to add an empty so at least
     * an empty form is being displayed at all times.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$notificationDestinations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'notification_destinations'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;notificationDestinations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;canAddMoreNotificationDestinations&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;removeNotificationDestination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canAddMoreNotificationDestinations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;notificationDestinations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'livewire.notification-destinations'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cm"&gt;/*
     * We are going to merge the channel-specific fields with the possible notification
     * types (these are the values that back all the checkboxes)
     */&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$channelSpecifiedFields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$notificationTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NotificationDestinationModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;notificationTypes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;array_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'accepts_notification_classes'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$notificationTypes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="nv"&gt;$channelSpecifiedFields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And here's the &lt;code&gt;notification-destinations&lt;/code&gt; view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form method="POST"&amp;gt;
    @method('PUT')
    @csrf

    &amp;lt;div&amp;gt;
        @foreach($notificationDestinations as $i =&amp;gt; $notificationDestination)
            &amp;lt;livewire:notification-destination
                :key="$i"
                :fields="$this-&amp;gt;fields()"
                :notificationDestination="$notificationDestination"
                :index="$i"
            /&amp;gt;

        @endforeach

        &amp;lt;div class="flex items-center justify-end mb-4"&amp;gt;
            @if ($this-&amp;gt;canAddMoreNotificationDestinations())
                &amp;lt;button wire:click.prevent="add" class="button is-secondary mr-4"&amp;gt;
                    Add another
                &amp;lt;/button&amp;gt;
            @endif
            &amp;lt;button type="submit" class="button"&amp;gt;
                Update
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This view is responsible for rendering the outer form, the "Add another", and "Update" button. The form fields themselves are rendered in the &lt;code&gt;livewire:notification-destination&lt;/code&gt; component. It gets passed the definitions of the fields and the notification destination values. Let's take a look at that component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Livewire&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Illuminate\View\View&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Livewire\Component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationDestination&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$notificationDestination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$notificationDestination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;notificationDestination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$notificationDestination&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$fields&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'livewire.notification-destination'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;emitUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'removeNotificationDestination'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;You can see, it's a straightforward component. The only thing to note here is that when &lt;code&gt;remove&lt;/code&gt; is called, it emits up an event where the &lt;code&gt;NotificationDestinations&lt;/code&gt; component. This is done because that component is in charge of the state. &lt;code&gt;NotificationDestinations&lt;/code&gt; will execute &lt;code&gt;removeNotificationDestination&lt;/code&gt;, which will update its &lt;code&gt;notificationDestinations&lt;/code&gt; array, which will re-render the entire component.&lt;/p&gt;

&lt;p&gt;Let's take a look at the &lt;code&gt;notification-destination&lt;/code&gt; view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="mt-8"&amp;gt;
    @foreach($fields as $field)
        &amp;lt;label class="form-group is-row"&amp;gt;

            &amp;lt;span class="label"&amp;gt;{{ $field['label'] }}&amp;lt;/span&amp;gt;
            &amp;lt;input placeholder="{{ $field['placeholder'] }}"
                   value="{{ $notificationDestination['destination'][$field['name']] ?? '' }}"
                   type="{{ $field['type'] ?? 'text' }}"
                   name="notification_destinations[{{ $index }}][destination][{{ $field['name'] }}]"
            &amp;gt;
        &amp;lt;/label&amp;gt;

        @error("notification_destinations.{$index}.destination.{$field['name']}")
        &amp;lt;div class="-mt-4 mb-4"&amp;gt;
            &amp;lt;strong class="form-error"&amp;gt;
                {{ $errors-&amp;gt;first("notification_destinations.{$index}.destination.{$field['name']}") }}
            &amp;lt;/strong&amp;gt;
        &amp;lt;/div&amp;gt;
        @enderror
    @endforeach

    &amp;lt;div class="notification-toggles is-removable"&amp;gt;
        &amp;lt;div class="notification-toggles-left"&amp;gt;
            &amp;lt;button wire:click.prevent="remove" class="text-xs text-gray-500"&amp;gt;
                Remove
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;

        @include('app.settings.team.notifications.partials.notifications', [
             'enabledNotificationTypes' =&amp;gt; $notificationDestination['accepts_notification_classes'],
             'index' =&amp;gt; $index,
            ])
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In the code above, you can see the specific fields being rendered based on their definition. &lt;/p&gt;

&lt;p&gt;And that is all there is to it!&lt;/p&gt;

&lt;h2&gt;
  
  
  In closing
&lt;/h2&gt;

&lt;p&gt;Working with Livewire was fun. It enabled us to write some complex form and behaviors without having to write a single line of JavaScript. It makes these forms more maintainable and also more testable.&lt;/p&gt;

&lt;p&gt;If you want to see these forms in action, &lt;a href="https://ohdear.app/register"&gt;start a free Oh Dear trial&lt;/a&gt; and head over to &lt;a href="https://ohdear.app/team-settings/notifications/mail"&gt;the team notifications screen&lt;/a&gt;. When you click "Add another", or "Remove", you might want to open your inspector so you can see the chunks of server-rendered HTML being passed to the client. Beautiful!&lt;/p&gt;

&lt;p&gt;For more on Livewire, be sure to read &lt;a href="https://laravel-livewire.com/docs"&gt;the Livewire docs&lt;/a&gt; and &lt;a href="https://twitter.com/calebporzio"&gt;follow Caleb Porzio on Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>php</category>
    </item>
    <item>
      <title>How to check which version of PHP you are running</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Mon, 16 Mar 2020 09:49:00 +0000</pubDate>
      <link>https://dev.to/freekmurze/how-to-check-which-version-of-php-you-are-running-12k4</link>
      <guid>https://dev.to/freekmurze/how-to-check-which-version-of-php-you-are-running-12k4</guid>
      <description>&lt;p&gt;When working on open source code, I like using the latest version of PHP. When developers that are not on the latest version use the package, they might see syntax errors.&lt;/p&gt;

&lt;p&gt;You might ask why Composer doesn't protect against this? When composer.json requires the latest version, how do devs, not on the latest version, can even install the package?&lt;/p&gt;

&lt;p&gt;Well, there seemingly are a lot of people that only upgrade the PHP version on the command line. For handling web requests, they are unknowingly using an older version of PHP. Here's how to make sure you are on the latest version of PHP on both the CLI and for handling web requests.&lt;/p&gt;

&lt;p&gt;On the CLI type this command to see your PHP version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;To check the PHP version that handles web requests, create a &lt;code&gt;.php&lt;/code&gt; file with this content somewhere the public directory of your app.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nb"&gt;phpinfo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Point your browser to that file. You will now see which version of PHP you are running. Don't forget to delete the file afterward.&lt;/p&gt;

</description>
      <category>php</category>
    </item>
    <item>
      <title>How to configure and use multiple SES accounts in a Laravel app</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Mon, 16 Mar 2020 09:48:22 +0000</pubDate>
      <link>https://dev.to/freekmurze/how-to-configure-and-use-multiple-ses-accounts-in-a-laravel-app-33ij</link>
      <guid>https://dev.to/freekmurze/how-to-configure-and-use-multiple-ses-accounts-in-a-laravel-app-33ij</guid>
      <description>&lt;p&gt;When working on &lt;a href="https://mailcoach.app"&gt;Mailcoach&lt;/a&gt;, it was unclear to me how I could use multiple SES (or Mailgun or Postmark) accounts inside a Laravel app. In this short blog post, I'd like to explain that to you.&lt;/p&gt;

&lt;p&gt;Laravel 7 introduced the ability to configure multiple mailers. Inside the &lt;code&gt;mail.php&lt;/code&gt; config file, there's now a &lt;code&gt;mailers&lt;/code&gt; key when an entry for each email provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in config/mail.php&lt;/span&gt;

&lt;span class="s1"&gt;'mailers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'smtp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'smtp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MAIL_HOST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'smtp.mailgun.org'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MAIL_PORT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'encryption'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MAIL_ENCRYPTION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tls'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'username'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MAIL_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MAIL_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'ses'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ses'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'mailgun'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'mailgun'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'postmark'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'postmark'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'sendmail'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sendmail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/usr/sbin/sendmail -bs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'log'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'log'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'channel'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MAIL_LOG_CHANNEL'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'array'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;For some of these mailers, the Laravel documentation &lt;a href="https://laravel.com/docs/7.x/mail#driver-prerequisites"&gt;specifies&lt;/a&gt; that you should add some extra configuration values in &lt;code&gt;config/services.php&lt;/code&gt;. Here's what you should add to that file if you want to use &lt;code&gt;ses&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in config/services.php&lt;/span&gt;

&lt;span class="s1"&gt;'ses'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'your-ses-key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'secret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'your-ses-secret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'region'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'a-ses-region'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here's how you could send a mail using one of these configured mailers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// send a mail using ses&lt;/span&gt;
&lt;span class="nb"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ses'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;YourAwesomeMailable&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// send a mail using smtp&lt;/span&gt;
&lt;span class="nb"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'smtp'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;YourAwesomeMailable&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This works fine when you want to send mail using different email providers. But how would you send mail using different accounts on the same service? Let's say, for instance, you want to use two SES mailers? How would you do that, there's only one entry in the &lt;code&gt;config/services.php&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---DpGNSoC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/mailconfig/source.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---DpGNSoC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/mailconfig/source.png" alt="source"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The answer lies in the source code! Let's take a look at the code where Laravel is &lt;a href="https://github.com/laravel/framework/blob/bd67cc22af6fbc1f5745b0e3ce79e0a12b092048/src/Illuminate/Mail/MailManager.php#L255-L269"&gt;reading the mail config to build the SES transport&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createSesTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'config'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.ses'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'latest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'service'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Arr&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;except&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'transport'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SesTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SesClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;addSesCredentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'options'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here you can see that if there's no &lt;code&gt;secret&lt;/code&gt; set on the config, Laravel will look in the &lt;code&gt;services&lt;/code&gt; config file if there are configuration values present in the &lt;code&gt;ses&lt;/code&gt; key. So, if you specify a &lt;code&gt;secret&lt;/code&gt; in the &lt;code&gt;ses&lt;/code&gt; mailer in the &lt;code&gt;mail&lt;/code&gt; config file, Laravel will use those values.&lt;/p&gt;

&lt;p&gt;So here's how you can define and use multiple SES accounts in one Laravel app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in config/mail.php&lt;/span&gt;

&lt;span class="s1"&gt;'mailers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'ses'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ses'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'your-ses-key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'secret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'your-ses-secret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'region'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'a-ses-region'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'latest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'service'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'mail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'another-ses'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ses'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'another-ses-key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'secret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'another-ses-secret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'region'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'another-ses-region'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'latest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'service'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'mail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Those &lt;code&gt;versions&lt;/code&gt; and &lt;code&gt;service&lt;/code&gt; keys are required too. In the source code above, you can see that Laravel only automatically adds those when you've put your configuration in &lt;code&gt;services.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With all this in place, you can start using the mailers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// send a mail using ses&lt;/span&gt;
&lt;span class="nb"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ses'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;YourAwesomeMailable&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// send a mail using another ses account&lt;/span&gt;
&lt;span class="nb"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'another-ses'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;YourAwesomeMailable&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Laravel reads the config for the other mailers like Mailgun and Postmark &lt;a href="https://github.com/laravel/framework/blob/bd67cc22af6fbc1f5745b0e3ce79e0a12b092048/src/Illuminate/Mail/MailManager.php#L302-L329"&gt;in a similar fashion&lt;/a&gt;. So, if you need to use multiple accounts of the same service, define the config in the &lt;code&gt;mail.php&lt;/code&gt; config file, and not in &lt;code&gt;services.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;(Thanks Taylor, for pointing me in the right direction)&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>mail</category>
    </item>
    <item>
      <title>Mailcoach v2 has been released with support for custom HTML editors and multiple mailers</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Mon, 16 Mar 2020 09:37:23 +0000</pubDate>
      <link>https://dev.to/freekmurze/mailcoach-v2-has-been-released-with-support-for-custom-html-editors-and-multiple-mailers-1mbn</link>
      <guid>https://dev.to/freekmurze/mailcoach-v2-has-been-released-with-support-for-custom-html-editors-and-multiple-mailers-1mbn</guid>
      <description>&lt;p&gt;A couple of months ago, &lt;a href="https://spatie.be"&gt;my team&lt;/a&gt; released &lt;a href="https://mailcoach.app"&gt;Mailcoach&lt;/a&gt;, a self-hosted solution to send out newsletters. It sends out mail via services like Amazon SES, Mailgun, Sendgrid, and Postmark. It can optionally track opens and clicks. When your email list grows, this is a much more cost-effective solution when compared to a service like Mailchimp.&lt;/p&gt;

&lt;p&gt;Mailcoach can be used as a premium Laravel package or as a stand-alone app. When installed into a Laravel app, it can be greatly customized. The Mailcoach stand alone app can be used without knowing how to program.&lt;/p&gt;

&lt;p&gt;Today we're releasing v2 of Mailcoach. It offers support for Laravel 7, &lt;a href="https://mailcoach.app/docs/v2/package/customizing-the-editor/introduction"&gt;html editors&lt;/a&gt;, and &lt;a href="https://mailcoach.app/docs/v2/package/general/installation-and-setup#configure-an-email-sending-service"&gt;multiple mailers&lt;/a&gt;, together with a bunch of quality of life improvements. In this blog post, I'd like to walk you through these features and show some technical details.&lt;/p&gt;

&lt;p&gt;If you already bought Mailcoach, you'll be happy to know that this is a free upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Mailcoach
&lt;/h2&gt;

&lt;p&gt;If you're not familiar with Mailcoach yet, you can watch this video to see the core features in action.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/380361695" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Support for custom HTML editors
&lt;/h2&gt;

&lt;p&gt;By default, Mailcoach uses a plain text area field to edit campaigns and tables. One of the most requested features after launching Mailcoach v1, was support for an HTML editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unlayer, an easy to use HTML editor
&lt;/h3&gt;

&lt;p&gt;In Mailcoach v2 we've added the ability to install editors via add on packages, so you can bring any editor you like. We've created two add-on packages ourselves. The first one brings the free version of &lt;a href="https://unlayer.com"&gt;Unlayer&lt;/a&gt;, a powerful WYSIWYG HTML editor to Mailcoach.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AnRWZTUk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/mailcoach2/unlayer.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AnRWZTUk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/mailcoach2/unlayer.png" alt="image of unlayer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When using this editor, you don't need any knowledge of HTML to compose your campaign. It also can handle image uploads. Any uploads will be handled by &lt;a href="https://docs.spatie.be/laravel-medialibrary/v8/introduction/"&gt;our laravel-medialibrary package&lt;/a&gt;, but users don't need to care about that, it all happens behind the scenes.&lt;/p&gt;

&lt;p&gt;Installing the Unlayer editor in Mailcoach is easy. You just have to require the add on package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require spatie/laravel-mailcoach-unlayer
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Next, you'll have to &lt;a href="https://mailcoach.app/docs/v2/package/customizing-the-editor/unlayer#publish-and-run-the-migration"&gt;run the migration&lt;/a&gt; provided by the package, &lt;a href="https://mailcoach.app/docs/v2/package/customizing-the-editor/unlayer#add-the-route-macro"&gt;add the route macro&lt;/a&gt;, and set the &lt;code&gt;editor&lt;/code&gt; key in the &lt;code&gt;mailcoach.app&lt;/code&gt; config file to &lt;code&gt;\Spatie\MailcoachUnlayer\UnlayerEditor::class&lt;/code&gt;. If you're familiar with adding packages to a Laravel app, this shouldn't take you long.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monaco, a powerful code editor
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://microsoft.github.io/monaco-editor/"&gt;Monaco&lt;/a&gt; is a powerful code editor created by Microsoft. It&lt;br&gt;
provides code highlighting, auto-completion, and much more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KOlAu0th--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/mailcoach2/monaco.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KOlAu0th--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/mailcoach2/monaco.png" alt="image of monaco editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Installing this package is dead simple too. Just require the package...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require spatie/laravel-mailcoach-monaco
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Install the assets...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt; mailcoach-monaco-assets &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Set the editor in the &lt;code&gt;mailcoach.app&lt;/code&gt; config file...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'editor'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;\Spatie\MailcoachMonaco\MonacoEditor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And you're good to go!&lt;/p&gt;

&lt;p&gt;Optionally, you can customize the looks of Monaco by specifying some options in the &lt;code&gt;monaco&lt;/code&gt; key of the &lt;code&gt;mailcoach.php&lt;/code&gt; config file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'monaco'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'theme'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'vs-light'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// vs-light or vs-dark&lt;/span&gt;
    &lt;span class="s1"&gt;'fontFamily'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Jetbrains Mono'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'fontLigatures'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'fontWeight'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'fontSize'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// No units&lt;/span&gt;
    &lt;span class="s1"&gt;'lineHeight'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'24'&lt;/span&gt; &lt;span class="c1"&gt;// No units&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;When using Mailcoach as a stand-alone app, you can configure these options on the editor screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PI-poO_w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/mailcoach2/monaco-config.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PI-poO_w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://freek.dev/uploads/media/mailcoach2/monaco-config.png" alt="image of monaco editor settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using multiple mailers
&lt;/h2&gt;

&lt;p&gt;Some email providers have very strict rules on sending mails. They require to keep a low bounce rate at all times. Confirmation emails have a higher chance of bouncing because they are sent to unverified email addresses.&lt;/p&gt;

&lt;p&gt;Mailcoach v1 was built on Laravel v6. In that version of the framework, you could only define one mailer. In Laravel v7 &lt;a href="https://laravel.com/docs/7.x/mail#configuration"&gt;support for multiple mailers&lt;/a&gt; was introduced. Mailcoach makes use of that feature to allow confirmation mails to be sent using a different mailer.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;mailcoach.php&lt;/code&gt; config file, there are a couple of values that govern which mailer should be used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;

    &lt;span class="cm"&gt;/*
     * The mailer used by Mailcoach for password resets and summary emails.
     * Mailcoach will use the default Laravel mailer if this is not set.
     */&lt;/span&gt;
    &lt;span class="s1"&gt;'mailer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="cm"&gt;/*
     * The default mailer used by Mailcoach for sending campaigns.
     */&lt;/span&gt;
    &lt;span class="s1"&gt;'campaign_mailer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="cm"&gt;/*
     * The default mailer used by Mailcoach for confirmation and welcome mails.
     */&lt;/span&gt;
    &lt;span class="s1"&gt;'transactional_mailer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;On the settings screen of an email list, you can also specify which mailer should be used for that particular list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Mailcoach
&lt;/h2&gt;

&lt;p&gt;While building Mailcoach, I've recorded &lt;a href="https://mailcoach.app/videos"&gt;a video course&lt;/a&gt; on how to use Mailcoach, and how it was built. You can use these techniques to improve your projects as well. I've made a couple of videos available for free. &lt;/p&gt;

&lt;p&gt;When creating a function, it's a good idea to keep the number of arguments as low as possible. In this video, you'll learn a technique on how to do this.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/381539561" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Most developers have probably have seen complex conditionals in legacy code. In this video, I show you how to refactor those.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/381650670" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Want to see more videos like this? Then head over to &lt;a href="https://mailcoach.app/videos"&gt;the video course page on Mailcoach&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Even if you don't require a self-hosted email solution, you could still benefit from purchasing it. Mailcoach is a labor of love, and our team polished it a lot. You can probably learn some nice things and pick up some tricks by reading the source code. The test suite of Mailcoach has been opensource, you can view it in &lt;a href="https://github.com/spatie/laravel-mailcoach-tests"&gt;this repo on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  In closing
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mailcoach.app"&gt;Mailcoach v2&lt;/a&gt; has a few smaller new features too, such as adding a delay for a welcome mail, some new customization options when sending mail, support for the latest version of &lt;a href="https://github.com/spatie/laravel-medialibrary"&gt;laravel-medialibrary&lt;/a&gt;, support for &lt;a href="https://postmarkapp.com"&gt;Postmark&lt;/a&gt; ...&lt;/p&gt;

&lt;p&gt;I'd like to thank &lt;a href="https://twitter.com/riasvdv?lang=en"&gt;my colleague Rias&lt;/a&gt;, who has done a lot getting v2 ready.&lt;/p&gt;

&lt;p&gt;If you want to know more about Mailcoach, check out &lt;a href="https://mailcoach.app/docs"&gt;the extensive documentation&lt;/a&gt;. If you like what you see, consider &lt;a href="https://mailcoach.app/register"&gt;getting your license&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here are some more blog posts on Mailcoach&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;a href="https://freek.dev/1558-building-mailcoach"&gt;The origin story or Mailoach&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt; &lt;a href="https://freek.dev/1523-building-mailcoach-deleting-unconfirmed-users-automatically"&gt;Take a look at how I added and tested a feature to delete unconfirmed users automatically&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; My colleague &lt;a href="https://sebastiandedeyne.com"&gt;Sebastian&lt;/a&gt; blogged about &lt;a href="https://sebastiandedeyne.com/mailcoachs-lack-of-javascript-stack/"&gt;how JavaScript is used in the Mailcoach UI&lt;/a&gt;. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://mailcoach.app"&gt;Mailcoach&lt;/a&gt; is probably the best package I've worked on. I use it myself to &lt;a href="https://freek.dev/newsletter"&gt;send my own newsletter&lt;/a&gt;. I hope you'll like it too!&lt;/p&gt;

</description>
      <category>mail</category>
      <category>dphp</category>
    </item>
    <item>
      <title>On using arrow functions in PHP 7.4</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Wed, 11 Mar 2020 07:45:44 +0000</pubDate>
      <link>https://dev.to/freekmurze/on-using-arrow-functions-in-php-7-4-291o</link>
      <guid>https://dev.to/freekmurze/on-using-arrow-functions-in-php-7-4-291o</guid>
      <description>&lt;p&gt;In PHP 7.4 a widely requested feature landed: &lt;a href="https://stitcher.io/blog/short-closures-in-php"&gt;arrow function&lt;/a&gt;. In this blogpost I'd like to show you how I like to use them.&lt;/p&gt;

&lt;p&gt;Let's start with an example from the &lt;a href="https://ohdear.app"&gt;Oh Dear&lt;/a&gt; codebase &lt;a href="https://twitter.com/freekmurze/status/1236065696125247488"&gt;I tweeted out&lt;/a&gt; a few days ago.&lt;/p&gt;

&lt;p&gt;In Laravel 6 and PHP 7.3 you could do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;creating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Team&lt;/span&gt; &lt;span class="nv"&gt;$team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$team&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;webhook_signing_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In Laravel 7 and PHP 7.4 you can rewrite it to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;booted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;creating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Team&lt;/span&gt; &lt;span class="nv"&gt;$team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$team&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;webhook_signing_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In the code snippet above, a short closure is used. It's a bit beyond the scope of this post, but in Laravel 7, the &lt;code&gt;booted&lt;/code&gt; lifecycle method was introduced, which will, unsuprisingly, be called we the model has booted. Using that method, you can lose the &lt;code&gt;static::boot();&lt;/code&gt; call from the initial code snippet. &lt;/p&gt;

&lt;p&gt;A lot of people seemed to like it. Some didn't, and that's ok. When introducing new syntax, I don't think there's a single thing where all programmars will agree on. A comment I agree on however, is &lt;a href="https://twitter.com/michaeldyrynda/status/1236104078318391296"&gt;the line lenght&lt;/a&gt;. Luckily this can be easily fixed by moving the closure itself to it's own line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;booted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;creating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Team&lt;/span&gt; &lt;span class="nv"&gt;$team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$team&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;webhook_signing_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="na"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;To make the line length shorter, you could opt to &lt;a href="https://twitter.com/adamwathan/status/1236248177013030912"&gt;remove the typehint&lt;/a&gt;. You could even &lt;a href="https://twitter.com/themsaid/status/1236258792255471618"&gt;rename the &lt;code&gt;$team&lt;/code&gt; variable&lt;/a&gt; to &lt;code&gt;$t&lt;/code&gt;, this is pretty common practice in the JavaScript. Personally I live typehints and full variable names, so personally I stick to the code snippet above.&lt;/p&gt;

&lt;p&gt;I think it would be silly to blindly switch to short closures wherever it is technically possible. The question you should keep in mind when converting a closure to a short one is: "Does this make the code more readable?". Of course, the answer is subjective. When in doubt, I recommend just asking your team members, they're the ones that will have to read your code too.&lt;/p&gt;

&lt;p&gt;If you use PhpStorm you can easily switch between a normal and a short closurse, so you can quickly get a feel of how it reads.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WwuK9OEs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://freek.dev/uploads/media/short-closures/convert.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WwuK9OEs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://freek.dev/uploads/media/short-closures/convert.gif" alt="convert"&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's end by taking a look at an example where I think short closures really shine: in collection chains (thanks for the example, Liam).&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;
      &lt;div class="ltag__twitter-tweet__media"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PnFwywtb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/media/ESizH-CXYAQ5l2i.jpg" alt="unknown tweet media content"&gt;
      &lt;/div&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--Hx90muTp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/1236048705297485837/oGeWeWid_normal.jpg" alt="Liam Hammett profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Liam Hammett
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        &lt;a class="comment-mentioned-user" href="https://dev.to/liamhammett"&gt;@liamhammett&lt;/a&gt;

      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P4t6ys1m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      &lt;a href="https://twitter.com/mattiasgeniar"&gt;@mattiasgeniar&lt;/a&gt; Take #2 
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      23:34 PM - 07 Mar 2020
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1236435058006929409" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-reply-action.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1236435058006929409" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-retweet-action.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      1
      &lt;a href="https://twitter.com/intent/like?tweet_id=1236435058006929409" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-like-action.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
      6
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


</description>
      <category>php</category>
    </item>
    <item>
      <title>Writing readable PHP: decrease indentation by returning early</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Wed, 11 Mar 2020 07:43:53 +0000</pubDate>
      <link>https://dev.to/freekmurze/writing-readable-php-decrease-indentation-by-returning-early-5h4c</link>
      <guid>https://dev.to/freekmurze/writing-readable-php-decrease-indentation-by-returning-early-5h4c</guid>
      <description>&lt;p&gt;When working older code, or code submitted through PRs, I sometimes see this pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someParameter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** can be any kind of test */&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someParameter&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// do the actual work&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;At the beginning of the function, there's a test being performed. The actual work is in the &lt;code&gt;if&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;This can be refactored by reversing the condition and using an early return.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someParameter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someParameter&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// do the actual work&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Doing this has a couple of benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A level of indention is lost. This alone makes it more pleasant to read. You don't need to keep in your head that the code is wrapped in something.&lt;/li&gt;
&lt;li&gt;Using an early return is great for humans too. When somebody reading your code is interested in the case concerning the early return, he or she doesn't need further that that early return. In the first example, there could still be some code to be executed after the if block.&lt;/li&gt;
&lt;li&gt;When you're respecting a line length limit (you should), you now have more characters available when performing the actual work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This technique also works when there are multiple conditions. Consider this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someParameter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$someOtherParameter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** can be any kind of test */&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someParameter&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someOtherParameter&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// do the actual work&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;By using early returns, you can rewrite it to something more readable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someParameter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$someOtherParameter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someParameter&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someOtherParameter&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// do the actual work&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;You may be tempted to rewrite combine that if statement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$someParameter&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$someOtherParameter&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In most cases, I don't do this for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Personally, I need more brainpower to parse this.&lt;/li&gt;
&lt;li&gt;It's easy to make mistakes (should I use &lt;code&gt;||&lt;/code&gt; or &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;) when there are more conditions&lt;/li&gt;
&lt;li&gt;Using early returns is easier to debug. You could put a breakpoint or dump statement inside those if blocks. That way, you know which condition causes an early return.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post summarised: whenever you see some code executed within an if block, check if you can reverse the condition to lose a level of indentation.&lt;/p&gt;

</description>
      <category>php</category>
    </item>
    <item>
      <title>Behind the scenes of Flare (or How to structure big Laravel applications)</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Wed, 04 Mar 2020 15:28:26 +0000</pubDate>
      <link>https://dev.to/freekmurze/behind-the-scenes-of-flare-or-how-to-structure-big-laravel-applications-1o42</link>
      <guid>https://dev.to/freekmurze/behind-the-scenes-of-flare-or-how-to-structure-big-laravel-applications-1o42</guid>
      <description>&lt;p&gt;At Laracon AU, I gave a talk on how you can structure a big Laravel application. The codebases of both &lt;a href="https://flareapp.io"&gt;Flare&lt;/a&gt; and &lt;a href="https://ohdear.app"&gt;Oh Dear&lt;/a&gt; served as examples.&lt;/p&gt;

&lt;p&gt;By watching this talk you'll gain some valuable insights that could be applicable to your projects as well. Enjoy!&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/A7JEfsUeey0"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here are some of the resources mentioned in the video:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stitcher.io/blog/laravel-beyond-crud-01-domain-oriented-laravel"&gt;Laravel beyond CRUD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freek.dev/1391-how-to-handle-front-end-authorization-using-laravel-inertia-and-typescript"&gt;How to handle front-end authorization using Laravel, Inertia and TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freek.dev/1371-refactoring-to-actions"&gt;Refactoring to actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freek.dev/1433-supercharging-common-controllers"&gt;Supercharging common controllers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://inertiajs.com"&gt;Inertia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sebastiandedeyne.com/how-much-javascript-do-we-really-need/"&gt;How much JavaScript do we really need&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>How to use a MySQL database on GitHub Actions</title>
      <dc:creator>Freek Van der Herten</dc:creator>
      <pubDate>Wed, 04 Mar 2020 15:27:20 +0000</pubDate>
      <link>https://dev.to/freekmurze/how-to-use-a-mysql-database-on-github-actions-1ehn</link>
      <guid>https://dev.to/freekmurze/how-to-use-a-mysql-database-on-github-actions-1ehn</guid>
      <description>&lt;p&gt;Recently we started using &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt; to test &lt;a href="https://spatie.be/open-source/packages"&gt;all our packages&lt;/a&gt;. You can read more about our general setup in &lt;a href="https://freek.dev/1546-using-github-actions-to-run-the-tests-of-laravel-projects-and-packages"&gt;this blog post&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;For most of the packages, this works great. However, some of our packages, such as Laravel Tags, use JSON functions that are not available in SQLite. Luckily it's straightforward to use a database like MySQL in GitHub Actions.&lt;/p&gt;

&lt;p&gt;In your test workflow, you need to add &lt;code&gt;MySQL&lt;/code&gt; to the &lt;code&gt;services&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:5.7&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_ALLOW_EMPTY_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;laravel_tags&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;3306&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In the step that executes the test, you should add an env variable &lt;code&gt;DB_PORT&lt;/code&gt;. Laravel uses that environment variable to set up the connection to the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Execute tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vendor/bin/phpunit&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ job.services.mysql.ports[3306] }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;phpunit.xml.dist&lt;/code&gt; you should add this section.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;php&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;env&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"DB_CONNECTION"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"mysql"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;env&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"DB_USERNAME"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;env&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"DB_DATABASE"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"laravel_tags"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;env&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"DB_HOST"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;env&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"DB_PORT"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"3306"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/php&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;DB_PORT&lt;/code&gt; there is used for local tests. On GitHub Actions, it will be overwritten by the port set in the workflow.&lt;/p&gt;

&lt;p&gt;And that is all there is to it. Take a look at &lt;a href="https://github.com/spatie/laravel-tags/blob/975347a4b03dd7ced4c64000a6210e67e8dd4e41/.github/workflows/run-tests.yml#L28-L36"&gt;the entire GitHub Actions workflow&lt;/a&gt; and &lt;a href="https://github.com/spatie/laravel-tags/blob/975347a4b03dd7ced4c64000a6210e67e8dd4e41/phpunit.xml.dist#L22-L28"&gt;phpunit config file&lt;/a&gt;, to get a little bit more context where you need to use the code snippets above.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>laravel</category>
      <category>github</category>
    </item>
  </channel>
</rss>
