<?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: Matteo Barbero</title>
    <description>The latest articles on DEV Community by Matteo Barbero (@maiobarbero).</description>
    <link>https://dev.to/maiobarbero</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F976354%2F830142ac-fbe9-4f2e-8df8-21c4817ce327.png</url>
      <title>DEV Community: Matteo Barbero</title>
      <link>https://dev.to/maiobarbero</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maiobarbero"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Wed, 16 Sep 2026 08:32:13 +0000</pubDate>
      <link>https://dev.to/maiobarbero/-1fb8</link>
      <guid>https://dev.to/maiobarbero/-1fb8</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/maiobarbero/finding-the-commit-that-introduced-a-laravel-bug-with-pest-and-git-bisect-54hi" class="crayons-story__hidden-navigation-link"&gt;Finding the Commit That Introduced a Laravel Bug with Pest and Git Bisect&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/maiobarbero" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F976354%2F830142ac-fbe9-4f2e-8df8-21c4817ce327.png" alt="maiobarbero profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/maiobarbero" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Matteo Barbero
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Matteo Barbero
                
                
              
              &lt;div id="story-author-preview-content-4536839" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/maiobarbero" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F976354%2F830142ac-fbe9-4f2e-8df8-21c4817ce327.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Matteo Barbero&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/maiobarbero/finding-the-commit-that-introduced-a-laravel-bug-with-pest-and-git-bisect-54hi" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 31&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/maiobarbero/finding-the-commit-that-introduced-a-laravel-bug-with-pest-and-git-bisect-54hi" id="article-link-4536839"&gt;
          Finding the Commit That Introduced a Laravel Bug with Pest and Git Bisect
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/laravel"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;laravel&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/git"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;git&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/testing"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;testing&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/php"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;php&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/maiobarbero/finding-the-commit-that-introduced-a-laravel-bug-with-pest-and-git-bisect-54hi#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              3&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Inside the Laravel 13 Scheduler: From Cron Entry to Event Execution</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Wed, 16 Sep 2026 06:25:53 +0000</pubDate>
      <link>https://dev.to/maiobarbero/inside-the-laravel-13-scheduler-from-cron-entry-to-event-execution-5ca0</link>
      <guid>https://dev.to/maiobarbero/inside-the-laravel-13-scheduler-from-cron-entry-to-event-execution-5ca0</guid>
      <description>&lt;p&gt;Laravel's scheduler replaces a crontab full of application commands with one entry that wakes the framework every minute. The individual tasks stay in the codebase, where they can be reviewed, tested, and deployed with the rest of the application.&lt;/p&gt;

&lt;p&gt;That is the useful promise described in the &lt;a href="https://laravel.com/docs/13.x/scheduling" rel="noopener noreferrer"&gt;Laravel 13 task scheduling documentation&lt;/a&gt;. It also hides quite a lot of work. By the time an Artisan command runs, Laravel has built an in-memory schedule, evaluated cron expressions and application state, checked filters and locks, started foreground or background processes, and dispatched events that other parts of the application can observe.&lt;/p&gt;

&lt;p&gt;This article follows that path through Laravel 13. It assumes you already know how to define a scheduled task and concentrates on what the framework does after you run &lt;code&gt;schedule:run&lt;/code&gt;. The source links point to Laravel's &lt;code&gt;13.x&lt;/code&gt; branch as it stood on September 12, 2026. If you are investigating a production application, compare them with the exact framework version installed in &lt;code&gt;composer.lock&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cron entry is only a wake-up call
&lt;/h2&gt;

&lt;p&gt;The server still needs an external process to start Laravel. On a traditional Linux server, the documented cron entry is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* * * * * cd /path-to-your-project &amp;amp;&amp;amp; php artisan schedule:run &amp;gt;&amp;gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cron knows nothing about &lt;code&gt;reports:generate&lt;/code&gt;, &lt;code&gt;emails:send&lt;/code&gt;, or any closure registered by the application. Its responsibility ends after starting &lt;code&gt;php artisan schedule:run&lt;/code&gt; in the correct project directory. Running &lt;code&gt;crontab -l&lt;/code&gt; therefore shows the launcher, not Laravel's task definitions.&lt;/p&gt;

&lt;p&gt;Those definitions usually live in &lt;code&gt;routes/console.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Jobs\RefreshSearchIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Schedule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reports:generate'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hourly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RefreshSearchIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;everyTenMinutes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/usr/local/bin/archive-reports'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dailyAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'02:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeleteExpiredInvitations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;daily&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laravel 13 also supports defining them with &lt;code&gt;withSchedule()&lt;/code&gt; in &lt;code&gt;bootstrap/app.php&lt;/code&gt;. Packages and application bootstrapping may register more events, so &lt;code&gt;routes/console.php&lt;/code&gt; is the conventional location, not the scheduler's storage mechanism.&lt;/p&gt;

&lt;p&gt;There is no schedule table for &lt;code&gt;schedule:run&lt;/code&gt; to query. &lt;code&gt;FoundationServiceProvider&lt;/code&gt; binds &lt;code&gt;Illuminate\Console\Scheduling\Schedule&lt;/code&gt; as a singleton for the current application process. When that singleton is resolved, the console kernel creates a &lt;code&gt;Schedule&lt;/code&gt;, applies its timezone and cache store, and the application's definitions add &lt;code&gt;Event&lt;/code&gt; or &lt;code&gt;CallbackEvent&lt;/code&gt; instances to its internal &lt;code&gt;$events&lt;/code&gt; array.&lt;/p&gt;

&lt;p&gt;The singleton distinction matters when reading commands such as &lt;code&gt;schedule:run&lt;/code&gt; and &lt;code&gt;schedule:list&lt;/code&gt;. Both receive a &lt;code&gt;Schedule&lt;/code&gt; from the service container, but two Artisan invocations are two separate PHP processes. Each boots the application and reconstructs its own in-memory schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a scheduled definition becomes
&lt;/h2&gt;

&lt;p&gt;The fluent API produces two main event shapes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Schedule::command()&lt;/code&gt; and &lt;code&gt;Schedule::exec()&lt;/code&gt; create an &lt;code&gt;Illuminate\Console\Scheduling\Event&lt;/code&gt;. An Artisan command is converted into a command line that can later be executed as a process. A system command is already in that form.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Schedule::call()&lt;/code&gt; creates a &lt;code&gt;CallbackEvent&lt;/code&gt;, a subclass of &lt;code&gt;Event&lt;/code&gt; that invokes a closure, callable, or invokable object through Laravel's service container. &lt;code&gt;Schedule::job()&lt;/code&gt; also creates a &lt;code&gt;CallbackEvent&lt;/code&gt;; its callback dispatches the supplied job through Laravel's bus. If the job implements &lt;code&gt;ShouldQueue&lt;/code&gt;, the scheduler's work is the dispatch, while a queue worker performs the job itself.&lt;/p&gt;

&lt;p&gt;Every event carries more than a cron expression. It can contain a timezone, environment constraints, &lt;code&gt;when()&lt;/code&gt; and &lt;code&gt;skip()&lt;/code&gt; callbacks, overlap and single-server settings, output configuration, before and after hooks, a description, and a sub-minute repeat interval. The scheduler evaluates these attributes in stages rather than with one boolean check.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;code&gt;schedule:run&lt;/code&gt; selects due events
&lt;/h2&gt;

&lt;p&gt;The command is implemented by &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php" rel="noopener noreferrer"&gt;&lt;code&gt;Illuminate\Console\Scheduling\ScheduleRunCommand&lt;/code&gt;&lt;/a&gt;. Its &lt;code&gt;handle()&lt;/code&gt; method receives four dependencies from the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;Schedule&lt;/span&gt; &lt;span class="nv"&gt;$schedule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;Dispatcher&lt;/span&gt; &lt;span class="nv"&gt;$dispatcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;Cache&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;ExceptionHandler&lt;/span&gt; &lt;span class="nv"&gt;$handler&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 schedule supplies events, the dispatcher emits lifecycle events, the cache stores pause and interrupt signals, and the exception handler reports task failures. The first selection is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dueEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;laravel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/Schedule.php" rel="noopener noreferrer"&gt;&lt;code&gt;Schedule::dueEvents()&lt;/code&gt;&lt;/a&gt; wraps the registered events in a collection and filters it through each event's &lt;code&gt;isDue()&lt;/code&gt; method. In Laravel 13, &lt;code&gt;isDue()&lt;/code&gt; checks three things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;isDue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runsInMaintenanceMode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isDownForMaintenance&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;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;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="nf"&gt;expressionPasses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="nf"&gt;runsInEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;environment&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;&lt;code&gt;expressionPasses()&lt;/code&gt; obtains the current time through Laravel's &lt;code&gt;Date&lt;/code&gt; facade, converts it to the event timezone when one is configured, and asks &lt;code&gt;dragonmantank/cron-expression&lt;/code&gt; whether the five-part expression is due. The environment check accepts the event when it has no environment restriction or when the current application environment appears in its configured list.&lt;/p&gt;

&lt;p&gt;This produces a collection of &lt;em&gt;temporally due&lt;/em&gt; events. It does not prove that they will execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Due does not mean runnable
&lt;/h2&gt;

&lt;p&gt;After &lt;code&gt;dueEvents()&lt;/code&gt;, the main command loop applies the remaining gates in a deliberate order.&lt;/p&gt;

&lt;p&gt;First, Laravel checks whether scheduled processing has been paused. Laravel 13 provides &lt;code&gt;schedule:pause&lt;/code&gt; and &lt;code&gt;schedule:continue&lt;/code&gt;, which store the state in the application cache. A task is skipped while paused unless it has been marked with &lt;code&gt;evenWhenPaused()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, &lt;code&gt;filtersPass()&lt;/code&gt; evaluates the callbacks registered through &lt;code&gt;when()&lt;/code&gt; and &lt;code&gt;skip()&lt;/code&gt;. All &lt;code&gt;when()&lt;/code&gt; callbacks must return a truthy value, while every &lt;code&gt;skip()&lt;/code&gt; callback must remain false. Laravel dispatches &lt;code&gt;ScheduledTaskSkipped&lt;/code&gt; when the pause check or these filters reject an event.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;onOneServer()&lt;/code&gt; is considered. &lt;code&gt;Schedule::serverShouldRun()&lt;/code&gt; asks the scheduling mutex to acquire a cache lock based on the event's mutex name and the scheduler's start minute. In a multi-server deployment, only the process that acquires this lock continues to the event. All scheduler nodes must use a shared, lock-capable cache for this coordination to work as intended.&lt;/p&gt;

&lt;p&gt;Only after those checks does &lt;code&gt;ScheduleRunCommand&lt;/code&gt; call &lt;code&gt;runEvent()&lt;/code&gt;. A compact view of the path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;registered event
    -&amp;gt; maintenance mode, cron expression, environment
    -&amp;gt; scheduler pause
    -&amp;gt; when() and skip() filters
    -&amp;gt; onOneServer() scheduling mutex
    -&amp;gt; withoutOverlapping() event mutex
    -&amp;gt; execute callback or command process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;withoutOverlapping()&lt;/code&gt; belongs one level lower. &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/Event.php" rel="noopener noreferrer"&gt;&lt;code&gt;Event::run()&lt;/code&gt;&lt;/a&gt; attempts to acquire the event mutex and returns early if another execution still owns it. This mutex prevents two executions of the same task from overlapping over time; the &lt;code&gt;onOneServer()&lt;/code&gt; scheduling mutex chooses one server for a particular due minute. They solve different concurrency problems and use different lock names and lifetimes.&lt;/p&gt;

&lt;p&gt;By default, the overlap mutex expires after 1,440 minutes. The value is a safety net for a process that terminates without releasing its lock, not a recommended task duration. Laravel normally releases the mutex when a foreground event finishes, and it can release it on supported termination signals. A stale lock can be cleared with &lt;code&gt;php artisan schedule:clear-cache&lt;/code&gt;, but doing so while the original task is still running removes the protection against overlap.&lt;/p&gt;

&lt;p&gt;Callback events require a name before &lt;code&gt;withoutOverlapping()&lt;/code&gt; or &lt;code&gt;onOneServer()&lt;/code&gt; can be used. Their default mutex identity would otherwise not distinguish anonymous callbacks reliably:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RefreshPartnerData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'refresh-partner-data'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withoutOverlapping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onOneServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;everyFiveMinutes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What &lt;code&gt;runEvent()&lt;/code&gt; actually coordinates
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;runEvent()&lt;/code&gt; prepares a readable command summary, then wraps execution in a console task. Before delegating to the event, it dispatches &lt;code&gt;ScheduledTaskStarting&lt;/code&gt; and records a start time. When &lt;code&gt;Event::run()&lt;/code&gt; returns, it dispatches &lt;code&gt;ScheduledTaskFinished&lt;/code&gt; with the elapsed time.&lt;/p&gt;

