<?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>Pragmatic Domain-Driven Design in Laravel, with Laravel Boost</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/maiobarbero/pragmatic-domain-driven-design-in-laravel-with-laravel-boost-3bcm</link>
      <guid>https://dev.to/maiobarbero/pragmatic-domain-driven-design-in-laravel-with-laravel-boost-3bcm</guid>
      <description>&lt;p&gt;Laravel Boost is widely used to give coding agents context about Laravel projects. It can provide guidelines and skills that shape how agents create and change files. That made me ask a question: could Boost also be a place to guide an application toward Domain-Driven Design?&lt;/p&gt;

&lt;p&gt;I wanted to try that without asking people to remove Laravel from their domain code. Eloquent models, policies, events, queues, service providers, and the usual Laravel entry points are useful parts of an application. DDD, as I wanted to apply it, should help decide where behaviour belongs, not require replacing those tools with a framework of our own.&lt;/p&gt;

&lt;p&gt;That idea led me to build &lt;a href="https://github.com/maiobarbero/laravel-boost-ddd" rel="noopener noreferrer"&gt;Laravel Boost DDD&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundaries matter more than the folder names
&lt;/h2&gt;

&lt;p&gt;The package gives Boost a set of guidelines and task-specific skills for working with an adopted DDD structure.To be sure that the agent follows the architecture I had in mind, the package also publishes Pest architecture tests to check some of the dependency boundaries.&lt;/p&gt;

&lt;p&gt;The basic structure is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;App\Domain\&amp;lt;Capability&amp;gt;&lt;/code&gt; for business behaviour and invariants.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;App\Application\&amp;lt;Capability&amp;gt;&lt;/code&gt; for use cases and orchestration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;App\Infrastructure&lt;/code&gt; for external integrations and adapters.&lt;/li&gt;
&lt;li&gt;Laravel’s usual directories for controllers, commands, jobs, listeners, policies, providers, migrations, and factories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The word “capability” here does not mean every folder is a separate bounded context. Orders and Billing might be separate capabilities in the same context, and their models may collaborate directly.&lt;/p&gt;

&lt;p&gt;Consider cancelling an order. The order model can own the rule about which states allow cancellation:&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;cancel&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderCannotBeCancelled&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&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;An application Action coordinates the use case: load the order, authorise the actor through a Laravel policy, invoke &lt;code&gt;cancel()&lt;/code&gt;, and save the model. If the operation also writes an audit record, the Action can put both writes in a database transaction.&lt;/p&gt;

&lt;p&gt;The model is still an Eloquent model. The policy is still a Laravel policy. The Action uses Laravel’s authorisation and database facilities. The boundary is about responsibility: the model decides whether its state transition is valid, while the Action coordinates the use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use abstractions when the code needs them
&lt;/h2&gt;

&lt;p&gt;A DDD structure can encourage people to add repositories, DTOs, domain services, factories, and adapters everywhere. Those patterns are useful when they solve a real problem, but they can also make ordinary work harder to follow.&lt;/p&gt;

&lt;p&gt;In this approach, direct Eloquent persistence is the default. A scalar order ID does not need a Data object wrapped around it. A simple authorised read can stay in a controller. A repository makes sense when direct Eloquent is insufficient, not because every model must be hidden behind one.&lt;/p&gt;

&lt;p&gt;Contracts belong at actual dependency boundaries. If an Action needs a payment gateway, the application defines the contract it needs and Infrastructure provides the implementation. That keeps the application independent of a specific SDK while leaving Laravel’s container available to connect the pieces.&lt;/p&gt;

&lt;p&gt;The goal is a structure with meaningful boundaries and as little extra machinery as the use case allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adoption can happen one feature at a time
&lt;/h2&gt;

&lt;p&gt;Installing the package does not move existing application code. New capabilities can follow the structure, while legacy code stays where it is until migration is part of the work.&lt;/p&gt;

