<?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: Steve McDougall</title>
    <description>The latest articles on DEV Community by Steve McDougall (@juststevemcd).</description>
    <link>https://dev.to/juststevemcd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F95943%2F1189e345-5ada-4adb-ad56-9033d3ef454c.jpeg</url>
      <title>DEV Community: Steve McDougall</title>
      <link>https://dev.to/juststevemcd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/juststevemcd"/>
    <language>en</language>
    <item>
      <title>The Mid-Level Mindset</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 12:01:05 +0000</pubDate>
      <link>https://dev.to/juststevemcd/the-mid-level-mindset-41d9</link>
      <guid>https://dev.to/juststevemcd/the-mid-level-mindset-41d9</guid>
      <description>&lt;p&gt;We started this series with a single paragraph from a fictional client. A vague brief about a tool where clients can submit requests and developers can track them. Nine articles later, we have a fully interrogated set of requirements, a collection of properly shaped features, a trusted data model, a system architecture diagram, a layered application design, and a sequenced build plan with individual tickets ready to pick up.&lt;/p&gt;

&lt;p&gt;We have not written a single migration. We have not scaffolded a controller or configured a route. And yet the most important work is done.&lt;/p&gt;

&lt;p&gt;That is the point this entire series has been building towards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changed
&lt;/h2&gt;

&lt;p&gt;Think back to where we started. The instinct to open your editor the moment a brief lands. The pull towards action, towards visible progress, towards the feeling of productivity that comes from lines of code appearing on a screen.&lt;/p&gt;

&lt;p&gt;That instinct is not wrong. It is just premature.&lt;/p&gt;

&lt;p&gt;What changed over the course of this series is not the tools you have access to. Laravel was always capable of building Clarity. The database was always going to need an &lt;code&gt;organisations&lt;/code&gt; table and a &lt;code&gt;users&lt;/code&gt; table and a &lt;code&gt;requests&lt;/code&gt; table with the right foreign keys. The layered architecture was always the right approach. None of that knowledge was new.&lt;/p&gt;

&lt;p&gt;What changed is the order of operations. The willingness to invest thinking before typing. The discipline to ask "what do I not yet know?" before asking "how do I build this?". That shift in sequence is what defines the move from junior to mid-level, and it compounds over time in ways that are hard to see until you look back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Mid-Level Actually Means
&lt;/h2&gt;

&lt;p&gt;The industry uses the term loosely, but when I think about what a mid-level developer actually is, I come back to a few specific qualities.&lt;/p&gt;

&lt;p&gt;A mid-level developer can be handed a brief and produce something buildable from it without constant direction. Not because they know every answer upfront, but because they know which questions to ask and how to find the answers before they start building.&lt;/p&gt;

&lt;p&gt;A mid-level developer understands that the most expensive code to write is code that solves the wrong problem. They protect against that by doing the thinking work first.&lt;/p&gt;

&lt;p&gt;A mid-level developer can communicate the shape of a problem to other people. They can draw a diagram, write a pitch, or explain a data model in plain language. That communication ability is what makes them valuable beyond their own output.&lt;/p&gt;

&lt;p&gt;A mid-level developer knows when to apply a pattern and when to leave it out. They have moved past the phase of applying everything they have learned uniformly, and into the phase of using judgment about what a given situation actually needs.&lt;/p&gt;

&lt;p&gt;None of those things are about programming language knowledge or framework familiarity. They are about thinking habits. And thinking habits are built through deliberate practice, not through reading documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Process in Brief
&lt;/h2&gt;

&lt;p&gt;If you take one thing from this series, let it be this sequence. Apply it to every project you work on, every feature you are handed, every ticket that lands in your queue.&lt;/p&gt;

&lt;p&gt;First, interrogate the requirement. Surface every unstated assumption. Ask the business questions before the technical ones. Write down what you know and what you do not know yet.&lt;/p&gt;

&lt;p&gt;Second, define the actors and their needs. Write user stories with acceptance criteria. Make sure every feature has a clear definition of done that is anchored to a real user getting real value.&lt;/p&gt;

&lt;p&gt;Third, shape the work. Decide how much it is worth before you estimate how long it will take. Draw the edges of what is included and write down what is explicitly out of scope.&lt;/p&gt;

&lt;p&gt;Fourth, model the data. Draw the ERD before you touch a migration. Check every relationship for cardinality, every foreign key for direction, every array in a column for the related table it should be.&lt;/p&gt;

&lt;p&gt;Fifth, draw the system. Show the major components, how they connect, where external dependencies sit, and where data flows. Write the plain-language description. If you cannot describe it clearly in prose, the diagram needs more work.&lt;/p&gt;

&lt;p&gt;Sixth, sequence the build. Map the dependencies. Identify what has to exist before each feature can be built. Break each feature into discrete tickets with clear definitions of done.&lt;/p&gt;

&lt;p&gt;Then build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Take This
&lt;/h2&gt;

&lt;p&gt;This process is a foundation, not a ceiling. As you apply it to more projects, you will start to develop instincts about where the complexity hides in a brief, which ERD patterns recur across different problem domains, and which architectural decisions tend to cause the most pain if you get them wrong.&lt;/p&gt;

&lt;p&gt;Those instincts are what senior developers have. They have run this process enough times, on enough different problems, that parts of it become automatic. They spot the ambiguous noun in a brief before the client finishes reading it to them. They see the missing pivot table in a data model before anyone has drawn it. They know from experience which features sound small and turn out to be enormous.&lt;/p&gt;

&lt;p&gt;You build those instincts by doing the work deliberately, even when it feels slower than just opening your editor. Especially then.&lt;/p&gt;

&lt;p&gt;A few resources worth spending time with as you continue:&lt;/p&gt;

&lt;p&gt;Ryan Singer's Shape Up is the deepest treatment of the shaping process I have found. It is free, it is short, and every developer who works on product software should read it.&lt;/p&gt;

&lt;p&gt;Jeff Patton's User Story Mapping goes further on the user story side than we covered here, and is particularly useful when you are working on complex products with many different user types.&lt;/p&gt;

&lt;p&gt;John Ousterhout's A Philosophy of Software Design is the best writing I know of on the question of where complexity comes from and how to fight it. It will change the way you think about every design decision you make.&lt;/p&gt;

&lt;p&gt;And then there is the work itself. Take a project, real or fictional, and run the full process from brief to build plan before you write the first line of code. Do it again on the next one. The process becomes faster with practice, but it never stops being valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Final Word on Clarity
&lt;/h2&gt;

&lt;p&gt;We built something real in this series. Clarity started as a vague paragraph and became a fully designed application with a clear data model, a considered architecture, and a sequenced plan ready to execute.&lt;/p&gt;

&lt;p&gt;We never wrote a migration. We never wrote a controller. But anyone who has followed this series could pick up the build plan from article nine and start building Clarity today, with confidence that the foundations are solid and the direction is clear.&lt;/p&gt;

&lt;p&gt;That confidence, grounded in thinking rather than assumption, is what the mid-level mindset feels like from the inside.&lt;/p&gt;

&lt;p&gt;It is worth developing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for following along with From Requirements to Reality. If this series helped you think differently about how you approach a problem before you start building, that is exactly what it was designed to do.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>developermindset</category>
      <category>careergrowth</category>
      <category>architecture</category>
      <category>requirements</category>
    </item>
    <item>
      <title>From Diagram To Implementation Plan</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 12:01:04 +0000</pubDate>
      <link>https://dev.to/juststevemcd/from-diagram-to-implementation-plan-5coi</link>
      <guid>https://dev.to/juststevemcd/from-diagram-to-implementation-plan-5coi</guid>
      <description>&lt;p&gt;We have covered a lot of ground in this series. We started with a vague client brief, interrogated it until the real requirements surfaced, wrote user stories with acceptance criteria, shaped those stories into bounded features, drew an ERD, built a system diagram, and looked at how layers give every piece of code a clear home.&lt;/p&gt;

&lt;p&gt;That is the full design process. Now we need to translate it into something a developer can actually execute.&lt;/p&gt;

&lt;p&gt;This article is about sequencing. Taking everything we have designed and turning it into an ordered build plan that respects dependencies, minimises wasted work, and lets you deliver value as early as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Sequence Matters
&lt;/h2&gt;

&lt;p&gt;A common junior developer instinct is to start with the most interesting part. The feature that sounds fun to build, the component that uses a technology you want to learn, the part of the system that feels most novel. That is understandable, but it is usually the wrong starting point.&lt;/p&gt;

&lt;p&gt;The right starting point is the foundation. The code that everything else depends on. If you build the request management system before you have authentication in place, you will either need to stub out user identity in ways that do not reflect reality, or you will need to go back and retrofit it later. Both options cost time you cannot get back.&lt;/p&gt;

&lt;p&gt;Sequencing is the discipline of asking: what has to exist before this can be built? Work backwards from every feature until you hit something that has no dependencies, and that is where you start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying Dependencies
&lt;/h2&gt;

&lt;p&gt;Let me map the dependencies for the Clarity features we shaped in article four.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client authentication and onboarding&lt;/strong&gt; has no dependencies on other features. It needs the database and the user model, but those are foundational. This goes first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team member management&lt;/strong&gt; depends on authentication being in place (admins need to be logged in to invite people) and on the user model having role support. It is almost as foundational as authentication itself, and it needs to exist before any meaningful testing of the application can happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request submission&lt;/strong&gt; depends on authentication (to know who is submitting) and on organisations existing (to associate the request correctly). Team member management should also be done first so there is at least one admin user to test with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request status and assignment&lt;/strong&gt; depends on requests existing. It also depends on team members existing, because you cannot assign a request to a developer who does not exist in the system. This comes after submission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment thread&lt;/strong&gt; depends on requests existing. It could technically be built in parallel with request status and assignment, but sharing the request detail page means they are better built in sequence to avoid integration friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request list views&lt;/strong&gt; depend on requests existing and on the basic request detail page being in place. This should be the last feature built, because it is a read view over data that all the other features create.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Build Order
&lt;/h2&gt;

&lt;p&gt;With dependencies mapped, the sequence becomes clear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Foundations: database, models, migrations, and base application structure&lt;/li&gt;
&lt;li&gt;Authentication: client login, team member login, invitation flow&lt;/li&gt;
&lt;li&gt;Team member management: invite, role assignment, deactivation&lt;/li&gt;
&lt;li&gt;Request submission: form, validation, attachment upload, status initialisation&lt;/li&gt;
&lt;li&gt;Request status and assignment: status transitions, developer assignment, client visibility&lt;/li&gt;
&lt;li&gt;Comment thread: posting, internal flag, client-facing view&lt;/li&gt;
&lt;li&gt;Request list views: client list, team list, basic status filtering&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step builds on the last. Each step produces something testable before the next step begins. And each step delivers a piece of working software rather than a collection of half-built components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the Tickets
&lt;/h2&gt;

&lt;p&gt;A build order is not yet a set of tickets. You still need to break each step into discrete pieces of work that a developer can pick up and complete in a day or two.&lt;/p&gt;

&lt;p&gt;Here is how I would break down step four, request submission, into individual tickets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ticket: Request model and migration&lt;/strong&gt;&lt;br&gt;
Create the &lt;code&gt;requests&lt;/code&gt; table migration with all columns from the ERD. Create the &lt;code&gt;Request&lt;/code&gt; Eloquent model with the &lt;code&gt;HasUlids&lt;/code&gt; trait, fillable columns, and the &lt;code&gt;submitter&lt;/code&gt; and &lt;code&gt;assignee&lt;/code&gt; relationships. Write a model factory for use in tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ticket: Attachment model and migration&lt;/strong&gt;&lt;br&gt;
Create the &lt;code&gt;attachments&lt;/code&gt; table migration. Create the &lt;code&gt;Attachment&lt;/code&gt; model with its relationship back to &lt;code&gt;Request&lt;/code&gt;. Configure the file storage driver. Write a model factory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ticket: Submit request endpoint&lt;/strong&gt;&lt;br&gt;
Create the &lt;code&gt;StoreRequestRequest&lt;/code&gt; form request with validation rules and a &lt;code&gt;payload()&lt;/code&gt; method. Create the &lt;code&gt;StoreRequestPayload&lt;/code&gt; DTO. Create the &lt;code&gt;SubmitRequest&lt;/code&gt; action. Create the &lt;code&gt;StoreController&lt;/code&gt;. Wire up the route. Write feature tests covering successful submission, validation failures, and attachment handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ticket: Request detail page&lt;/strong&gt;&lt;br&gt;
Create the Livewire component for the request detail view. Display title, description, status, assignee, and attachments. Scope the view so clients only see their own requests. Write feature tests covering client access and unauthorised access attempts.&lt;/p&gt;

&lt;p&gt;Four tickets for one feature, each independently completable and testable. A developer picking up any one of these knows exactly what done looks like, because the acceptance criteria from the user stories map directly onto the test cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Relationship Between Stories and Tickets
&lt;/h2&gt;

&lt;p&gt;User stories describe what the system should do from a user's perspective. Tickets describe the technical work required to make that happen. They are not the same thing, and conflating them is a common source of confusion.&lt;/p&gt;

&lt;p&gt;A single user story often maps to multiple tickets. The "submit a request" story from article three maps to at least the four tickets above. That is fine. The story is the goal. The tickets are the path.&lt;/p&gt;

&lt;p&gt;When you write a ticket, you should be able to point to the user story it is serving. If a ticket cannot be connected to a user story, ask whether it needs to exist. Infrastructure work (setting up CI, configuring deployment, writing base test helpers) is an exception, but application feature work should always be traceable back to a user need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the Plan Against the ERD
&lt;/h2&gt;

&lt;p&gt;Before you start building, do one final check. Go back to your ERD and make sure every entity and every relationship has a corresponding ticket.&lt;/p&gt;

&lt;p&gt;In the Clarity plan, let me verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organisation: covered in the authentication step&lt;/li&gt;
&lt;li&gt;User: covered in authentication and team member management&lt;/li&gt;
&lt;li&gt;Request: covered in request submission&lt;/li&gt;
&lt;li&gt;Comment: covered in the comment thread step&lt;/li&gt;
&lt;li&gt;Attachment: covered in request submission&lt;/li&gt;
&lt;li&gt;All relationships: each foreign key maps to a ticket that creates the parent entity before the child entity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any entity in your ERD does not have a corresponding ticket, you have a gap. Either you have forgotten something, or that entity is not actually needed for the current scope. Either way, better to find that now than two weeks into the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plan as a Living Document
&lt;/h2&gt;

&lt;p&gt;A build plan is not a contract. Requirements change, technical discoveries shift priorities, and features that seemed simple turn out to be more complex than they appeared. A good plan is one that absorbs those changes without falling apart.&lt;/p&gt;

&lt;p&gt;The way you make a plan resilient is by keeping the dependencies clear. If you know that request status depends on request submission, and request submission depends on authentication, then when authentication takes longer than expected you can immediately see what is blocked and communicate that clearly. You are not surprised. You are just updating the schedule with an accurate picture of what is affected.&lt;/p&gt;