&lt;p&gt;For a normal &lt;code&gt;Event&lt;/code&gt;, the work continues inside &lt;code&gt;Event::run()&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Acquire the overlap mutex when required.&lt;/li&gt;
&lt;li&gt;Register signal-aware mutex cleanup.&lt;/li&gt;
&lt;li&gt;Invoke callbacks registered with &lt;code&gt;before()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Build and execute the command process.&lt;/li&gt;
&lt;li&gt;Store its exit code.&lt;/li&gt;
&lt;li&gt;Invoke the appropriate after, success, and failure callbacks.&lt;/li&gt;
&lt;li&gt;Release the overlap mutex in a &lt;code&gt;finally&lt;/code&gt; block.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The command is executed from the application's base path using Symfony Process. Foreground output is redirected to the configured destination, &lt;code&gt;/dev/null&lt;/code&gt; by default on Unix-like systems and &lt;code&gt;NUL&lt;/code&gt; on Windows. &lt;code&gt;sendOutputTo()&lt;/code&gt; and &lt;code&gt;appendOutputTo()&lt;/code&gt; replace that destination, while the email-output methods arrange to read the captured output after execution.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;CallbackEvent&lt;/code&gt; inherits the surrounding lifecycle but replaces process execution with a container call. Returning exactly &lt;code&gt;false&lt;/code&gt; from the callback becomes exit code &lt;code&gt;1&lt;/code&gt;; other returned values become exit code &lt;code&gt;0&lt;/code&gt;. An exception is captured, converted to a failed exit code for cleanup, and then rethrown so &lt;code&gt;ScheduleRunCommand&lt;/code&gt; can dispatch &lt;code&gt;ScheduledTaskFailed&lt;/code&gt; and report it through Laravel's exception handler.&lt;/p&gt;

&lt;p&gt;For foreground commands, a non-zero exit code is also turned into an exception after the finished event has been dispatched. &lt;code&gt;ScheduledTaskFinished&lt;/code&gt; therefore means that execution returned, not that it succeeded. Listeners may observe both &lt;code&gt;ScheduledTaskFinished&lt;/code&gt; and &lt;code&gt;ScheduledTaskFailed&lt;/code&gt; for a foreground command that exits unsuccessfully. The failure is caught and reported inside &lt;code&gt;runEvent()&lt;/code&gt;, allowing the scheduler loop to continue with later events.&lt;/p&gt;

&lt;p&gt;One smaller detail is easy to miss when building monitoring around scheduler events: the pause and filter checks explicitly dispatch &lt;code&gt;ScheduledTaskSkipped&lt;/code&gt;, while the overlap check happens inside &lt;code&gt;Event::run()&lt;/code&gt; after &lt;code&gt;ScheduledTaskStarting&lt;/code&gt;. The event records &lt;code&gt;skippedBecauseOverlapping&lt;/code&gt;, but the outer command has already entered its execution lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sequential and background execution
&lt;/h2&gt;

&lt;p&gt;Events due at the same time run sequentially in the order they were registered. If the first foreground command takes five minutes, later events in that &lt;code&gt;schedule:run&lt;/code&gt; process wait five minutes before they start. Meanwhile, cron may start another scheduler process at the next minute boundary. Laravel does not place one global mutex around the complete &lt;code&gt;schedule:run&lt;/code&gt; command, so concurrency should be controlled on the tasks that need it.&lt;/p&gt;

&lt;p&gt;For independent command or system-command events, &lt;code&gt;runInBackground()&lt;/code&gt; avoids blocking the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'analytics:aggregate'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hourly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runInBackground&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Background execution is available only for events created through &lt;code&gt;command()&lt;/code&gt; and &lt;code&gt;exec()&lt;/code&gt;, not closures or invokable callbacks. &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/CommandBuilder.php" rel="noopener noreferrer"&gt;&lt;code&gt;CommandBuilder&lt;/code&gt;&lt;/a&gt; constructs a platform-specific shell command that starts the task asynchronously. On completion, that shell command invokes Laravel's hidden &lt;code&gt;schedule:finish&lt;/code&gt; command with the event mutex name and actual exit code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/ScheduleFinishCommand.php" rel="noopener noreferrer"&gt;&lt;code&gt;ScheduleFinishCommand&lt;/code&gt;&lt;/a&gt; boots a fresh Laravel process, rebuilds the schedule, locates the matching event, calls its &lt;code&gt;finish()&lt;/code&gt; method, and dispatches &lt;code&gt;ScheduledBackgroundTaskFinished&lt;/code&gt;. The rebuild is another reason scheduled definitions should be deterministic and inexpensive to register.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one cron entry can run tasks every second
&lt;/h2&gt;

&lt;p&gt;Cron still starts Laravel once per minute. Sub-minute scheduling works because &lt;code&gt;schedule:run&lt;/code&gt; may remain alive for the rest of that minute.&lt;/p&gt;

&lt;p&gt;If the due collection contains an event configured with &lt;code&gt;everySecond()&lt;/code&gt;, &lt;code&gt;everyTenSeconds()&lt;/code&gt;, or another repeat interval below one minute, Laravel enters &lt;code&gt;repeatEvents()&lt;/code&gt;. The loop runs until the end of the minute captured when &lt;code&gt;ScheduleRunCommand&lt;/code&gt; was constructed. On each pass it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checks the interrupt signal;&lt;/li&gt;
&lt;li&gt;checks whether each event's repeat interval has elapsed;&lt;/li&gt;
&lt;li&gt;rechecks maintenance and pause state;&lt;/li&gt;
&lt;li&gt;reevaluates &lt;code&gt;when()&lt;/code&gt; and &lt;code&gt;skip()&lt;/code&gt; filters;&lt;/li&gt;
&lt;li&gt;repeats single-server coordination when configured;&lt;/li&gt;
&lt;li&gt;sleeps for 100 milliseconds before polling again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A slow foreground task inside this loop delays later repetitions. The Laravel documentation therefore recommends that sub-minute schedules dispatch queued jobs or start background commands, keeping the scheduler itself free to maintain timing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeleteExpiredSessions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;everyTenSeconds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'metrics:sample'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;everyTenSeconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runInBackground&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a deployment consequence. A scheduler process already inside the loop continues using the code loaded before the deployment. Running &lt;code&gt;php artisan schedule:interrupt&lt;/code&gt; after the new release is live sets a cache signal that asks the old loop to stop. The next cron invocation boots the new code.&lt;/p&gt;

&lt;p&gt;For local development, &lt;code&gt;php artisan schedule:work&lt;/code&gt; keeps a foreground worker alive and invokes the scheduler every minute. It is a development convenience, not a replacement for a production process manager or platform scheduler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspecting the schedule Laravel actually built
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;crontab -l&lt;/code&gt; can only confirm that the external launcher exists. To inspect the application-level events produced during the current boot, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan schedule:list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/ScheduleListCommand.php" rel="noopener noreferrer"&gt;&lt;code&gt;ScheduleListCommand&lt;/code&gt;&lt;/a&gt; receives its own process-local &lt;code&gt;Schedule&lt;/code&gt; instance and wraps either &lt;code&gt;events()&lt;/code&gt; or &lt;code&gt;eventsForEnvironments()&lt;/code&gt; in a collection. Laravel 13 exposes four useful options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--timezone=          Display times in this timezone
--environment=*     Display tasks configured for an environment
--next               Sort tasks by their next due date
--json               Return machine-readable JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;--next&lt;/code&gt;, events remain in definition order. With &lt;code&gt;--environment=production&lt;/code&gt;, Laravel filters by the event's configured environment list; it does not temporarily change &lt;code&gt;APP_ENV&lt;/code&gt; or evaluate whether the event is due now.&lt;/p&gt;

&lt;p&gt;The JSON representation contains these fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expression
command
description
next_due_date
next_due_date_human
timezone
has_mutex
repeat_seconds
environments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few useful limits to that output.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;next_due_date&lt;/code&gt; is the next time allowed by the cron expression and, for an active sub-minute window, the repeat interval. It does not predict maintenance mode, pause state, &lt;code&gt;when()&lt;/code&gt; and &lt;code&gt;skip()&lt;/code&gt; results, overlap locks, or which server will acquire an &lt;code&gt;onOneServer()&lt;/code&gt; mutex. It is the next temporal opportunity, not a guarantee that the task will execute.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;timezone&lt;/code&gt; field is the selected display timezone, which defaults to &lt;code&gt;config('app.timezone')&lt;/code&gt;. Laravel may convert the displayed cron expression to that timezone, and one event can produce multiple displayed expressions when a day boundary makes the conversion more complex.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;has_mutex&lt;/code&gt; performs a live &lt;code&gt;EventMutex::exists()&lt;/code&gt; check. It reports whether the event mutex exists at the moment the list is generated; it is not a boolean copy of the &lt;code&gt;withoutOverlapping&lt;/code&gt; configuration and does not describe the separate scheduling mutex used by &lt;code&gt;onOneServer()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, &lt;code&gt;schedule:list&lt;/code&gt; lists scheduled events, not every Artisan command registered with the application. Use &lt;code&gt;php artisan list&lt;/code&gt; for the latter. When you need to execute one registered scheduled event directly, Laravel also provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan schedule:test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;--name&lt;/code&gt;, it presents an interactive selection. &lt;code&gt;schedule:test --name=reports:generate&lt;/code&gt; can select a uniquely matching scheduled Artisan command. The test command forces a normally background event to run in the foreground, which is useful for observing its output, but it is still real execution against the current application environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical way to debug a task that did not run
&lt;/h2&gt;

&lt;p&gt;The internal order suggests a more precise checklist than starting with cron syntax alone.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;crontab -l&lt;/code&gt;, your platform dashboard, or process-manager configuration to confirm that something invokes &lt;code&gt;schedule:run&lt;/code&gt; every minute from the correct release directory.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;php artisan schedule:list --next -v&lt;/code&gt; in the same environment and release. Confirm that the event exists, its cron expression is correct, and its next due time uses the timezone you expect.&lt;/li&gt;
&lt;li&gt;Check maintenance mode, &lt;code&gt;APP_ENV&lt;/code&gt;, scheduler pause state, and any &lt;code&gt;when()&lt;/code&gt; or &lt;code&gt;skip()&lt;/code&gt; callbacks.&lt;/li&gt;
&lt;li&gt;Inspect the cache store used for &lt;code&gt;withoutOverlapping()&lt;/code&gt; and &lt;code&gt;onOneServer()&lt;/code&gt;. A local cache on each node cannot coordinate multiple servers.&lt;/li&gt;
&lt;li&gt;Check the task's configured output, Laravel logs, and listeners for scheduler lifecycle events.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;schedule:test&lt;/code&gt; only when running the task immediately is safe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The distinction to keep throughout the investigation is simple: registered, due, eligible, started, and completed are different states. Laravel's scheduler moves an event through them one check at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel 13 scheduler source map
&lt;/h2&gt;

&lt;p&gt;The relevant implementation is small enough to read directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/Schedule.php" rel="noopener noreferrer"&gt;&lt;code&gt;Schedule&lt;/code&gt;&lt;/a&gt; stores events and provides the fluent entry points.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/Event.php" rel="noopener noreferrer"&gt;&lt;code&gt;Event&lt;/code&gt;&lt;/a&gt; handles command execution, hooks, output, exit codes, and overlap mutexes.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/CallbackEvent.php" rel="noopener noreferrer"&gt;&lt;code&gt;CallbackEvent&lt;/code&gt;&lt;/a&gt; executes closures, callables, invokable objects, and scheduled job dispatches.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php" rel="noopener noreferrer"&gt;&lt;code&gt;ScheduleRunCommand&lt;/code&gt;&lt;/a&gt; selects and coordinates due events.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/ScheduleListCommand.php" rel="noopener noreferrer"&gt;&lt;code&gt;ScheduleListCommand&lt;/code&gt;&lt;/a&gt; renders the schedule reconstructed for the current process.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/CommandBuilder.php" rel="noopener noreferrer"&gt;&lt;code&gt;CommandBuilder&lt;/code&gt;&lt;/a&gt; creates foreground and background shell commands.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Console/Scheduling/ScheduleFinishCommand.php" rel="noopener noreferrer"&gt;&lt;code&gt;ScheduleFinishCommand&lt;/code&gt;&lt;/a&gt; completes background events in a new Artisan process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The public scheduling API remains compact because these classes divide the work carefully. The cron daemon wakes Laravel. The application reconstructs the schedule. &lt;code&gt;isDue()&lt;/code&gt; handles time, maintenance mode, and environment, then the runner applies operational constraints before the event deals with locking and execution. Once those boundaries are visible, both the source and the failures seen in production become much easier to reason about.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>Finding the Commit That Introduced a Laravel Bug with Pest and Git Bisect</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:52:00 +0000</pubDate>
      <link>https://dev.to/maiobarbero/finding-the-commit-that-introduced-a-laravel-bug-with-pest-and-git-bisect-54hi</link>
      <guid>https://dev.to/maiobarbero/finding-the-commit-that-introduced-a-laravel-bug-with-pest-and-git-bisect-54hi</guid>
      <description>&lt;p&gt;A shipping quote API should charge EUR 5.90 for a domestic parcel weighing up to and including one kilogram. Instead, it returns EUR 9.90 when the weight is exactly 1,000 grams.&lt;/p&gt;