&lt;p&gt;That matters because introducing boundaries and reorganising an application are separate tasks. If I am fixing a bug in an existing controller, that does not automatically mean I should move the controller’s whole feature into new namespaces. If I am building a new capability, I can start with the structure and apply the boundaries there.&lt;/p&gt;

&lt;p&gt;The architecture tests follow the same rule. They check namespaces such as &lt;code&gt;App\Domain&lt;/code&gt; and &lt;code&gt;App\Application&lt;/code&gt; when those directories exist. An application that has not adopted those layers gets skipped checks. Once code enters those namespaces, it is covered by the rules, while legacy code outside them remains untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Boost contributes
&lt;/h2&gt;

&lt;p&gt;Boost provides the place where an agent can receive these conventions while working in a Laravel project. Laravel Boost DDD adds guidelines for the overall structure, skills for specific work such as creating an Action or testing a domain rule, and architecture tests for a small set of static constraints.&lt;/p&gt;

&lt;p&gt;The tests can check, for example, that Domain code does not depend on Application or Infrastructure, and that Actions expose a &lt;code&gt;handle()&lt;/code&gt; method. They cannot establish that authorisation is correct, that a transaction covers the right writes, or that an event is dispatched at the right time. Those behaviours still need ordinary tests and review.&lt;/p&gt;

&lt;p&gt;The package is an attempt to make one particular balance explicit: keep Laravel as the application framework, use DDD boundaries where they clarify responsibility, and adopt the structure incrementally. I’m interested in how other Laravel teams draw that line, especially where they find that an abstraction starts paying for itself.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>software</category>
      <category>ai</category>
    </item>
    <item>
      <title>Laravel: Anatomy of a Query</title>
      <dc:creator>Matteo Barbero</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:29:37 +0000</pubDate>
      <link>https://dev.to/maiobarbero/laravel-anatomy-of-a-query-5dg7</link>
      <guid>https://dev.to/maiobarbero/laravel-anatomy-of-a-query-5dg7</guid>
      <description>&lt;p&gt;I wanted to compare three ways of writing a query in Laravel: Eloquent with &lt;code&gt;Model::query()&lt;/code&gt;, Query Builder with &lt;code&gt;DB::table()&lt;/code&gt;, and raw SQL with &lt;code&gt;DB::select()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They offer different ways of working with our data, but they share much of the same execution path. Before reaching the database, the builders produce SQL. Once the results come back, Laravel turns them into the objects we use in our application.&lt;/p&gt;

&lt;p&gt;To understand the cost of these abstractions, I followed that journey in Laravel 13, then compared the three approaches with result sets ranging from 10 to 100,000 users. The &lt;a href="https://github.com/maiobarbero/eloquent_builder_raw" rel="noopener noreferrer"&gt;benchmark repository&lt;/a&gt; contains the command used for the comparison.&lt;/p&gt;

&lt;p&gt;The source links below point to Laravel's moving &lt;code&gt;13.x&lt;/code&gt; branch. When comparing this with your application, check the framework version installed in &lt;code&gt;composer.lock&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to ask for the same data
&lt;/h2&gt;

&lt;p&gt;Let's start with a simple query: retrieve the first 10 users, ordered by their ID.&lt;/p&gt;

&lt;p&gt;With Eloquent:&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\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$users&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;query&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;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'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;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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;With Query Builder:&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="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&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;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'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;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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;With raw SQL, using MySQL syntax:&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="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'select * from `users` order by `id` asc limit 10'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assuming the model uses the same connection and table, without additional scopes or default eager loading, these examples request the same records. Model configuration can add conditions or trigger additional queries. The benchmark explicitly disables global scopes and default eager loads, then checks that the SQL and bindings match before measuring anything. You can see those checks in the &lt;a href="https://github.com/maiobarbero/eloquent_builder_raw/blob/main/app/Console/Commands/BenchmarkDatabaseAccess.php" rel="noopener noreferrer"&gt;benchmark command&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The database receives SQL through all three paths. It has no knowledge of our fluent PHP calls or our &lt;code&gt;User&lt;/code&gt; model.&lt;/p&gt;