&lt;p&gt;That kind of clarity is what teams rely on mid-level developers to provide. Not perfect predictions, but an accurate, up-to-date map of where the work stands and what it connects to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Into Practice
&lt;/h2&gt;

&lt;p&gt;Take the project you have been working with throughout this series. Map the dependencies between your shaped features. Write a build order. Then break the first two steps into individual tickets.&lt;/p&gt;

&lt;p&gt;For each ticket, make sure you can answer these questions: what does done look like? What does it depend on? Which user story does it serve?&lt;/p&gt;

&lt;p&gt;If any of those questions is hard to answer, the ticket needs more work before anyone picks it up.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the final article, we are going to step back from Clarity and talk about what this entire process represents: the shift in thinking that defines a mid-level developer, and where to take these skills from here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>implementationplanning</category>
      <category>dependencies</category>
      <category>projectplanning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Thinking In Layers</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 12:00:32 +0000</pubDate>
      <link>https://dev.to/juststevemcd/thinking-in-layers-3843</link>
      <guid>https://dev.to/juststevemcd/thinking-in-layers-3843</guid>
      <description>&lt;p&gt;Ask a junior developer where a piece of logic should live and you will usually get one of two answers: the controller, or the model. Ask a mid-level developer the same question and they will ask you a question back: what kind of logic is it?&lt;/p&gt;

&lt;p&gt;That distinction is the heart of this article.&lt;/p&gt;

&lt;p&gt;Separation of concerns is one of those principles you hear about early in your career and nod along to without fully internalising what it means in practice. It sounds obvious. Of course different concerns should be separated. But when you are staring at a controller that has grown to three hundred lines and you are not sure how it got there, the theory suddenly feels less clear than it did.&lt;/p&gt;

&lt;p&gt;In this article we are going to look at what separation of concerns actually means inside a Laravel application, why fat controllers are a symptom of an architectural problem rather than a code quality problem, and how thinking in layers changes the way you make decisions about where code should live.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Layer Actually Is
&lt;/h2&gt;

&lt;p&gt;A layer is a group of code that has a single, well-defined job. Code inside a layer should only do that job. When it needs to do something outside that job, it should hand off to a different layer rather than doing the work itself.&lt;/p&gt;

&lt;p&gt;In a Laravel application, the layers I work with look like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The HTTP layer&lt;/strong&gt; handles everything related to the incoming request and the outgoing response. Controllers, middleware, form requests, and responses live here. This layer's job is to receive input, validate it, hand it to the business layer, and return a response. It does not make business decisions. It does not query the database directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The business layer&lt;/strong&gt; contains the logic that makes your application do what it is supposed to do. Actions, services, and domain objects live here. This layer does not know anything about HTTP. It receives data, applies rules, and returns results. It can talk to the data layer to read or write records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data layer&lt;/strong&gt; handles persistence. Models, query builders, and repository classes live here. This layer knows how to find, create, update, and delete records. It does not apply business rules. It does not know about HTTP.&lt;/p&gt;

&lt;p&gt;Three layers. Three jobs. Each one knowing about its own responsibilities and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Fat Controllers Happen
&lt;/h2&gt;

&lt;p&gt;A fat controller is not a discipline problem. It is an architectural vacuum.&lt;/p&gt;

&lt;p&gt;When a team does not have a clear shared understanding of what belongs where, logic accumulates in the most obvious place. Controllers are obvious. They are where the request arrives, so they are where people start writing code. And once code is there, more code gets added next to it because it is easier to extend an existing file than to create a new one.&lt;/p&gt;

&lt;p&gt;Before long you have a controller that validates input, queries the database, sends emails, dispatches jobs, formats responses, and handles error cases. It has absorbed everything because nothing had a clear home.&lt;/p&gt;

&lt;p&gt;The fix is not to refactor the controller. It is to give every kind of logic a home so that developers always know where to put new code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying Layers to Clarity
&lt;/h2&gt;

&lt;p&gt;Let me walk through a concrete example using Clarity's request submission feature.&lt;/p&gt;

&lt;p&gt;A client submits a new request. Here is what needs to happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the input (title required, description required, attachments optional)&lt;/li&gt;
&lt;li&gt;Create the request record associated with the client's organisation&lt;/li&gt;
&lt;li&gt;Store any uploaded attachments&lt;/li&gt;
&lt;li&gt;Set the initial status to "submitted"&lt;/li&gt;
&lt;li&gt;Return a response confirming the submission&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without layers, all of that logic tends to end up in the controller. Here is what that looks like, and why it is a problem:&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;// The kind of controller you want to avoid&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;store&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="kt"&gt;JsonResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$validated&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="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:255'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'attachments.*'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'nullable'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:10240'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$projectRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProjectRequest&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;'organisation_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;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="n"&gt;organisation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'submitted_by'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$validated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$validated&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;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'submitted'&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;'attachments'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;'attachments'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;'attachments'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$projectRequest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;attachments&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="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="s1"&gt;'filename'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;getClientOriginalName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'mime_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="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;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;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$projectRequest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'attachments'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not terrible code. It works. But it is doing too many jobs at once. It is validating, creating records, handling file storage, and formatting a response, all in the same method. Testing it in isolation is difficult because it is deeply coupled to the HTTP request object. Reusing any of this logic (say, if requests could also be submitted via an import job) means duplicating it or extracting it under pressure.&lt;/p&gt;

&lt;p&gt;Now here is the same feature with layers applied:&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;// The Form Request handles validation&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoreRequestRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rules&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;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:255'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'attachments.*'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'nullable'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:10240'&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;payload&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;StoreRequestPayload&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StoreRequestPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;organisationId&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;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="n"&gt;organisation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;submittedBy&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="n"&gt;title&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;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;description&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;input&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="n"&gt;attachments&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="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'attachments'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The Action handles the business logic&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SubmitRequest&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;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StoreRequestPayload&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ProjectRequest&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$projectRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProjectRequest&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;'organisation_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;organisationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'submitted_by'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;submittedBy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'submitted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;attachments&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;'attachments'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$projectRequest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;attachments&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="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;submittedBy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'filename'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;getClientOriginalName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'mime_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="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;$projectRequest&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The Controller handles HTTP in and HTTP out&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoreController&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;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;SubmitRequest&lt;/span&gt; &lt;span class="nv"&gt;$submitRequest&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;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StoreRequestRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$projectRequest&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;submitRequest&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="n"&gt;payload&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="nf"&gt;payload&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="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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$projectRequest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'attachments'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller is now four lines of meaningful logic. It receives a validated payload, calls an action, and returns a response. It knows nothing about how the request is created or how attachments are stored. Those are the action's job.&lt;/p&gt;

&lt;p&gt;The action knows nothing about HTTP. It receives a payload object and does the work. You could call it from a console command, a queued job, or a test, and it would behave identically each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Model Fits In
&lt;/h2&gt;

&lt;p&gt;You will notice the model does not appear much in the code above beyond &lt;code&gt;ProjectRequest::create()&lt;/code&gt; and the &lt;code&gt;attachments()&lt;/code&gt; relationship. That is intentional.&lt;/p&gt;

&lt;p&gt;Models in a layered application are responsible for representing a database record and its relationships. They are not responsible for business rules, validation, or application logic. A model that has methods like &lt;code&gt;submitAndNotifyClient()&lt;/code&gt; or &lt;code&gt;assignToAvailableDeveloper()&lt;/code&gt; is doing the action's job, and it will become harder to test and maintain as that logic grows.&lt;/p&gt;

&lt;p&gt;Keep models focused. They know about their table, their columns, their relationships, and their casts. Everything else belongs in a layer above them.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Is Not Dogma
&lt;/h2&gt;

&lt;p&gt;I want to be clear that layers are a tool, not a religion. A simple CRUD endpoint that reads a record and returns it does not need an action class. Adding indirection where none is needed creates noise without creating value.&lt;/p&gt;

&lt;p&gt;The question to ask is: is this logic complex enough, or reusable enough, that it deserves its own home? If the answer is yes, extract it. If the answer is no, keep it in the controller and move on.&lt;/p&gt;

&lt;p&gt;Mid-level developers learn to make that call. Juniors often apply patterns uniformly regardless of context, which leads to over-engineering simple things. The goal is judgment, not rule-following.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Into Practice
&lt;/h2&gt;

&lt;p&gt;Take a controller from a project you have worked on and look at it honestly. How many jobs is it doing? Can you identify which lines belong in the HTTP layer, which belong in the business layer, and which belong in the data layer?&lt;/p&gt;

&lt;p&gt;You do not need to refactor it right now. Just practice the act of categorising what you see. That categorisation instinct is what you are building, and it comes from practice more than from reading.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article, we are going to bring everything together and look at how to turn your ERD and architecture diagram into a sequenced build plan that a developer can actually follow.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>separationofconcerns</category>
      <category>layeredarchitecture</category>
      <category>laravel</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Introduction To Systems Architecture</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 12:00:30 +0000</pubDate>
      <link>https://dev.to/juststevemcd/introduction-to-systems-architecture-4188</link>
      <guid>https://dev.to/juststevemcd/introduction-to-systems-architecture-4188</guid>
      <description>&lt;p&gt;We have a clear problem definition, a set of shaped features, and a data model we trust. At this point, a lot of developers would consider the design work done and start writing code. And honestly, for a project the size of Clarity, you probably could. The application is small enough that its structure is mostly implied by the framework.&lt;/p&gt;

&lt;p&gt;But this series is about building the habits that carry you through larger, more complex projects. And on those projects, skipping the architecture diagram is where things start to quietly go wrong.&lt;/p&gt;

&lt;p&gt;In this article I want to introduce you to what architecture actually means for application developers, and show you how to draw a simple system diagram for Clarity that captures how the pieces fit together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Architecture Means at This Level
&lt;/h2&gt;

&lt;p&gt;When most developers hear "architecture", they think about large distributed systems, microservices, cloud infrastructure diagrams with dozens of boxes and arrows going everywhere. That is one kind of architecture, and it is not what we are talking about here.&lt;/p&gt;

&lt;p&gt;For application developers, architecture is about a simpler set of questions. What are the major components of this system? How does data flow between them? What are the boundaries between different concerns? Where do external dependencies plug in?&lt;/p&gt;

&lt;p&gt;You do not need to be designing a distributed system to benefit from answering those questions. Even a straightforward Laravel application has meaningful architecture: a web layer that handles HTTP, a business logic layer that does the actual work, a data layer that talks to the database, and often a set of external integrations sitting alongside all of that. Understanding where those layers are and how they connect is what lets you make good decisions about where code should live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components vs Layers
&lt;/h2&gt;

&lt;p&gt;There are two ways to think about application structure, and both are useful in different contexts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layers&lt;/strong&gt; describe a vertical slice through your application. Think of the classic presentation layer, business logic layer, data access layer split. Data comes in at the top, passes through the layers, and comes out as a response. Each layer only talks to the layer directly below it. This is a useful mental model for thinking about separation of concerns inside a single application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Components&lt;/strong&gt; describe horizontal groupings of related functionality. Authentication is a component. The request management system is a component. Notifications are a component. A component might span multiple layers internally, but it has a clear boundary and a clear responsibility.&lt;/p&gt;

&lt;p&gt;In practice, most applications use both mental models at once. The layers tell you how code flows inside a component. The components tell you how the system is divided at a higher level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawing a System Diagram
&lt;/h2&gt;

&lt;p&gt;A system diagram does not need to be elaborate. Its job is to show the major components of your system, how they connect, and where external dependencies sit. You are not trying to document every class or every database query. You are drawing a map that helps you and your team understand the shape of what you are building.&lt;/p&gt;

&lt;p&gt;Here is a Mermaid diagram for Clarity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    Browser["Browser / Client"]

    subgraph App ["Clarity Application"]
        Web["Web Layer\n(Routes, Controllers, Middleware)"]
        Auth["Auth Component\n(Login, Invitation, Sessions)"]
        Requests["Request Management\n(Submit, Status, Assignment)"]
        Comments["Comments\n(Post, Internal Flag)"]
        Attachments["Attachments\n(Upload, Storage)"]
        Notifications["Notifications\n(Status Changes, Comments)"]
    end

    subgraph Data ["Data Layer"]
        DB[("PostgreSQL")]
        Storage["File Storage"]
        Cache["Cache"]
    end

    Browser --&amp;gt; Web
    Web --&amp;gt; Auth
    Web --&amp;gt; Requests
    Web --&amp;gt; Comments
    Web --&amp;gt; Attachments
    Requests --&amp;gt; Notifications
    Comments --&amp;gt; Notifications
    Auth --&amp;gt; DB
    Requests --&amp;gt; DB
    Comments --&amp;gt; DB
    Attachments --&amp;gt; DB
    Attachments --&amp;gt; Storage
    Auth --&amp;gt; Cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a detailed technical specification. It is a conversation starter. It shows that Clarity has a web layer handling incoming requests, five functional components sitting behind it, a database and file storage for persistence, a cache layer, and a notifications pathway that gets triggered by changes in the request and comment components.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Diagram Reveals
&lt;/h2&gt;

&lt;p&gt;Even a simple diagram like this surfaces useful questions.&lt;/p&gt;

&lt;p&gt;The notifications component appears as a box but is currently undefined. What does it actually do? Does it send emails? Does it push in-app notifications? Does it use a queue? That is a decision we have deferred, and the diagram makes that deferral visible. For the initial version of Clarity, we scoped email notifications on comments out of the first cycle. The component box is still there, which means when we come back to build it, we have a clear place for it to sit.&lt;/p&gt;

&lt;p&gt;The file storage box is separate from the database. That is intentional. Attachments are stored on disk or in an object store, not as blobs in the database. The diagram captures that decision explicitly. A developer joining the project later can see at a glance that attachments have their own storage concern, rather than discovering it buried in a model.&lt;/p&gt;

&lt;p&gt;The cache layer connects only to the auth component in this version. That is a starting point, not a permanent constraint. As Clarity grows and request list queries become more expensive, caching will expand to cover other components. Having it on the diagram from the start means that growth is natural rather than bolted on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture as a Communication Tool
&lt;/h2&gt;

&lt;p&gt;One of the things I value most about architecture diagrams is that they create a shared vocabulary for a team. When everyone can point at the same diagram and say "the request management component" or "the web layer", discussions become sharper. You spend less time talking past each other and more time talking about the actual problem.&lt;/p&gt;

&lt;p&gt;That shared vocabulary is especially valuable when something goes wrong. If a bug is affecting comments but not requests, a team with a clear component diagram can focus their investigation quickly. If performance is degrading under load, having the data flow documented means you can reason about where the bottleneck is likely to be.&lt;/p&gt;

&lt;p&gt;This is one of the reasons mid-level developers tend to be better debugging partners than juniors, even when the junior has been on the codebase longer. It is not that they know more code. It is that they think in components and flows rather than individual files and functions. The architecture diagram is how you start building that mental model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clarity's Architecture in Plain Language
&lt;/h2&gt;