&lt;p&gt;I prepared a small &lt;a href="https://github.com/maiobarbero/laravel-pest-bisect" rel="noopener noreferrer"&gt;Laravel playground repository&lt;/a&gt; with this bug and a deliberately long commit history. The application is simple enough to follow, while the history makes it useful for trying a debugging technique that also works on much larger projects: combining a focused Pest test with &lt;code&gt;git bisect&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the shipping price bug
&lt;/h2&gt;

&lt;p&gt;Clone and prepare the repository, then start Laravel's development server on port 8001:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/maiobarbero/laravel-pest-bisect.git
&lt;span class="nb"&gt;cd &lt;/span&gt;laravel-pest-bisect
composer setup
php artisan serve &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal, request a quote for a 1,000-gram domestic parcel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST http://localhost:8001/api/shipping/quote &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"weight_grams":1000,"zone":"domestic"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response identifies the parcel as &lt;code&gt;small&lt;/code&gt;, but returns &lt;code&gt;990&lt;/code&gt; in &lt;code&gt;price_cents&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"weight_grams"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"domestic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price_cents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;990&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"weight_band"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"small"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We know the expected price is 590 cents, and we know the current version is wrong. What we don't know yet is which commit changed the behaviour.&lt;/p&gt;

&lt;p&gt;One option is to check out each earlier commit, repeat the request, and continue until the response changes. This is manageable with a handful of commits. With hundreds or thousands of commits, it becomes slow enough that it is rarely a practical approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Git bisect narrows the search
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://git-scm.com/docs/git-bisect" rel="noopener noreferrer"&gt;&lt;code&gt;git bisect&lt;/code&gt;&lt;/a&gt; uses a binary search through the commit history. We give Git one commit where the behaviour is broken and one older commit where it still works. Git checks out a commit near the middle and asks whether that revision is good or bad, then discards the half of the range that cannot contain the regression.&lt;/p&gt;

&lt;p&gt;The number of candidates is roughly halved after every check. Searching 1,000 commits therefore takes about 10 checks in the ideal case, rather than testing every commit one by one.&lt;/p&gt;

&lt;p&gt;The check can be performed manually, but this example has a behaviour we can express as an automated test. That allows Git to run the entire search for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning the bug into a Pest test
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;tests/Feature/ShippingQuoteTest.php&lt;/code&gt; with a feature test for the one-kilogram boundary:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Tests\Feature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Foundation\Testing\TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TestCase&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applies the small parcel tariff up to one kilogram included'&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="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="nf"&gt;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/shipping/quote'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'weight_grams'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'zone'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'domestic'&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="nf"&gt;assertOk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertJsonPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'price_cents'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;590&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 test calls the real API route and records the expected behaviour in one place. Run it by filtering the suite with the test filename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ShippingQuote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fails on the current revision because the endpoint returns 990 instead of 590. This failure is useful beyond confirming the bug: a passing test exits with status code &lt;code&gt;0&lt;/code&gt;, while a failing test exits with a non-zero status. &lt;code&gt;git bisect run&lt;/code&gt; uses that status to classify every revision automatically as good or bad.&lt;/p&gt;

&lt;p&gt;Keep the new test uncommitted while running the bisect. It needs to remain in the working tree as Git checks out each revision in the search range. Before starting, check &lt;code&gt;git status&lt;/code&gt; and make sure there are no unrelated changes that could affect the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the automated bisect
&lt;/h2&gt;

&lt;p&gt;Start a bisect session and mark the current commit as bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git bisect start
git bisect bad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this playground, commit &lt;code&gt;06bc68c5&lt;/code&gt; is a known good revision. It contains the shipping quote endpoint before the regression was introduced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git bisect good 06bc68c5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git now has both ends of the range. Pass the focused Pest command to &lt;code&gt;git bisect run&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git bisect run php artisan &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ShippingQuote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git repeatedly checks out a candidate commit and runs the test. A pass moves the good boundary forward; a failure moves the bad boundary backward. It continues until only the first failing revision remains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9e8d96614e62c10f47efd915614836b8b42c74e9 is the first bad commit
commit 9e8d96614e62c10f47efd915614836b8b42c74e9
Author: Maio Barbero
Date:   Mon Aug 31 10:15:17 2026 +0200

    26 refactor shipping prices using a match expression

 app/Services/ShippingPriceCalculator.php | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)
bisect found first bad commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The search points to &lt;a href="https://github.com/maiobarbero/laravel-pest-bisect/commit/9e8d96614e62c10f47efd915614836b8b42c74e9" rel="noopener noreferrer"&gt;commit &lt;code&gt;9e8d966&lt;/code&gt;&lt;/a&gt;, a refactor of &lt;code&gt;ShippingPriceCalculator&lt;/code&gt;. Its diff contains the boundary mistake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$weightGrams&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;590&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$weightGrams&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;990&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1590&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 small-parcel condition changed from &lt;code&gt;&amp;lt;= 1000&lt;/code&gt; to &lt;code&gt;&amp;lt; 1000&lt;/code&gt;. A parcel weighing exactly 1,000 grams no longer matches the first arm and falls through to the medium price. The rest of the refactor is unrelated to the failure.&lt;/p&gt;

&lt;p&gt;Once the investigation is complete, leave bisect mode and return to the branch and commit that were checked out before the search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git bisect reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point the failing Pest test has done two jobs. It found the regression in the existing history, and it can remain in the suite as a boundary test to prevent the same comparison error from returning after the fix.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>git</category>
      <category>testing</category>
      <category>php</category>
    </item>
    <item>
      <title>AI Skills for Project Management</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Fri, 08 May 2026 05:29:00 +0000</pubDate>
      <link>https://dev.to/maiobarbero/ai-skills-for-project-management-4clg</link>
      <guid>https://dev.to/maiobarbero/ai-skills-for-project-management-4clg</guid>
      <description>&lt;p&gt;After completing the DelftX project management course in TU Delft's Engineering Project Management series, the idea that stayed with me was not a template, a checklist, or a new way to make a Gantt chart look more impressive.&lt;br&gt;
It was simpler than that: before you manage a project, you need to understand what kind of complexity you are actually dealing with.&lt;/p&gt;

&lt;p&gt;That sounds obvious until you look at how many projects are managed as if all complexity were the same thing.&lt;br&gt;
More tasks? Add more planning.&lt;br&gt;
More uncertainty? Add more meetings.&lt;br&gt;
More stakeholders? Add a RACI and hope it survives contact with reality.&lt;/p&gt;

&lt;p&gt;Sometimes that works. Often it does not.&lt;br&gt;
The problem is that "this project is complex" is not a diagnosis. It is only a warning light.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ocw.tudelft.nl/course-lectures/2-1-1-toe-framework/" rel="noopener noreferrer"&gt;TOE framework&lt;/a&gt; developed by TU Delft researchers gave me a better way to think about that warning light. TOE stands for &lt;strong&gt;Technical&lt;/strong&gt;, &lt;strong&gt;Organizational&lt;/strong&gt;, and &lt;strong&gt;External&lt;/strong&gt; complexity.&lt;/p&gt;

&lt;p&gt;The framework was developed to help project teams grasp complexity in large engineering projects and adapt the front-end development phase accordingly. The published paper behind it is &lt;a href="https://research.tudelft.nl/en/publications/grasping-project-complexity-in-large-engineering-projects-the-toe/" rel="noopener noreferrer"&gt;Grasping project complexity in large engineering projects: the TOE framework&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I kept thinking about how useful this would be inside an AI-assisted workflow.&lt;br&gt;
Not because AI should manage the project for you.&lt;br&gt;
Because AI is useful when it is forced to structure messy thinking, and project complexity is usually very messy thinking.&lt;/p&gt;

&lt;p&gt;So I built two new skills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/maiobarbero/my-ai-workflow/tree/main/skills/project-complexity-mapper" rel="noopener noreferrer"&gt;&lt;code&gt;project-complexity-mapper&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/maiobarbero/my-ai-workflow/tree/main/skills/project-complexity-action-planner" rel="noopener noreferrer"&gt;&lt;code&gt;project-complexity-action-planner&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They live in the same &lt;a href="https://github.com/maiobarbero/my-ai-workflow" rel="noopener noreferrer"&gt;my-ai-workflow&lt;/a&gt; repository as the rest of my AI workflow skills.&lt;/p&gt;
&lt;h2&gt;
  
  
  The core idea: not all complexity asks for the same response
&lt;/h2&gt;

&lt;p&gt;The mistake I see often, including in my own thinking, is treating complexity as a single number.&lt;br&gt;
We say a project is "simple", "complicated", or "very complex", then we jump straight to a management response.&lt;/p&gt;

&lt;p&gt;That loses the interesting part.&lt;/p&gt;

&lt;p&gt;A project can be technically complicated but organizationally stable.&lt;br&gt;
Another project can be technically simple but politically unpredictable.&lt;br&gt;
Another one can have both: many technical interfaces and stakeholders who are still negotiating what success even means.&lt;/p&gt;

&lt;p&gt;Those are not the same project.&lt;br&gt;
They should not be managed in the same way.&lt;/p&gt;

&lt;p&gt;The TOE framework helps by separating complexity into three families.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical complexity&lt;/strong&gt; is about the content of the project: goals, scope, technology, tasks, dependencies, quality requirements, disciplines involved, and technical risks.&lt;br&gt;
In software terms, this is where we find things like legacy integrations, unclear architecture boundaries, strict performance requirements, many work packages, unfamiliar infrastructure, or unknown delivery methods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Organizational complexity&lt;/strong&gt; is about the internal project organization: team size, resource availability, contracts, trust, roles, methods, tools, funding sources, schedules, and coordination across disciplines or locations.&lt;br&gt;
In software teams, this often shows up as unclear ownership, unavailable specialists, misaligned delivery methods between teams, pressure to hit a date that was chosen before discovery, or contract structures that reward the wrong behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;External complexity&lt;/strong&gt; is about the context around the project: external stakeholders, approvals, political influence, market instability, regulatory pressure, dependencies outside the team, strategic pressure, public trust, and other external risks.&lt;br&gt;
In a product or platform context, this may be customers with conflicting expectations, compliance deadlines, procurement constraints, partner APIs, legal uncertainty, or leadership changes that alter priorities halfway through the work.&lt;/p&gt;

&lt;p&gt;The useful part is not the labels themselves.&lt;br&gt;
The useful part is that they stop you from pretending a technical solution can fix an organizational problem, or that a weekly status report can resolve an unstable external context.&lt;/p&gt;
&lt;h2&gt;
  
  
  Detail complexity and dynamic complexity
&lt;/h2&gt;

&lt;p&gt;The other distinction I wanted the skills to preserve is the difference between &lt;strong&gt;detail complexity&lt;/strong&gt; and &lt;strong&gt;dynamic complexity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Detail complexity is about the number of parts and the structure between them.&lt;br&gt;
Many components, many interfaces, many work packages, many actors, many dependencies, many contracts.&lt;br&gt;
This kind of complexity often benefits from control: decomposition, interface registers, baselines, dependency maps, quality criteria, clear ownership, decision gates.&lt;/p&gt;

&lt;p&gt;Dynamic complexity is about change, uncertainty, and unpredictability.&lt;br&gt;
Stakeholders shift position.&lt;br&gt;
Scope moves.&lt;br&gt;
The market changes.&lt;br&gt;
The regulator clarifies something late.&lt;br&gt;
The team learns that the original solution path does not work.&lt;br&gt;
The facts are incomplete and the project cannot simply be decomposed into stable pieces.&lt;/p&gt;

&lt;p&gt;Dynamic complexity needs more than control.&lt;br&gt;
It needs interaction: shared problem framing, short feedback cycles, scenario exploration, visible assumptions, alignment sessions, escalation paths, and repeated reassessment.&lt;/p&gt;