&lt;p&gt;The builders therefore introduce some work before execution. They need to represent the query in PHP and compile it into SQL. The question is how much that work contributes to the time and memory we measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  From PHP to a SQL string
&lt;/h2&gt;

&lt;p&gt;When we call &lt;code&gt;orderBy()&lt;/code&gt; or &lt;code&gt;limit()&lt;/code&gt;, Laravel records information about the query. It doesn't retrieve any users yet.&lt;/p&gt;

&lt;p&gt;The query's clauses live in the underlying &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Query/Builder.php" rel="noopener noreferrer"&gt;Query Builder&lt;/a&gt;. For our example, a simplified view of its state looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query Builder
├── from = users
├── columns = [*] when get() executes
├── wheres = []
├── orders = [{ column: id, direction: asc }]
├── limit = 10
└── bindings = []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eloquent adds another object around that builder, holding the model and Eloquent-specific configuration. Its constructor receives the underlying Query Builder, and many query operations are forwarded to it. This relationship is visible in &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Eloquent/Builder.php" rel="noopener noreferrer"&gt;Eloquent's Builder implementation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At this stage, memory use depends on the query's structure and its bindings. Changing the limit from 10 to 100,000 doesn't create 100,000 objects in PHP. Adding thousands of values to a &lt;code&gt;whereIn()&lt;/code&gt; clause, however, would make the query representation larger.&lt;/p&gt;