&lt;p&gt;Let me describe the Clarity architecture in prose, the way you might explain it to a new team member.&lt;/p&gt;

&lt;p&gt;Clarity is a single Laravel application served over HTTP. All incoming requests pass through the web layer, which handles routing, authentication middleware, and request validation. Behind the web layer, the application is divided into four main functional areas: request management, comments, attachments, and authentication.&lt;/p&gt;

&lt;p&gt;Request management is the core of the application. It covers the full lifecycle of a client request from submission through completion, including status transitions and developer assignment. Comments sit alongside request management and share the same request context, with the addition of the internal flag that controls client visibility. Attachments handle file uploads and connect to external file storage rather than the database.&lt;/p&gt;

&lt;p&gt;Authentication covers client login via password, team member login, and the invitation flow. Session state is cached to reduce database load on authenticated requests.&lt;/p&gt;

&lt;p&gt;Notifications are triggered by events in the request and comment components. In the first version of Clarity, notifications are out of scope. The component is acknowledged in the architecture but not built yet.&lt;/p&gt;

&lt;p&gt;Data persistence uses PostgreSQL for relational data and a file storage service for attachments. The application does not use a search index or a message queue in the initial version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Into Practice
&lt;/h2&gt;

&lt;p&gt;Draw a system diagram for your own project using the same approach. Start with the major components you can identify, then add the data layer and any external dependencies. Draw arrows to show how data flows between them.&lt;/p&gt;

&lt;p&gt;Then write a plain-language description of the architecture the way I did above. If you struggle to write it in a few clear paragraphs, the diagram probably needs more work. The prose and the diagram should tell the same story.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article, we are going to look at separation of concerns in more detail, understand why the "fat controller" problem is really an architectural symptom, and start thinking about what belongs in each layer of a Laravel application.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>systemdesign</category>
      <category>applicationdesign</category>
      <category>planning</category>
    </item>
    <item>
      <title>Relationships, Cardinality, and the Questions Your Schema Is Asking</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 11:59:58 +0000</pubDate>
      <link>https://dev.to/juststevemcd/relationships-cardinality-and-the-questions-your-schema-is-asking-1559</link>
      <guid>https://dev.to/juststevemcd/relationships-cardinality-and-the-questions-your-schema-is-asking-1559</guid>
      <description>&lt;p&gt;In the last article we drew the Clarity ERD and ended up with a clean diagram covering five entities and seven relationships. If you followed along and drew one for your own project, you probably found a few things that surprised you. That is normal, and it is exactly the point.&lt;/p&gt;

&lt;p&gt;Now I want to go deeper. Drawing an ERD is one skill. Reading what it is telling you is another, and the second one is where the real value lives.&lt;/p&gt;

&lt;p&gt;In this article we are going to look at the three relationship types in detail, walk through the most common mistakes I see developers make when modelling data, and show you how to catch them at the diagram stage before they become painful migrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Relationships, Properly
&lt;/h2&gt;

&lt;p&gt;We covered these briefly in the last article, but they deserve more attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-to-One
&lt;/h3&gt;

&lt;p&gt;A one-to-one relationship means one record in table A maps to exactly one record in table B, and that mapping is unique on both sides.&lt;/p&gt;

&lt;p&gt;These are less common than people expect. When a developer reaches for a one-to-one, I always ask: why is this data in a separate table? Sometimes there is a good reason. You might have a &lt;code&gt;users&lt;/code&gt; table and a &lt;code&gt;user_profiles&lt;/code&gt; table where the profile data is large, infrequently accessed, and cleaner to separate. Or you might have a base &lt;code&gt;users&lt;/code&gt; table that is extended by either a &lt;code&gt;client_profiles&lt;/code&gt; table or a &lt;code&gt;team_member_profiles&lt;/code&gt; table depending on role.&lt;/p&gt;

&lt;p&gt;But a lot of the time, a one-to-one relationship is a sign that the data should just live in the same table. Before you commit to one, ask yourself whether there is a genuine reason for the separation or whether you are adding complexity without adding value.&lt;/p&gt;

&lt;p&gt;In Clarity, we do not have any one-to-one relationships. The model is clean enough that everything fits in its own entity without needing auxiliary tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-to-Many
&lt;/h3&gt;

&lt;p&gt;This is the workhorse of relational data modelling. One record in table A is referenced by many records in table B. The foreign key lives on the "many" side.&lt;/p&gt;

&lt;p&gt;In Clarity: one organisation has many users, so &lt;code&gt;organisation_id&lt;/code&gt; lives on the &lt;code&gt;users&lt;/code&gt; table. One request has many comments, so &lt;code&gt;request_id&lt;/code&gt; lives on the &lt;code&gt;comments&lt;/code&gt; table. The pattern is consistent.&lt;/p&gt;

&lt;p&gt;The mistake I see most often with one-to-many is putting the foreign key on the wrong side. If you put &lt;code&gt;user_ids&lt;/code&gt; (as some kind of serialised array) on the &lt;code&gt;organisations&lt;/code&gt; table instead of &lt;code&gt;organisation_id&lt;/code&gt; on the &lt;code&gt;users&lt;/code&gt; table, you have inverted the relationship and created a mess. You cannot join efficiently, you cannot enforce referential integrity, and you will hit a wall the moment you need to query it properly.&lt;/p&gt;

&lt;p&gt;The rule is simple: the foreign key always goes on the child. The "many" side. If you are unsure which side is the child, ask yourself which record is owned by the other. A user is owned by an organisation. A comment is owned by a request. The owned record holds the foreign key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Many-to-Many
&lt;/h3&gt;

&lt;p&gt;A many-to-many relationship means records on both sides can be connected to multiple records on the other side. You cannot model this directly with a foreign key on either table. You need a pivot table that sits between them and holds the connection.&lt;/p&gt;

&lt;p&gt;Clarity does not have a many-to-many in the current design, so let me add one to illustrate the point. Suppose we wanted to add tagging to requests, where a request can have many tags and a tag can be applied to many requests. That is a classic many-to-many.&lt;/p&gt;

&lt;p&gt;The entities involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request {
    uuid id
    string title
    ...
}

Tag {
    uuid id
    string name
    string colour
}