&lt;p&gt;This creates four broad management postures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low detail and low dynamic complexity: keep it simple.&lt;/li&gt;
&lt;li&gt;High detail and low dynamic complexity: use systems management and control.&lt;/li&gt;
&lt;li&gt;Low detail and high dynamic complexity: use interactive management and connecting work.&lt;/li&gt;
&lt;li&gt;High detail and high dynamic complexity: use dynamic management, combining control with interaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last category is where many real projects live.&lt;br&gt;
The risk is overcorrecting in one direction.&lt;br&gt;
Too much control, and the project becomes rigid exactly when it needs to learn.&lt;br&gt;
Too much interaction, and everyone keeps talking while decisions never land.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this belongs in an AI workflow
&lt;/h2&gt;

&lt;p&gt;AI-generated project plans are often too tidy.&lt;br&gt;
They look coherent because they are formatted well, not because the underlying diagnosis is correct.&lt;/p&gt;

&lt;p&gt;That is the danger.&lt;br&gt;
A model can produce a confident plan from a weak description.&lt;br&gt;
It can turn uncertainty into bullet points so smoothly that you forget the uncertainty was still there.&lt;/p&gt;

&lt;p&gt;The TOE structure gives the model friction.&lt;br&gt;
It forces the conversation to separate the type of complexity from the action taken in response to it.&lt;br&gt;
It also makes confidence visible. A low-confidence hotspot is not ignored just because the model cannot fully prove it. If it could affect approval, trust, compliance, delivery feasibility, or project value, it deserves attention.&lt;/p&gt;

&lt;p&gt;This is the same principle behind my earlier &lt;a href="https://dev.to/articles/ai-assisted-workflow/"&gt;AI-assisted workflow&lt;/a&gt;: the AI can accelerate the production of artifacts, but the judgment remains yours.&lt;br&gt;
For project management, the artifact is not code, a PRD, or an issue list.&lt;br&gt;
It is a clearer picture of the project before the plan hardens.&lt;/p&gt;
&lt;h2&gt;
  
  
  Skill 1: &lt;code&gt;project-complexity-mapper&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The first skill is diagnostic.&lt;br&gt;
It takes a rough project description, messy notes, or a partially formed idea and turns it into a structured complexity assessment.&lt;/p&gt;

&lt;p&gt;By default it runs as a fast complexity scan.&lt;br&gt;
That means it does not walk through every TOE element one by one. It reads the description, infers likely hotspots, asks only the few questions that would materially change the diagnosis, and marks confidence where evidence is thin.&lt;/p&gt;

&lt;p&gt;If the project is high-risk, expensive, politically sensitive, or already showing signs of failure, it can switch to a complete TOE assessment.&lt;br&gt;
That mode uses the full 47-element TOE checklist and scores each element from 1 to 5: none, little, some, substantial, or very much.&lt;/p&gt;

&lt;p&gt;The mapper is deliberately not an action planner.&lt;br&gt;
It produces a diagnosis, not a backlog.&lt;/p&gt;

&lt;p&gt;The output includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A summary of the assessment mode and top complexity hotspots.&lt;/li&gt;
&lt;li&gt;Findings by Technical, Organizational, and External category.&lt;/li&gt;
&lt;li&gt;Scores for TOE contribution, detail complexity, dynamic complexity, severity, impact, and confidence.&lt;/li&gt;
&lt;li&gt;An overall quadrant for the project.&lt;/li&gt;
&lt;li&gt;Separate hotspot quadrants, because one average score hides too much.&lt;/li&gt;
&lt;li&gt;A Mermaid quadrant chart to visualize where the main hotspots sit.&lt;/li&gt;
&lt;li&gt;Management-fit guidance.&lt;/li&gt;
&lt;li&gt;A handoff table that another skill can consume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The handoff table matters.&lt;br&gt;
It makes the next step traceable.&lt;br&gt;
The action planner should not invent a generic project-management checklist. It should act on the diagnosed hotspots.&lt;/p&gt;
&lt;h2&gt;
  
  
  Skill 2: &lt;code&gt;project-complexity-action-planner&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The second skill starts where the mapper stops.&lt;br&gt;
It reads the diagnosis, especially the handoff table, management fit, hotspot quadrants, and findings by category. Then it converts those hotspots into a practical intervention plan.&lt;/p&gt;

&lt;p&gt;The important word is &lt;strong&gt;intervention&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is not a full project schedule.&lt;br&gt;
It is not a Gantt chart.&lt;br&gt;
It is not a ticket breakdown.&lt;/p&gt;

&lt;p&gt;An intervention is a management action chosen because a specific hotspot needs it.&lt;br&gt;
If a project has high technical detail complexity around integrations, an intervention might be an interface register, a prototype for the riskiest API, or a dependency map with clear owners.&lt;br&gt;
If the hotspot is external and dynamic, the intervention might be a stakeholder alignment session, a scenario review, or an approval map.&lt;br&gt;
If the hotspot is both detailed and dynamic, the intervention should combine control and connection, for example a requirements baseline with a controlled discovery window, or decision gates paired with assumption reviews.&lt;/p&gt;

&lt;p&gt;The action planner sequences work into three horizons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Now&lt;/strong&gt;: actions that clarify uncertainty, prevent blockage, or create needed control immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next&lt;/strong&gt;: actions that depend on first facts, decisions, or alignment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Later&lt;/strong&gt;: actions to reassess after a phase change, pilot, procurement decision, design freeze, or other major shift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also assigns owner roles, not names, unless names are provided.&lt;br&gt;
That keeps the output useful across teams: project manager, sponsor, product owner, technical lead, integration lead, contract manager, legal or compliance lead, stakeholder lead, steering committee.&lt;/p&gt;

&lt;p&gt;The final output includes an intervention backlog, sequencing logic, decision gates, reassessment triggers, risks in the plan, and a short handoff for execution.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I split it into two skills
&lt;/h2&gt;

&lt;p&gt;I could have made one skill that diagnoses complexity and creates a plan in one pass.&lt;br&gt;
That would have been faster.&lt;br&gt;
It would also have been worse.&lt;/p&gt;

&lt;p&gt;Diagnosis and action are different mental moves.&lt;br&gt;
When they happen at the same time, the model tends to rush toward advice.&lt;br&gt;
The format becomes a polished recommendation before the project has been understood.&lt;/p&gt;

&lt;p&gt;Splitting the workflow keeps the first skill honest.&lt;br&gt;
Its job is to say, "Here is where complexity appears to be coming from, here is how severe it looks, here is how confident we are, and here is the management posture that seems to fit."&lt;/p&gt;

&lt;p&gt;Only after that does the second skill ask, "Given that diagnosis, what should we do first?"&lt;/p&gt;

&lt;p&gt;That separation mirrors how I want to work with AI in general.&lt;br&gt;
Do not let the model skip the thinking artifact.&lt;br&gt;
Make the thinking artifact reviewable.&lt;br&gt;
Then use it as input to the next step.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I would use it
&lt;/h2&gt;

&lt;p&gt;I would start with a project description written in normal language.&lt;br&gt;
No template.&lt;br&gt;
No forced structure.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We need to migrate a legacy reporting system into a new platform.
The old system is used by three departments, each with different reporting definitions.
The source data comes from two internal APIs and one external vendor.
The deadline is connected to a contract renewal, but the scope is still moving.
The engineering team understands the new platform, but nobody fully owns the old reporting logic.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I would run the mapper and read the output critically.&lt;br&gt;
The important questions are not "Do I like this report?" but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did it classify the real hotspots?&lt;/li&gt;
&lt;li&gt;Did it confuse technical complexity with organizational complexity?&lt;/li&gt;
&lt;li&gt;Did it miss an external dependency?&lt;/li&gt;
&lt;li&gt;Are the low-confidence findings actually important?&lt;/li&gt;
&lt;li&gt;Are we averaging away a hotspot that needs separate attention?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the diagnosis looks useful, I would pass the handoff into the action planner in a new session with clear context window.&lt;br&gt;
Then I would review the intervention backlog with the same skepticism.&lt;/p&gt;

&lt;p&gt;Every action should trace back to a hotspot.&lt;br&gt;
If an action does not trace back to a hotspot, it is probably generic advice.&lt;br&gt;
If a high-severity hotspot has no action, the plan is probably incomplete.&lt;br&gt;
If a dynamic hotspot only gets control artifacts, the plan is probably too rigid.&lt;br&gt;
If a high-detail hotspot only gets workshops, the plan is probably too vague.&lt;/p&gt;

&lt;p&gt;The output should become a conversation artifact for the team, not a decree from the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is useful
&lt;/h2&gt;

&lt;p&gt;I see this being useful before a project execution plan, before a large PRD, before a replatforming effort, before a vendor integration, before a compliance-heavy feature, or when a project already feels stuck but the reason is not obvious.&lt;/p&gt;

&lt;p&gt;It is especially useful when people disagree about what the problem is.&lt;br&gt;
One person says the project is technically hard.&lt;br&gt;
Another says the scope keeps changing.&lt;br&gt;
Another says the real blocker is approvals.&lt;br&gt;
Another says the team does not have the right people.&lt;/p&gt;

&lt;p&gt;All of those can be true.&lt;br&gt;
The TOE framework gives you a way to hold those truths separately instead of collapsing them into one vague complaint.&lt;/p&gt;

&lt;p&gt;It also helps with AI-assisted development because it sits upstream of implementation.&lt;br&gt;
If the project is organizationally unstable, breaking work into better tickets will not fix it.&lt;br&gt;
If the external context is volatile, a perfect architecture diagram will not make the decision process predictable.&lt;br&gt;
If the technical interfaces are unclear, more stakeholder alignment will not define the API contract by itself.&lt;/p&gt;

&lt;p&gt;Different complexity asks for different management behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would not use it
&lt;/h2&gt;

&lt;p&gt;I would not use this for every task.&lt;/p&gt;

&lt;p&gt;If the work is small, local, and well understood, this is overhead.&lt;br&gt;
If one person can explain the scope, implement it, test it, and ship it without meaningful dependencies, a TOE scan is probably unnecessary.&lt;/p&gt;

&lt;p&gt;I also would not use it to justify a decision that has already been made.&lt;br&gt;
That is one of the easiest ways to misuse AI: ask for a framework, feed it selective context, and receive a professional-looking confirmation.&lt;/p&gt;

&lt;p&gt;And I would be careful using it without the people who actually understand the project.&lt;br&gt;
Complexity is subjective and dynamic.&lt;br&gt;
Different actors see different risks because they sit in different parts of the system.&lt;br&gt;
The skill can structure the conversation, but it cannot replace the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The human part
&lt;/h2&gt;

&lt;p&gt;The most valuable output of these skills is not the table.&lt;br&gt;
It is the disagreement the table makes visible.&lt;/p&gt;

&lt;p&gt;If a stakeholder says a hotspot is scored too high, good.&lt;br&gt;
Ask why.&lt;br&gt;
If the technical lead says the integration risk is understated, good.&lt;br&gt;
Update the diagnosis.&lt;br&gt;
If the sponsor thinks the external pressure is a value opportunity rather than only a risk, good.&lt;br&gt;
That belongs in the management fit.&lt;/p&gt;

&lt;p&gt;The goal is not to make project complexity disappear.&lt;br&gt;
Some complexity cannot be removed.&lt;br&gt;
Some complexity should not be removed, because it is tied to project value.&lt;/p&gt;

&lt;p&gt;The goal is to stop managing every project with the same reflex.&lt;/p&gt;

&lt;p&gt;Control where the project needs control.&lt;br&gt;
Connect where the project needs interaction.&lt;br&gt;
Reassess when the context changes.&lt;br&gt;
And keep the judgment with the humans who are accountable for the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skills
&lt;/h2&gt;

&lt;p&gt;The two skills are available in my &lt;a href="https://github.com/maiobarbero/my-ai-workflow" rel="noopener noreferrer"&gt;AI workflow repository&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/maiobarbero/my-ai-workflow/tree/main/skills/project-complexity-mapper" rel="noopener noreferrer"&gt;&lt;code&gt;project-complexity-mapper&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/maiobarbero/my-ai-workflow/tree/main/skills/project-complexity-action-planner" rel="noopener noreferrer"&gt;&lt;code&gt;project-complexity-action-planner&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are not meant to sell a methodology.&lt;br&gt;
They are my attempt to encode a useful way of thinking so I can reuse it, challenge it, and improve it over time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>management</category>
      <category>productivity</category>
      <category>claude</category>
    </item>
    <item>
      <title>My AI-Assisted workflow</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Wed, 15 Apr 2026 07:17:46 +0000</pubDate>
      <link>https://dev.to/maiobarbero/my-ai-assisted-workflow-20ke</link>
      <guid>https://dev.to/maiobarbero/my-ai-assisted-workflow-20ke</guid>
      <description>&lt;p&gt;You open a chat, describe what you want, iterate on the output, and ship something that more or less works. It feels fast.&lt;br&gt;