&lt;p&gt;When execution begins, the builder's grammar compiles those pieces into SQL. Laravel's base &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Query/Grammars/Grammar.php" rel="noopener noreferrer"&gt;query grammar&lt;/a&gt; contains the common compilation logic, while database-specific grammars provide the relevant variations. For MySQL, that includes wrapping identifiers in backticks through &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php" rel="noopener noreferrer"&gt;MySqlGrammar&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this query, the construction is straightforward:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Builder state&lt;/th&gt;
&lt;th&gt;Compiled SQL fragment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;columns = ['*']&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;select *&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;from = 'users'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;from `users`&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;orders = [...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;order by `id` asc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;limit = 10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;limit 10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The grammar combines the fragments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;`users`&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nv"&gt;`id`&lt;/span&gt; &lt;span class="k"&gt;asc&lt;/span&gt; &lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we added &lt;code&gt;where('id', '&amp;gt;', 100)&lt;/code&gt;, the condition would compile to &lt;code&gt;where `id` &amp;gt; ?&lt;/code&gt;, with &lt;code&gt;100&lt;/code&gt; stored separately as a binding. SQL compilation doesn't mean inserting every value into the query string.&lt;/p&gt;

&lt;p&gt;The three paths now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw SQL        → SQL already supplied
Query Builder  → query state → grammar → SQL
Eloquent       → Eloquent Builder → Query Builder → grammar → SQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raw SQL skips Laravel's builder compilation. Laravel still prepares and executes the statement through PDO; whether preparation happens on the server or is emulated depends on the driver and connection settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The connection layer
&lt;/h2&gt;

&lt;p&gt;All three paths converge on Laravel's connection layer. Query Builder's &lt;code&gt;runSelect()&lt;/code&gt; passes the compiled SQL and bindings to &lt;code&gt;Connection::select()&lt;/code&gt;. Eloquent reaches that same method through its underlying builder. Raw SQL starts closer to it.&lt;/p&gt;

&lt;p&gt;The connection provides access to PDO, PHP's database interface. It also owns the query grammar used by its builders, so the grammar is already available before execution reaches &lt;code&gt;select()&lt;/code&gt;. The relevant execution code is in &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Connection.php" rel="noopener noreferrer"&gt;Laravel's Connection class&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;select()&lt;/code&gt;, the central steps are:&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;$statement&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;prepared&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;getPdoForSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$useReadPdo&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;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&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;bindValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$statement&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;prepareBindings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bindings&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nv"&gt;$statement&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&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;$statement&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$fetchUsing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laravel prepares the statement, binds the values, executes it, and fetches the results. These operations also happen when we use &lt;code&gt;DB::select()&lt;/code&gt;. Writing raw SQL doesn't bypass Laravel's connection handling.&lt;/p&gt;

&lt;p&gt;For equivalent SQL, bindings, and connection settings, the database has the same work to do. The builder has added PHP work before that point, but it hasn't introduced a different kind of database query.&lt;/p&gt;

&lt;p&gt;There is still another part of the journey to account for: what happens to the returned rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to PHP
&lt;/h2&gt;

&lt;p&gt;With the default fetch mode, &lt;code&gt;DB::select()&lt;/code&gt; returns an array of &lt;code&gt;stdClass&lt;/code&gt; objects. Query Builder wraps those row objects in an &lt;code&gt;Illuminate\Support\Collection&lt;/code&gt;. Eloquent returns an Eloquent collection containing model instances.&lt;/p&gt;

&lt;p&gt;For a nonempty result, we can inspect the difference without dumping every 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;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_debug_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$users&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_debug_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$users&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected types are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB::select()
    array
    stdClass

DB::table()-&amp;gt;get()
    Illuminate\Support\Collection
    stdClass

User::query()-&amp;gt;get()
    Illuminate\Database\Eloquent\Collection
    App\Models\User
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query Builder's collection wrapper adds little work around the fetched rows. Eloquent has to construct a model for each one. The respective return paths are visible in &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Query/Builder.php" rel="noopener noreferrer"&gt;Query Builder's &lt;code&gt;get()&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Eloquent/Builder.php" rel="noopener noreferrer"&gt;Eloquent's &lt;code&gt;getModels()&lt;/code&gt; and &lt;code&gt;hydrate()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hydration
&lt;/h3&gt;

&lt;p&gt;Hydration is the process of turning a database row into a model instance.&lt;/p&gt;

&lt;p&gt;Eloquent loops over the fetched rows and calls &lt;code&gt;newFromBuilder()&lt;/code&gt; for each one. That method creates a model marked as already existing, assigns its raw attributes, synchronizes its original attribute state, sets its connection, and fires the &lt;code&gt;retrieved&lt;/code&gt; model event. The implementation is in &lt;a href="https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Eloquent/Model.php" rel="noopener noreferrer"&gt;Model.php&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;User&lt;/code&gt; therefore carries more state than a plain row object. It has the machinery needed to track changes and participate in the model lifecycle. This doesn't mean hydration immediately loads every relationship or evaluates every accessor; the initial operation assigns raw attributes.&lt;/p&gt;

&lt;p&gt;It also doesn't mean every model contains a separate copy of its methods, or that each attribute snapshot immediately duplicates every underlying value. PHP's reference counting and copy-on-write behavior make that accounting more complicated than counting properties. The PHP manual's &lt;a href="https://www.php.net/manual/en/features.gc.refcounting-basics.php" rel="noopener noreferrer"&gt;reference-counting explanation&lt;/a&gt; gives some background on value sharing.&lt;/p&gt;

&lt;p&gt;What does grow with the result set is the number of model instances. Retrieving 10 users creates 10 models. Retrieving 100,000 users creates 100,000 models, each passing through that initialization process.&lt;/p&gt;

&lt;p&gt;That gives us two different sources of overhead to examine: building the query, which depends on its structure, and creating the result objects, which depends on how many rows come back.&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://github.com/maiobarbero/eloquent_builder_raw" rel="noopener noreferrer"&gt;repository&lt;/a&gt; contains an Artisan command called &lt;code&gt;benchmark:database&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After installing the project dependencies, configuring the database, and running the migrations, seed an empty benchmark database:&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 db:seed &lt;span class="nt"&gt;--class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UserSeeder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current &lt;a href="https://github.com/maiobarbero/eloquent_builder_raw/blob/main/database/seeders/UserSeeder.php" rel="noopener noreferrer"&gt;UserSeeder.php&lt;/a&gt; inserts 100,000 users in batches of 5,000. To create fewer users, change &lt;code&gt;$total&lt;/code&gt; in that file. Its inner loop always creates a full batch, so also adjust &lt;code&gt;$chunkSize&lt;/code&gt; when needed: for example, &lt;code&gt;$total = 1_000&lt;/code&gt; and &lt;code&gt;$chunkSize = 1_000&lt;/code&gt; will create 1,000 users.&lt;/p&gt;

&lt;p&gt;The seeder uses predictable, unique email addresses. Running it again against the same populated table can produce duplicate-email errors.&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 benchmark:database &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10000 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--iterations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--warmup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--show-sql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--rows&lt;/code&gt; controls the maximum number of records fetched by each query. &lt;code&gt;--iterations&lt;/code&gt; sets the number of measured executions per strategy, while &lt;code&gt;--warmup&lt;/code&gt; runs each strategy before collecting samples. &lt;code&gt;--show-sql&lt;/code&gt; prints the SQL and any bindings.&lt;/p&gt;

&lt;p&gt;Repeat with &lt;code&gt;--rows=10&lt;/code&gt;, &lt;code&gt;100&lt;/code&gt;, &lt;code&gt;1000&lt;/code&gt;, &lt;code&gt;10000&lt;/code&gt;, and &lt;code&gt;100000&lt;/code&gt; to compare different result sizes. The command reports the actual number returned and warns if the database contains fewer records than requested.&lt;/p&gt;

&lt;p&gt;There are a few details in the &lt;a href="https://github.com/maiobarbero/eloquent_builder_raw/blob/main/app/Console/Commands/BenchmarkDatabaseAccess.php" rel="noopener noreferrer"&gt;measurement implementation&lt;/a&gt; that affect how we read the results.&lt;/p&gt;

&lt;p&gt;The raw SQL string is compiled once, outside the timed section. Query Builder and Eloquent rebuild their queries on every measured execution. All strategies use the same model connection, query logging is disabled, and their execution order rotates between iterations.&lt;/p&gt;

&lt;p&gt;Timing covers the strategy call through result creation. Memory is measured while the result is still alive, with the peak reset before each sample. Result destruction happens outside the timed section.&lt;/p&gt;

&lt;p&gt;This measures fetching and holding the results in an already running application. It doesn't include application startup, rendering a response, or serializing all the models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;These are the measurements from my runs, grouped into one table.&lt;/p&gt;

&lt;p&gt;Median and p95 describe execution time; p95 is the sample's 95th percentile. With 30 iterations, this command reports the second-slowest sample as p95, so it gives only a rough view of the slower executions. &lt;code&gt;Memory Δ&lt;/code&gt; is the median increase in PHP-reported memory while the result remains alive. &lt;code&gt;Peak Δ&lt;/code&gt; is the median increase in peak memory during each execution. Neither memory column represents the entire process's memory consumption.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;Memory Δ&lt;/th&gt;
&lt;th&gt;Peak Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Raw SQL&lt;/td&gt;
&lt;td&gt;0.037 ms&lt;/td&gt;
&lt;td&gt;0.042 ms&lt;/td&gt;
&lt;td&gt;8.22 KiB&lt;/td&gt;
&lt;td&gt;10.49 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Query Builder&lt;/td&gt;
&lt;td&gt;0.054 ms&lt;/td&gt;
&lt;td&gt;0.061 ms&lt;/td&gt;
&lt;td&gt;8.39 KiB&lt;/td&gt;
&lt;td&gt;11.74 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Eloquent&lt;/td&gt;
&lt;td&gt;0.187 ms&lt;/td&gt;
&lt;td&gt;0.200 ms&lt;/td&gt;
&lt;td&gt;22.14 KiB&lt;/td&gt;
&lt;td&gt;29.40 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;Raw SQL&lt;/td&gt;
&lt;td&gt;0.098 ms&lt;/td&gt;
&lt;td&gt;0.119 ms&lt;/td&gt;
&lt;td&gt;75.09 KiB&lt;/td&gt;
&lt;td&gt;77.37 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;Query Builder&lt;/td&gt;
&lt;td&gt;0.117 ms&lt;/td&gt;
&lt;td&gt;0.149 ms&lt;/td&gt;
&lt;td&gt;75.27 KiB&lt;/td&gt;
&lt;td&gt;78.62 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;Eloquent&lt;/td&gt;
&lt;td&gt;0.829 ms&lt;/td&gt;
&lt;td&gt;0.857 ms&lt;/td&gt;
&lt;td&gt;212.77 KiB&lt;/td&gt;
&lt;td&gt;225.73 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;Raw SQL&lt;/td&gt;
&lt;td&gt;0.675 ms&lt;/td&gt;
&lt;td&gt;0.699 ms&lt;/td&gt;
&lt;td&gt;746.50 KiB&lt;/td&gt;
&lt;td&gt;748.77 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;Query Builder&lt;/td&gt;
&lt;td&gt;0.691 ms&lt;/td&gt;
&lt;td&gt;0.707 ms&lt;/td&gt;
&lt;td&gt;746.67 KiB&lt;/td&gt;
&lt;td&gt;750.02 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;Eloquent&lt;/td&gt;
&lt;td&gt;7.225 ms&lt;/td&gt;
&lt;td&gt;7.274 ms&lt;/td&gt;
&lt;td&gt;2.07 MiB&lt;/td&gt;
&lt;td&gt;2.14 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;Raw SQL&lt;/td&gt;
&lt;td&gt;6.867 ms&lt;/td&gt;
&lt;td&gt;8.458 ms&lt;/td&gt;
&lt;td&gt;7.35 MiB&lt;/td&gt;
&lt;td&gt;7.35 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;Query Builder&lt;/td&gt;
&lt;td&gt;6.803 ms&lt;/td&gt;
&lt;td&gt;7.375 ms&lt;/td&gt;
&lt;td&gt;7.35 MiB&lt;/td&gt;
&lt;td&gt;7.35 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;Eloquent&lt;/td&gt;
&lt;td&gt;73.451 ms&lt;/td&gt;
&lt;td&gt;75.817 ms&lt;/td&gt;
&lt;td&gt;20.78 MiB&lt;/td&gt;
&lt;td&gt;21.42 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;Raw SQL&lt;/td&gt;
&lt;td&gt;93.046 ms&lt;/td&gt;
&lt;td&gt;94.269 ms&lt;/td&gt;
&lt;td&gt;72.96 MiB&lt;/td&gt;
&lt;td&gt;72.96 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;Query Builder&lt;/td&gt;
&lt;td&gt;93.053 ms&lt;/td&gt;
&lt;td&gt;94.270 ms&lt;/td&gt;
&lt;td&gt;72.96 MiB&lt;/td&gt;
&lt;td&gt;72.96 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;Eloquent&lt;/td&gt;
&lt;td&gt;802.986 ms&lt;/td&gt;
&lt;td&gt;882.579 ms&lt;/td&gt;
&lt;td&gt;207.23 MiB&lt;/td&gt;
&lt;td&gt;213.06 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The environment table isn't included with these recorded results, so the absolute timings should be read as observations from these runs, without assuming a particular database driver, machine, or PHP configuration. MySQL above is the compilation example. When repeating the benchmark, keep the environment output alongside your results, and also record the database version, hardware, and whether OPcache, JIT, or Xdebug is enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query Builder and raw SQL stay close
&lt;/h3&gt;

&lt;p&gt;At 10, 100, and 1,000 rows, Query Builder adds approximately 0.016–0.019 milliseconds to the median time.&lt;/p&gt;

&lt;p&gt;At 10,000 rows it appears slightly faster than raw SQL, by 0.064 milliseconds. At 100,000 rows it is slower by 0.007 milliseconds. Those small differences don't establish a performance advantage in either direction; they are consistent with variation between runs.&lt;/p&gt;

&lt;p&gt;The retained-memory difference is similarly small: 176 bytes in the smaller runs and 192 bytes in the larger ones. That isn't the total amount of memory allocated while building the query. Temporary builder state can already be gone when the command measures the returned result.&lt;/p&gt;

&lt;p&gt;For this simple query, constructing and compiling the builder adds little measurable cost. The difference remains small as the result grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eloquent's additional cost grows with the rows
&lt;/h3&gt;

&lt;p&gt;The Eloquent difference follows another pattern:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Extra median time over Query Builder&lt;/th&gt;
&lt;th&gt;Extra retained memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;+0.133 ms&lt;/td&gt;
&lt;td&gt;13.75 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;+0.713 ms&lt;/td&gt;
&lt;td&gt;137.50 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;+6.534 ms&lt;/td&gt;
&lt;td&gt;1.34 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;+66.647 ms&lt;/td&gt;
&lt;td&gt;13.43 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;+709.933 ms&lt;/td&gt;
&lt;td&gt;134.28 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These differences come from the command's unrounded summaries, so subtracting the displayed medians can occasionally differ by 0.001 milliseconds.&lt;/p&gt;

&lt;p&gt;At 10 rows, the absolute cost is small. At 100,000 rows, Eloquent adds about 710 milliseconds and 134 MiB of retained memory over Query Builder. Its total median time is roughly 8.6 times as high, and its retained-memory increase is about 2.8 times as large.&lt;/p&gt;

&lt;p&gt;The additional retained memory works out to approximately 1.375 KiB per row throughout these runs. That consistency fits the model creation path we followed earlier. It is a measurement for this model and dataset, not a fixed size for every Eloquent model.&lt;/p&gt;

&lt;p&gt;The benchmark doesn't isolate hydration from all other Eloquent work. The difference includes Eloquent's query setup and result handling as well as model creation. Still, the way the gap grows with the result count is consistent with hydration being the main additional cost in this example.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from this comparison
&lt;/h2&gt;

&lt;p&gt;For this query, I don't see a performance reason to replace Query Builder with raw SQL. The measured difference is very small, even when retrieving 100,000 records.&lt;/p&gt;

&lt;p&gt;Eloquent involves a more substantial decision because it returns models with state and behavior. With a small result set, the absolute overhead in these runs is a fraction of a millisecond. With a large result set, creating and retaining all those models becomes a significant part of the operation.&lt;/p&gt;

&lt;p&gt;For a read that only needs row data, Query Builder already avoids that model creation step. When replacing an Eloquent query with &lt;code&gt;DB::table()&lt;/code&gt;, any conditions previously supplied by global scopes, such as soft-delete or tenant filters, need to be preserved explicitly. For code that uses model behavior, Eloquent's additional work has a purpose.&lt;/p&gt;

&lt;p&gt;I would also look at the number of records being loaded before changing the query API. Even raw SQL retained nearly 73 MiB for 100,000 users in this test. Avoiding hydration reduces that cost, but fetching fewer rows or processing them in batches addresses the size of the result itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;For another walk through Laravel's internals, &lt;a href="https://dev.to/articles/laravel-13-scheduler-internals/"&gt;Inside the Laravel 13 Scheduler&lt;/a&gt; follows scheduled tasks from their PHP definitions to execution. The &lt;a href="https://github.com/maiobarbero/eloquent_builder_raw/blob/main/app/Console/Commands/BenchmarkDatabaseAccess.php" rel="noopener noreferrer"&gt;benchmark source&lt;/a&gt; is the place to start if you want to repeat this comparison with your own model and database.&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>programming</category>
      <category>coding</category>
    </item>
    <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>
  </channel>
</rss>