RequestTag {
    uuid request_id
    uuid tag_id
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;RequestTag&lt;/code&gt; pivot table is the relationship. It holds nothing but the two foreign keys (and sometimes additional data about the relationship itself, like a &lt;code&gt;created_at&lt;/code&gt; timestamp or a user who applied the tag).&lt;/p&gt;

&lt;p&gt;In Laravel, this maps to a &lt;code&gt;belongsToMany&lt;/code&gt; relationship on both models:&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;// Request model&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;tags&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;BelongsToMany&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;belongsToMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Tag&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;'request_tags'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Tag model&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;requests&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;BelongsToMany&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;belongsToMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Request&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;'request_tags'&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 common mistake here is not recognising when you have a many-to-many. Developers often start by modelling it as a one-to-many and then discover mid-build that the constraint they assumed does not hold. When you spot a noun in your application that sounds like it could belong to many different records of another type, treat it as a signal to check whether you are looking at a pivot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common ERD Mistakes
&lt;/h2&gt;

&lt;p&gt;Let me walk through the mistakes I see most often and how to spot them before they cause problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storing arrays in a column
&lt;/h3&gt;

&lt;p&gt;If you ever find yourself writing &lt;code&gt;tags: string&lt;/code&gt; or &lt;code&gt;assigned_to: json&lt;/code&gt; on an entity, stop. That is almost always a sign that the data belongs in a related table. Arrays in columns cannot be indexed properly, cannot be joined against, and are a maintenance headache the moment the data inside them grows or needs to be queried individually.&lt;/p&gt;

&lt;p&gt;In the Clarity ERD, &lt;code&gt;assigned_to&lt;/code&gt; is a single foreign key pointing at one user. That is correct because our business rule says a request has one assignee. But if the requirement had been "a request can be assigned to multiple developers", the right answer would be an &lt;code&gt;assignments&lt;/code&gt; pivot table, not a JSON array of user IDs in a column.&lt;/p&gt;

&lt;h3&gt;
  
  
  The missing pivot
&lt;/h3&gt;

&lt;p&gt;Related to the above: when you draw a many-to-many relationship between two entities without drawing the pivot table, you have an incomplete diagram. Always draw the pivot explicitly. It forces you to think about what data the relationship itself carries, and it makes the migration obvious when you get to that stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nullable foreign keys as a shortcut
&lt;/h3&gt;

&lt;p&gt;It is tempting to make a foreign key nullable when you are not sure whether a relationship will always exist. Sometimes that is correct. In Clarity, &lt;code&gt;assigned_to&lt;/code&gt; on &lt;code&gt;Request&lt;/code&gt; is nullable because a request might not have an assignee yet. That is a real business rule.&lt;/p&gt;

&lt;p&gt;But sometimes a nullable foreign key is a sign that the relationship is modelled incorrectly. If you find yourself with a record that should always belong to a parent but the foreign key keeps being null in practice, the relationship direction might be wrong, or the data might belong in a different table entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conflating user roles with user types
&lt;/h3&gt;

&lt;p&gt;This one is specific to applications like Clarity, but it comes up constantly. When your system has multiple types of users (clients and team members, customers and staff, students and teachers), there is a temptation to use a single &lt;code&gt;role&lt;/code&gt; column and call it done.&lt;/p&gt;

&lt;p&gt;Sometimes that is fine. But if the two user types have genuinely different data requirements, different relationships to other entities, or different constraints on what they can do, a single role column will not hold the weight. You end up with columns that are only relevant to one type of user, nullable columns everywhere, and logic scattered across your codebase to handle the differences.&lt;/p&gt;

&lt;p&gt;In the Clarity design I chose a single &lt;code&gt;users&lt;/code&gt; table with a &lt;code&gt;role&lt;/code&gt; column and a nullable &lt;code&gt;sub_role&lt;/code&gt;. That works for our use case because the data requirements for clients and team members are similar enough. But it is a decision worth examining on your own projects. If the profiles for your two user types are genuinely different shapes, a polymorphic users table or a base users table with separate profile tables might serve you better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisiting the Clarity ERD
&lt;/h2&gt;

&lt;p&gt;Now that we have covered these in detail, let me take one more look at the Clarity diagram and point out the decisions that are worth noting.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Request&lt;/code&gt; entity has two foreign keys pointing at &lt;code&gt;User&lt;/code&gt;: &lt;code&gt;submitted_by&lt;/code&gt; and &lt;code&gt;assigned_to&lt;/code&gt;. This is intentional, but it means the Eloquent relationships on &lt;code&gt;Request&lt;/code&gt; need to be named explicitly to avoid ambiguity:&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;submitter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;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="s1"&gt;'submitted_by'&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;assignee&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;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="s1"&gt;'assigned_to'&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;If you wrote &lt;code&gt;$this-&amp;gt;belongsTo(User::class)&lt;/code&gt; without the second argument, Laravel would look for a &lt;code&gt;user_id&lt;/code&gt; column that does not exist. Naming the foreign key explicitly in the relationship definition is the correct approach here, and the ERD is what made this obvious before we wrote a single line of code.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Comment&lt;/code&gt; entity has &lt;code&gt;is_internal&lt;/code&gt; as a boolean. This is a simple field, but it carries real access control logic: when &lt;code&gt;is_internal&lt;/code&gt; is true, the comment must be excluded from any query that is serving data to a client user. That is an application-layer concern rather than a schema concern, but spotting it on the diagram is a reminder to handle it consistently across every query that touches comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Into Practice
&lt;/h2&gt;

&lt;p&gt;Go back to the ERD you drew in the last exercise. Now look at every relationship line and ask these questions for each one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the foreign key on the correct side?&lt;/li&gt;
&lt;li&gt;Is this truly a one-to-many, or could it become a many-to-many as the application grows?&lt;/li&gt;
&lt;li&gt;Are there any arrays or JSON fields that should be a related table?&lt;/li&gt;
&lt;li&gt;Are there any nullable foreign keys that feel like shortcuts rather than genuine business rules?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix what you find. A diagram is cheap to change. A migration is not.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article, we are going to step back from the data model and look at the bigger picture: what application architecture actually means for backend developers, and how to draw a simple system diagram that shows how the pieces of Clarity fit together.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>erd</category>
      <category>cardinality</category>
      <category>databasedesign</category>
      <category>datamodeling</category>
    </item>
    <item>
      <title>Your First ERD</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 11:59:56 +0000</pubDate>
      <link>https://dev.to/juststevemcd/your-first-erd-52h6</link>
      <guid>https://dev.to/juststevemcd/your-first-erd-52h6</guid>
      <description>&lt;p&gt;Up until now, everything we have done has been about understanding the problem. We interrogated the brief, surfaced the assumptions, wrote user stories, and shaped the features into bounded pieces of work. That is a lot of thinking before touching anything technical, and it is exactly the right order to do things in.&lt;/p&gt;

&lt;p&gt;Now we get to use that thinking. Because once you know what your application needs to do, the next question is: what does it need to remember?&lt;/p&gt;

&lt;p&gt;That question is what an Entity Relationship Diagram is designed to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an ERD Actually Is
&lt;/h2&gt;

&lt;p&gt;An ERD is a visual map of the data your application works with. It shows you the things your system needs to store (entities), the properties those things have (attributes), and the connections between them (relationships).&lt;/p&gt;

&lt;p&gt;Before you write a single migration, before you think about table names or column types, drawing an ERD gives you a bird's-eye view of your entire data model. It lets you spot problems, ask questions, and make decisions on paper, where changing your mind costs nothing.&lt;/p&gt;

&lt;p&gt;I have seen developers skip this step and pay for it later. A misunderstood relationship between two entities can mean a painful migration weeks into a build, or worse, a data model that quietly produces wrong results and nobody notices until a client reports it. Fifteen minutes with a pencil and a blank page is a much cheaper way to find those problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Building Blocks
&lt;/h2&gt;

&lt;p&gt;Every ERD is made of three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entities&lt;/strong&gt; are the nouns in your system. The things your application tracks and stores. In Clarity, the entities we can already see from our user stories are: organisations, users, requests, comments, and attachments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attributes&lt;/strong&gt; are the properties of each entity. A user has a name, an email address, and a role. A request has a title, a description, and a status. Attributes map directly to the columns you will eventually write in your migrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relationships&lt;/strong&gt; are the connections between entities. An organisation has many users. A user can submit many requests. A request can have many comments. These connections are what give your data model its shape, and getting them right is the most important part of the exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cardinality
&lt;/h2&gt;

&lt;p&gt;When you draw a relationship between two entities, you need to describe how many of one thing can be connected to how many of another. This is called cardinality, and there are three basic types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One-to-one.&lt;/strong&gt; One record in table A corresponds to exactly one record in table B. These are relatively rare. An example might be a user having one profile, where the profile data lives in a separate table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One-to-many.&lt;/strong&gt; One record in table A corresponds to many records in table B. This is the most common relationship in most applications. One organisation has many users. One user has many requests. One request has many comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Many-to-many.&lt;/strong&gt; Many records in table A can be connected to many records in table B. This always requires a pivot table. An example in Clarity might be if we allowed a request to be tagged, where a request can have many tags and a tag can belong to many requests.&lt;/p&gt;

&lt;p&gt;Getting cardinality wrong is the most expensive ERD mistake. If you model a one-to-many relationship as a one-to-one, you will hit a wall the moment a second record tries to attach itself somewhere it cannot go. Drawing it out first makes these mistakes obvious before they are baked into your schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawing the Clarity ERD
&lt;/h2&gt;

&lt;p&gt;Let me walk through how I would build the Clarity ERD from our user stories and shaped features.&lt;/p&gt;

&lt;p&gt;I start by listing every entity I can see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organisation&lt;/li&gt;
&lt;li&gt;User&lt;/li&gt;
&lt;li&gt;Request&lt;/li&gt;
&lt;li&gt;Comment&lt;/li&gt;
&lt;li&gt;Attachment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then for each entity, I list its attributes. I am not worrying about data types yet, just the fields that need to exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Organisation:&lt;/strong&gt; id, name, created_at&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; id, organisation_id, name, email, password, role (client or team_member), sub_role (developer or admin, nullable), invited_at, created_at&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request:&lt;/strong&gt; id, organisation_id, submitted_by (user_id), assigned_to (user_id, nullable), title, description, status, created_at, updated_at&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment:&lt;/strong&gt; id, request_id, user_id, body, is_internal, created_at&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attachment:&lt;/strong&gt; id, request_id, user_id, filename, path, mime_type, created_at&lt;/p&gt;

&lt;p&gt;Now the relationships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Organisation has many Users&lt;/li&gt;
&lt;li&gt;An Organisation has many Requests&lt;/li&gt;
&lt;li&gt;A User (client) submits many Requests&lt;/li&gt;
&lt;li&gt;A User (developer) is assigned to many Requests&lt;/li&gt;
&lt;li&gt;A Request has many Comments&lt;/li&gt;
&lt;li&gt;A Request has many Attachments&lt;/li&gt;
&lt;li&gt;A User posts many Comments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Mermaid syntax, which I would recommend learning because it lets you write diagrams as code and store them in version control alongside your project, this looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;erDiagram
    Organisation {
        uuid id
        string name
        timestamp created_at
    }

    User {
        uuid id
        uuid organisation_id
        string name
        string email
        string role
        string sub_role
        timestamp invited_at
        timestamp created_at
    }

    Request {
        uuid id
        uuid organisation_id
        uuid submitted_by
        uuid assigned_to
        string title
        text description
        string status
        timestamp created_at
        timestamp updated_at
    }

    Comment {
        uuid id
        uuid request_id
        uuid user_id
        text body
        boolean is_internal
        timestamp created_at
    }

    Attachment {
        uuid id
        uuid request_id
        uuid user_id
        string filename
        string path
        string mime_type
        timestamp created_at
    }

    Organisation ||--o{ User : "has many"
    Organisation ||--o{ Request : "has many"
    User ||--o{ Request : "submits"
    User ||--o{ Comment : "posts"
    Request ||--o{ Comment : "has many"
    Request ||--o{ Attachment : "has many"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render that in any Mermaid viewer and you get a complete picture of the Clarity data model. Every entity, every attribute, every relationship, on a single page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the Diagram
&lt;/h2&gt;

&lt;p&gt;The notation on the relationship lines is worth understanding. In Mermaid ERDs, each side of a relationship is described with two symbols: one for the minimum cardinality and one for the maximum.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;||&lt;/code&gt; means exactly one. &lt;code&gt;o{&lt;/code&gt; means zero or more. So &lt;code&gt;||--o{&lt;/code&gt; reads as: exactly one on the left, zero or more on the right.&lt;/p&gt;

&lt;p&gt;Put together, &lt;code&gt;Organisation ||--o{ User&lt;/code&gt; reads as: one organisation has zero or more users. An organisation can exist with no users yet (useful when you first create it), but every user belongs to exactly one organisation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the ERD Is Telling You
&lt;/h2&gt;

&lt;p&gt;The real value of drawing this out is what you notice when you step back and look at it.&lt;/p&gt;

&lt;p&gt;I can already see a potential question in the Clarity diagram. The &lt;code&gt;Request&lt;/code&gt; entity has two foreign keys pointing at &lt;code&gt;User&lt;/code&gt;: &lt;code&gt;submitted_by&lt;/code&gt; and &lt;code&gt;assigned_to&lt;/code&gt;. That is fine and intentional, but it means when you build the Eloquent relationships on the &lt;code&gt;Request&lt;/code&gt; model, you will need named relationships rather than a single &lt;code&gt;belongsTo&lt;/code&gt;. Something like &lt;code&gt;submitter()&lt;/code&gt; and &lt;code&gt;assignee()&lt;/code&gt; rather than just &lt;code&gt;user()&lt;/code&gt;. That is a small thing, but it is much better to notice it here than halfway through writing a controller.&lt;/p&gt;

&lt;p&gt;I can also see that &lt;code&gt;Comment&lt;/code&gt; and &lt;code&gt;Attachment&lt;/code&gt; both have a &lt;code&gt;user_id&lt;/code&gt;. That means both clients and team members can post comments and add attachments, which matches our user stories. If the stories said only team members could add attachments, that &lt;code&gt;user_id&lt;/code&gt; column would need a constraint we would need to enforce at the application layer. The diagram surfaces that decision.&lt;/p&gt;

&lt;p&gt;This is what ERDs are actually for. Not documentation. Not formality. They are a thinking tool that forces you to look at your data model as a whole before you start building pieces of it in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Into Practice
&lt;/h2&gt;

&lt;p&gt;Take the project from the previous exercises and draw an ERD for it. Start with just the entities and relationships, then add attributes once the structure feels right.&lt;/p&gt;

&lt;p&gt;Use Mermaid if you want to keep it in code, or draw.io if you prefer a visual tool. The format matters less than the act of drawing it out and looking at what it tells you.&lt;/p&gt;

&lt;p&gt;Pay particular attention to your foreign keys. For every relationship line you draw, ask yourself: does this direction make sense? What happens if the parent record is deleted? Is there anything here that I assumed was a one-to-many that might actually be a many-to-many?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article, we are going to go deeper on relationships and cardinality, look at some of the most common ERD mistakes, and talk about how to catch the wrong relationship on paper before it becomes the wrong migration in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>erd</category>
      <category>databasedesign</category>
      <category>datamodeling</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Shaping Before You Build</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 11:59:24 +0000</pubDate>
      <link>https://dev.to/juststevemcd/shaping-before-you-build-5c5l</link>
      <guid>https://dev.to/juststevemcd/shaping-before-you-build-5c5l</guid>
      <description>&lt;p&gt;We now have a solid set of user stories for Clarity. We know who the actors are, what they need to do, and what done looks like for each feature. That is real progress. But there is a gap between a well-written user story and something a developer can confidently pick up and build within a reasonable timeframe.&lt;/p&gt;

&lt;p&gt;That gap is what shaping is designed to close.&lt;/p&gt;

&lt;p&gt;Shaping is a concept from the Shape Up methodology, written by Ryan Singer at Basecamp. If you have not read it, I would strongly recommend it. It is free at basecamp.com/shapeup, and it is one of the most practical pieces of writing on software product development I have come across. This article is not a replacement for it, but it will introduce you to the core ideas and show you how to apply them to Clarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Shaping Solves
&lt;/h2&gt;

&lt;p&gt;User stories are great for describing what a user needs. They are not great at describing how much work that need represents, or how to build it in a way that is actually feasible within a given timeframe.&lt;/p&gt;

&lt;p&gt;Without shaping, a story like "as a client, I want to add a comment to my request" can balloon into a real-time threaded messaging system with emoji reactions and read receipts, or it can be a simple text box that posts to a list. Both satisfy the story. One takes two days to build, the other takes two months.&lt;/p&gt;

&lt;p&gt;Shaping forces you to make that decision deliberately, before anyone writes a line of code. It gives features a specific form: a defined scope, a considered approach, and an explicit boundary around what is included and what is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Elements of a Shaped Feature
&lt;/h2&gt;

&lt;p&gt;Every shaped feature has three components.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;appetite&lt;/strong&gt;. This is the amount of time you are willing to spend on the feature, decided upfront. Not an estimate of how long it will take, but a deliberate decision about how much it is worth. This is one of the most powerful ideas in Shape Up, and it inverts the normal way teams think about time. Instead of estimating a feature and then planning around that estimate, you decide how much time a feature deserves and then shape the work to fit inside that boundary.&lt;/p&gt;

&lt;p&gt;The second is &lt;strong&gt;the solution&lt;/strong&gt;. This is a rough sketch of how the feature will work, at a level of detail that is enough to build from without being so prescriptive that it removes all creative problem-solving from the developer. Shape Up calls these "fat marker sketches" because the point is to communicate structure and flow, not pixel-perfect detail.&lt;/p&gt;

&lt;p&gt;The third is &lt;strong&gt;the boundaries&lt;/strong&gt;. This is an explicit list of what is not included. Boundaries are as important as the solution itself, because they are what prevent scope from quietly expanding during the build. If you do not write down what is out of scope, someone will assume it is in scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing a Pitch
&lt;/h2&gt;

&lt;p&gt;The output of shaping is called a pitch. A pitch is a short document that captures all three elements and makes the case for why the feature is worth building in this cycle.&lt;/p&gt;

&lt;p&gt;A pitch is not a specification. It does not tell developers exactly how to implement something. It tells them what problem they are solving, roughly how it should work, how much time they have, and what the edges of the work are.&lt;/p&gt;

&lt;p&gt;Let me write a pitch for one of the Clarity features.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Pitch: Request Comments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Appetite:&lt;/strong&gt; Two days&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Clients and team members currently have no way to communicate directly on a specific request within Clarity. Context gets lost in email threads, and there is no record of decisions or questions attached to the work itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; A simple comment thread on each request detail page. Any user with access to the request can post a comment. Team members can mark a comment as internal, which hides it from clients. Comments appear in chronological order. No threading, no reactions, no editing after posting in the first version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rough sketch:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request detail page
---------------------
[ Request title ]
[ Status badge ] [ Assigned to: Sarah ]
[ Description ]

Comments
---------
[ Client User ] 14 May
Can you confirm the deadline for this?

[ Developer ] 15 May  [internal]
Need to check with the PM before responding.

[ Text area: Add a comment... ]
[ Internal? checkbox ] [ Post comment ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Out of scope:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Threaded replies&lt;/li&gt;
&lt;li&gt;Editing or deleting comments&lt;/li&gt;
&lt;li&gt;Emoji reactions or mentions&lt;/li&gt;
&lt;li&gt;Email notifications on new comments (deferred to a later cycle)&lt;/li&gt;
&lt;li&gt;File attachments on comments&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Look at what that pitch gives you. A developer picking this up knows exactly what they are building, roughly how it should look and behave, how long they have to build it, and where the edges of the work are. They are not going to spend a day building a notification system because they assumed it was included. They are not going to build a threaded reply UI because no one said not to.&lt;/p&gt;

&lt;p&gt;That clarity (again, no pun intended) is what makes teams fast. Not velocity metrics, not sprint ceremonies. Clear, bounded work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appetite as a Design Tool
&lt;/h2&gt;

&lt;p&gt;I want to spend a moment on appetite specifically, because it is the idea most developers initially resist.&lt;/p&gt;

&lt;p&gt;The instinct when you hear "two days for a comment system" is to think about all the things a comment system could be and conclude that two days is not enough. But that framing is backwards. The appetite is not a constraint on what you could build. It is a statement about what the problem is worth solving right now.&lt;/p&gt;

&lt;p&gt;A comment thread that works and ships in two days is more valuable than a fully-featured messaging system that takes six weeks and delays everything else. You can always come back and add threading, notifications, and mentions in a later cycle if the basic version proves its value. What you cannot do is un-spend six weeks.&lt;/p&gt;

&lt;p&gt;This is one of the most important mental shifts in moving from junior to mid-level. Juniors often think about features in terms of what they could be. Mid-level developers think about features in terms of what they need to be, right now, to solve the problem in front of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shaping the Clarity Features
&lt;/h2&gt;

&lt;p&gt;Let me apply appetites to the full set of Clarity user stories, so we have a shaped backlog to build from as the series continues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client authentication and onboarding&lt;/strong&gt;&lt;br&gt;
Appetite: three days. Clients are invited by an admin, set a password via an invitation link, and log in. No social auth, no self-registration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submitting a request&lt;/strong&gt;&lt;br&gt;
Appetite: two days. Title, description, and optional file attachments. Status set to "submitted" on creation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request status and assignment&lt;/strong&gt;&lt;br&gt;
Appetite: two days. Team members can update status and assign a developer. Status and assignee are visible to clients. No real-time updates in the first version; a page refresh is acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment thread&lt;/strong&gt;&lt;br&gt;
Appetite: two days. As pitched above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team member management&lt;/strong&gt;&lt;br&gt;
Appetite: one day. Admins can invite team members by email, set their role, and deactivate their account. Invitations expire after 48 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request list views&lt;/strong&gt;&lt;br&gt;
Appetite: one day. Clients see a list of their own requests. Team members see all requests with basic filtering by status.&lt;/p&gt;

&lt;p&gt;That is the full Clarity application shaped into six discrete pieces of work, with a total appetite of twelve days. That is not an estimate of how long it will take. It is a decision about how much time the initial version of Clarity is worth. If any piece of work is running over its appetite, the right question is not "how do we go faster?" but "what can we remove from this piece to fit inside the time we agreed it was worth?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Into Practice
&lt;/h2&gt;

&lt;p&gt;Take one of the user stories you wrote in the last exercise and write a pitch for it. Define an appetite, sketch a rough solution, and write out the out of scope list.&lt;/p&gt;

&lt;p&gt;The out of scope list is the most important part. Be honest about what you are tempted to include, and then deliberately leave it out.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article, we are going to move from features into data, and look at how to draw your first Entity Relationship Diagram before you write a single migration.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shaping</category>
      <category>scopemanagement</category>
      <category>productthinking</category>
      <category>planning</category>
    </item>
    <item>
      <title>Writing Features Not Tasks</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 11:59:22 +0000</pubDate>
      <link>https://dev.to/juststevemcd/writing-features-not-tasks-2idd</link>
      <guid>https://dev.to/juststevemcd/writing-features-not-tasks-2idd</guid>
      <description>&lt;p&gt;If you have ever looked at a ticket in your backlog and felt genuinely unsure where to start, there is a good chance the ticket was written as a task rather than a feature. It is one of the most common sources of confusion for junior developers, and it is almost never talked about directly.&lt;/p&gt;

&lt;p&gt;The difference matters more than you might think. Tasks describe what to build. Features describe why it needs to exist and what it should enable. That distinction changes everything from how you estimate work, to how you test it, to how you know when you are actually done.&lt;/p&gt;

&lt;p&gt;In this article we are going to look at what a properly written feature actually looks like, introduce the concept of user stories as a tool for capturing them, and apply all of it to Clarity using the confirmed assumptions we established in the last article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tasks vs Features
&lt;/h2&gt;

&lt;p&gt;Let me show you the difference with a concrete example.&lt;/p&gt;

&lt;p&gt;Here is a task:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add a status column to the requests table.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the same thing written as a feature:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As a client, I want to see the current status of my submitted request, so that I know whether the team has started working on it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The task tells a developer to do something. The feature tells everyone involved why it matters, who benefits from it, and what outcome it is trying to produce.&lt;/p&gt;

&lt;p&gt;That distinction has real consequences. If I hand you a task, you can complete it and technically be done. You can add the column, run the migration, and close the ticket. But you have not built anything a user can actually interact with. The feature, on the other hand, only counts as done when a client can log in, find their request, and read a meaningful status value. That is a fundamentally different definition of done.&lt;/p&gt;

&lt;p&gt;The format I just used has a name. It is a user story.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a User Story Actually Is
&lt;/h2&gt;

&lt;p&gt;A user story follows a simple template:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As a [type of user], I want to [do something], so that [I achieve some goal].&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each part of that template is doing a specific job.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;actor&lt;/strong&gt; ("as a client") anchors the story to a real person in your system with a real perspective. It forces you to think about who is actually going to use this thing, rather than thinking about it in the abstract.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;action&lt;/strong&gt; ("I want to see the current status") describes the behaviour the system needs to support. It is written from the user's point of view, not the developer's. You will notice it says "see the status", not "query the status from the database". That is deliberate. Implementation details do not belong in a user story.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;goal&lt;/strong&gt; ("so that I know whether the team has started working on it") is the part most people skip, and it is often the most valuable. The goal is what gives you a basis for questioning whether you are building the right thing. If you cannot articulate a meaningful goal for a feature, that is a signal worth paying attention to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acceptance Criteria
&lt;/h2&gt;

&lt;p&gt;A user story on its own is not quite enough to build from. You also need acceptance criteria: a concrete list of conditions that must be true for the story to be considered complete.&lt;/p&gt;

&lt;p&gt;Acceptance criteria answer the question: "how do we know this is done?"&lt;/p&gt;

&lt;p&gt;Here is the status visibility story with acceptance criteria added:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Story:&lt;/strong&gt; As a client, I want to see the current status of my submitted request, so that I know whether the team has started working on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acceptance criteria:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request detail page displays the current status&lt;/li&gt;
&lt;li&gt;The status reflects one of the defined values: submitted, in review, in progress, on hold, completed&lt;/li&gt;
&lt;li&gt;The status updates in real time when a team member changes it, without requiring a page refresh&lt;/li&gt;
&lt;li&gt;The client cannot change the status themselves; the field is read-only for them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you have something you can actually build against. You have a clear definition of done, a set of testable conditions, and a user perspective that keeps you grounded in what actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Stories for Clarity
&lt;/h2&gt;

&lt;p&gt;Let us take the confirmed assumptions from the last article and turn them into a proper set of user stories. I am going to organise them by actor, because that is the most natural way to think about the scope of work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client stories
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Submitting a request&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a client, I want to submit a project request with a title, description, and optional attachments, so that I can communicate what I need from the team.&lt;/p&gt;

&lt;p&gt;Acceptance criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The submission form requires a title and description&lt;/li&gt;
&lt;li&gt;Attachments are optional and support common file types&lt;/li&gt;
&lt;li&gt;After submission, the request status is set to "submitted" automatically&lt;/li&gt;
&lt;li&gt;The client is redirected to the request detail page after a successful submission&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Editing a request&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a client, I want to edit my request while it is in the submitted state, so that I can correct mistakes or add information before the team begins reviewing it.&lt;/p&gt;

&lt;p&gt;Acceptance criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edit controls are visible only when the request is in the "submitted" state&lt;/li&gt;
&lt;li&gt;Once the status moves past "submitted", the edit controls are hidden&lt;/li&gt;
&lt;li&gt;Edits do not reset the request status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tracking request status&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a client, I want to see the current status of my request and who it has been assigned to, so that I have visibility into where things stand without needing to chase the team.&lt;/p&gt;

&lt;p&gt;Acceptance criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request detail page shows the current status and the assigned developer's name&lt;/li&gt;
&lt;li&gt;If no developer has been assigned yet, a placeholder is shown rather than a blank field&lt;/li&gt;
&lt;li&gt;Status changes are reflected without requiring a page refresh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Adding a comment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a client, I want to add a comment to my request, so that I can ask questions or provide additional context as the work progresses.&lt;/p&gt;

&lt;p&gt;Acceptance criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clients can post a comment on any request they own&lt;/li&gt;
&lt;li&gt;Comments appear in chronological order&lt;/li&gt;
&lt;li&gt;Internal comments marked by team members are not visible to clients&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Team member stories
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Reviewing and updating request status&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a team member, I want to update the status of a request, so that clients and colleagues have an accurate picture of where the work stands.&lt;/p&gt;

&lt;p&gt;Acceptance criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Team members can move a request to any valid status&lt;/li&gt;
&lt;li&gt;Status changes are logged with a timestamp and the team member's name&lt;/li&gt;
&lt;li&gt;The client is notified when the status changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Assigning a request&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a team member, I want to assign a request to a developer, so that ownership is clear and the developer knows what they are responsible for.&lt;/p&gt;

&lt;p&gt;Acceptance criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any team member can assign a request&lt;/li&gt;
&lt;li&gt;Only one developer can be assigned at a time&lt;/li&gt;
&lt;li&gt;Reassigning a request replaces the previous assignment rather than stacking them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Adding an internal comment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a team member, I want to mark a comment as internal, so that I can communicate with colleagues about a request without the client seeing the discussion.&lt;/p&gt;

&lt;p&gt;Acceptance criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal comments are visually distinct in the team view&lt;/li&gt;
&lt;li&gt;Internal comments are completely hidden in the client view&lt;/li&gt;
&lt;li&gt;A team member can toggle the internal flag before submitting a comment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Admin stories
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Managing team members&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As an admin, I want to invite and manage team members, so that I can control who has access to the system and what role they hold.&lt;/p&gt;

&lt;p&gt;Acceptance criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admins can invite users by email&lt;/li&gt;
&lt;li&gt;Invitations expire after 48 hours if not accepted&lt;/li&gt;
&lt;li&gt;Admins can change a team member's role or deactivate their account&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What This Set of Stories Gives You
&lt;/h2&gt;

&lt;p&gt;Look at what we have built here. From a single vague paragraph in a client brief, we now have a complete set of structured, testable, actor-anchored stories that describe the entire surface area of the Clarity application.&lt;/p&gt;

&lt;p&gt;Every story has a clear definition of done. Every story is written from a user's perspective. And every story is independent enough that a developer could pick one up and work on it without needing to understand the whole system first.&lt;/p&gt;

&lt;p&gt;That last point matters a lot when you are working in a team. Vague tasks create dependencies and confusion. Well-written stories create clarity (no pun intended) and autonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Into Practice
&lt;/h2&gt;

&lt;p&gt;Take the project from the last article's exercise and write three user stories from it. Pick stories from different actors if you can. For each story, write at least three acceptance criteria.&lt;/p&gt;

&lt;p&gt;Then ask yourself honestly: could someone else on your team pick up one of those stories and know exactly what done looks like? If the answer is no, the story needs more work.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article, we are going to move from user stories to feature shaping, and look at how the Shape Up methodology gives you a practical framework for deciding what to build and how much time it is worth.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>requirements</category>
      <category>userstories</category>
      <category>productthinking</category>
      <category>developermindset</category>
    </item>
    <item>
      <title>Reading Between the Lines</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 11:58:50 +0000</pubDate>
      <link>https://dev.to/juststevemcd/reading-between-the-lines-4me2</link>
      <guid>https://dev.to/juststevemcd/reading-between-the-lines-4me2</guid>
      <description>&lt;p&gt;In the last article, I asked you to take the Clarity brief and write down every question you would want answered before you started building. If you did that exercise, you probably found somewhere between five and fifteen questions depending on how deeply you read into it.&lt;/p&gt;

&lt;p&gt;Here is what I have found over the years: the first time developers do that exercise, they mostly surface technical questions. Things like "should I use a polymorphic relationship?" or "what database should I use?". Those are not bad questions, but they are the wrong starting point. They are implementation questions dressed up as requirements questions, and they put the cart firmly in front of the horse.&lt;/p&gt;

&lt;p&gt;The questions that actually matter before you write a line of code are business questions. And learning to ask them is one of the most valuable skills you can develop as you move towards mid-level.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Brief, Revisited
&lt;/h2&gt;

&lt;p&gt;Let us look at the Clarity brief again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We need a tool where our clients can submit project requests, and our team can manage and track them. Clients should be able to see the status of their requests, and we need to be able to assign them to developers."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On the surface, this looks like a reasonable starting point. You could probably sketch out a rough data model from it in ten minutes. But read it again, and this time treat every noun and every verb with suspicion.&lt;/p&gt;

&lt;p&gt;Every noun is a potential entity. Every verb is a potential action. And every place where the brief is vague is a place where an unstated assumption is hiding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interrogating the Nouns
&lt;/h2&gt;

&lt;p&gt;Let us pull out the nouns: clients, tool, project requests, team, status, developers.&lt;/p&gt;

&lt;p&gt;These seem obvious until you start asking questions about them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clients.&lt;/strong&gt; Are clients individual people, or organisations? Can a single organisation have multiple client users? If a client submits a request, can another person from the same organisation see it? This alone could mean the difference between a simple &lt;code&gt;users&lt;/code&gt; table and a full &lt;code&gt;organisations -&amp;gt; users&lt;/code&gt; relationship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project requests.&lt;/strong&gt; What actually constitutes a request? Is it just a title and a description? Does it have attachments? Can it have a priority? Can a client submit multiple requests at the same time, and if so, are those requests independent or grouped somehow?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team.&lt;/strong&gt; Who is "our team"? Are these people with admin access to everything, or do different team members have different permissions? Can a developer only see requests assigned to them, or all requests?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status.&lt;/strong&gt; This one word is carrying an enormous amount of weight. Is "status" a simple label (pending, in progress, done) or is it a proper workflow with defined transitions? Can a client change the status, or only view it? Can a developer set any status, or only move it forward?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developers.&lt;/strong&gt; Are developers a separate user type from clients, or just users with a different role? Can a developer also be a client on a different project?&lt;/p&gt;

&lt;p&gt;None of these are trick questions. They are the kind of thing a product manager or a client would answer in a thirty-minute call. But if you skip straight to building, you are making silent choices about all of them, and those choices will come back to cost you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interrogating the Verbs
&lt;/h2&gt;

&lt;p&gt;Now the verbs: submit, manage, track, see, assign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submit.&lt;/strong&gt; Can a client edit a request after submitting it? Can they delete it? Is there a draft state before submission, or is it submit-or-nothing?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manage and track.&lt;/strong&gt; These are vague by nature. What does managing a request look like? Adding notes? Updating status? Logging time? The brief does not say, which means this is a gap you need to close before you start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See.&lt;/strong&gt; Can clients only see the status, or can they see the full request detail and any notes added by the team? Is there a concept of private notes that clients should not see?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assign.&lt;/strong&gt; Can a request be assigned to multiple developers? What happens when the assigned developer is unavailable? Is assignment visible to the client?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Questions Worth Asking
&lt;/h2&gt;

&lt;p&gt;When I am working from a brief like this, I structure my questions into three categories.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;scope questions&lt;/strong&gt;. These define the boundaries of what we are building. What is explicitly included? What is explicitly out of scope? For Clarity, that might be: are we building client authentication ourselves, or are clients invited via a link?&lt;/p&gt;

&lt;p&gt;The second is &lt;strong&gt;actor questions&lt;/strong&gt;. These define who can do what. For every type of user in the system, I want to know what actions they can take, what they can see, and what restrictions apply to them.&lt;/p&gt;

&lt;p&gt;The third is &lt;strong&gt;data questions&lt;/strong&gt;. These define the shape of the information we are working with. What does each entity look like? What are its required fields? What are its optional fields? What are the relationships between entities?&lt;/p&gt;

&lt;p&gt;You do not need formal workshops or long documents to work through these. A simple shared notes document and a conversation with whoever owns the brief is often enough. The goal is not to write a specification novel. It is to surface the decisions that have already been made implicitly, and make them explicit before they become mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying This to Clarity
&lt;/h2&gt;

&lt;p&gt;After running this process on the Clarity brief, here is what I would want confirmed before I drew a single diagram:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On actors:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There are two distinct user types: clients and team members&lt;/li&gt;
&lt;li&gt;Clients belong to an organisation, and organisations can have multiple client users&lt;/li&gt;
&lt;li&gt;Team members have a role of either developer or admin&lt;/li&gt;
&lt;li&gt;Admins can manage everything; developers can manage requests assigned to them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On requests:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A request has a title, description, and optional file attachments&lt;/li&gt;
&lt;li&gt;Requests are submitted by a client user and belong to their organisation&lt;/li&gt;
&lt;li&gt;Requests move through a defined set of statuses: submitted, in review, in progress, on hold, completed&lt;/li&gt;
&lt;li&gt;Only team members can change the status of a request&lt;/li&gt;
&lt;li&gt;Clients can edit their request while it is in the submitted state; after that, it is read-only for them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On assignment:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A request can be assigned to one developer at a time&lt;/li&gt;
&lt;li&gt;Assignment is visible to the client&lt;/li&gt;
&lt;li&gt;Developers can see all requests, not just their own&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On comments:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both clients and team members can add comments to a request&lt;/li&gt;
&lt;li&gt;Team members can mark a comment as internal, which hides it from clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the confirmed assumptions we will carry forward through the rest of this series. Notice that none of them are technical decisions. We have not decided on a database, a framework, a caching strategy, or a queue driver. We have simply defined the shape of the problem we are solving.&lt;/p&gt;

&lt;p&gt;That comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Into Practice
&lt;/h2&gt;

&lt;p&gt;Take a project you are currently working on, or one you have worked on recently. Go back to the original brief or requirements you were given. Now apply the noun-and-verb interrogation to it. How many questions can you surface that were never explicitly answered? How many of those questions did you answer yourself, silently, while you were building?&lt;/p&gt;

&lt;p&gt;Write them down. You might be surprised how many decisions were made without anyone realising a decision was being made.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article, we will take the confirmed Clarity assumptions and turn them into properly structured user stories, and explore what the difference between a task and a feature actually means in practice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>requirements</category>
      <category>requirementsanalysis</category>
      <category>businessrequirements</category>
      <category>developermindset</category>
    </item>
    <item>
      <title>Stop Writing Code First</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 19 May 2026 11:58:48 +0000</pubDate>
      <link>https://dev.to/juststevemcd/stop-writing-code-first-3n4m</link>
      <guid>https://dev.to/juststevemcd/stop-writing-code-first-3n4m</guid>
      <description>&lt;p&gt;There is a habit almost every junior developer shares. A client sends over a brief, or a ticket lands in your queue, and within minutes your editor is open and your fingers are moving. It feels productive. It feels like progress.&lt;/p&gt;

&lt;p&gt;It is almost always the wrong move.&lt;/p&gt;

&lt;p&gt;This is the first article in a series called &lt;strong&gt;From Requirements to Reality&lt;/strong&gt;, designed for developers who are ready to stop thinking like juniors and start thinking like mid-level engineers. Over the course of this series, we are going to work through a fictional product called &lt;strong&gt;Clarity&lt;/strong&gt;, a client project management tool where clients submit requests and developers scope and track them. Every concept we explore will be applied to Clarity, so by the end you will have seen a complete design process play out from a vague brief all the way to an implementation plan.&lt;/p&gt;

&lt;p&gt;But before we can do any of that, we need to talk about the most expensive habit in software development.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost of Jumping Straight In
&lt;/h2&gt;

&lt;p&gt;When you are junior, speed feels like the measure of your value. The faster you can translate a requirement into working code, the better you are doing. That instinct makes sense early on. You are learning by doing, and doing means writing code.&lt;/p&gt;

&lt;p&gt;The problem is that this habit does not scale. As projects grow in complexity and your role in them grows with it, the cost of building the wrong thing becomes enormous. Refactoring a misunderstood feature three weeks after it was built is not just slower than getting it right the first time. It is often more expensive than the original build, because now you have tests to update, documentation to revise, and sometimes downstream code that relied on the wrong behaviour.&lt;/p&gt;

&lt;p&gt;Mid-level developers do not move faster than juniors by typing more quickly. They move faster by spending time upfront that prevents expensive mistakes downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Designing Before You Build" Actually Means
&lt;/h2&gt;

&lt;p&gt;When I say design, I do not mean wireframes or Figma files (though those have their place). I mean asking a specific set of questions before a single migration, controller, or model gets created.&lt;/p&gt;

&lt;p&gt;Those questions look something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What problem is this feature actually solving?&lt;/li&gt;
&lt;li&gt;Who is it for, and what do they need to be able to do?&lt;/li&gt;
&lt;li&gt;What data does this feature create, read, update, or delete?&lt;/li&gt;
&lt;li&gt;How does it connect to what already exists?&lt;/li&gt;
&lt;li&gt;What does success look like, and how would we know if it had not worked?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not big questions. They do not require a week of planning or a product manager. They require maybe thirty minutes of thinking, a notepad, and the willingness to slow down before you speed up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Clarity
&lt;/h2&gt;

&lt;p&gt;Throughout this series we will apply every concept to Clarity. Here is the initial brief, written exactly the way a real client might send it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We need a tool where our clients can submit project requests, and our team can manage and track them. Clients should be able to see the status of their requests, and we need to be able to assign them to developers."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you opened your editor right now, what would you build? A &lt;code&gt;requests&lt;/code&gt; table? A &lt;code&gt;users&lt;/code&gt; table? A status column? Maybe a polymorphic relationship between clients and developers?&lt;/p&gt;

&lt;p&gt;All of those might end up being correct. But right now, you do not actually know enough to make those decisions well. There are at least a dozen unstated assumptions buried inside that brief. We have no idea whether "clients" and "developers" live in the same users table or separate ones. We do not know what "status" means: is it a free-text field, an enum, a workflow? We do not know if one request can be assigned to multiple developers, or only one. We do not know if clients can edit their requests after submission.&lt;/p&gt;

&lt;p&gt;Every one of those unknowns is a fork in your schema. Pick the wrong path and you are doing a data migration at the worst possible time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mindset Shift
&lt;/h2&gt;

&lt;p&gt;The thing that separates a junior developer from a mid-level one is not the number of Laravel packages they have memorised or how quickly they can scaffold a resource. It is the ability to look at a requirement and ask "what do I not yet know?" before asking "how do I build this?"&lt;/p&gt;

&lt;p&gt;This series is going to build that muscle systematically. We will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to read a client brief and extract what they actually need (not just what they said)&lt;/li&gt;
&lt;li&gt;How to turn requirements into properly shaped features using user stories&lt;/li&gt;
&lt;li&gt;How to draw an ERD before you write a migration&lt;/li&gt;
&lt;li&gt;How to think about system architecture at the application level&lt;/li&gt;
&lt;li&gt;How to turn all of that design work into a sequenced build plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each article will apply its concepts directly to Clarity, so you will see the thinking, not just the theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Into Practice
&lt;/h2&gt;

&lt;p&gt;Before the next article, take the Clarity brief above and write down every question you would want answered before you started building. Do not try to answer them, just list them. How many can you find?&lt;/p&gt;

&lt;p&gt;The number might surprise you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article, we will take that brief apart properly and look at how to extract the real requirements hiding inside it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>requirements</category>
      <category>planning</category>
      <category>architecture</category>
      <category>developermindset</category>
    </item>
    <item>
      <title>Introducing Signal: documentation that lives in your code</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Thu, 30 Apr 2026 09:35:58 +0000</pubDate>
      <link>https://dev.to/juststevemcd/introducing-signal-documentation-that-lives-in-your-code-gfg</link>
      <guid>https://dev.to/juststevemcd/introducing-signal-documentation-that-lives-in-your-code-gfg</guid>
      <description>&lt;p&gt;I have a confession. I have shipped more than a few projects where the documentation was a lie.&lt;/p&gt;

&lt;p&gt;Not deliberately. Nobody sits down and thinks "I'll write docs that contradict my code." It happens gradually. You refactor a method, update the logic, rename a route, and the comment block at the top of the class quietly becomes fiction. The docs say one thing. The code does another. Whoever opens that file next, usually someone who isn't you, has to figure out which one to trust.&lt;/p&gt;

&lt;p&gt;I got tired of that problem. So I built Signal.&lt;/p&gt;

&lt;p&gt;Signal is a PHP library that turns PHP attributes into living documentation. You annotate your classes and methods directly in the source, and a single CLI command generates Markdown and JSON docs that always reflect what the code actually does. No separate documentation site to keep in sync. No wiki pages that rot. No README sections that nobody updates after the initial commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why attributes?
&lt;/h2&gt;

&lt;p&gt;PHP 8 gave us native attributes and, honestly, they remain underused. Most people know &lt;code&gt;#[Route]&lt;/code&gt; from Symfony or &lt;code&gt;#[Column]&lt;/code&gt; from Doctrine, but the mechanism itself is just a structured, machine-readable annotation system built into the language. That is exactly what documentation needs to be: structured and machine-readable, not a comment that any editor will happily let drift out of sync with reality.&lt;/p&gt;

&lt;p&gt;The key difference with Signal is that attributes are not comments. PHP will parse them, your IDE understands them, and Signal can reflect on them at any point. If you delete a method, the attribute goes with it. If you rename a class, your tooling will tell you. The docs cannot lie because the docs are the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation and setup
&lt;/h2&gt;

&lt;p&gt;Getting started is one line:&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 juststeveking/signal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a &lt;code&gt;signal.json&lt;/code&gt; config at your project root. This tells Signal where to look and where to write:&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;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docs/"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"src/Attributes/"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;exclude&lt;/code&gt; array is useful for telling Signal to skip directories it does not need to reflect on. You almost certainly do not want Signal documenting its own attributes if you are using the library inside a package, and you may have internal bootstrap classes that should stay out of the generated output.&lt;/p&gt;

&lt;p&gt;Once you have annotated your classes, 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 vendor/bin/signal generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signal scans the &lt;code&gt;input&lt;/code&gt; directory, reflects on every annotated class, and writes &lt;code&gt;docs/signal.md&lt;/code&gt; and &lt;code&gt;docs/signal.json&lt;/code&gt;. Both are safe to commit. Both can go into CI as required artefacts. If a developer removes an annotation, the next generate run will remove it from the docs automatically.&lt;/p&gt;

&lt;p&gt;If you keep your config somewhere other than the project root, you can point to it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php vendor/bin/signal generate &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;config/signal.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The attribute system
&lt;/h2&gt;

&lt;p&gt;Signal ships with 24 attributes split across three groups: class type attributes that identify what a class is, class metadata attributes that describe relationships and status, and method attributes that document individual methods. Let me walk through each group properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Class type attributes
&lt;/h2&gt;

&lt;p&gt;These attributes go on the class declaration itself. They tell Signal what kind of class it is looking at, which determines how the generated output groups and presents the information.&lt;/p&gt;

&lt;p&gt;Each class type attribute accepts a &lt;code&gt;description&lt;/code&gt; string and a &lt;code&gt;tags&lt;/code&gt; array. The description becomes the first thing a reader sees in the generated docs for that class. The tags appear as metadata and can be used to filter or group output in custom tooling downstream.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;#[Module]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a top-level application module that groups related functionality 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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;JustSteveKing\Signal\Attributes\Module&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Handles everything related to billing, invoicing, and payment processing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'billing'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BillingModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Service]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks an application or domain service containing business logic.&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;JustSteveKing\Signal\Attributes\Service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Processes subscription renewals and handles billing retries'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'subscriptions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'billing'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SubscriptionRenewalService&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Repository]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a data access layer class that wraps persistence.&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;JustSteveKing\Signal\Attributes\Repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Reads and writes Order records to the database'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'persistence'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Action]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a single-purpose action class. If you follow a use-case or interactor pattern, this is the attribute you will use most.&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;JustSteveKing\Signal\Attributes\Action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Places a new customer order and reserves the required inventory'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrderAction&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Controller]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks an HTTP controller that handles incoming requests.&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;JustSteveKing\Signal\Attributes\Controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Manages order CRUD endpoints'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderController&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Event]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a domain event representing something that happened in the system.&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;JustSteveKing\Signal\Attributes\Event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Fired when a customer successfully places an order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'events'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPlacedEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Listener]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks an event listener that reacts to one or more domain events. You will typically combine this with &lt;code&gt;#[ListensTo]&lt;/code&gt; from the metadata group.&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;JustSteveKing\Signal\Attributes\Listener&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Listener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Handles post-order notification emails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderNotificationListener&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Middleware]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks HTTP middleware. It accepts an optional &lt;code&gt;priority&lt;/code&gt; integer that controls the order in which middleware appears in the generated output, which is useful when execution order matters.&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;JustSteveKing\Signal\Attributes\Middleware&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Validates the Bearer token on every authenticated request'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthMiddleware&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Job]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a queueable background job. It accepts an optional &lt;code&gt;queue&lt;/code&gt; string so the target queue is visible in the generated docs without anyone having to open the job class.&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;JustSteveKing\Signal\Attributes\Job&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Sends the customer invoice PDF by email after an order is placed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'billing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'invoices'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendInvoiceJob&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Command]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a console command.&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;JustSteveKing\Signal\Attributes\Command&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Recalculates all open subscription invoices for the current billing cycle'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'billing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cli'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RecalculateInvoicesCommand&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Query]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a read-only query class on the query side of a CQRS pattern.&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;JustSteveKing\Signal\Attributes\Query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Fetches a paginated list of orders for the currently authenticated user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cqrs'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetUserOrdersQuery&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Aggregate]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a DDD aggregate root that owns a consistency 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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;JustSteveKing\Signal\Attributes\Aggregate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Order aggregate root managing the full order lifecycle from placement to fulfilment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ddd'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderAggregate&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[ValueObject]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a DDD value object. Immutable and identity-less.&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;JustSteveKing\Signal\Attributes\ValueObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ValueObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Represents a monetary amount with currency, safe for arithmetic operations'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'money'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ddd'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Class metadata attributes
&lt;/h2&gt;

&lt;p&gt;These attributes sit alongside the class type attribute and add relational and status context. They describe what a class depends on, what events it handles, whether it is deprecated, and whether it is internal.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;#[DependsOn]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Declares an explicit dependency on another class. It is repeatable, so you can stack it to declare every dependency a class has.&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;JustSteveKing\Signal\Attributes\Service&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;JustSteveKing\Signal\Attributes\DependsOn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Service(description: 'Orchestrates the end-to-end checkout process')]&lt;/span&gt;
&lt;span class="na"&gt;#[DependsOn(class: PaymentGateway::class, description: 'Charges the customer')]&lt;/span&gt;
&lt;span class="na"&gt;#[DependsOn(class: InventoryService::class, description: 'Reserves stock before payment is taken')]&lt;/span&gt;
&lt;span class="na"&gt;#[DependsOn(class: OrderRepository::class)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutService&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated docs will include a dependency table for this class. At a glance you can see what a service needs to function, without reading through the constructor or hunting down injected types. That is particularly useful when you are onboarding someone new or trying to understand the blast radius of a change before you make it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;#[ListensTo]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Declares which domain events a listener class handles. Also repeatable.&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;JustSteveKing\Signal\Attributes\Listener&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;JustSteveKing\Signal\Attributes\ListensTo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Listener(description: 'Handles all post-order notification events')]&lt;/span&gt;
&lt;span class="na"&gt;#[ListensTo(event: 'OrderPlaced', description: 'Sends order confirmation email', tags: ['email'])]&lt;/span&gt;
&lt;span class="na"&gt;#[ListensTo(event: 'OrderCancelled', description: 'Sends cancellation notice', tags: ['email'])]&lt;/span&gt;
&lt;span class="na"&gt;#[ListensTo(event: 'OrderRefunded', description: 'Sends refund confirmation', tags: ['email'])]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderNotificationListener&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Event-driven systems can be hard to trace because the connection between emitter and listener is often implicit and scattered across service providers or config files. Signal surfaces it directly on the class.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;#[Deprecated]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a class or method as deprecated with an optional version and reason. This is one of those attributes that pays off specifically in larger teams and older codebases.&lt;/p&gt;

&lt;p&gt;On a class:&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;JustSteveKing\Signal\Attributes\Service&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;JustSteveKing\Signal\Attributes\Deprecated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Service(description: 'Legacy payment handler from the pre-Stripe era')]&lt;/span&gt;
&lt;span class="na"&gt;#[Deprecated(reason: 'Replaced by StripePaymentService', since: '2.0.0')]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LegacyPaymentService&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a method:&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="na"&gt;#[Deprecated(reason: 'Use processRefundV2() instead', since: '1.8.0')]&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;processRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$orderId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated docs flag deprecated classes and methods clearly, so a reader knows immediately whether they should be using something or looking for its replacement.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;#[Internal]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Marks a class or method as internal, meaning it is not part of the public API and should not be relied upon by external consumers.&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;JustSteveKing\Signal\Attributes\Internal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Internal(reason: 'Framework bootstrap only, do not use directly')]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KernelBootstrapper&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful if you are building a package. Signal will note internal classes in the output so that anyone reading the docs understands the stability contract, or absence of one, for a given class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method attributes
&lt;/h2&gt;

&lt;p&gt;This is where Signal earns its keep on a daily basis. The method-level attributes document the HTTP layer, access control, validation, caching, events, exceptions, and side effects. None of these are inferred from your code. You declare them explicitly, which forces you to think about them.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;#[Route]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Binds a method to an HTTP verb and path.&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;JustSteveKing\Signal\Attributes\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Route(method: 'GET', path: '/v1/orders', description: 'Paginated list of orders for the authenticated user')]&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;index&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="kt"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;#[Route(method: 'POST', path: '/v1/orders', description: 'Place a new order')]&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;store&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="kt"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;#[Route(method: 'DELETE', path: '/v1/orders/{id}', description: 'Cancel an existing order')]&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;destroy&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;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Authorize]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Declares the authorization ability required to call a method. Repeatable for methods that require multiple abilities to be satisfied.&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;JustSteveKing\Signal\Attributes\Authorize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Authorize(ability: 'orders.view', description: 'User must own the order or be an admin')]&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;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;#[Authorize(ability: 'orders.update')]&lt;/span&gt;
&lt;span class="na"&gt;#[Authorize(ability: 'orders.approve')]&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;approve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Validates]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Documents the validation rules applied to request fields. Repeatable, and accepts an optional &lt;code&gt;description&lt;/code&gt; per field.&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;JustSteveKing\Signal\Attributes\Validates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Validates(field: 'email', rules: 'required|email', description: 'Customer email address')]&lt;/span&gt;
&lt;span class="na"&gt;#[Validates(field: 'items', rules: 'required|array|min:1')]&lt;/span&gt;
&lt;span class="na"&gt;#[Validates(field: 'items.*.product_id', rules: 'required|integer|exists:products,id')]&lt;/span&gt;
&lt;span class="na"&gt;#[Validates(field: 'items.*.quantity', rules: 'required|integer|min:1')]&lt;/span&gt;
&lt;span class="na"&gt;#[Validates(field: 'coupon_code', rules: 'nullable|string|max:20')]&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;store&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="kt"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not meant to replace your Form Request class. The rules you declare here should mirror what the Form Request enforces. The point is to surface them in the generated docs so that someone reading the API reference does not have to open the Form Request file to understand what a POST body should look like.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;#[Cached]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Documents that a method's result is cached, with an optional TTL in seconds and cache key pattern.&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;JustSteveKing\Signal\Attributes\Cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Cached(ttl: 300, key: 'orders.user.{userId}', description: 'Cached per user for 5 minutes')]&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;forUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Collection&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;#[Cached(ttl: 3600, key: 'products.catalogue')]&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;all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Collection&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a teammate asks why a query is not returning live data, having the caching behaviour visible in the generated docs is a much faster path to the answer than grepping through the codebase for a cache key.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;#[Emits]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Documents domain events dispatched by a method. Repeatable and accepts an optional &lt;code&gt;tags&lt;/code&gt; array.&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;JustSteveKing\Signal\Attributes\Emits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Emits(event: 'OrderPlaced', description: 'Fired after the order is persisted', tags: ['orders'])]&lt;/span&gt;
&lt;span class="na"&gt;#[Emits(event: 'StockReserved', description: 'Fired once inventory is locked', tags: ['inventory'])]&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;store&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="kt"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[Throws]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Documents exceptions a method may throw. Repeatable.&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;JustSteveKing\Signal\Attributes\Throws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Throws(exception: PaymentFailedException::class, description: 'When the payment gateway rejects the charge')]&lt;/span&gt;
&lt;span class="na"&gt;#[Throws(exception: InsufficientStockException::class, description: 'When a product cannot be reserved')]&lt;/span&gt;
&lt;span class="na"&gt;#[Throws(exception: OrderLimitExceededException::class, description: 'When the user has too many open orders')]&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;store&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="kt"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;#[SideEffect]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Documents observable side effects beyond the return value. This is the attribute I find most clarifying to write, because it forces you to acknowledge everything your method does beyond returning a value.&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;JustSteveKing\Signal\Attributes\SideEffect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[SideEffect(description: 'Sends an order confirmation email to the customer', tags: ['email'])]&lt;/span&gt;
&lt;span class="na"&gt;#[SideEffect(description: 'Decrements inventory for each line item', tags: ['inventory'])]&lt;/span&gt;
&lt;span class="na"&gt;#[SideEffect(description: 'Publishes an OrderPlaced message to the event bus', tags: ['events'])]&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;store&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="kt"&gt;JsonResponse&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 find yourself stacking five or six &lt;code&gt;#[SideEffect]&lt;/code&gt; attributes on a single method, that is a signal worth listening to. A method with that many side effects is probably doing too much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Here is a complete, realistic controller with the full set of Signal annotations showing how the attributes compose in practice:&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="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers\Api\V1&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;JustSteveKing\Signal\Attributes\Controller&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;JustSteveKing\Signal\Attributes\DependsOn&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;JustSteveKing\Signal\Attributes\Route&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;JustSteveKing\Signal\Attributes\Authorize&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;JustSteveKing\Signal\Attributes\Validates&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;JustSteveKing\Signal\Attributes\Emits&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;JustSteveKing\Signal\Attributes\Throws&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;JustSteveKing\Signal\Attributes\SideEffect&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;JustSteveKing\Signal\Attributes\Cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'RESTful controller for managing customer orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'v1'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="na"&gt;#[DependsOn(class: OrderService::class, description: 'Handles order business logic')]&lt;/span&gt;
&lt;span class="na"&gt;#[DependsOn(class: OrderRepository::class)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[Route(method: 'GET', path: '/v1/orders', description: 'Paginated list of orders for the authenticated user')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Authorize(ability: 'orders.viewAny')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Cached(ttl: 60, key: 'orders.index.user.{userId}.page.{page}')]&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;index&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="kt"&gt;JsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;#[Route(method: 'GET', path: '/v1/orders/{id}', description: 'Fetch a single order by ID')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Authorize(ability: 'orders.view', description: 'User must own the order or be an admin')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Throws(exception: OrderNotFoundException::class, description: 'When the order does not exist')]&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;show&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;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;#[Route(method: 'POST', path: '/v1/orders', description: 'Place a new order')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Authorize(ability: 'orders.create')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Validates(field: 'items', rules: 'required|array|min:1', description: 'Line items for the order')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Validates(field: 'items.*.product_id', rules: 'required|integer|exists:products,id')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Validates(field: 'items.*.quantity', rules: 'required|integer|min:1')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Validates(field: 'payment_method', rules: 'required|in:card,bank_transfer')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Emits(event: 'OrderPlaced', description: 'Dispatched after the order is persisted')]&lt;/span&gt;
    &lt;span class="na"&gt;#[SideEffect(description: 'Sends order confirmation email to the customer', tags: ['email'])]&lt;/span&gt;
    &lt;span class="na"&gt;#[SideEffect(description: 'Reserves inventory for each line item', tags: ['inventory'])]&lt;/span&gt;
    &lt;span class="na"&gt;#[Throws(exception: PaymentFailedException::class, description: 'When the gateway rejects the charge')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Throws(exception: InsufficientStockException::class, description: 'When a product cannot be reserved')]&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;store&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="kt"&gt;JsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;#[Route(method: 'DELETE', path: '/v1/orders/{id}', description: 'Cancel an existing order')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Authorize(ability: 'orders.cancel', description: 'Order owner or admin only')]&lt;/span&gt;
    &lt;span class="na"&gt;#[Emits(event: 'OrderCancelled')]&lt;/span&gt;
    &lt;span class="na"&gt;#[SideEffect(description: 'Releases reserved inventory back to stock', tags: ['inventory'])]&lt;/span&gt;
    &lt;span class="na"&gt;#[Throws(exception: OrderNotFoundException::class)]&lt;/span&gt;
    &lt;span class="na"&gt;#[Throws(exception: OrderAlreadyShippedException::class, description: 'When the order has already left the warehouse')]&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;destroy&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;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;php vendor/bin/signal generate&lt;/code&gt; against that file produces a Markdown section with full route, authorization, validation, event, side effect, and exception tables, grouped under the Controllers heading with a table of contents entry. Everything is structured and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the generated Markdown looks like
&lt;/h2&gt;

&lt;p&gt;For the &lt;code&gt;store()&lt;/code&gt; method above, Signal produces something close to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;#### `store()`&lt;/span&gt;

&lt;span class="gs"&gt;**Route:**&lt;/span&gt; &lt;span class="sb"&gt;`POST /v1/orders`&lt;/span&gt; - Place a new order

&lt;span class="gs"&gt;**Requires Authorization:**&lt;/span&gt;

| Ability | Description |
|---------|-------------|
| &lt;span class="sb"&gt;`orders.create`&lt;/span&gt; | - |

&lt;span class="gs"&gt;**Validates:**&lt;/span&gt;

| Field | Rules | Description |
|-------|-------|-------------|
| &lt;span class="sb"&gt;`items`&lt;/span&gt; | &lt;span class="sb"&gt;`required\|array\|min:1`&lt;/span&gt; | Line items for the order |
| &lt;span class="sb"&gt;`items.*.product_id`&lt;/span&gt; | &lt;span class="sb"&gt;`required\|integer\|exists:products,id`&lt;/span&gt; | - |
| &lt;span class="sb"&gt;`items.*.quantity`&lt;/span&gt; | &lt;span class="sb"&gt;`required\|integer\|min:1`&lt;/span&gt; | - |
| &lt;span class="sb"&gt;`payment_method`&lt;/span&gt; | &lt;span class="sb"&gt;`required\|in:card,bank_transfer`&lt;/span&gt; | - |

&lt;span class="gs"&gt;**Emits:**&lt;/span&gt;

| Event | Description |
|-------|-------------|
| &lt;span class="sb"&gt;`OrderPlaced`&lt;/span&gt; | Dispatched after the order is persisted |

&lt;span class="gs"&gt;**Side Effects:**&lt;/span&gt;

| Description | Tags |
|-------------|------|
| Sends order confirmation email to the customer | &lt;span class="sb"&gt;`email`&lt;/span&gt; |
| Reserves inventory for each line item | &lt;span class="sb"&gt;`inventory`&lt;/span&gt; |

&lt;span class="gs"&gt;**Throws:**&lt;/span&gt;

| Exception | Description |
|-----------|-------------|
| &lt;span class="sb"&gt;`PaymentFailedException`&lt;/span&gt; | When the gateway rejects the charge |
| &lt;span class="sb"&gt;`InsufficientStockException`&lt;/span&gt; | When a product cannot be reserved |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean tables. No noise. Everything a developer needs to understand a method without opening the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JSON output
&lt;/h2&gt;

&lt;p&gt;The JSON format deserves attention on its own because it is more than a documentation format. It is machine-readable, which means you can use it as input for custom tooling.&lt;/p&gt;

&lt;p&gt;You could pipe it into an internal developer portal. You could use it as context when generating tests or reviewing code with an LLM. You could write a validation step in CI that ensures every public controller method has at least a &lt;code&gt;#[Route]&lt;/code&gt; and a &lt;code&gt;#[Authorize]&lt;/code&gt; attribute before a pull request can merge.&lt;/p&gt;

&lt;p&gt;Here is a trimmed example of what the JSON looks like for the controller above:&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;"generated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-04-30T09:00:00+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"classes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderController"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"App&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Http&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Controllers&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Api&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;V1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"fully_qualified_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"App&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Http&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Controllers&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Api&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;V1&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;OrderController"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/Http/Controllers/Api/V1/OrderController.php"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"controller"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RESTful controller for managing customer orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Handles order business logic"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderRepository"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"methods"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/v1/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Place a new order"&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"authorize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders.create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"validates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required|array|min:1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Line items for the order"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"items.*.product_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required|integer|exists:products,id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"emits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderPlaced"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dispatched after the order is persisted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"side_effects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sends order confirmation email to the customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"throws"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"exception"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PaymentFailedException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"When the gateway rejects the charge"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure is consistent and predictable. Every class has the same shape. Every method has the same shape. That predictability is what makes it useful as a data source rather than just a document to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fitting Signal into your workflow
&lt;/h2&gt;

&lt;p&gt;The simplest integration is to run &lt;code&gt;php vendor/bin/signal generate&lt;/code&gt; locally before committing. But you can go further.&lt;/p&gt;

&lt;p&gt;Add it to your CI pipeline and commit the output. If the generated output changes, the diff shows up in the pull request alongside the code change that caused it. Reviewers can see what the documentation impact of a change is without running anything locally.&lt;/p&gt;

&lt;p&gt;You can also use it as a gate. Write a step that runs Signal and then checks whether the output contains a &lt;code&gt;#[Route]&lt;/code&gt; and at least one &lt;code&gt;#[Authorize]&lt;/code&gt; for every controller method. If something is missing, the build fails. Documentation becomes enforced rather than encouraged.&lt;/p&gt;

&lt;p&gt;For teams that maintain an internal developer portal, the JSON output is a natural feed. Parse it, store it, render it however you need. The generation step can run on every merge to main and push updated docs automatically. You get a portal that stays current without anyone having to remember to update it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changes about how you write code
&lt;/h2&gt;

&lt;p&gt;The real value of Signal is not the docs themselves. It is the habit it creates.&lt;/p&gt;

&lt;p&gt;When you know that an attribute will appear in the generated output, you start thinking more carefully about what you are building. Declaring &lt;code&gt;#[Throws]&lt;/code&gt; forces you to consider what exceptions a method should actually surface. Declaring &lt;code&gt;#[SideEffect]&lt;/code&gt; forces you to think about what your code does beyond the return value. Declaring &lt;code&gt;#[DependsOn]&lt;/code&gt; makes implicit coupling explicit. Declaring &lt;code&gt;#[Emits]&lt;/code&gt; means you have to name the events you are dispatching, which tends to make you think harder about whether they are well-named in the first place.&lt;/p&gt;

&lt;p&gt;Documentation is usually an afterthought. With Signal, it is part of the act of writing the class. That shift is small. The compounding effect across a codebase and a team is significant.&lt;/p&gt;

&lt;p&gt;If you have ever inherited a codebase where the docs were useless and the only source of truth was reading the implementation line by line, you already know why that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;Signal is open source and available at &lt;a href="https://github.com/JustSteveKing/signal" rel="noopener noreferrer"&gt;github.com/JustSteveKing/signal&lt;/a&gt;. Install it, annotate a controller or service, run the generator, and see what comes out. Issues and contributions are welcome.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you build something interesting with the JSON output, whether that is a custom portal, a CI gate, or something I haven't thought of, I would genuinely like to hear about it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>documentation</category>
      <category>attributes</category>
      <category>devrel</category>
    </item>
    <item>
      <title>Building Modern Laravel APIs: The Action Pattern</title>
      <dc:creator>Steve McDougall</dc:creator>
      <pubDate>Tue, 14 Apr 2026 09:18:24 +0000</pubDate>
      <link>https://dev.to/juststevemcd/building-modern-laravel-apis-the-action-pattern-4b0l</link>
      <guid>https://dev.to/juststevemcd/building-modern-laravel-apis-the-action-pattern-4b0l</guid>
      <description>&lt;p&gt;We have a lead ingestion endpoint. Leads arrive, get validated, and get persisted with a &lt;code&gt;pending&lt;/code&gt; status and a score of zero. That is the raw intake. Now we need to do something with them.&lt;/p&gt;

&lt;p&gt;In this article we are going to build the processing pipeline that transforms a raw lead into something genuinely useful for a sales team. That means AI-powered enrichment via the Laravel AI SDK, a scoring engine that assigns a priority value, and a clean pipeline that wires it all together using composable Action classes.&lt;/p&gt;

&lt;p&gt;This is also the article where the Action pattern pays off most visibly. We are not building one thing - we are building three distinct operations and then composing them. The architecture makes that composition clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Processing Pipeline
&lt;/h2&gt;

&lt;p&gt;Before writing any code, let's be clear about what the pipeline does. When a lead arrives it has a &lt;code&gt;pending&lt;/code&gt; status. We need to:&lt;/p&gt;

&lt;p&gt;Take the raw payload and use AI to infer or fill in missing context - company size, industry, seniority level, anything that was not explicitly provided but can be reasonably inferred. Store that as &lt;code&gt;enriched_data&lt;/code&gt; on the lead. Then calculate a score between 0 and 100 based on the enriched data and the scoring rules we care about. Finally update the lead status to &lt;code&gt;enriched&lt;/code&gt; and persist both the enriched data and the score.&lt;/p&gt;

&lt;p&gt;Three jobs. Three actions. One pipeline action that composes them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Laravel AI SDK
&lt;/h2&gt;

&lt;p&gt;The AI SDK is already part of Laravel 13. Install it and publish the config:&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
php artisan vendor:publish &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Laravel&lt;/span&gt;&lt;span class="se"&gt;\A&lt;/span&gt;&lt;span class="s2"&gt;i&lt;/span&gt;&lt;span class="se"&gt;\A&lt;/span&gt;&lt;span class="s2"&gt;iServiceProvider"&lt;/span&gt;
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration creates the &lt;code&gt;agent_conversations&lt;/code&gt; and &lt;code&gt;agent_conversation_messages&lt;/code&gt; tables that power the SDK's conversation storage. Configure your provider credentials in &lt;code&gt;.env&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;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-key-here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK supports OpenAI, Anthropic, Gemini, and several others. Because we are using the SDK's abstraction layer, swapping providers is a config change, not a code change. That is exactly the kind of flexibility a production application needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Enrichment Agent
&lt;/h2&gt;

&lt;p&gt;The Laravel AI SDK introduces the concept of an Agent - a dedicated PHP class that encapsulates the instructions and output schema for interacting with a language model. Agents implement contracts rather than extend a base class, which keeps them clean and composable.&lt;/p&gt;

&lt;p&gt;Generate one:&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:agent LeadEnrichmentAgent &lt;span class="nt"&gt;--structured&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a class in &lt;code&gt;app/Ai/Agents/&lt;/code&gt; that implements &lt;code&gt;Agent&lt;/code&gt; and &lt;code&gt;HasStructuredOutput&lt;/code&gt; and uses the &lt;code&gt;Promptable&lt;/code&gt; trait. Update it at &lt;code&gt;app/Ai/Agents/LeadEnrichmentAgent.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="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Ai\Agents&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\JsonSchema\JsonSchema&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\Contracts\Agent&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\Contracts\HasStructuredOutput&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\Promptable&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;Stringable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeadEnrichmentAgent&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HasStructuredOutput&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;Promptable&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;instructions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Stringable&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;return&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;PROMPT
        You are a B2B sales intelligence assistant. Given the information about a lead,
        infer and return structured enrichment data that would help a sales team prioritise
        and personalise their outreach.

        Use only the information provided. Do not invent specific facts. Where something
        cannot be reasonably inferred, return null for that field.
        PROMPT;&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;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;JsonSchema&lt;/span&gt; &lt;span class="nv"&gt;$schema&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;'industry'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&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="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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The lead\'s industry sector, e.g. "SaaS", "FinTech", "Healthcare"'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'company_size'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&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="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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Estimated company size, e.g. "1-10", "11-50", "51-200", "201-1000", "1000+"'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'seniority_level'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&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="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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The lead\'s seniority, e.g. "Junior", "Mid", "Senior", "Director", "C-Level"'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'is_decision_maker'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;boolean&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Whether this lead is likely a decision maker based on their job title'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'inferred_pain_points'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$schema&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'A list of likely pain points based on the lead\'s role and company context'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'enrichment_confidence'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;number&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;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'A confidence score from 0.0 to 1.0 indicating how much context was available for enrichment'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;schema()&lt;/code&gt; method receives a &lt;code&gt;JsonSchema $schema&lt;/code&gt; instance and returns an array of field definitions. Each field uses the fluent schema builder to describe its type, nullability, and what it represents. The SDK enforces this shape on the model's response, so we always get back a typed, predictable structure rather than freeform text we have to parse and validate ourselves.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;instructions()&lt;/code&gt; method defines the system prompt. It tells the model exactly what its job is and explicitly instructs it not to invent specific facts - a critical constraint for a production application where hallucinated data in a CRM causes real problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Enrichment Action
&lt;/h2&gt;

&lt;p&gt;Now the Action that uses the agent.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;app/Actions/Leads/EnrichLead.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="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Actions\Leads&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\Ai\Agents\LeadEnrichmentAgent&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\Enums\LeadStatus&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\Lead&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnrichLead&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;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;LeadEnrichmentAgent&lt;/span&gt; &lt;span class="nv"&gt;$agent&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;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Lead&lt;/span&gt; &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Lead&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$enrichmentData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prompt&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;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'enriched_data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$enrichmentData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;LeadStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Enriching&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;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Lead&lt;/span&gt; &lt;span class="nv"&gt;$lead&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="nv"&gt;$context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s2"&gt;"Email: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"Name: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"Company: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;job_title&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"Job title: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;job_title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"Phone: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"Lead source: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Enrich this lead:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$context&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;A few things worth calling out. The agent is injected via the constructor - the container resolves it automatically, which means we can swap the underlying AI provider by changing configuration, not code.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;LeadEnrichmentAgent&lt;/code&gt; implements &lt;code&gt;HasStructuredOutput&lt;/code&gt;, calling &lt;code&gt;-&amp;gt;prompt()&lt;/code&gt; returns the structured response directly as an object. We cast it to an array for storage in the &lt;code&gt;enriched_data&lt;/code&gt; JSON column.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;buildPrompt()&lt;/code&gt; method uses &lt;code&gt;array_filter()&lt;/code&gt; to remove null values before building the context string. We only send the model data we actually have. Sending empty fields wastes tokens and can confuse the enrichment output.&lt;/p&gt;

&lt;p&gt;We set status to &lt;code&gt;LeadStatus::Enriching&lt;/code&gt; rather than &lt;code&gt;LeadStatus::Enriched&lt;/code&gt; at this stage - the lead is mid-pipeline. The orchestrating &lt;code&gt;ProcessLead&lt;/code&gt; action sets it to &lt;code&gt;Enriched&lt;/code&gt; once scoring is also complete.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$lead-&amp;gt;fresh()&lt;/code&gt; at the end returns a fresh model instance from the database with the updated values. Returning the lead rather than void keeps the action composable - the next action in the pipeline receives the updated model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scoring Action
&lt;/h2&gt;

&lt;p&gt;Scoring translates the enriched data into a single integer between 0 and 100 that tells the sales team how much to prioritise this lead. The scoring rules are business logic, not AI - they should be explicit, deterministic, and easy to adjust.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;app/Actions/Leads/ScoreLead.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="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Actions\Leads&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\Lead&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ScoreLead&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;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Lead&lt;/span&gt; &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Lead&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$score&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;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'score'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$score&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;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Lead&lt;/span&gt; &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$enriched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;enriched_data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="c1"&gt;// Decision makers are high value&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;$enriched&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'is_decision_maker'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Seniority signals&lt;/span&gt;
        &lt;span class="nv"&gt;$score&lt;/span&gt; &lt;span class="o"&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;$enriched&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'seniority_level'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'C-Level'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'Director'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'Senior'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'Mid'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="c1"&gt;// Company size signals - larger companies are more valuable&lt;/span&gt;
        &lt;span class="nv"&gt;$score&lt;/span&gt; &lt;span class="o"&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;$enriched&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'company_size'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'1000+'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'201-1000'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'51-200'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'11-50'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="c1"&gt;// Reward high enrichment confidence&lt;/span&gt;
        &lt;span class="nv"&gt;$confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$enriched&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'enrichment_confidence'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$score&lt;/span&gt; &lt;span class="o"&gt;+=&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;$confidence&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Penalise missing key fields&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$score&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="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="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;job_title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$score&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;max&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="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$score&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scoring rules are explicit match expressions rather than conditionals buried in if/else chains. That makes them easy to read and easy to adjust when the business decides that company size matters more than seniority, or that a new tier needs adding.&lt;/p&gt;

&lt;p&gt;The final &lt;code&gt;max(0, min(100, $score))&lt;/code&gt; clamps the result to the valid range regardless of how the rules interact. Defensive but correct.&lt;/p&gt;

&lt;p&gt;The enrichment confidence score from the AI agent feeds directly into the scoring calculation. A lead enriched from a rich dataset of context signals - job title, company, industry - gets a small bonus. A lead where the model had little to work with gets less. This creates a natural feedback loop that rewards more complete inbound data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline Action
&lt;/h2&gt;

&lt;p&gt;Now we compose. &lt;code&gt;ProcessLead&lt;/code&gt; is the orchestrating action that runs the full pipeline end-to-end.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;app/Actions/Leads/ProcessLead.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="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Actions\Leads&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\Lead&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;Throwable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessLead&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;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;EnrichLead&lt;/span&gt; &lt;span class="nv"&gt;$enrichLead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;ScoreLead&lt;/span&gt; &lt;span class="nv"&gt;$scoreLead&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;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Lead&lt;/span&gt; &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Lead&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="nv"&gt;$lead&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;enrichLead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$lead&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;scoreLead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'enriched'&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;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fresh&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="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

            &lt;span class="k"&gt;throw&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="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 pipeline is explicit and linear. Enrich, then score, then mark as enriched. If anything throws, mark the lead as &lt;code&gt;failed&lt;/code&gt; and re-throw so the caller can handle it at the appropriate level.&lt;/p&gt;

&lt;p&gt;Re-throwing rather than swallowing the exception is important. Swallowing exceptions in pipelines is one of the more reliable ways to create debugging nightmares - leads silently fail and nobody knows why. We catch to set the status, then re-throw so the problem surfaces properly.&lt;/p&gt;

&lt;p&gt;The dependency injection chain here is worth appreciating. &lt;code&gt;ProcessLead&lt;/code&gt; declares its dependencies. Laravel's container resolves &lt;code&gt;EnrichLead&lt;/code&gt;, which in turn declares its dependency on &lt;code&gt;LeadEnrichmentAgent&lt;/code&gt;, which the container also resolves. Nothing is manually instantiated. The whole pipeline wires together through the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lead Status as an Enum
&lt;/h2&gt;

&lt;p&gt;As we add more statuses it becomes clear that representing them as raw strings is fragile. Let's introduce a proper enum.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;app/Enums/LeadStatus.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="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&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;LeadStatus&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;Pending&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="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Enriching&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'enriching'&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;Enriched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'enriched'&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;Failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'failed'&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;Update the &lt;code&gt;Lead&lt;/code&gt; model to cast the status column to this enum:&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;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;'raw_payload'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'enriched_data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'score'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;LeadStatus&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;Update the &lt;code&gt;IngestLead&lt;/code&gt; action to use the enum:&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="nc"&gt;Lead&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="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'raw_payload'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;LeadStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'score'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update &lt;code&gt;ProcessLead&lt;/code&gt; and &lt;code&gt;EnrichLead&lt;/code&gt; to use the enum constants rather than raw strings:&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;// In EnrichLead&lt;/span&gt;
&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'enriched_data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$enrichmentData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;LeadStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Enriching&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// In ProcessLead&lt;/span&gt;
&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;LeadStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Enriched&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// and on failure:&lt;/span&gt;
&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;LeadStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if a status value is mistyped or a new case needs adding, the type system catches it rather than silent string comparison failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triggering the Pipeline
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ProcessLead&lt;/code&gt; action needs to be called somewhere. For now, we can trigger it directly from the &lt;code&gt;IngestLead&lt;/code&gt; action for demonstration - but in Article 9 we will move it to a queued job so the HTTP response is not blocked by the AI enrichment call.&lt;/p&gt;

&lt;p&gt;Update &lt;code&gt;app/Actions/Leads/IngestLead.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="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Actions\Leads&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\Enums\LeadStatus&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\Http\Payloads\Leads\StoreLeadPayload&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\Lead&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IngestLead&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;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;ProcessLead&lt;/span&gt; &lt;span class="nv"&gt;$processLead&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;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StoreLeadPayload&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Lead&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$lead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Lead&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="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'raw_payload'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;LeadStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'score'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;processLead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the synchronous version. The controller calls &lt;code&gt;IngestLead&lt;/code&gt;, which creates the lead and immediately kicks off enrichment. In a real production environment this would block the HTTP response while the AI call completes - acceptable for now, properly addressed with queues in Article 9.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the AI Provider
&lt;/h2&gt;

&lt;p&gt;Add your provider key to &lt;code&gt;.env&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;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-key-here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default provider is configured in &lt;code&gt;config/ai.php&lt;/code&gt;. For Pulse-Link, Anthropic's Claude is a sensible default given the quality of structured output for enrichment tasks:&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="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AI_PROVIDER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because we are using the SDK's agent abstraction, switching to OpenAI is a one-line change to the default provider config. The &lt;code&gt;LeadEnrichmentAgent&lt;/code&gt; does not know or care which model is running underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Have Now
&lt;/h2&gt;

&lt;p&gt;The Action pattern is doing real work. Three single-responsibility actions - &lt;code&gt;EnrichLead&lt;/code&gt;, &lt;code&gt;ScoreLead&lt;/code&gt;, and &lt;code&gt;ProcessLead&lt;/code&gt; - compose into a pipeline that takes a raw lead from &lt;code&gt;pending&lt;/code&gt; to &lt;code&gt;enriched&lt;/code&gt; with an AI-generated context payload and a deterministic score.&lt;/p&gt;

&lt;p&gt;Each action is independently testable. Swap the agent for a fake in tests, test the scoring logic in isolation, test the pipeline orchestration without touching the AI layer. We will do exactly that in Article 8.&lt;/p&gt;

&lt;p&gt;The pipeline is also extensible. Adding a deduplication step, a geographic enrichment step, or a company data lookup is a matter of creating a new action and adding it to &lt;code&gt;ProcessLead&lt;/code&gt;. The existing actions do not change.&lt;/p&gt;

&lt;p&gt;In the next article we are going to build the lead scoring and prioritisation layer in more depth - turning the enriched data into a ranked list that the sales team can actually work from.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next: Lead Scoring and Prioritisation - building the ranking engine that surfaces the highest-value leads first using enriched data and configurable scoring rules.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>apidesign</category>
      <category>apiarchitecture</category>
      <category>restapi</category>
    </item>
  </channel>
</rss>