The features work, technically. But nobody, &lt;strong&gt;including me&lt;/strong&gt;, fully understood what is there. Edge cases nobody thought to handle, architecture that made sense in the moment but didn't survive contact with the next feature. A growing sense that I was building faster and understanding less.&lt;/p&gt;

&lt;p&gt;The problem I kept running into was this: how do you get the speed benefits of AI assistance without losing the clarity and intentionality that makes software maintainable?&lt;br&gt;
The short answer is that the real work happens before the coding starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: thinking in writing, not in code
&lt;/h2&gt;

&lt;p&gt;What is AI actually good at? Implementation. What is it genuinely bad at? Figuring out what you actually want, catching the assumptions you forgot to make explicit, and telling you when your mental model of the problem is wrong.&lt;br&gt;
&lt;strong&gt;That's your job. It will always be your job.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The single most valuable shift I made was treating every feature as a thinking problem first and an implementation problem second. The workflow is designed to force that thinking to happen before any code is written, and to use AI to stress-test it, not to skip it.&lt;/p&gt;

&lt;p&gt;This workflow has been adapted to work with me from Mark Pocock's &lt;a href="https://github.com/mattpocock/skills/tree/main" rel="noopener noreferrer"&gt;skills&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Free-form plan
&lt;/h3&gt;

&lt;p&gt;Everything starts with a document I write myself, in plain language, with no required structure. I describe the problem, my initial thinking about the solution, the constraints I'm aware of, and the things I'm uncertain about. This is not a deliverable, nobody reads it but me. Its only purpose is to get the thinking out of my head and into a form I can examine.&lt;/p&gt;

&lt;p&gt;The quality of everything downstream depends entirely on the quality of this step. A vague plan produces a vague &lt;strong&gt;&lt;a href="https://www.maiobarbero.dev/glossary/#prd" rel="noopener noreferrer"&gt;PRD&lt;/a&gt;&lt;/strong&gt;, which produces vague issues, which produces code that technically runs but doesn't do what you meant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: PRD via &lt;code&gt;write-a-prd&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The free-form plan becomes the input to a structured interview process. The skill explores the codebase to understand the current state of things, then interviews me relentlessly about every aspect of the plan, walking down each branch of the design tree, resolving dependencies between decisions one by one.&lt;br&gt;
This is the step where bad ideas get caught. Not because the AI is smarter than me, but because being forced to answer specific questions about your own plan reveals the places where you were hand-waving. "How does this behave when the user isn't authenticated?" "What happens if this operation partially fails?" "You said this replaces the existing feature, what happens to users who depend on the current behavior?"&lt;/p&gt;

&lt;p&gt;The output is a structured &lt;strong&gt;PRD&lt;/strong&gt; file containing a problem statement, a solution description, an extensive list of user stories, implementation decisions (modules, interfaces, schema changes, API contracts), module design, testing decisions, and explicit out-of-scope items. Everything is &lt;strong&gt;explicit&lt;/strong&gt;.&lt;br&gt;
The user stories are the backbone of everything that follows. They need to be specific enough that acceptance criteria can be derived from them unambiguously downstream, not at this stage, but when scope is defined at the issue level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Issues via &lt;code&gt;prd-to-issues&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;PRD&lt;/strong&gt; becomes a set of issues using &lt;strong&gt;&lt;a href="https://www.maiobarbero.dev//glossary/#vertical-slice" rel="noopener noreferrer"&gt;vertical slices&lt;/a&gt;&lt;/strong&gt;, tracer bullets that cut through every integration layer end-to-end rather than horizontal slices of a single layer. A slice that only touches the database, or only touches the UI, is not a valid slice. Each issue should deliver a narrow but complete path that is demoable or verifiable on its own.&lt;br&gt;
Each issue is classified as either &lt;strong&gt;AFK&lt;/strong&gt; (the AI can implement and the change can be merged without human interaction) or &lt;strong&gt;&lt;a href="https://www.maiobarbero.dev//glossary/#hitl" rel="noopener noreferrer"&gt;HITL&lt;/a&gt;&lt;/strong&gt; (a human decision is required at some point during implementation). Preferring AFK over HITL wherever possible keeps the work moving without becoming a bottleneck on my attention.&lt;/p&gt;

&lt;p&gt;Before anything is written, the skill presents the proposed breakdown and asks: does the granularity feel right, are the dependency relationships correct, should anything be merged or split? Issues are written in dependency order so that cross-references between them use real numbers.&lt;/p&gt;

&lt;p&gt;Each issue contains a concise description of the end-to-end behaviour, a "how to verify" section describing exactly how to confirm the slice is complete, acceptance criteria in Given/When/Then format including error cases, a list of blockers, and references back to the user stories it addresses.&lt;br&gt;
Everything lives in files. I work across different platforms, sometimes GitHub, sometimes GitLab, and keeping the workflow file-based means it doesn't depend on any particular tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Tasks via &lt;code&gt;issues-to-tasks&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Each issue is broken down into concrete, ordered tasks, one task per focused AI session. The constraint is deliberate: if a task can't be completed in a single session, it's too large.&lt;br&gt;
The skill explores the specific parts of the codebase touched by the issue, identifies existing patterns to follow, and produces a task list with types (WRITE, TEST, MIGRATE, CONFIG, REVIEW), explicit outputs, and dependency order. Schema before logic, logic before API, API before UI, tests interleaved rather than batched at the end.&lt;/p&gt;

&lt;p&gt;The key design decision in the task descriptions: they are written as instructions to the AI that will execute them, not as notes to a human developer. Each task specifies which files to touch, which existing patterns to follow, and what the output looks like when done. No code snippets, just intent, not implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: The handoff to code
&lt;/h3&gt;

&lt;p&gt;Each task description is a self-contained prompt. When I'm ready to implement a task, I open a fresh session and paste the task description along with the parent issue for context. The task description was written for this purpose, it specifies scope, references the right files and patterns, and defines what done looks like.&lt;br&gt;
Fresh context per task is intentional. Long sessions with accumulated context tend to drift: the model starts making decisions based on what it already did rather than what the task requires. Starting clean with a well-scoped task consistently produces better output than continuing a long session.&lt;br&gt;
For REVIEW tasks, the ones flagged as requiring a human decision, I stop, make the decision, update the task file with the outcome, and continue. These are the moments where the workflow earns its keep: the decision is made deliberately, in context, not buried in a long generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Code review via &lt;code&gt;code-review&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Every PR goes through a structured six-pass review before merge. The passes cover logic errors, operation ordering, bad practices, security, magic strings and values, and pattern improvements.&lt;/p&gt;

&lt;p&gt;Operation ordering deserves particular attention in AI-generated code. Models tend to produce code that does the right things but sometimes in the wrong sequence: sending a notification before committing a transaction, writing an audit log after the action it should record, mutating state before validating input. These bugs are easy to miss in review because the code looks correct at a glance.&lt;/p&gt;

&lt;p&gt;The review runs on a file or diff, not on the whole feature. Scope is deliberately narrow, catching issues at the PR level is far cheaper than finding them later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Final audit via &lt;code&gt;final-audit&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;At the end of a feature, a cross-cutting audit looks at things that can only be evaluated across the whole implementation. Not individual bugs, those should have been caught per PR, but systemic issues: inconsistencies between modules, patterns that were introduced early and replicated incorrectly everywhere, security assumptions that hold in isolation but break down across the full surface area.&lt;/p&gt;

&lt;p&gt;The audit reads the full implementation before flagging anything, which is the point. It groups findings by severity, gives an explicit overall verdict on whether the feature is safe to leave in production, and asks for approval before making any changes. Unsupervised fixes on already-merged code are riskier than fixes made during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this workflow is not
&lt;/h2&gt;

&lt;p&gt;It is not fast to set up. The planning and &lt;strong&gt;PRD&lt;/strong&gt; steps take real time, and the temptation to skip them in favour of going straight to code is constant. The workflow only pays off if you genuinely believe that thinking time before coding is cheaper than debugging time after.&lt;/p&gt;

&lt;p&gt;It is also not a replacement for engineering judgement. The AI will suggest reasonable things at every step that are wrong for your specific situation. The review steps, where you evaluate the breakdown before anything is created, exist precisely because the AI's output needs to be validated against knowledge it doesn't have: your team's conventions, your users' actual behavior, the parts of the codebase that have hidden complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The underlying principle
&lt;/h2&gt;

&lt;p&gt;Every step in this workflow has the same structure: AI produces something, you review it with full context, then it gets created. The AI accelerates the production. &lt;strong&gt;The review is yours, always&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The workflow is designed to make that review as effective as possible, by ensuring that when you're evaluating an issue, you have a &lt;strong&gt;PRD&lt;/strong&gt; to check it against; when you're evaluating a task, you have an issue to check it against; and when you're reviewing code, you have acceptance criteria to check it against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skills
&lt;/h2&gt;

&lt;p&gt;Now if you read this article to the end you at least deserve a link with my skills.&lt;br&gt;
Check my &lt;a href="https://github.com/maiobarbero/my-ai-workflow" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>programming</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>How to test Filament resources | Laravel Personal Finance Dashboard</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Thu, 19 Feb 2026 09:41:59 +0000</pubDate>
      <link>https://dev.to/maiobarbero/how-to-test-filament-resources-laravel-personal-finance-dashboard-eei</link>
      <guid>https://dev.to/maiobarbero/how-to-test-filament-resources-laravel-personal-finance-dashboard-eei</guid>
      <description>&lt;p&gt;Now that we have our resources set up, it's time to ensure they work as expected—and stay that way. In this lesson, we're going to write feature tests for our &lt;code&gt;BankAccount&lt;/code&gt;, &lt;code&gt;Budget&lt;/code&gt;, and &lt;code&gt;Category&lt;/code&gt; resources. We'll focus particularly on ensuring that users can only see &lt;em&gt;their own&lt;/em&gt; data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Pest
&lt;/h2&gt;

&lt;p&gt;First, we need to add &lt;strong&gt;Pest&lt;/strong&gt; to our project. Pest is a testing framework with a focus on simplicity and elegance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require pestphp/pest &lt;span class="nt"&gt;--dev&lt;/span&gt; &lt;span class="nt"&gt;--with-all-dependencies&lt;/span&gt;
php artisan pest:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to make sure our &lt;code&gt;User&lt;/code&gt; model is ready for Filament testing. To properly simulate a Filament user in our tests, our &lt;code&gt;User&lt;/code&gt; model should implement the &lt;code&gt;FilamentUser&lt;/code&gt; contract. This ensures that when we use &lt;code&gt;actingAs&lt;/code&gt;, the user is correctly recognized as having access to the panel.&lt;/p&gt;

&lt;p&gt;Update &lt;code&gt;app/Models/User.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Filament\Models\Contracts\FilamentUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Authenticatable&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;FilamentUser&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;canAccessPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Filament\Panel&lt;/span&gt; &lt;span class="nv"&gt;$panel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can view the setup and model changes in &lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/b7df8af" rel="noopener noreferrer"&gt;this commit&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Bank Account Resource
&lt;/h2&gt;

&lt;p&gt;Let's start with the &lt;code&gt;BankAccountResource&lt;/code&gt;. We want to verify that we can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render the page.&lt;/li&gt;
&lt;li&gt;List bank accounts belonging to the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NOT&lt;/strong&gt; list bank accounts belonging to other users.&lt;/li&gt;
&lt;li&gt;Create, edit, and delete accounts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create a new test file: &lt;code&gt;tests/Feature/Filament/Resources/BankAccountResourceTest.php&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rendering and Listing
&lt;/h3&gt;

&lt;p&gt;We use &lt;code&gt;actingAs($user)&lt;/code&gt; to sign in as a user, and then access the page URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Filament\Resources\BankAccounts\Pages\ManageBankAccounts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\BankAccount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Livewire\Livewire&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;Pest\Laravel\actingAs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'can render page'&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ManageBankAccounts&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getUrl&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSuccessful&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'can list bank accounts'&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$bankAccount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BankAccount&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Livewire&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ManageBankAccounts&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertCanSeeTableRecords&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$bankAccount&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;
  
  
  Testing Scope (The "Other User" Test)
&lt;/h3&gt;

&lt;p&gt;This is a critical test. We create a user and &lt;em&gt;another&lt;/em&gt; user. We expect the current user effectively &lt;em&gt;not&lt;/em&gt; to see the other user's record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cannot see other users bank accounts'&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$otherUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$otherBankAccount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BankAccount&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$otherUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Livewire&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ManageBankAccounts&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertCanNotSeeTableRecords&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$otherBankAccount&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;
  
  
  Actions: Create, Edit, Delete
&lt;/h3&gt;

&lt;p&gt;Filament provides excellent helpers to test actions directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'can create bank account'&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Livewire&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ManageBankAccounts&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;mountAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'create'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setActionData&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;'My Main Bank'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'balance'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1000.50'&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="nf"&gt;callMountedAction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertHasNoActionErrors&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;assertDatabaseHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bank_accounts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&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;'My Main Bank'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'balance'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100050&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Value as integer thanks to our Caster&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;Notice how we assert the database has the value &lt;code&gt;100050&lt;/code&gt; because of our logic handling money as integers!&lt;/p&gt;

&lt;p&gt;You can see the full test suite for the Bank Account resource in &lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/26ca59e" rel="noopener noreferrer"&gt;this commit&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Budget and Category Resources
&lt;/h2&gt;

&lt;p&gt;The tests for &lt;code&gt;BudgetResource&lt;/code&gt; and &lt;code&gt;CategoryResource&lt;/code&gt; follow the exact same pattern. We need to ensure that detailed actions like creating and editing work, but most importantly, we must verify the scoping rules.&lt;/p&gt;

&lt;p&gt;For example, the Category tests ensure that a user can only edit their own categories.&lt;/p&gt;

&lt;p&gt;You can check the implementation for the &lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/9f3d821" rel="noopener noreferrer"&gt;Budget resource tests here&lt;/a&gt; and the &lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/fb1ac7a" rel="noopener noreferrer"&gt;Category resource tests here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Tests
&lt;/h2&gt;

&lt;p&gt;Finally, let's run our test suite to see everything green!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see an output confirming that all resource tests are passing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS Tests\Feature\Filament\Resources\BankAccountResourceTest
✓ it can render page
✓ it can list bank accounts
✓ it cannot see other users bank accounts
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎓 Build this App &amp;amp; Get Certified (Free)
&lt;/h2&gt;

&lt;p&gt;If you found this guide helpful, you can build the entire application from scratch with my free course. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🏗️ Advanced Laravel Architecture&lt;/li&gt;
&lt;li&gt;📊 Building Dashboards with FilamentPHP&lt;/li&gt;
&lt;li&gt;💰 Handling Money professionally in code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.maiobarbero.dev/courses/personal-finance-with-laravel/" rel="noopener noreferrer"&gt;&lt;strong&gt;→ Join other students on maiobarbero.dev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to build a Voice-to-Text App with Laravel AI SDK</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Wed, 11 Feb 2026 08:39:27 +0000</pubDate>
      <link>https://dev.to/maiobarbero/how-to-build-a-voice-to-text-app-with-laravel-ai-sdk-1ida</link>
      <guid>https://dev.to/maiobarbero/how-to-build-a-voice-to-text-app-with-laravel-ai-sdk-1ida</guid>
      <description>&lt;h1&gt;
  
  
  Stop Writing Boilerplate: Build a Voice-to-Text App with Laravel AI SDK
&lt;/h1&gt;

&lt;p&gt;If you are a PHP developer, you have likely looked at AI integration with a mix of excitement and fatigue. Excitement for the possibilities, but fatigue from WRITING THE SAME BOILERPLATE code to handle multipart requests, API keys, and error handling for OpenAI or Anthropic.&lt;/p&gt;

&lt;p&gt;The new &lt;strong&gt;Laravel AI SDK&lt;/strong&gt; changes this. It turns complex AI interactions into fluid, Laravel-style syntax.&lt;/p&gt;

&lt;p&gt;In this guide, we won't just talk about it—we will build a functional &lt;strong&gt;Voice-to-Text&lt;/strong&gt; application in under 10 minutes. I call mine "Dettami", but you can call yours whatever you want.&lt;/p&gt;

&lt;p&gt;Let's write some code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Laravel 10+ (or the latest version)&lt;/li&gt;
&lt;li&gt;PHP 8.2+&lt;/li&gt;
&lt;li&gt;An OpenAI API Key&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Install the SDK
&lt;/h2&gt;

&lt;p&gt;First, forget about &lt;code&gt;guzzlehttp/guzzle&lt;/code&gt; manual calls. Pull in the first-party package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/ai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, publish the configuration file. This is where you will define your "drivers" (OpenAI, Gemini, Mistral, etc.).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan &lt;span class="nb"&gt;install&lt;/span&gt;:ai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your key to your &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPENAI_API_KEY=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Define the Route
&lt;/h2&gt;

&lt;p&gt;We need a single endpoint to accept the audio file. In your &lt;code&gt;routes/web.php&lt;/code&gt; or &lt;code&gt;routes/api.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\TranscribeAudioController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/transcribe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TranscribeAudioController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'transcribe'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: The Logic (Where the Magic Happens)
&lt;/h2&gt;

&lt;p&gt;This is the part that usually hurts. Handling file uploads + external API calls is often messy.&lt;/p&gt;

&lt;p&gt;Create your controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:controller TranscribeAudioController &lt;span class="nt"&gt;--invokable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, look at how clean this solution is using the &lt;code&gt;Transcription&lt;/code&gt; facade. We don't need to manually construct a POST request to &lt;code&gt;https://api.openai.com/v1/audio/transcriptions&lt;/code&gt;. The SDK does it for us.&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Storage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\AI\Facades\Transcription&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;TranscribeAudioController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&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="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'audio'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|file|mimes:webm,mp3,wav'&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&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="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'audio'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 1. Save the file temporarily&lt;/span&gt;
        &lt;span class="c1"&gt;// We need a physical path to pass to the SDK&lt;/span&gt;
        &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'temp_recordings'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$fullPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// 2. Transcribe with one fluent method chain&lt;/span&gt;
            &lt;span class="c1"&gt;// The SDK automatically uses your configured default driver&lt;/span&gt;
            &lt;span class="nv"&gt;$text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Transcription&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fullPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMimeType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// 3. Cleanup&lt;/span&gt;
            &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'success'&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;'text'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$text&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;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Log the error and return a user-friendly message&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Transcription failed'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;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;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;Notice line 27: &lt;code&gt;Transcription::fromPath(...)&lt;/code&gt;. &lt;br&gt;
That is the abstraction we have been waiting for. It removes the cognitive load of remembering specific API parameters for every different provider. Today you use OpenAI, tomorrow you switch to Gemini—the code stays strictly the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The Frontend (The "Recorder")
&lt;/h2&gt;

&lt;p&gt;For the frontend, you don't need a heavy React app. A simple Blade view with vanilla JS covers it perfectly.&lt;/p&gt;

&lt;p&gt;The core requirement is the browser's &lt;code&gt;MediaRecorder&lt;/code&gt; API. Here is the pseudo-code for what your JavaScript needs to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request Microphone access (&lt;code&gt;navigator.mediaDevices.getUserMedia&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Record chunks of audio into a &lt;code&gt;Blob&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Send that Blob to our Laravel endpoint via &lt;code&gt;FormData&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a glimpse of the UI I built for this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3r22zuh95pfwlgdy152.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3r22zuh95pfwlgdy152.png" alt="Dettami Interface" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;We just built a fully functional Voice-to-Text backend in &lt;strong&gt;about 20 lines of code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Laravel AI SDK respects your time. It handles the "plumbing" so you can focus on building the actual product.&lt;/p&gt;

&lt;p&gt;If you want to inspect the full source code for the demo project "Dettami", check the repository here:&lt;br&gt;
&lt;a href="https://github.com/maiobarbero/dettami" rel="noopener noreferrer"&gt;GitHub: maiobarbero/dettami&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep moving forward.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to create a resource in Filament v5 | Laravel Personal Finance Dashboard</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Tue, 10 Feb 2026 07:15:52 +0000</pubDate>
      <link>https://dev.to/maiobarbero/laravel-personal-finance-dashboard-add-filament-v5-resources-37lh</link>
      <guid>https://dev.to/maiobarbero/laravel-personal-finance-dashboard-add-filament-v5-resources-37lh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;👋 Note:&lt;/strong&gt; This article is an excerpt from my &lt;strong&gt;Free Laravel &amp;amp; Filament Finance Course&lt;/strong&gt;.&lt;br&gt;
The full course includes code-along lessons, quizzes, and a &lt;strong&gt;Completion Certificate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.maiobarbero.dev/courses/personal-finance-with-laravel/" rel="noopener noreferrer"&gt;&lt;strong&gt;Start the Free Course &amp;amp; Get Certified&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  The First Filament Admin Panel
&lt;/h1&gt;

&lt;p&gt;We have our database structure. It's solid, typed, and waiting for data. But a database without an interface is like a library without doors, secure, but useless.&lt;/p&gt;

&lt;p&gt;Today, we build the doors.&lt;/p&gt;

&lt;p&gt;We are going to create our first &lt;strong&gt;Filament Resources&lt;/strong&gt;. In Filament, a "Resource" is the static representation of an entity in your admin panel. It describes how your model looks in a table, how it behaves in a form, and how it relates to the rest of your application.&lt;/p&gt;

&lt;p&gt;We will start with something fundamental: &lt;strong&gt;Categories&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Creating the Category Resource
&lt;/h2&gt;

&lt;p&gt;Filament provides a powerful artisan command to generate resources. We want our categories to be manageable quickly, without navigating away from the list. We want a "Simple Resource", one that handles Creating, Reading, Updating, and Deleting (CRUD) all on a single page using modals.&lt;/p&gt;

&lt;p&gt;Run this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:filament-resource Category &lt;span class="nt"&gt;--simple&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates two key files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;app/Filament/Resources/CategoryResource.php&lt;/code&gt;: The definition of the table and form.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/Filament/Resources/CategoryResource/Pages/ManageCategories.php&lt;/code&gt;: The page controller that handles the actions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can see the initial structure in &lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/4935d64" rel="noopener noreferrer"&gt;this commit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you visit &lt;code&gt;/admin/categories&lt;/code&gt; now, you might see a form with a &lt;code&gt;User&lt;/code&gt; dropdown. This is technically correct, a category belongs to a user, but practically wrong. When I create a category, it should be &lt;em&gt;mine&lt;/em&gt;. I shouldn't have to select myself from a list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnp3zz98yxvis0n9hpl8b.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnp3zz98yxvis0n9hpl8b.webp" alt="Category Resource Initial" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Scoping and Context
&lt;/h2&gt;

&lt;p&gt;We have two problems to solve:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Visibility&lt;/strong&gt;: I can see categories belonging to other users.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Creation&lt;/strong&gt;: I have to manually select the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Fixing Visibility
&lt;/h3&gt;

&lt;p&gt;To ensure I only see &lt;em&gt;my&lt;/em&gt; records, we strictly scope the Eloquent query used by the resource. We override the &lt;code&gt;getEloquentQuery&lt;/code&gt; method in &lt;code&gt;CategoryResource.php&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;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="n"&gt;getEloquentQuery&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Builder&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;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getEloquentQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&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, the table will only ever load records where &lt;code&gt;user_id&lt;/code&gt; matches the logged-in user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing Creation
&lt;/h3&gt;

&lt;p&gt;Next, we clean up the form. In &lt;code&gt;CategoryResource::form()&lt;/code&gt;, remove the &lt;code&gt;user_id&lt;/code&gt; input completely. We don't want the user to worry about it.&lt;/p&gt;

&lt;p&gt;But the database &lt;em&gt;needs&lt;/em&gt; that ID. So, we inject it programmatically.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;app/Filament/Resources/CategoryResource/Pages/ManageCategories.php&lt;/code&gt;, we modify the &lt;code&gt;CreateAction&lt;/code&gt;. We use &lt;code&gt;mutateFormDataUsing&lt;/code&gt; to intercept the form submission and add the current user's ID before it hits the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getHeaderActions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&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="nc"&gt;CreateAction&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&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="nf"&gt;mutateDataUsing&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="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&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="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;--- This implies the user&lt;/span&gt;
                &lt;span class="k"&gt;return&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also remove the &lt;code&gt;user&lt;/code&gt; column from the table to keep it clean.&lt;/p&gt;

&lt;p&gt;You can view the specific changes to scope the data in &lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/481115e" rel="noopener noreferrer"&gt;this commit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrt408gfsxba4fcfjls0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrt408gfsxba4fcfjls0.webp" alt="Category Resource Scoped" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bank Accounts
&lt;/h2&gt;

&lt;p&gt;Money needs a home. Let's apply the exact same logic to &lt;strong&gt;Bank Accounts&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Generate the resource: &lt;code&gt;php artisan make:filament-resource BankAccount --simple&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Scope the query in &lt;code&gt;BankAccountResource&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Inject the &lt;code&gt;user_id&lt;/code&gt; in &lt;code&gt;ManageBankAccounts&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you check your database or use artisan tinker, you'll notice our &lt;code&gt;MoneyCast&lt;/code&gt; is working silently in the background. When you type "1000" in the balance field, it's stored as &lt;code&gt;100000&lt;/code&gt; (cents) in the database, assuming you treated the input as a distinct number.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Wait, there's a nuance here.&lt;/em&gt; Our form is currently generic. Later, we'll want to ensure we are handling decimals correctly in the UI so the user types "10.00" and we store "1000". For now, we are trusting the raw input.&lt;/p&gt;

&lt;p&gt;Check the implementation of the Bank Account resource &lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/388d79e" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Budgets and Presentation
&lt;/h2&gt;

&lt;p&gt;Finally, let's tackle &lt;strong&gt;Budgets&lt;/strong&gt;. This is where Filament's UI components really shine.&lt;/p&gt;

&lt;p&gt;Generate the resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:filament-resource Budget &lt;span class="nt"&gt;--simple&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Budgets have a &lt;code&gt;type&lt;/code&gt; (Reset or Rollover). In our database, this is an Enum string. In Filament, we want to visualize this instantly using colors.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Badge Column
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;BudgetResource.php&lt;/code&gt;, we can use a &lt;code&gt;TextColumn&lt;/code&gt; but format it as a &lt;code&gt;badge&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;TextColumn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&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="nf"&gt;badge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;BudgetType&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;BudgetType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Reset&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;BudgetType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Rollover&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'warning'&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="nf"&gt;searchable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple chaining transforms a boring text string into a vibrant, communicative UI element.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1zvib5f9i6kqha3s2rm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1zvib5f9i6kqha3s2rm.webp" alt="Budget Resource Badge" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Formatting Money
&lt;/h3&gt;

&lt;p&gt;We also want to display the budget amount as actual currency, not just a raw number. Filament has a dedicated &lt;code&gt;money&lt;/code&gt; formatter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;TextColumn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'EUR'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sortable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a polished, professional look immediately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbs7xetjfmf173uy3jv2e.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbs7xetjfmf173uy3jv2e.webp" alt="Budget Resource Money" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can explore the Budget resource implementation in &lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/8c43685" rel="noopener noreferrer"&gt;this commit&lt;/a&gt;, and see how we refined the badge colors &lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/b9a292c" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;We now have three functional pillars of our personal finance application:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Categories&lt;/strong&gt; to label our spending.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Bank Accounts&lt;/strong&gt; to store our wealth.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Budgets&lt;/strong&gt; to control our impulses.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And importantly, they are completely private to the logged-in user.&lt;/p&gt;

&lt;p&gt;In the next lesson, we will test our first Resources. Keep building.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎓 Build this App &amp;amp; Get Certified (Free)
&lt;/h2&gt;

&lt;p&gt;If you found this guide helpful, you can build the entire application from scratch with my free course. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🏗️ Advanced Laravel Architecture&lt;/li&gt;
&lt;li&gt;📊 Building Dashboards with FilamentPHP&lt;/li&gt;
&lt;li&gt;💰 Handling Money professionally in code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.maiobarbero.dev/courses/personal-finance-with-laravel/" rel="noopener noreferrer"&gt;&lt;strong&gt;→ Join other students on maiobarbero.dev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>laravel</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Testing the New Laravel AI SDK: Building 'Dettami' a voice-to-text app</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Sat, 07 Feb 2026 13:46:06 +0000</pubDate>
      <link>https://dev.to/maiobarbero/testing-the-new-laravel-ai-sdk-building-dettami-a-voice-to-text-app-8mf</link>
      <guid>https://dev.to/maiobarbero/testing-the-new-laravel-ai-sdk-building-dettami-a-voice-to-text-app-8mf</guid>
      <description>&lt;p&gt;The landscape of AI integration in PHP has just shifted. With the release of the &lt;strong&gt;Laravel AI SDK&lt;/strong&gt;, what used to require complex API wrangling or third-party packages is now a first-party citizen of the framework we love.&lt;/p&gt;

&lt;p&gt;I couldn't resist. I had to take it for a spin.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;Dettami&lt;/strong&gt; (Italian for "Dictate to me"), a simple web app that records your voice and transcribes it instantly. And when I say "simple," I mean it. The speed at which I went from &lt;code&gt;laravel new&lt;/code&gt; to a working prototype was exhilarating.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal: Frictionless Audio Transcription
&lt;/h2&gt;

&lt;p&gt;I wanted to build something tangible. Not just a "Hello World" for AI, but a feature I would actually use: a quick way to record thoughts and get them as text.&lt;/p&gt;

&lt;p&gt;Here is what the interface looks like. Clean, focused, and ready to listen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub618yiz7u605sdwox0s.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub618yiz7u605sdwox0s.webp" alt="Dettami Homepage" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;Installing the SDK was the first step, and as you'd expect from the Laravel ecosystem, it was seamless.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/ai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the real magic happened in the controller. In the past, handling audio uploads, converting formats, and sending them to an API like OpenAI's Whisper or Google's Speech-to-Text involved a fair bit of boilerplate. You had to manage the file, ensure the format was correct, build the multipart request, handle the response...&lt;/p&gt;

&lt;p&gt;With the Laravel AI SDK, the complexity vanishes. It abstracts the "how" and lets you focus on the "what."&lt;/p&gt;

&lt;p&gt;Here is the core logic from my &lt;code&gt;Dettami&lt;/code&gt; controller. Look at how expressive it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&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="k"&gt;if&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="nf"&gt;hasFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'audio'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&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="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'audio'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Store the file temporarily&lt;/span&gt;
        &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;storeAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dettami'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'recording.webm'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$fullPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// The magic happens here. One fluents line.&lt;/span&gt;
            &lt;span class="nv"&gt;$transcription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Transcription&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fullPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'audio/webm'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Clean up&lt;/span&gt;
            &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'success'&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;'transcription'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$transcription&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;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// ... error handling&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;That's it. &lt;code&gt;Transcription::fromPath(...) -&amp;gt; generate()&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The SDK handles the heavy lifting of communicating with the configured AI provider. It feels native. It feels right.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Frontend Experience
&lt;/h3&gt;

&lt;p&gt;For the frontend, I kept it simple using Blade and vanilla JavaScript with the &lt;code&gt;MediaRecorder&lt;/code&gt; API. The goal was to provide immediate feedback. You click record, you speak, you get text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdo3zp99o2dnenmpjt276.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdo3zp99o2dnenmpjt276.webp" alt="Recording in progress" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Transcription: Translation
&lt;/h2&gt;

&lt;p&gt;The power of this SDK doesn't stop at transcription. Once you have the text, the possibilities are endless. You can summarize it, rewrite it, or—as I experimented with—translate it.&lt;/p&gt;

&lt;p&gt;Because the SDK provides a unified interface for these operations, adding a translation step is just as trivial as the transcription itself. Whether you are using OpenAI, Anthropic, or Gemini, the code remains consistent and clean.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztk5ixkwd0lzb58dev60.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztk5ixkwd0lzb58dev60.webp" alt="Translated Output" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflections
&lt;/h2&gt;

&lt;p&gt;Building &lt;strong&gt;Dettami&lt;/strong&gt; was a reminder of why I love this framework. It respects my time. The &lt;strong&gt;Laravel AI SDK&lt;/strong&gt; isn't just a wrapper; it's an opinionated, elegant way to bring intelligence into your applications without the headache.&lt;/p&gt;

&lt;p&gt;For architects and developers, this means we can stop worrying about &lt;em&gt;connecting&lt;/em&gt; to AI and start worrying about &lt;em&gt;what we can build&lt;/em&gt; with it. Ideally, the barrier to entry has been lowered to the floor.&lt;/p&gt;

&lt;p&gt;If you haven't tried it yet, fire up a terminal.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;composer require laravel/ai&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And build something amazing. You can find the full source code for Dettami on &lt;a href="https://github.com/maiobarbero/dettami" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. I recommend starting with &lt;a href="https://github.com/maiobarbero/dettami/commit/33f3380" rel="noopener noreferrer"&gt;this commit&lt;/a&gt; to see the transcription logic in isolation.&lt;/p&gt;

&lt;p&gt;Keep moving forward.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>ai</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Laravel Personal Finance Dashboard: creating the database</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Thu, 05 Feb 2026 07:59:55 +0000</pubDate>
      <link>https://dev.to/maiobarbero/laravel-personal-finance-dashboard-creating-the-database-644</link>
      <guid>https://dev.to/maiobarbero/laravel-personal-finance-dashboard-creating-the-database-644</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;👋 Note:&lt;/strong&gt; This article is an excerpt from my &lt;strong&gt;Free Laravel &amp;amp; Filament Finance Course&lt;/strong&gt;.&lt;br&gt;
The full course includes code-along lessons, quizzes, and a &lt;strong&gt;Completion Certificate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.maiobarbero.dev/courses/personal-finance-with-laravel/" rel="noopener noreferrer"&gt;&lt;strong&gt;Start the Free Course &amp;amp; Get Certified&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Creating the Database
&lt;/h1&gt;

&lt;p&gt;Every great application stands on the shoulders of a solid data model. If the database schema isn't right, everything else, from your eloquently written controllers to your beautiful Filament resources, will feel like fighting gravity.&lt;/p&gt;

&lt;p&gt;In this lesson, we are going to define the core entities of our Personal Finance application. We aren't just creating tables; we are defining the &lt;em&gt;vocabulary&lt;/em&gt; of our domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Entities
&lt;/h2&gt;

&lt;p&gt;Our application needs to track money moving in and out. To do that effectively, we need four primary concepts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Bank Accounts&lt;/strong&gt;: Where the money lives.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Categories&lt;/strong&gt;: How we classify the money (e.g., "Utilities", "Dining Out").&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Budgets&lt;/strong&gt;: Our financial goals or limits.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Transactions&lt;/strong&gt;: The heart of the system, the actual record of spending or earning.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Generating the Artifacts
&lt;/h2&gt;

&lt;p&gt;Laravel makes scaffolding these incredible easy. We need a Model, a Migration, a Factory, and a Seeder for each of our entities. Instead of running four commands per entity, we can use the &lt;code&gt;-mfs&lt;/code&gt; flags.&lt;/p&gt;

&lt;p&gt;Run the following commands in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:model Category &lt;span class="nt"&gt;-mfs&lt;/span&gt;
php artisan make:model Budget &lt;span class="nt"&gt;-mfs&lt;/span&gt;
php artisan make:model BankAccount &lt;span class="nt"&gt;-mfs&lt;/span&gt;
php artisan make:model Transaction &lt;span class="nt"&gt;-mfs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a flurry of green success messages. We now have our files ready to be sculpted.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on "Down" Methods
&lt;/h2&gt;

&lt;p&gt;Open up one of your new migration files. You'll see an &lt;code&gt;up&lt;/code&gt; method and a &lt;code&gt;down&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;I'm going to ask you to do something that might feel rebellious: &lt;strong&gt;delete the &lt;code&gt;down&lt;/code&gt; method.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why? In this project, we are adopting a &lt;strong&gt;Fix Forward&lt;/strong&gt; strategy. In a production environment with real data, rolling back a migration (especially one that drops columns or tables) is destructive and risky. If we make a mistake in a migration that has already run, we don't roll it back, we create a &lt;em&gt;new&lt;/em&gt; migration to fix the issue. This keeps our database history linear and truthful.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, go ahead and remove &lt;code&gt;public function down(): void&lt;/code&gt; from all your new migration files. It clarifies our intent: we only move forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the Schema
&lt;/h2&gt;

&lt;p&gt;Let's define the structure of our tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Enums
&lt;/h3&gt;

&lt;p&gt;Before we get to the tables, we need a way to define the type of a Budget. Is it a fixed budget that we reset every month? Or is it a rolling budget that we keep track of over time? Let's use a PHP Enum for this to ensure type safety.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:enum Enums/BudgetType
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;app/Enums/BudgetType.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;BudgetType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Reset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'reset'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Rollover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'rollover'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getLabel&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Reset&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Reset'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Rollover&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Rollover'&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;h3&gt;
  
  
  2. Migrations
&lt;/h3&gt;

&lt;p&gt;Now, let's fill in our &lt;code&gt;up&lt;/code&gt; methods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;create_categories_table.php&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Categories are simple. They have a name and belong to a user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'categories'&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="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&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="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;create_budgets_table.php&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
A budget tracks a limit for a specific period.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'budgets'&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="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&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="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&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="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'fixed'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;create_bank_accounts_table.php&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Represents a physical or digital account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bank_accounts'&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="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&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="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedBigInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'balance'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&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="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;create_transactions_table.php&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
The center of our universe. Links everything together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'transactions'&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="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bank_account_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'category_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'budget_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'note'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&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;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
Notice we store money as &lt;code&gt;integers&lt;/code&gt; (cents) rather than &lt;code&gt;floats&lt;/code&gt; or &lt;code&gt;decimals&lt;/code&gt;. Floating point math can be imprecise. storing $10.00 as &lt;code&gt;1000&lt;/code&gt; is deeply robust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. The Money Cast
&lt;/h2&gt;

&lt;p&gt;Since we are storing money as integers (cents) but want to work with it as standard units (dollars/euros) in our code, let's create a custom Cast. This encourages consistency across our application.&lt;/p&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:cast MoneyCast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;app/Casts/MoneyCast.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Contracts\Database\Eloquent\CastsAttributes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Model&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;MoneyCast&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;CastsAttributes&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * Cast the given value.
     *
     * @param  array&amp;lt;string, mixed&amp;gt;  $attributes
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Model&lt;/span&gt; &lt;span class="nv"&gt;$model&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;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;mixed&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;$value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Prepare the given value for storage.
     *
     * @param  array&amp;lt;string, mixed&amp;gt;  $attributes
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Model&lt;/span&gt; &lt;span class="nv"&gt;$model&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;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;mixed&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="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;round&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="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. The Models
&lt;/h2&gt;

&lt;p&gt;Now we breathe life into our schemas by defining relationships and behaviors in our Models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;User.php&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
The user owns everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ... imports&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Relations\HasMany&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;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Authenticatable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... traits&lt;/span&gt;

    &lt;span class="c1"&gt;// ... fillable &amp;amp; hidden&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;bankAccounts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;HasMany&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="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BankAccount&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;HasMany&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="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Category&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;budgets&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;HasMany&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="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Budget&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;HasMany&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="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Transaction&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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;&lt;strong&gt;&lt;code&gt;Category.php&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Category&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @use HasFactory&amp;lt;\Database\Factories\CategoryFactory&amp;gt; */&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasFactory&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;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'user_id'&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="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\BelongsTo&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="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\HasMany&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="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Transaction&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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;&lt;strong&gt;&lt;code&gt;Budget.php&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Here we treat our &lt;code&gt;type&lt;/code&gt; as an Enum and &lt;code&gt;amount&lt;/code&gt; with our new MoneyCast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Budget&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @use HasFactory&amp;lt;\Database\Factories\BudgetFactory&amp;gt; */&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasFactory&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;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'user_id'&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="s1"&gt;'amount'&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="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="n"&gt;casts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&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;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Enums\BudgetType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'amount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Casts\MoneyCast&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\BelongsTo&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="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\HasMany&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="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Transaction&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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;&lt;strong&gt;&lt;code&gt;BankAccount.php&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BankAccount&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @use HasFactory&amp;lt;\Database\Factories\BankAccountFactory&amp;gt; */&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasFactory&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;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'user_id'&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="s1"&gt;'balance'&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="n"&gt;casts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&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;'balance'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Casts\MoneyCast&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\BelongsTo&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="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\HasMany&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="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Transaction&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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;&lt;strong&gt;&lt;code&gt;Transaction.php&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
The nexus. Note the mass assignment protection and casting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Transaction&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @use HasFactory&amp;lt;\Database\Factories\TransactionFactory&amp;gt; */&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasFactory&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;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'bank_account_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'category_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'budget_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'note'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'date'&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="n"&gt;casts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&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;'amount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Casts\MoneyCast&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\BelongsTo&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="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;bankAccount&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\BelongsTo&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="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BankAccount&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\BelongsTo&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="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Category&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\Illuminate\Database\Eloquent\Relations\BelongsTo&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="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Budget&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Seeding and Verification
&lt;/h2&gt;

&lt;p&gt;With our structure defined, let's spin up the database.&lt;/p&gt;

&lt;p&gt;First, update your &lt;code&gt;DatabaseSeeder.php&lt;/code&gt; to call the new seeders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseSeeder&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Seeder&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;WithoutModelEvents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Seed the application's database.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserSeeder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategorySeeder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BudgetSeeder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BankAccountSeeder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TransactionSeeder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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'll cover Factories in depth in a future lesson, but for now, ensure your factories create dummy data).&lt;/p&gt;

&lt;p&gt;Then, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan migrate:fresh &lt;span class="nt"&gt;--seeder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's verify our work using Tinker, the best tool for checking your data reality.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan tinker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try fetching a user and their transactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\App\Models\User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see a Collection of Transaction models, congratulations. You have successfully mapped the physical world of finance into your digital domain.&lt;/p&gt;

&lt;p&gt;Finally, before we commit, let's make sure our code style is impeccable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./vendor/bin/pint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep moving forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎓 Build this App &amp;amp; Get Certified (Free)
&lt;/h2&gt;

&lt;p&gt;If you found this guide helpful, you can build the entire application from scratch with my free course. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🏗️ Advanced Laravel Architecture&lt;/li&gt;
&lt;li&gt;📊 Building Dashboards with FilamentPHP&lt;/li&gt;
&lt;li&gt;💰 Handling Money professionally in code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.maiobarbero.dev/courses/personal-finance-with-laravel/" rel="noopener noreferrer"&gt;&lt;strong&gt;→ Join other students on maiobarbero.dev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>laravel</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Laravel Personal Finance Dashboard: setting up the project with Filament v5</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Tue, 03 Feb 2026 08:33:26 +0000</pubDate>
      <link>https://dev.to/maiobarbero/setting-up-laravel-and-filament-for-personal-finance-5om</link>
      <guid>https://dev.to/maiobarbero/setting-up-laravel-and-filament-for-personal-finance-5om</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;👋 Note:&lt;/strong&gt; This article is an excerpt from my &lt;strong&gt;Free Laravel &amp;amp; Filament Finance Course&lt;/strong&gt;.&lt;br&gt;
The full course includes code-along lessons, quizzes, and a &lt;strong&gt;Completion Certificate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.maiobarbero.dev/courses/personal-finance-with-laravel/" rel="noopener noreferrer"&gt;&lt;strong&gt;Start the Free Course &amp;amp; Get Certified&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Starting a new project is always a mix of excitement and anxiety. The blank slate. Infinite possibilities. But today, we're not just writing code; we're building a tool for financial freedom. A system that serves &lt;em&gt;us&lt;/em&gt;, tailored to our specific needs, without the bloat of generic SaaS products.&lt;/p&gt;

&lt;p&gt;In this first lesson, we will lay the foundation. We are going to build a robust, modern application using strictly typed &lt;strong&gt;Laravel 12&lt;/strong&gt; and the elegant &lt;strong&gt;Filament&lt;/strong&gt; admin panel.&lt;/p&gt;

&lt;p&gt;Let's get our hands dirty.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Foundation
&lt;/h2&gt;

&lt;p&gt;First, let's create our canvas. Open your terminal and breathe in that new project smell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;laravel new laravel_filament_personal_finance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We want a clean slate, so when promoted, select:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;No starter kit&lt;/strong&gt;: We are building this artisan-style.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pest&lt;/strong&gt;: For testing. It’s elegant and expressive.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;SQLite&lt;/strong&gt;: Simplify the infrastructure. For a personal tool, it's fast, zero-conf, and easy to backup.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Init Git repo&lt;/strong&gt;: Yes. Version control is non-negotiable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the command finishes, navigate into your new fortress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;laravel_filament_personal_finance
composer &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the heart is beating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://localhost:8000&lt;/code&gt;. You should see the default Laravel splash screen. We are live.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/1e9aefa" rel="noopener noreferrer"&gt;View the initial setup commit&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Supercharging with AI
&lt;/h2&gt;

&lt;p&gt;I believe in leveraging every tool available to maintain flow. We'll add &lt;strong&gt;Laravel Boost&lt;/strong&gt;, an MCP server that acts as a bridge between our app and AI agents. Think of it as giving your AI pair programmer direct sensory access to your codebase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/boost &lt;span class="nt"&gt;--dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan boost:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted, select your IDE or specific configuration. This will generate a configuration block like this (the below one is the Google Antigravity configuration):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"laravel-boost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/usr/bin/php"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/home/&amp;lt;your-username&amp;gt;/&amp;lt;path-to-project&amp;gt;/artisan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"boost:mcp"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, your AI assistant can understand your project structure, run artisan commands, and debugging becomes a conversation rather than a struggle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/8d05ee9" rel="noopener noreferrer"&gt;View the Boost configuration commit&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Admin Panel
&lt;/h2&gt;

&lt;p&gt;We don't want to spend weeks building authentication views and CRUD tables from scratch. We want to focus on the &lt;em&gt;logic&lt;/em&gt; of our finances. Enter &lt;strong&gt;Filament v5&lt;/strong&gt;. It’s the gold standard for TALL stack admin panels.&lt;/p&gt;

&lt;p&gt;Install it via composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require filament/filament:&lt;span class="s2"&gt;"^5.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, install the panel. We'll call ours &lt;code&gt;admin&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan filament:install &lt;span class="nt"&gt;--panels&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Enter &lt;code&gt;admin&lt;/code&gt; when asked for the ID.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This scaffolds the entire admin infrastructure. It's almost magical how much time this saves us.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/maiobarbero/laravel_filament_personal_finance/commit/18c5b5e" rel="noopener noreferrer"&gt;View the Filament setup commit&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. First Access
&lt;/h2&gt;

&lt;p&gt;Reference without access is useless. Let's create an admin user to unlock the gates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:filament-user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the prompts to set your name, email, and password. Then, restart your server if it's not running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to &lt;code&gt;/admin&lt;/code&gt;. Login with your new credentials.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmokf82ultop07a82iji.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmokf82ultop07a82iji.webp" alt="Admin Dashboard" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You are now looking at the control center of your future financial system. It's empty now, but soon it will be the source of truth for your wealth.&lt;/p&gt;

&lt;p&gt;We have a running application, a powerful admin interface, and the AI tooling to help us build faster. The structure is set. In the next lesson, we will start modeling the domain.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;h2&gt;
  
  
  🎓 Build this App &amp;amp; Get Certified (Free)
&lt;/h2&gt;

&lt;p&gt;If you found this guide helpful, you can build the entire application from scratch with my free course. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🏗️ Advanced Laravel Architecture&lt;/li&gt;
&lt;li&gt;📊 Building Dashboards with FilamentPHP&lt;/li&gt;
&lt;li&gt;💰 Handling Money professionally in code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.maiobarbero.dev/courses/personal-finance-with-laravel/" rel="noopener noreferrer"&gt;&lt;strong&gt;→ Join other students on maiobarbero.dev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>laravel</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to set up a global .gitignore</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Wed, 07 May 2025 06:33:13 +0000</pubDate>
      <link>https://dev.to/maiobarbero/how-to-set-up-a-global-gitignore-4e09</link>
      <guid>https://dev.to/maiobarbero/how-to-set-up-a-global-gitignore-4e09</guid>
      <description>&lt;p&gt;If you work with multiple Git repositories, you’ve likely run into the frustration of ignoring the same files (like &lt;code&gt;.DS_Store&lt;/code&gt;, &lt;code&gt;Thumbs.db&lt;/code&gt;, or your IDE settings) over and over again. Fortunately, Git allows you to define a global &lt;code&gt;.gitignore&lt;/code&gt; file that applies to all repositories on your system. This is done through the &lt;code&gt;core.excludesfile&lt;/code&gt; configuration option.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk you through how to set up a global &lt;code&gt;.gitignore&lt;/code&gt; file using &lt;code&gt;git config&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Why Use a Global .gitignore?
&lt;/h2&gt;

&lt;p&gt;Some files are specific to your system or development environment and should never be committed to &lt;em&gt;any&lt;/em&gt; project. Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS system files (&lt;code&gt;.DS_Store&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Windows thumbnails (&lt;code&gt;Thumbs.db&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;IDE configs (&lt;code&gt;.idea/&lt;/code&gt;, &lt;code&gt;.vscode/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Log files (&lt;code&gt;*.log&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Local environment files (&lt;code&gt;.env&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of duplicating the same ignore rules in every repo, a global &lt;code&gt;.gitignore&lt;/code&gt; keeps things DRY (Don't Repeat Yourself).&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 Step-by-Step Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create the Global .gitignore File
&lt;/h3&gt;

&lt;p&gt;Start by creating a &lt;code&gt;.gitignore_global&lt;/code&gt; file in your home directory (or anywhere you prefer):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; ~/.gitignore_global
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, edit it and add the rules you want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
.DS_Store

&lt;span class="c"&gt;# Windows&lt;/span&gt;
Thumbs.db

&lt;span class="c"&gt;# IDEs&lt;/span&gt;
.vscode/
.idea/

&lt;span class="c"&gt;# Logs&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;.log

&lt;span class="c"&gt;# Env files&lt;/span&gt;
.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Configure Git to Use the Global File
&lt;/h3&gt;

&lt;p&gt;Tell Git to use this file globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; core.excludesfile ~/.gitignore_global
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can verify it worked with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git config --global core.excludesfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should output the path you just set.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
