{
    "version": "https://jsonfeed.org/version/1.1",
    "title": "Phil Stephens - all posts",
    "description": "All of my posts",
    "home_page_url": "https://philstephens.com",
    "feed_url": "https://philstephens.com/feed/posts/json",
    "language": "en-US",
    "authors": [{
            "name": "Phil Stephens"
        }
    ],
    "items": [{
            "id": "https://philstephens.com/112",
            "title": "The risks of adopting AI-led development (my humble opinion)",
            "url": "https://philstephens.com/blog/the-risks-of-adopting-ai-led-development-my-humble-opinion",
            "content_html": "<p>There is a lot of pressure right now to adopt AI-led development practices, and most of it is coming from outside engineering. Boards want productivity gains. Investors want AI in the pitch deck. CEOs want to be seen leading the transition rather than catching up to it. The question that tends to get skipped in all of that is the one engineering leaders actually have to answer: if we change how our teams work, what can go wrong, and what do we do about it?<\/p>\n<p>I am genuinely enthusiastic about these tools. I use Claude Code and OpenAI Codex daily, and I think the productivity gains for experienced engineers are real. But I also think the disposition this transition needs (especially from the people leading it) is <em>well-founded<\/em> enthusiasm rather than <em>uncritical<\/em> enthusiasm. There are three risks I keep coming back to, in roughly this order of importance.<\/p>\n<h2 id=\"erosion-of-review-discipline\">Erosion of review discipline<\/h2>\n<p>The biggest risk is not AI writing bad code. AI writes pretty good code, most of the time. The risk is humans stopping reading it carefully. Output that looks plausible gets waved through. What used to be a meaningful review becomes a rubber stamp. Six months in, you have a codebase where nobody is entirely sure who understood what before it shipped.<\/p>\n<p>Part of what makes this risk so pernicious is that it is partly structural, not just a matter of individual laziness. AI coding agents dramatically increase the throughput of code being created. They do not, on their own, increase the speed at which code gets reviewed. Review is still a human activity, constrained by how much context a person can hold in their head and how carefully they can reason about a change. So the bottleneck shifts. Writing code stops being the slow part. Reviewing it <em>properly<\/em> becomes the new constraint on delivery.<\/p>\n<p>That is not, in itself, a problem. It is arguably a healthy rebalancing. The problem is what happens next. Teams that were previously judged on how much code they shipped now find their throughput pinned at the review stage, and the pressure to loosen up starts to build. Pull requests sit open for days. A backlog of AI-generated changes accumulates. Someone, somewhere, decides that reviews need to move faster, and the obvious way to make that happen is to read less carefully. The discipline does not collapse because anyone consciously chose to abandon it; it erodes because the system is now producing more output than the old review process was designed to absorb.<\/p>\n<p>The mitigation, I think, is to be very explicit with the team that AI changes who writes the first draft, but not who is accountable for what ships. Standards for testing, observability, and CI become more important, not less. Review criteria should tighten rather than relax. If a reviewer cannot explain what a change does and why it is correct, it is not ready to merge, regardless of who or what drafted it. And if that genuinely slows delivery, the honest answer is that the team's actual delivery rate was always constrained by how much code it could responsibly ship, not by how much it could type. AI tools have just made that constraint visible.<\/p>\n<p>Most of the security and IP-leakage incidents I have seen discussed are downstream symptoms of this same discipline gap. Treat the discipline as the primary control point and a lot of the secondary concerns become easier to manage.<\/p>\n<h2 id=\"skill-atrophy-particularly-for-less-experienced-engineers\">Skill atrophy, particularly for less experienced engineers<\/h2>\n<p>Mid and senior engineers have the mental models to spot when AI-generated code is wrong in subtle ways. Juniors often do not yet, and handing them a tool that produces confident, articulate output can short-circuit the deliberate practice they need to build those models in the first place.<\/p>\n<p>This one worries me more the longer I think about it. The path from junior to senior runs through a lot of staring at broken code, reading documentation, and figuring out why the obvious solution is wrong. If that path is replaced by prompting until something works, we risk producing a generation of engineers who are extremely productive in familiar territory and completely stuck the moment they leave it.<\/p>\n<p>The mitigation is pairing, structured learning expectations, and being deliberate about which work gets done with AI assistance and which gets done as foundational skill-building. Juniors will benefit enormously from these tools - but only if we protect the conditions that let them become seniors.<\/p>\n<h2 id=\"cargo-culting-complex-patterns-where-simpler-ones-would-do\">Cargo-culting complex patterns where simpler ones would do<\/h2>\n<p>It is tempting, particularly when your board is asking about AI strategy, to reach for autonomous agents or elaborate multi-step workflows when a constrained chain or a well-scoped function call would deliver the same outcome more reliably, more cheaply, and more predictably.<\/p>\n<p>Knowing when <em>not<\/em> to use agents is, I think, a genuine signal of engineering maturity. A boring, reliable LLM feature that customers can depend on is almost always more valuable than an impressive demo that works in the pitch and fails in production.<\/p>\n<hr \/>\n<p>These three risks are connected, and the connection matters. AI accelerates whatever foundation already exists - good practice or bad. Teams with solid review culture, clear testing standards, and engineers who know when to reach for simple tools will ship better software, faster. Teams without those things will ship worse software, faster.<\/p>\n<p>That means the work of leading an AI-led development transition is at least as much about strengthening fundamentals as it is about rolling out tooling. I would even argue the tooling is the easier part. Tools can be installed in an afternoon. Changing how a team thinks about review, about junior development, and about architectural restraint takes considerably longer - and without those changes, the tools amplify exactly the problems you were hoping they would solve.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: The risks of adopting AI-led development (my humble opinion)\">Email a comment<\/a><\/p>",
            "summary": "<p>There is a lot of pressure right now to adopt AI-led development practices, and most of it is coming from outside engineering. Boards want productivity gains. Investors want AI in the pitch deck. CEOs want to be seen leading the transition rather than catching up to it. The question that tends to get skipped in all of that is the one engineering leaders actually have to answer: if we change how our teams work, what can go wrong, and what do we do about it?<\/p>\n<p>I am genuinely enthusiastic about these tools. I use Claude Code and OpenAI Codex daily, and I think the productivity gains for experienced engineers are real. But I also think the disposition this transition needs (especially from the people leading it) is <em>well-founded<\/em> enthusiasm rather than <em>uncritical<\/em> enthusiasm. There are three risks I keep coming back to, in roughly this order of importance.<\/p>\n<h2 id=\"erosion-of-review-discipline\">Erosion of review discipline<\/h2>\n<p>The biggest risk is not AI writing bad code. AI writes pretty good code, most of the time. The risk is humans stopping reading it carefully. Output that looks plausible gets waved through. What used to be a meaningful review becomes a rubber stamp. Six months in, you have a codebase where nobody is entirely sure who understood what before it shipped.<\/p>\n<p>Part of what makes this risk so pernicious is that it is partly structural, not just a matter of individual laziness. AI coding agents dramatically increase the throughput of code being created. They do not, on their own, increase the speed at which code gets reviewed. Review is still a human activity, constrained by how much context a person can hold in their head and how carefully they can reason about a change. So the bottleneck shifts. Writing code stops being the slow part. Reviewing it <em>properly<\/em> becomes the new constraint on delivery.<\/p>\n<p>That is not, in itself, a problem. It is arguably a healthy rebalancing. The problem is what happens next. Teams that were previously judged on how much code they shipped now find their throughput pinned at the review stage, and the pressure to loosen up starts to build. Pull requests sit open for days. A backlog of AI-generated changes accumulates. Someone, somewhere, decides that reviews need to move faster, and the obvious way to make that happen is to read less carefully. The discipline does not collapse because anyone consciously chose to abandon it; it erodes because the system is now producing more output than the old review process was designed to absorb.<\/p>\n<p>The mitigation, I think, is to be very explicit with the team that AI changes who writes the first draft, but not who is accountable for what ships. Standards for testing, observability, and CI become more important, not less. Review criteria should tighten rather than relax. If a reviewer cannot explain what a change does and why it is correct, it is not ready to merge, regardless of who or what drafted it. And if that genuinely slows delivery, the honest answer is that the team's actual delivery rate was always constrained by how much code it could responsibly ship, not by how much it could type. AI tools have just made that constraint visible.<\/p>\n<p>Most of the security and IP-leakage incidents I have seen discussed are downstream symptoms of this same discipline gap. Treat the discipline as the primary control point and a lot of the secondary concerns become easier to manage.<\/p>\n<h2 id=\"skill-atrophy-particularly-for-less-experienced-engineers\">Skill atrophy, particularly for less experienced engineers<\/h2>\n<p>Mid and senior engineers have the mental models to spot when AI-generated code is wrong in subtle ways. Juniors often do not yet, and handing them a tool that produces confident, articulate output can short-circuit the deliberate practice they need to build those models in the first place.<\/p>\n<p>This one worries me more the longer I think about it. The path from junior to senior runs through a lot of staring at broken code, reading documentation, and figuring out why the obvious solution is wrong. If that path is replaced by prompting until something works, we risk producing a generation of engineers who are extremely productive in familiar territory and completely stuck the moment they leave it.<\/p>\n<p>The mitigation is pairing, structured learning expectations, and being deliberate about which work gets done with AI assistance and which gets done as foundational skill-building. Juniors will benefit enormously from these tools - but only if we protect the conditions that let them become seniors.<\/p>\n<h2 id=\"cargo-culting-complex-patterns-where-simpler-ones-would-do\">Cargo-culting complex patterns where simpler ones would do<\/h2>\n<p>It is tempting, particularly when your board is asking about AI strategy, to reach for autonomous agents or elaborate multi-step workflows when a constrained chain or a well-scoped function call would deliver the same outcome more reliably, more cheaply, and more predictably.<\/p>\n<p>Knowing when <em>not<\/em> to use agents is, I think, a genuine signal of engineering maturity. A boring, reliable LLM feature that customers can depend on is almost always more valuable than an impressive demo that works in the pitch and fails in production.<\/p>\n<hr \/>\n<p>These three risks are connected, and the connection matters. AI accelerates whatever foundation already exists - good practice or bad. Teams with solid review culture, clear testing standards, and engineers who know when to reach for simple tools will ship better software, faster. Teams without those things will ship worse software, faster.<\/p>\n<p>That means the work of leading an AI-led development transition is at least as much about strengthening fundamentals as it is about rolling out tooling. I would even argue the tooling is the easier part. Tools can be installed in an afternoon. Changing how a team thinks about review, about junior development, and about architectural restraint takes considerably longer - and without those changes, the tools amplify exactly the problems you were hoping they would solve.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: The risks of adopting AI-led development (my humble opinion)\">Email a comment<\/a><\/p>",
            "date_published": "2026-04-21T00:00:00+10:00",
            "date_modified": "2026-04-21T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/111",
            "title": "How I Actually Use AI Coding Tools",
            "url": "https://philstephens.com/blog/how-i-actually-use-ai-coding-tools",
            "content_html": "<p>As I apply for new roles, I keep getting asked the same question: how do I use AI coding tools? It comes up in almost every conversation now - and each time, I find myself explaining the same framework. So I figured I'd document it.<\/p>\n<p>This is a snapshot of how I use these tools today. I fully expect it to change as the technology advances - it already looks different from six months ago. But right now, my usage has settled into four distinct modes, and I think the distinction matters more than most people realise. Prompting an AI to build you a throwaway script is a fundamentally different activity from using it to add features to a codebase you know inside out, and treating them the same is where people get into trouble.<\/p>\n<p>Some context: I've been on a career break since mid-2025, so most of my usage has been on personal projects rather than production systems. That's meant less pressure, more room to experiment, and a front-row seat to watch these tools go from promising-but-clunky to genuinely good. I use Claude Code and OpenAI's Codex as my primary tools, paying for both but not on the expensive Pro or Max tiers. I don't have the budget to burn through tokens the way some of the &quot;vibe coding&quot; crowd seem to. In practice, I bounce between the two - partly because they have different strengths, but honestly, often just to work around rate limits.<\/p>\n<h2 id=\"mode-1-accelerating-what-i-already-know\">Mode 1: Accelerating What I Already Know<\/h2>\n<p>When I'm working in PHP and Laravel - my bread and butter - I'm not asking the AI to figure out how to build something. I already know how. The AI is there to make the mechanical parts faster.<\/p>\n<p>I've been dropping back into pre-existing codebases with a coding agent to backfill tests, tighten up code against best practices, refactor, add documentation, and build new features. My personal blog runs on a custom CMS, and I've also rebuilt a micro-blog system from an earlier project. These are codebases I understand deeply, and that understanding is what makes the AI useful rather than dangerous.<\/p>\n<p>Over time, I've built up a set of custom commands and skills to streamline this work. For example, I have a <code>\/readme<\/code> command that appraises the codebase and updates the README with any changes. These aren't anything fancy - skills are just prompts - but I've built them iteratively as patterns emerge. If I find myself repeating a prompt, I convert it into a skill, then refine it over time as I get a better feel for what the models respond well to. It's the same instinct as any developer abstracting a repeated pattern. The tooling just happens to be natural language.<\/p>\n<p>In this mode, my existing expertise acts as a quality filter. I can spot when the AI suggests something that's technically correct but idiomatically wrong, or when it's taken a shortcut that'll cause problems later. This is where I think experienced developers get the most leverage - not by handing over control, but by accelerating the parts of coding that were already mechanical.<\/p>\n<h2 id=\"mode-2-learning-with-a-safety-net\">Mode 2: Learning With a Safety Net<\/h2>\n<p>I've been building a SwiftUI app for managing household chores - complete with iCloud sync and collaboration. It's a personal project, but I've approached it with the rigour of a production app, and I'll be publishing it to the App Store shortly.<\/p>\n<p>SwiftUI is not a language I have deep muscle memory in, and that changes the dynamic completely. The AI handles the syntax and platform-specific idioms I haven't internalised yet, while I focus on architecture and logic - things that transfer across languages and frameworks.<\/p>\n<p>But I'm reading every line. I'm asking why it chose one pattern over another. I'm looking up constructs I don't recognise. The AI is essentially a patient tutor who also writes working code, and the code itself becomes a learning artifact. This is genuinely faster than tutorials and documentation alone, because I'm learning in the context of the thing I actually want to build, not a toy example.<\/p>\n<p>The tradeoff is speed. This mode is slower than Mode 1 because I'm stopping to understand, not just shipping. But the goal isn't speed - it's competence. The spec tends to be very clear and focused, which helps keep the AI on track and gives me a solid basis to evaluate what it produces.<\/p>\n<h2 id=\"mode-3-disposable-tools\">Mode 3: Disposable Tools<\/h2>\n<p>Sometimes I need a tool and I genuinely don't care about the code.<\/p>\n<p>I built a CLI tool in Swift to export my Photos library into a specific folder structure - different from what Apple's built-in export gives you. I wrote a Python script to upload my entire Instagram history to my micro-blog. In both cases, I gave the AI maximum freedom. It picked the approach, the libraries, whatever it wanted. I ran the result, checked the output, and moved on.<\/p>\n<p>This is the mode where AI coding tools deliver the most obvious, immediate value. Things that would have taken half an hour of documentation-reading and boilerplate now take a few minutes of conversation. The ROI is hard to argue with.<\/p>\n<p>It's also the least interesting mode to talk about, because it doesn't require much skill or judgment. The AI handles it, it works (or you tell it what's wrong and it fixes it), and you get on with your day.<\/p>\n<h2 id=\"mode-4-prototypes\">Mode 4: Prototypes<\/h2>\n<p>This sits somewhere between Modes 2 and 3. I care about the code more than a throwaway tool, but I'm less sure about the approach. These are experiments that might graduate into Mode 1 projects if they prove out.<\/p>\n<p>I built a RAG-based chatbot using Confluence documents as a proof-of-concept for my wife to use at work. I've also built a simple SwiftUI app as a mobile companion for my micro-blog platform. Neither of these is production-ready, but they're more than disposable - they're me testing whether an idea has legs before committing to building it properly.<\/p>\n<h2 id=\"what-ive-learned-along-the-way\">What I've Learned Along the Way<\/h2>\n<p><strong>Iterative beats one-shot, every time.<\/strong> With the exception of genuinely disposable tools, I don't try to generate entire features in a single prompt. Even with a comprehensive spec and the most capable model available, one-shotting tends to produce code that <em>looks<\/em> right but isn't quite. The app appears to work, but when you dig into the detail, specified functionality is missing or subtly wrong. The model doesn't hallucinate - it just quietly doesn't do the thing. That's a harder failure mode to catch than an obvious error, and it's why I prefer building iteratively, the same way I would by hand. Catch issues early, maintain quality, and keep a handle on token usage in the process.<\/p>\n<p><strong>A fresh set of eyes matters - even when those eyes are artificial.<\/strong> Beyond balancing token usage, I switch between Claude Code and Codex when one model gets stuck on a problem. A concrete example: my SwiftUI chores app worked perfectly in the development environment, but when I pushed to TestFlight - which hits the production database - nothing synced. Codex went round in circles trying different fixes. I switched to Claude, which spotted immediately that the development environment was hardcoded. A fresh context, without the history of failed attempts, made the difference.<\/p>\n<p><strong>Different tools have different strengths.<\/strong> For the kind of work I do, Codex tends to be better at big-picture coding - scaffolding, broad strokes, getting a feature stood up. Claude is stronger in the details - spotting subtle bugs, understanding nuanced requirements, getting the specifics right. Having both available means I'm rarely completely stuck.<\/p>\n<p><strong>Model choice matters more than people think.<\/strong> Rather than defaulting to the most capable model for every prompt, I'll use a lighter model for simpler tasks - refactoring, renaming, documentation, generating commit messages. It saves tokens, it's faster, and for mechanical tasks the output is just as good. Treating model selection as a conscious decision rather than always reaching for the top shelf is one of the easiest ways to get more out of these tools on a budget.<\/p>\n<p><strong>Your expertise is the multiplier.<\/strong> This is the thing I keep coming back to. The better you already understand what you're building, the more value you extract from these tools. They're an amplifier. In Mode 1, my deep knowledge of Laravel means I can move fast and catch mistakes. In Mode 2, my general software engineering experience lets me evaluate SwiftUI code I couldn't have written from scratch. Even in Mode 3, knowing what the output <em>should<\/em> look like means I can validate it quickly. The tools are powerful, but the judgment is yours.<\/p>\n<hr \/>\n<p>There are thousands of blog posts about AI coding tools, written by people with different budgets, different experience levels, and different appetites for letting the AI drive. This is just how I've found myself working - four modes, each with its own balance of control and delegation. Your mileage will vary. But if there's one thing I'd suggest, it's being deliberate about which mode you're in. The tool doesn't change. How you use it does.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: How I Actually Use AI Coding Tools\">Email a comment<\/a><\/p>",
            "summary": "<p>As I apply for new roles, I keep getting asked the same question: how do I use AI coding tools? It comes up in almost every conversation now - and each time, I find myself explaining the same framework. So I figured I'd document it.<\/p>\n<p>This is a snapshot of how I use these tools today. I fully expect it to change as the technology advances - it already looks different from six months ago. But right now, my usage has settled into four distinct modes, and I think the distinction matters more than most people realise. Prompting an AI to build you a throwaway script is a fundamentally different activity from using it to add features to a codebase you know inside out, and treating them the same is where people get into trouble.<\/p>\n<p>Some context: I've been on a career break since mid-2025, so most of my usage has been on personal projects rather than production systems. That's meant less pressure, more room to experiment, and a front-row seat to watch these tools go from promising-but-clunky to genuinely good. I use Claude Code and OpenAI's Codex as my primary tools, paying for both but not on the expensive Pro or Max tiers. I don't have the budget to burn through tokens the way some of the &quot;vibe coding&quot; crowd seem to. In practice, I bounce between the two - partly because they have different strengths, but honestly, often just to work around rate limits.<\/p>\n<h2 id=\"mode-1-accelerating-what-i-already-know\">Mode 1: Accelerating What I Already Know<\/h2>\n<p>When I'm working in PHP and Laravel - my bread and butter - I'm not asking the AI to figure out how to build something. I already know how. The AI is there to make the mechanical parts faster.<\/p>\n<p>I've been dropping back into pre-existing codebases with a coding agent to backfill tests, tighten up code against best practices, refactor, add documentation, and build new features. My personal blog runs on a custom CMS, and I've also rebuilt a micro-blog system from an earlier project. These are codebases I understand deeply, and that understanding is what makes the AI useful rather than dangerous.<\/p>\n<p>Over time, I've built up a set of custom commands and skills to streamline this work. For example, I have a <code>\/readme<\/code> command that appraises the codebase and updates the README with any changes. These aren't anything fancy - skills are just prompts - but I've built them iteratively as patterns emerge. If I find myself repeating a prompt, I convert it into a skill, then refine it over time as I get a better feel for what the models respond well to. It's the same instinct as any developer abstracting a repeated pattern. The tooling just happens to be natural language.<\/p>\n<p>In this mode, my existing expertise acts as a quality filter. I can spot when the AI suggests something that's technically correct but idiomatically wrong, or when it's taken a shortcut that'll cause problems later. This is where I think experienced developers get the most leverage - not by handing over control, but by accelerating the parts of coding that were already mechanical.<\/p>\n<h2 id=\"mode-2-learning-with-a-safety-net\">Mode 2: Learning With a Safety Net<\/h2>\n<p>I've been building a SwiftUI app for managing household chores - complete with iCloud sync and collaboration. It's a personal project, but I've approached it with the rigour of a production app, and I'll be publishing it to the App Store shortly.<\/p>\n<p>SwiftUI is not a language I have deep muscle memory in, and that changes the dynamic completely. The AI handles the syntax and platform-specific idioms I haven't internalised yet, while I focus on architecture and logic - things that transfer across languages and frameworks.<\/p>\n<p>But I'm reading every line. I'm asking why it chose one pattern over another. I'm looking up constructs I don't recognise. The AI is essentially a patient tutor who also writes working code, and the code itself becomes a learning artifact. This is genuinely faster than tutorials and documentation alone, because I'm learning in the context of the thing I actually want to build, not a toy example.<\/p>\n<p>The tradeoff is speed. This mode is slower than Mode 1 because I'm stopping to understand, not just shipping. But the goal isn't speed - it's competence. The spec tends to be very clear and focused, which helps keep the AI on track and gives me a solid basis to evaluate what it produces.<\/p>\n<h2 id=\"mode-3-disposable-tools\">Mode 3: Disposable Tools<\/h2>\n<p>Sometimes I need a tool and I genuinely don't care about the code.<\/p>\n<p>I built a CLI tool in Swift to export my Photos library into a specific folder structure - different from what Apple's built-in export gives you. I wrote a Python script to upload my entire Instagram history to my micro-blog. In both cases, I gave the AI maximum freedom. It picked the approach, the libraries, whatever it wanted. I ran the result, checked the output, and moved on.<\/p>\n<p>This is the mode where AI coding tools deliver the most obvious, immediate value. Things that would have taken half an hour of documentation-reading and boilerplate now take a few minutes of conversation. The ROI is hard to argue with.<\/p>\n<p>It's also the least interesting mode to talk about, because it doesn't require much skill or judgment. The AI handles it, it works (or you tell it what's wrong and it fixes it), and you get on with your day.<\/p>\n<h2 id=\"mode-4-prototypes\">Mode 4: Prototypes<\/h2>\n<p>This sits somewhere between Modes 2 and 3. I care about the code more than a throwaway tool, but I'm less sure about the approach. These are experiments that might graduate into Mode 1 projects if they prove out.<\/p>\n<p>I built a RAG-based chatbot using Confluence documents as a proof-of-concept for my wife to use at work. I've also built a simple SwiftUI app as a mobile companion for my micro-blog platform. Neither of these is production-ready, but they're more than disposable - they're me testing whether an idea has legs before committing to building it properly.<\/p>\n<h2 id=\"what-ive-learned-along-the-way\">What I've Learned Along the Way<\/h2>\n<p><strong>Iterative beats one-shot, every time.<\/strong> With the exception of genuinely disposable tools, I don't try to generate entire features in a single prompt. Even with a comprehensive spec and the most capable model available, one-shotting tends to produce code that <em>looks<\/em> right but isn't quite. The app appears to work, but when you dig into the detail, specified functionality is missing or subtly wrong. The model doesn't hallucinate - it just quietly doesn't do the thing. That's a harder failure mode to catch than an obvious error, and it's why I prefer building iteratively, the same way I would by hand. Catch issues early, maintain quality, and keep a handle on token usage in the process.<\/p>\n<p><strong>A fresh set of eyes matters - even when those eyes are artificial.<\/strong> Beyond balancing token usage, I switch between Claude Code and Codex when one model gets stuck on a problem. A concrete example: my SwiftUI chores app worked perfectly in the development environment, but when I pushed to TestFlight - which hits the production database - nothing synced. Codex went round in circles trying different fixes. I switched to Claude, which spotted immediately that the development environment was hardcoded. A fresh context, without the history of failed attempts, made the difference.<\/p>\n<p><strong>Different tools have different strengths.<\/strong> For the kind of work I do, Codex tends to be better at big-picture coding - scaffolding, broad strokes, getting a feature stood up. Claude is stronger in the details - spotting subtle bugs, understanding nuanced requirements, getting the specifics right. Having both available means I'm rarely completely stuck.<\/p>\n<p><strong>Model choice matters more than people think.<\/strong> Rather than defaulting to the most capable model for every prompt, I'll use a lighter model for simpler tasks - refactoring, renaming, documentation, generating commit messages. It saves tokens, it's faster, and for mechanical tasks the output is just as good. Treating model selection as a conscious decision rather than always reaching for the top shelf is one of the easiest ways to get more out of these tools on a budget.<\/p>\n<p><strong>Your expertise is the multiplier.<\/strong> This is the thing I keep coming back to. The better you already understand what you're building, the more value you extract from these tools. They're an amplifier. In Mode 1, my deep knowledge of Laravel means I can move fast and catch mistakes. In Mode 2, my general software engineering experience lets me evaluate SwiftUI code I couldn't have written from scratch. Even in Mode 3, knowing what the output <em>should<\/em> look like means I can validate it quickly. The tools are powerful, but the judgment is yours.<\/p>\n<hr \/>\n<p>There are thousands of blog posts about AI coding tools, written by people with different budgets, different experience levels, and different appetites for letting the AI drive. This is just how I've found myself working - four modes, each with its own balance of control and delegation. Your mileage will vary. But if there's one thing I'd suggest, it's being deliberate about which mode you're in. The tool doesn't change. How you use it does.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: How I Actually Use AI Coding Tools\">Email a comment<\/a><\/p>",
            "date_published": "2026-04-01T00:00:00+10:00",
            "date_modified": "2026-04-01T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/110",
            "title": "I Let AI Interview Me to Rewrite My Resume",
            "url": "https://philstephens.com/blog/i-let-ai-interview-me-to-rewrite-my-resume",
            "content_html": "<p>I've been job hunting for about two months now. If you've read my earlier posts, you'll know it hasn't been smooth. The rejections have been consistent, the feedback generic, and the pattern frustratingly clear: my experience isn't the problem - my resume is.<\/p>\n<p>That's a hard thing to admit when you've spent weeks refining it. But the evidence was difficult to ignore. Roles I was genuinely qualified for were filtering me out before a human ever read a word. And when I did get through to a screening call, the outcome was the same polite, non-specific rejection that tells you nothing useful.<\/p>\n<p>So I tried something different. I sat down with <a href=\"https:\/\/claude.ai\">Claude<\/a> - Anthropic's AI assistant - and asked it to help me rewrite my resume. What I didn't expect was how the process would unfold.<\/p>\n<h2 id=\"the-resume-i-started-with\">The resume I started with<\/h2>\n<p>My original resume was pretty good. It listed my roles, my titles, my skills. It had bullet points that described what I did at each company. It was accurate - but it was completely failing to land.<\/p>\n<p>The problem, I think, is that I wrote it the way most of us write resumes: as a factual record. Here's where I worked, here's what my title was, here's a sanitised summary of my responsibilities. It read like a job description, not a story. And in a market where your CV has about six seconds to make an impression - assuming it gets past the ATS at all - &quot;accurate but unremarkable&quot; is a death sentence.<\/p>\n<h2 id=\"the-interview\">The interview<\/h2>\n<p>I expected Claude to take my resume, sprinkle some stronger verbs on it, and hand it back. Instead, it started asking questions. A lot of questions.<\/p>\n<p>Not surface-level ones either. It wanted to know what was actually broken when I joined <em>Company X<\/em>. How bad was the codebase? What did the team look like? What specific changes did I make, and what happened as a result? It asked about team sizes, revenue figures, client numbers, reporting lines. It pushed on the gap between my job titles and what I was actually doing. It asked about people I'd mentored, performance reviews I'd conducted, difficult conversations I'd had to have.<\/p>\n<p>It was, honestly, closer to a good interview than a writing exercise.<\/p>\n<p>And that's where things got interesting. Because when you're forced to articulate the full story - not just &quot;stabilised delivery&quot; but &quot;inherited a failing rebuild with two departing developers, recruited a new team, introduced structured communication, and regained executive confidence to the point where the CEO stopped attending our update meetings&quot; - you realise how much you've been underselling yourself.<\/p>\n<p>I found myself explaining how I rearchitected a marketing platform from hard-coded campaign types that took months to deliver into a config-driven system that cut that to two to four weeks. How I walked into a startup with no CTO, no engineering process, and an agency-built codebase, and built the entire function from scratch. How I ended up as the sole technical and product lead across two products worth two million dollars in combined annual revenue, after successive restructures stripped away every other support role around me.<\/p>\n<p>None of that was on my original resume. Not really. The facts were there in outline, but the impact was buried under vague, cautious language.<\/p>\n<h2 id=\"what-came-out-the-other-side\">What came out the other side<\/h2>\n<p>Claude produced two separate resumes - one aimed at engineering manager roles, the other at senior technical lead positions. Same career history, but framed very differently.<\/p>\n<p>The engineering manager version leads with a leadership capabilities section that front-loads hiring, performance management, mentoring, and delivery practices before getting into the role history. It's designed to get past the keyword filters that have been screening me out, while telling a coherent story about someone who's been doing engineering management for years under titles that didn't reflect the scope.<\/p>\n<p>The technical lead version leads with the tech stack and puts the architectural decisions, system rescues, and hands-on engineering front and centre. It reads as a strong individual contributor who leads from the front.<\/p>\n<p>Both are significantly longer and more detailed than what I started with. But every bullet point has a concrete claim behind it - a number, a before-and-after, a specific outcome. That's the difference between a resume that describes responsibilities and one that demonstrates impact.<\/p>\n<h2 id=\"what-i-actually-learned\">What I actually learned<\/h2>\n<p>The exercise surfaced a few things I hadn't fully reckoned with.<\/p>\n<p>First, I'd been protecting companies that no longer need protecting. I was writing carefully neutral descriptions of situations that were, frankly, messy. Stalled rebuilds, overpromised clients, departing teams, repeated restructures. The instinct is to smooth all of that over. But the mess is the story. Walking into chaos and creating order is the most valuable thing on my resume, and I was barely mentioning it.<\/p>\n<p>Second, I'd been assuming that the reader would infer impact from context. They won't. If you grew a client base to 750 agencies across three markets, you need to say that. If you cut feature delivery time from months to weeks, you need to say that. The reader isn't going to do the maths for you.<\/p>\n<p>Third - and this is the one that connects back to my <a href=\"\/blog\/the-title-trap-what-a-decade-of-wrong-job-titles-taught-me-about-career-progression\">earlier post about the title trap<\/a> - I'd been letting my job titles frame the narrative instead of framing it myself. Claude pushed me to describe what I actually did, not what the contract said. That shift in framing changed everything about how the experience reads.<\/p>\n<h2 id=\"a-note-on-using-ai-for-this\">A note on using AI for this<\/h2>\n<p>I want to be straightforward about something: I didn't ask Claude to fabricate anything. Every claim in both resumes is true and I could back it up in an interview. What Claude did was act as an interviewer - pulling out details I wouldn't have thought to include, asking follow-up questions that forced me to be specific, and then structuring the result in a way that's optimised for how resumes are actually read in 2026.<\/p>\n<p>I should also be clear: I didn't take what Claude produced and submit it unchanged. The output was a strong draft, but it was still a draft. I read through both versions carefully, adjusted phrasing that didn't sound like me, toned down anything that felt overstated, and made sure the final document was something I'd be comfortable defending line by line. AI is a useful collaborator for this kind of work, but your resume represents you - not your tools. Final editorial control matters.<\/p>\n<p>Could I have done this myself? Probably, eventually. But there's something about being interviewed - even by an AI - that bypasses the instinct to be modest. When someone asks you directly &quot;what was broken and what did you fix?&quot;, you answer honestly. When you're writing about yourself, you hedge.<\/p>\n<p>I don't know yet whether these new resumes will change my results. I'll find out soon enough. But for the first time in two months, I feel like my CV actually represents what I've done - not just where I've been.<\/p>\n<p>If you're in a similar position - experienced but struggling to land interviews - I'd genuinely recommend trying this approach. Not because AI is magic, but because the interview format forces you to say out loud what you've been too careful to write down. And sometimes that's all it takes.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: I Let AI Interview Me to Rewrite My Resume\">Email a comment<\/a><\/p>",
            "summary": "<p>I've been job hunting for about two months now. If you've read my earlier posts, you'll know it hasn't been smooth. The rejections have been consistent, the feedback generic, and the pattern frustratingly clear: my experience isn't the problem - my resume is.<\/p>\n<p>That's a hard thing to admit when you've spent weeks refining it. But the evidence was difficult to ignore. Roles I was genuinely qualified for were filtering me out before a human ever read a word. And when I did get through to a screening call, the outcome was the same polite, non-specific rejection that tells you nothing useful.<\/p>\n<p>So I tried something different. I sat down with <a href=\"https:\/\/claude.ai\">Claude<\/a> - Anthropic's AI assistant - and asked it to help me rewrite my resume. What I didn't expect was how the process would unfold.<\/p>\n<h2 id=\"the-resume-i-started-with\">The resume I started with<\/h2>\n<p>My original resume was pretty good. It listed my roles, my titles, my skills. It had bullet points that described what I did at each company. It was accurate - but it was completely failing to land.<\/p>\n<p>The problem, I think, is that I wrote it the way most of us write resumes: as a factual record. Here's where I worked, here's what my title was, here's a sanitised summary of my responsibilities. It read like a job description, not a story. And in a market where your CV has about six seconds to make an impression - assuming it gets past the ATS at all - &quot;accurate but unremarkable&quot; is a death sentence.<\/p>\n<h2 id=\"the-interview\">The interview<\/h2>\n<p>I expected Claude to take my resume, sprinkle some stronger verbs on it, and hand it back. Instead, it started asking questions. A lot of questions.<\/p>\n<p>Not surface-level ones either. It wanted to know what was actually broken when I joined <em>Company X<\/em>. How bad was the codebase? What did the team look like? What specific changes did I make, and what happened as a result? It asked about team sizes, revenue figures, client numbers, reporting lines. It pushed on the gap between my job titles and what I was actually doing. It asked about people I'd mentored, performance reviews I'd conducted, difficult conversations I'd had to have.<\/p>\n<p>It was, honestly, closer to a good interview than a writing exercise.<\/p>\n<p>And that's where things got interesting. Because when you're forced to articulate the full story - not just &quot;stabilised delivery&quot; but &quot;inherited a failing rebuild with two departing developers, recruited a new team, introduced structured communication, and regained executive confidence to the point where the CEO stopped attending our update meetings&quot; - you realise how much you've been underselling yourself.<\/p>\n<p>I found myself explaining how I rearchitected a marketing platform from hard-coded campaign types that took months to deliver into a config-driven system that cut that to two to four weeks. How I walked into a startup with no CTO, no engineering process, and an agency-built codebase, and built the entire function from scratch. How I ended up as the sole technical and product lead across two products worth two million dollars in combined annual revenue, after successive restructures stripped away every other support role around me.<\/p>\n<p>None of that was on my original resume. Not really. The facts were there in outline, but the impact was buried under vague, cautious language.<\/p>\n<h2 id=\"what-came-out-the-other-side\">What came out the other side<\/h2>\n<p>Claude produced two separate resumes - one aimed at engineering manager roles, the other at senior technical lead positions. Same career history, but framed very differently.<\/p>\n<p>The engineering manager version leads with a leadership capabilities section that front-loads hiring, performance management, mentoring, and delivery practices before getting into the role history. It's designed to get past the keyword filters that have been screening me out, while telling a coherent story about someone who's been doing engineering management for years under titles that didn't reflect the scope.<\/p>\n<p>The technical lead version leads with the tech stack and puts the architectural decisions, system rescues, and hands-on engineering front and centre. It reads as a strong individual contributor who leads from the front.<\/p>\n<p>Both are significantly longer and more detailed than what I started with. But every bullet point has a concrete claim behind it - a number, a before-and-after, a specific outcome. That's the difference between a resume that describes responsibilities and one that demonstrates impact.<\/p>\n<h2 id=\"what-i-actually-learned\">What I actually learned<\/h2>\n<p>The exercise surfaced a few things I hadn't fully reckoned with.<\/p>\n<p>First, I'd been protecting companies that no longer need protecting. I was writing carefully neutral descriptions of situations that were, frankly, messy. Stalled rebuilds, overpromised clients, departing teams, repeated restructures. The instinct is to smooth all of that over. But the mess is the story. Walking into chaos and creating order is the most valuable thing on my resume, and I was barely mentioning it.<\/p>\n<p>Second, I'd been assuming that the reader would infer impact from context. They won't. If you grew a client base to 750 agencies across three markets, you need to say that. If you cut feature delivery time from months to weeks, you need to say that. The reader isn't going to do the maths for you.<\/p>\n<p>Third - and this is the one that connects back to my <a href=\"\/blog\/the-title-trap-what-a-decade-of-wrong-job-titles-taught-me-about-career-progression\">earlier post about the title trap<\/a> - I'd been letting my job titles frame the narrative instead of framing it myself. Claude pushed me to describe what I actually did, not what the contract said. That shift in framing changed everything about how the experience reads.<\/p>\n<h2 id=\"a-note-on-using-ai-for-this\">A note on using AI for this<\/h2>\n<p>I want to be straightforward about something: I didn't ask Claude to fabricate anything. Every claim in both resumes is true and I could back it up in an interview. What Claude did was act as an interviewer - pulling out details I wouldn't have thought to include, asking follow-up questions that forced me to be specific, and then structuring the result in a way that's optimised for how resumes are actually read in 2026.<\/p>\n<p>I should also be clear: I didn't take what Claude produced and submit it unchanged. The output was a strong draft, but it was still a draft. I read through both versions carefully, adjusted phrasing that didn't sound like me, toned down anything that felt overstated, and made sure the final document was something I'd be comfortable defending line by line. AI is a useful collaborator for this kind of work, but your resume represents you - not your tools. Final editorial control matters.<\/p>\n<p>Could I have done this myself? Probably, eventually. But there's something about being interviewed - even by an AI - that bypasses the instinct to be modest. When someone asks you directly &quot;what was broken and what did you fix?&quot;, you answer honestly. When you're writing about yourself, you hedge.<\/p>\n<p>I don't know yet whether these new resumes will change my results. I'll find out soon enough. But for the first time in two months, I feel like my CV actually represents what I've done - not just where I've been.<\/p>\n<p>If you're in a similar position - experienced but struggling to land interviews - I'd genuinely recommend trying this approach. Not because AI is magic, but because the interview format forces you to say out loud what you've been too careful to write down. And sometimes that's all it takes.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: I Let AI Interview Me to Rewrite My Resume\">Email a comment<\/a><\/p>",
            "date_published": "2026-03-31T00:00:00+10:00",
            "date_modified": "2026-03-31T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/109",
            "title": "Laravel's Terminable Middleware: Run Code After the Response",
            "url": "https://philstephens.com/blog/laravels-terminable-middleware-run-code-after-the-response",
            "content_html": "<p>Most Laravel middleware runs before the response is sent to the browser. You check authentication, validate CSRF tokens, set headers. The request comes in, your middleware does its thing, and the response goes out. But some work doesn't need to happen before the response. It just needs to happen.<\/p>\n<p>Laravel's terminable middleware lets you run code <em>after<\/em> the response has been sent to the client. The user gets their page, and your application quietly finishes up in the background.<\/p>\n<h2 id=\"what-terminable-middleware-actually-does\">What terminable middleware actually does<\/h2>\n<p>A standard middleware has a <code>handle()<\/code> method. A terminable middleware adds a <code>terminate()<\/code> method. The difference is <em>when<\/em> each runs in the PHP lifecycle.<\/p>\n<p>When a request hits your Laravel app, the HTTP kernel boots the application, runs the middleware pipeline, executes the controller, and builds a response. That response is sent to the client. In a traditional PHP-FPM setup, that would normally be the end of the script's life. But Laravel's HTTP kernel calls <code>terminate()<\/code> on the application after sending the response, which in turn calls <code>terminate()<\/code> on any middleware that defines it.<\/p>\n<p>With PHP-FPM, the <code>fastcgi_finish_request()<\/code> function is what makes this possible. It flushes the response to the web server, closing the connection to the client, but the PHP process keeps running. Any code after that point executes without the user waiting for it. Laravel calls this function internally before running the termination callbacks.<\/p>\n<p>The result: your user gets a fast response, and your app does its housekeeping afterwards.<\/p>\n<h2 id=\"a-real-example-visit-tracking\">A real example: visit tracking<\/h2>\n<p>On this site, I track page visits for anonymous users. That means a database write on every request. It's a cheap operation, but there's no reason the visitor should wait for it. The visit data doesn't affect the response at all.<\/p>\n<p>Here's the middleware:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">namespace<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">Privateer\\Basecms\\Http\\Middleware<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Closure<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Http\\Request<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Privateer\\Basecms\\Services\\VisitTrackingService<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Symfony\\Component\\HttpFoundation\\Response<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">TrackWebsiteVisits<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">__construct<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">private<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">VisitTrackingService<\/span><span style=\"color:#E1E4E8\"> $visitTrackingService) {}<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">handle<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">Request<\/span><span style=\"color:#E1E4E8\"> $request, <\/span><span style=\"color:#79B8FF\">Closure<\/span><span style=\"color:#E1E4E8\"> $next)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Response<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $next($request);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">terminate<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">Request<\/span><span style=\"color:#E1E4E8\"> $request, <\/span><span style=\"color:#79B8FF\">Response<\/span><span style=\"color:#E1E4E8\"> $response)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">void<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">if<\/span><span style=\"color:#E1E4E8\"> (<\/span><span style=\"color:#F97583\">!<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">config<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;basecms.visits.track_visits&#39;<\/span><span style=\"color:#E1E4E8\">)) {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">if<\/span><span style=\"color:#E1E4E8\"> ($request<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">user<\/span><span style=\"color:#E1E4E8\">()) {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">visitTrackingService<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">trackVisit<\/span><span style=\"color:#E1E4E8\">($request);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>The <code>handle()<\/code> method does nothing. It just passes the request along. All the real work happens in <code>terminate()<\/code>, which runs after the response has already been sent. It checks whether tracking is enabled, skips authenticated users, and then records the visit.<\/p>\n<p>The tracking service itself does a simple <code>Visit::create()<\/code> with the path, method, IP address, session ID, and user agent. That database insert now happens entirely outside the request\/response cycle.<\/p>\n<h2 id=\"other-good-candidates\">Other good candidates<\/h2>\n<p>Terminable middleware works well for any side-effect that doesn't influence the response.<\/p>\n<p><strong>Logging and metrics.<\/strong> If you're recording response times, status codes, or request metadata to an external service like Datadog or a logging pipeline, you want the full picture (including the response status) but you don't want the logging call to slow down the response. A <code>terminate()<\/code> method receives both the <code>Request<\/code> and <code>Response<\/code> objects, so you have everything you need.<\/p>\n<p><strong>Session cleanup or cookie bookkeeping.<\/strong> If you need to write audit records about session activity or update &quot;last seen&quot; timestamps, these don't affect what the user sees. Deferring them to termination keeps the perceived response time lower.<\/p>\n<h2 id=\"preserving-state-between-handle-and-terminate\">Preserving state between handle() and terminate()<\/h2>\n<p>In the visit tracking example, <code>handle()<\/code> doesn't do anything, so there's no state to carry over. But sometimes you'll want to capture something during <code>handle()<\/code> and use it in <code>terminate()<\/code>. Timing a request is the classic case: you'd start a timer in <code>handle()<\/code> and record the elapsed time in <code>terminate()<\/code>.<\/p>\n<p>There's a catch. By default, Laravel resolves a fresh instance of the middleware for <code>terminate()<\/code>. Any properties you set in <code>handle()<\/code> won't be there.<\/p>\n<p>The fix is to register the middleware as a singleton in your <code>AppServiceProvider<\/code>:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">App\\Http\\Middleware\\TimingMiddleware<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">register<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">void<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">app<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">singleton<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">TimingMiddleware<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>This tells the container to reuse the same instance, so properties set during <code>handle()<\/code> are still available when <code>terminate()<\/code> runs:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">TimingMiddleware<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">private<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">float<\/span><span style=\"color:#E1E4E8\"> $startTime;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">handle<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">Request<\/span><span style=\"color:#E1E4E8\"> $request, <\/span><span style=\"color:#79B8FF\">Closure<\/span><span style=\"color:#E1E4E8\"> $next)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Response<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">startTime <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">microtime<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">true<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $next($request);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">terminate<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">Request<\/span><span style=\"color:#E1E4E8\"> $request, <\/span><span style=\"color:#79B8FF\">Response<\/span><span style=\"color:#E1E4E8\"> $response)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">void<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        $duration <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">microtime<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">true<\/span><span style=\"color:#E1E4E8\">) <\/span><span style=\"color:#F97583\">-<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">startTime;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#79B8FF\">Log<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">info<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;Request duration&#39;<\/span><span style=\"color:#E1E4E8\">, [<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $request<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">path<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#9ECBFF\">&#39;duration_ms&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">round<\/span><span style=\"color:#E1E4E8\">($duration <\/span><span style=\"color:#F97583\">*<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">1000<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#79B8FF\">2<\/span><span style=\"color:#E1E4E8\">),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#9ECBFF\">&#39;status&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $response<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getStatusCode<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        ]);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>Without the singleton registration, <code>$this-&gt;startTime<\/code> would be uninitialised in <code>terminate()<\/code> and you'd get an error.<\/p>\n<h2 id=\"when-not-to-use-it\">When not to use it<\/h2>\n<p>Terminable middleware isn't the right tool when the work affects the response. If you need to modify headers, set cookies, or change the response body based on some computation, that has to happen in <code>handle()<\/code>. The response is already gone by the time <code>terminate()<\/code> runs.<\/p>\n<p>It's also worth noting that the PHP process is still occupied during termination. If your <code>terminate()<\/code> method does something slow, like calling an external API that takes several seconds, it ties up that PHP-FPM worker. For heavy background processing, a queued job is still the better choice. Terminable middleware is best for quick fire-and-forget operations where the overhead of dispatching a job would be more than the work itself.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Laravel's Terminable Middleware: Run Code After the Response\">Email a comment<\/a><\/p>",
            "summary": "<p>Most Laravel middleware runs before the response is sent to the browser. You check authentication, validate CSRF tokens, set headers. The request comes in, your middleware does its thing, and the response goes out. But some work doesn't need to happen before the response. It just needs to happen.<\/p>\n<p>Laravel's terminable middleware lets you run code <em>after<\/em> the response has been sent to the client. The user gets their page, and your application quietly finishes up in the background.<\/p>\n<h2 id=\"what-terminable-middleware-actually-does\">What terminable middleware actually does<\/h2>\n<p>A standard middleware has a <code>handle()<\/code> method. A terminable middleware adds a <code>terminate()<\/code> method. The difference is <em>when<\/em> each runs in the PHP lifecycle.<\/p>\n<p>When a request hits your Laravel app, the HTTP kernel boots the application, runs the middleware pipeline, executes the controller, and builds a response. That response is sent to the client. In a traditional PHP-FPM setup, that would normally be the end of the script's life. But Laravel's HTTP kernel calls <code>terminate()<\/code> on the application after sending the response, which in turn calls <code>terminate()<\/code> on any middleware that defines it.<\/p>\n<p>With PHP-FPM, the <code>fastcgi_finish_request()<\/code> function is what makes this possible. It flushes the response to the web server, closing the connection to the client, but the PHP process keeps running. Any code after that point executes without the user waiting for it. Laravel calls this function internally before running the termination callbacks.<\/p>\n<p>The result: your user gets a fast response, and your app does its housekeeping afterwards.<\/p>\n<h2 id=\"a-real-example-visit-tracking\">A real example: visit tracking<\/h2>\n<p>On this site, I track page visits for anonymous users. That means a database write on every request. It's a cheap operation, but there's no reason the visitor should wait for it. The visit data doesn't affect the response at all.<\/p>\n<p>Here's the middleware:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">namespace<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">Privateer\\Basecms\\Http\\Middleware<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Closure<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Http\\Request<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Privateer\\Basecms\\Services\\VisitTrackingService<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Symfony\\Component\\HttpFoundation\\Response<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">TrackWebsiteVisits<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">__construct<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">private<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">VisitTrackingService<\/span><span style=\"color:#E1E4E8\"> $visitTrackingService) {}<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">handle<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">Request<\/span><span style=\"color:#E1E4E8\"> $request, <\/span><span style=\"color:#79B8FF\">Closure<\/span><span style=\"color:#E1E4E8\"> $next)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Response<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $next($request);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">terminate<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">Request<\/span><span style=\"color:#E1E4E8\"> $request, <\/span><span style=\"color:#79B8FF\">Response<\/span><span style=\"color:#E1E4E8\"> $response)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">void<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">if<\/span><span style=\"color:#E1E4E8\"> (<\/span><span style=\"color:#F97583\">!<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">config<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;basecms.visits.track_visits&#39;<\/span><span style=\"color:#E1E4E8\">)) {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">if<\/span><span style=\"color:#E1E4E8\"> ($request<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">user<\/span><span style=\"color:#E1E4E8\">()) {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">visitTrackingService<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">trackVisit<\/span><span style=\"color:#E1E4E8\">($request);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>The <code>handle()<\/code> method does nothing. It just passes the request along. All the real work happens in <code>terminate()<\/code>, which runs after the response has already been sent. It checks whether tracking is enabled, skips authenticated users, and then records the visit.<\/p>\n<p>The tracking service itself does a simple <code>Visit::create()<\/code> with the path, method, IP address, session ID, and user agent. That database insert now happens entirely outside the request\/response cycle.<\/p>\n<h2 id=\"other-good-candidates\">Other good candidates<\/h2>\n<p>Terminable middleware works well for any side-effect that doesn't influence the response.<\/p>\n<p><strong>Logging and metrics.<\/strong> If you're recording response times, status codes, or request metadata to an external service like Datadog or a logging pipeline, you want the full picture (including the response status) but you don't want the logging call to slow down the response. A <code>terminate()<\/code> method receives both the <code>Request<\/code> and <code>Response<\/code> objects, so you have everything you need.<\/p>\n<p><strong>Session cleanup or cookie bookkeeping.<\/strong> If you need to write audit records about session activity or update &quot;last seen&quot; timestamps, these don't affect what the user sees. Deferring them to termination keeps the perceived response time lower.<\/p>\n<h2 id=\"preserving-state-between-handle-and-terminate\">Preserving state between handle() and terminate()<\/h2>\n<p>In the visit tracking example, <code>handle()<\/code> doesn't do anything, so there's no state to carry over. But sometimes you'll want to capture something during <code>handle()<\/code> and use it in <code>terminate()<\/code>. Timing a request is the classic case: you'd start a timer in <code>handle()<\/code> and record the elapsed time in <code>terminate()<\/code>.<\/p>\n<p>There's a catch. By default, Laravel resolves a fresh instance of the middleware for <code>terminate()<\/code>. Any properties you set in <code>handle()<\/code> won't be there.<\/p>\n<p>The fix is to register the middleware as a singleton in your <code>AppServiceProvider<\/code>:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">App\\Http\\Middleware\\TimingMiddleware<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">register<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">void<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">app<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">singleton<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">TimingMiddleware<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>This tells the container to reuse the same instance, so properties set during <code>handle()<\/code> are still available when <code>terminate()<\/code> runs:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">TimingMiddleware<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">private<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">float<\/span><span style=\"color:#E1E4E8\"> $startTime;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">handle<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">Request<\/span><span style=\"color:#E1E4E8\"> $request, <\/span><span style=\"color:#79B8FF\">Closure<\/span><span style=\"color:#E1E4E8\"> $next)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Response<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">startTime <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">microtime<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">true<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $next($request);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">terminate<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">Request<\/span><span style=\"color:#E1E4E8\"> $request, <\/span><span style=\"color:#79B8FF\">Response<\/span><span style=\"color:#E1E4E8\"> $response)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">void<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        $duration <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">microtime<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">true<\/span><span style=\"color:#E1E4E8\">) <\/span><span style=\"color:#F97583\">-<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">startTime;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#79B8FF\">Log<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">info<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;Request duration&#39;<\/span><span style=\"color:#E1E4E8\">, [<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $request<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">path<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#9ECBFF\">&#39;duration_ms&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">round<\/span><span style=\"color:#E1E4E8\">($duration <\/span><span style=\"color:#F97583\">*<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">1000<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#79B8FF\">2<\/span><span style=\"color:#E1E4E8\">),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#9ECBFF\">&#39;status&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $response<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getStatusCode<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        ]);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>Without the singleton registration, <code>$this-&gt;startTime<\/code> would be uninitialised in <code>terminate()<\/code> and you'd get an error.<\/p>\n<h2 id=\"when-not-to-use-it\">When not to use it<\/h2>\n<p>Terminable middleware isn't the right tool when the work affects the response. If you need to modify headers, set cookies, or change the response body based on some computation, that has to happen in <code>handle()<\/code>. The response is already gone by the time <code>terminate()<\/code> runs.<\/p>\n<p>It's also worth noting that the PHP process is still occupied during termination. If your <code>terminate()<\/code> method does something slow, like calling an external API that takes several seconds, it ties up that PHP-FPM worker. For heavy background processing, a queued job is still the better choice. Terminable middleware is best for quick fire-and-forget operations where the overhead of dispatching a job would be more than the work itself.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Laravel's Terminable Middleware: Run Code After the Response\">Email a comment<\/a><\/p>",
            "date_published": "2026-03-30T00:00:00+10:00",
            "date_modified": "2026-03-30T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/107",
            "title": "The Title Trap: What a Decade of \"Wrong\" Job Titles Taught Me About Career Progression",
            "url": "https://philstephens.com/blog/the-title-trap-what-a-decade-of-wrong-job-titles-taught-me-about-career-progression",
            "content_html": "<p>I've spent the better part of a decade leading engineering teams, hiring developers, running sprint ceremonies, conducting performance reviews, and making the kind of decisions that shape products and people's careers. By any reasonable measure, I've been doing engineering management for years.<\/p>\n<p>But my CV doesn't say &quot;Engineering Manager.&quot; And right now, that's a problem.<\/p>\n<h2 id=\"the-roles-that-didnt-come-with-the-quotrightquot-name\">The roles that didn't come with the &quot;right&quot; name<\/h2>\n<p>Across startups, scale-ups, and agencies, I've held titles like Development Lead, Technical Director, and Technical Lead. At each of those companies, titles were an afterthought - something scribbled into a contract and never thought about again. What mattered was the work: shipping products, growing teams, unblocking people, and keeping the lights on.<\/p>\n<p>I didn't push back. Honestly, I didn't think I needed to. The work spoke for itself, or so I assumed.<\/p>\n<p>That assumption was wrong.<\/p>\n<h2 id=\"the-wall\">The wall<\/h2>\n<p>When I started actively pursuing Engineering Manager roles, the rejections came quickly and consistently. The feedback, when I got any, circled around the same theme: <em>lack of experience<\/em>. Not a lack of skill, not a failed interview - just a title mismatch that meant I never got to the interview at all.<\/p>\n<p>The uncomfortable truth is that many companies now rely on AI-driven applicant tracking systems that parse your CV long before a human ever sees it. These systems are pattern-matching machines. They're looking for keywords, and &quot;Technical Lead&quot; doesn't trigger the same signals as &quot;Engineering Manager&quot; - even when the underlying experience is identical. By the time a recruiter reviews the shortlist, you're already filtered out.<\/p>\n<p>It's frustrating. But it's also the reality of how hiring works in 2026.<\/p>\n<h2 id=\"what-id-do-differently-for-what-its-worth\">What I'd do differently (for what it's worth)<\/h2>\n<p>I clearly haven't cracked the code here - if I had, I'd be writing this from my Engineering Manager desk. But looking back, there are a few things I'd tell my earlier self. Take them for what they are: one person's hindsight, not a playbook.<\/p>\n<p><strong>Negotiate the title, not just the salary.<\/strong> At smaller companies, titles are often flexible precisely because they're seen as unimportant. That's your leverage. If you're doing the job of an engineering manager, ask for the title. It costs the company nothing and it costs you everything not to have it.<\/p>\n<p><strong>Document your scope in the language the market uses.<\/strong> Even if your company calls you a &quot;Lead,&quot; make sure your CV translates your responsibilities into terms the broader industry recognises. &quot;Managed a team of eight engineers&quot; lands differently than &quot;led technical delivery.&quot;<\/p>\n<p><strong>Pay attention to where the industry is heading, not just where your company is today.<\/strong> Startups and agencies often have their own vocabulary. That's fine internally, but your career extends beyond any single company. The market doesn't owe you the benefit of the doubt.<\/p>\n<h2 id=\"where-i-am-now\">Where I am now<\/h2>\n<p>I won't pretend this hasn't been humbling. After years of increasing responsibility, I'm now considering roles that might look like a lateral move - or even a step sideways - on paper. But I've made peace with that. Sometimes the fastest route to where you want to be isn't a straight line. If taking a role with the &quot;right&quot; title for a year or two is what it takes to unlock the next chapter, then that's a pragmatic decision, not a defeat.<\/p>\n<p>The experience doesn't disappear just because the title didn't match. I know what I've done. The challenge now is making sure the next opportunity reflects it - starting with the name on the tin.<\/p>\n<h2 id=\"if-this-sounds-familiar\">If this sounds familiar<\/h2>\n<p>If you're early in your career, or somewhere in the middle like I am, take this as a gentle nudge: titles matter more than you think they do. Not because they define your capability, but because they define how the market sees you. And in a hiring landscape increasingly shaped by automation and keyword matching, perception is the first gate you have to pass through.<\/p>\n<p>That said, this is just my experience and my opinion. I'm still figuring this out in real time, and I'm well aware there are people who've navigated this far more gracefully than I have.<\/p>\n<p>If you see it differently - or if you've been through something similar and found an approach that worked - I'd love to hear from you. Drop me an email. I'm always up for a conversation, especially one that challenges how I'm thinking about this.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: The Title Trap: What a Decade of \"Wrong\" Job Titles Taught Me About Career Progression\">Email a comment<\/a><\/p>",
            "summary": "<p>I've spent the better part of a decade leading engineering teams, hiring developers, running sprint ceremonies, conducting performance reviews, and making the kind of decisions that shape products and people's careers. By any reasonable measure, I've been doing engineering management for years.<\/p>\n<p>But my CV doesn't say &quot;Engineering Manager.&quot; And right now, that's a problem.<\/p>\n<h2 id=\"the-roles-that-didnt-come-with-the-quotrightquot-name\">The roles that didn't come with the &quot;right&quot; name<\/h2>\n<p>Across startups, scale-ups, and agencies, I've held titles like Development Lead, Technical Director, and Technical Lead. At each of those companies, titles were an afterthought - something scribbled into a contract and never thought about again. What mattered was the work: shipping products, growing teams, unblocking people, and keeping the lights on.<\/p>\n<p>I didn't push back. Honestly, I didn't think I needed to. The work spoke for itself, or so I assumed.<\/p>\n<p>That assumption was wrong.<\/p>\n<h2 id=\"the-wall\">The wall<\/h2>\n<p>When I started actively pursuing Engineering Manager roles, the rejections came quickly and consistently. The feedback, when I got any, circled around the same theme: <em>lack of experience<\/em>. Not a lack of skill, not a failed interview - just a title mismatch that meant I never got to the interview at all.<\/p>\n<p>The uncomfortable truth is that many companies now rely on AI-driven applicant tracking systems that parse your CV long before a human ever sees it. These systems are pattern-matching machines. They're looking for keywords, and &quot;Technical Lead&quot; doesn't trigger the same signals as &quot;Engineering Manager&quot; - even when the underlying experience is identical. By the time a recruiter reviews the shortlist, you're already filtered out.<\/p>\n<p>It's frustrating. But it's also the reality of how hiring works in 2026.<\/p>\n<h2 id=\"what-id-do-differently-for-what-its-worth\">What I'd do differently (for what it's worth)<\/h2>\n<p>I clearly haven't cracked the code here - if I had, I'd be writing this from my Engineering Manager desk. But looking back, there are a few things I'd tell my earlier self. Take them for what they are: one person's hindsight, not a playbook.<\/p>\n<p><strong>Negotiate the title, not just the salary.<\/strong> At smaller companies, titles are often flexible precisely because they're seen as unimportant. That's your leverage. If you're doing the job of an engineering manager, ask for the title. It costs the company nothing and it costs you everything not to have it.<\/p>\n<p><strong>Document your scope in the language the market uses.<\/strong> Even if your company calls you a &quot;Lead,&quot; make sure your CV translates your responsibilities into terms the broader industry recognises. &quot;Managed a team of eight engineers&quot; lands differently than &quot;led technical delivery.&quot;<\/p>\n<p><strong>Pay attention to where the industry is heading, not just where your company is today.<\/strong> Startups and agencies often have their own vocabulary. That's fine internally, but your career extends beyond any single company. The market doesn't owe you the benefit of the doubt.<\/p>\n<h2 id=\"where-i-am-now\">Where I am now<\/h2>\n<p>I won't pretend this hasn't been humbling. After years of increasing responsibility, I'm now considering roles that might look like a lateral move - or even a step sideways - on paper. But I've made peace with that. Sometimes the fastest route to where you want to be isn't a straight line. If taking a role with the &quot;right&quot; title for a year or two is what it takes to unlock the next chapter, then that's a pragmatic decision, not a defeat.<\/p>\n<p>The experience doesn't disappear just because the title didn't match. I know what I've done. The challenge now is making sure the next opportunity reflects it - starting with the name on the tin.<\/p>\n<h2 id=\"if-this-sounds-familiar\">If this sounds familiar<\/h2>\n<p>If you're early in your career, or somewhere in the middle like I am, take this as a gentle nudge: titles matter more than you think they do. Not because they define your capability, but because they define how the market sees you. And in a hiring landscape increasingly shaped by automation and keyword matching, perception is the first gate you have to pass through.<\/p>\n<p>That said, this is just my experience and my opinion. I'm still figuring this out in real time, and I'm well aware there are people who've navigated this far more gracefully than I have.<\/p>\n<p>If you see it differently - or if you've been through something similar and found an approach that worked - I'd love to hear from you. Drop me an email. I'm always up for a conversation, especially one that challenges how I'm thinking about this.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: The Title Trap: What a Decade of \"Wrong\" Job Titles Taught Me About Career Progression\">Email a comment<\/a><\/p>",
            "date_published": "2026-03-27T00:00:00+10:00",
            "date_modified": "2026-03-27T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/108",
            "title": "Tracking File Uploads from Filament's MarkdownEditor",
            "url": "https://philstephens.com/blog/tracking-file-uploads-from-filaments-markdowneditor",
            "content_html": "<p>The <code>MarkdownEditor<\/code> component that ships with <a href=\"https:\/\/filamentphp.com\">Filament<\/a> handles file uploads out of the box. Drop an image in, it gets stored to disk, and a URL is inserted into your markdown. Simple enough.<\/p>\n<p>But what happens to those files after that? There's no database record. No way to know which post an image belongs to. No audit trail of who uploaded what. If you delete a post, its images quietly become orphans sitting on your S3 bucket forever.<\/p>\n<p>I ran into this while building a Laravel package that uses Filament to manage blog content. I wanted every uploaded file tracked in the database, linked back to its parent post or page, and attributed to the user who uploaded it. The <code>MarkdownEditor<\/code> component has the hooks to make this work; you just need to wire them up.<\/p>\n<h2 id=\"the-hooks-filament-gives-you\">The hooks Filament gives you<\/h2>\n<p><code>MarkdownEditor<\/code> exposes three methods for customising file attachment behaviour:<\/p>\n<ul>\n<li><code>fileAttachmentsDisk()<\/code> sets which filesystem disk to store files on<\/li>\n<li><code>saveUploadedFileAttachmentUsing()<\/code> overrides the default upload handler<\/li>\n<li><code>getFileAttachmentUrlUsing()<\/code> controls how the inserted URL is resolved<\/li>\n<\/ul>\n<p>The default behaviour stores files and returns a URL. By overriding <code>saveUploadedFileAttachmentUsing<\/code>, you can intercept that upload and do whatever you need: save a database record, generate thumbnails, transform the URL through an image service, or all three.<\/p>\n<h2 id=\"the-asset-model\">The Asset model<\/h2>\n<p>First, a model to track uploads. Each record captures the file's storage location, metadata, and its relationship to the content it belongs to.<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Database\\Eloquent\\Model<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Database\\Eloquent\\Relations\\BelongsTo<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Database\\Eloquent\\Relations\\MorphTo<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">Asset<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">extends<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">Model<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">protected<\/span><span style=\"color:#E1E4E8\"> $fillable <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> [<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;directory&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;filename&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;mime_type&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;size&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;visibility&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;url&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;field&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;attachable_type&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;attachable_id&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    ];<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">protected<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">casts<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">array<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> [<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#9ECBFF\">&#39;size&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;integer&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        ];<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">attachable<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">MorphTo<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">morphTo<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">uploadedBy<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">BelongsTo<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">belongsTo<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">User<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>The <code>attachable<\/code> morph relationship means an Asset can belong to a Post, a Page, a Category, or any other model. The <code>field<\/code> column records which MarkdownEditor on the form triggered the upload, useful when a resource has more than one.<\/p>\n<p>The migration:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#79B8FF\">Schema<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">create<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;assets&#39;<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> (<\/span><span style=\"color:#79B8FF\">Blueprint<\/span><span style=\"color:#E1E4E8\"> $table) {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">id<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;directory&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullable<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;filename&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;mime_type&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullable<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">unsignedBigInteger<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;size&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullable<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;visibility&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">default<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;public&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">text<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;url&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;field&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">foreignId<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullable<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">constrained<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;users&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullOnDelete<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullableMorphs<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;attachable&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">timestamps<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">unique<\/span><span style=\"color:#E1E4E8\">([<\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\">]);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">});<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>The unique constraint on <code>['disk', 'path']<\/code> prevents duplicate records if the same file somehow gets processed twice.<\/p>\n<h2 id=\"building-the-service\">Building the service<\/h2>\n<p>The service needs to do two things: configure a <code>MarkdownEditor<\/code> instance with custom upload handling, and perform the actual upload-and-record-creation when a file comes in.<\/p>\n<p>Start with a constant for the storage disk. Hard-code a sensible default and change it when your environment needs something different:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">namespace<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">App\\Services<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Filament\\Forms\\Components\\MarkdownEditor<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">MarkdownEditorAssetService<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">private<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">const<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">ATTACHMENTS_DISK<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;s3&#39;<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">static<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">string<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">self::<\/span><span style=\"color:#79B8FF\">ATTACHMENTS_DISK<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>Next, the upload handler. This receives Livewire's <code>TemporaryUploadedFile<\/code>, stores it to disk, and creates the Asset record:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#6A737D\">\/\/...<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Database\\Eloquent\\Model<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Support\\Facades\\Storage<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Features\\SupportFileUploads\\TemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">MarkdownEditorAssetService<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><span style=\"color:#6A737D\">\/\/...<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">storeUploadedAttachment<\/span><span style=\"color:#E1E4E8\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#79B8FF\">TemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\"> $file,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#E1E4E8\"> $component,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#79B8FF\">Model<\/span><span style=\"color:#E1E4E8\"> $record <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#E1E4E8\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t$diskName <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentsDiskName<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t$directory <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentsDirectory<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t$path <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">store<\/span><span style=\"color:#E1E4E8\">($directory, $diskName);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t$disk <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Storage<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">disk<\/span><span style=\"color:#E1E4E8\">($diskName);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#B392F0\">rescue<\/span><span style=\"color:#E1E4E8\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#F97583\">fn<\/span><span style=\"color:#E1E4E8\"> ()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">bool<\/span><span style=\"color:#E1E4E8\"> =&gt; $disk<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">setVisibility<\/span><span style=\"color:#E1E4E8\">($path, $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentsVisibility<\/span><span style=\"color:#E1E4E8\">()),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#B392F0\">report<\/span><span style=\"color:#E1E4E8\">: <\/span><span style=\"color:#79B8FF\">false<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">create<\/span><span style=\"color:#E1E4E8\">([<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $diskName,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $path,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;directory&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">pathinfo<\/span><span style=\"color:#E1E4E8\">($path, <\/span><span style=\"color:#79B8FF\">PATHINFO_DIRNAME<\/span><span style=\"color:#E1E4E8\">) <\/span><span style=\"color:#F97583\">!==<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;.&#39;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">pathinfo<\/span><span style=\"color:#E1E4E8\">($path, <\/span><span style=\"color:#79B8FF\">PATHINFO_DIRNAME<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;filename&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getClientOriginalName<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;mime_type&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getMimeType<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;size&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getSize<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;visibility&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentsVisibility<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;url&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $disk<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">url<\/span><span style=\"color:#E1E4E8\">($path),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;field&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getName<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">auth<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">id<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;attachable_type&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $record<\/span><span style=\"color:#F97583\">?-&gt;<\/span><span style=\"color:#E1E4E8\">exists <\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#E1E4E8\"> $record<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;attachable_id&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $record<\/span><span style=\"color:#F97583\">?-&gt;<\/span><span style=\"color:#E1E4E8\">exists <\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#E1E4E8\"> $record<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getKey<\/span><span style=\"color:#E1E4E8\">() <\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t]);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t}<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>A few things to note here. The method reads its configuration from the component itself (disk name, directory, visibility) rather than hardcoding values. This means individual MarkdownEditor fields can override settings if needed.<\/p>\n<p>The <code>rescue()<\/code> call around <code>setVisibility<\/code> is a pragmatic choice. Some storage drivers (local disk, for instance) don't support visibility settings and will throw. Wrapping it means the upload still succeeds.<\/p>\n<p>The <code>$record<\/code> parameter might be null. When a user creates a new post and drags an image in before saving, there's no database record yet. The Asset gets created without an <code>attachable<\/code> link. You'd then associate it after the parent record is saved, or leave it unlinked and clean up orphans periodically.<\/p>\n<p>Finally, wire it all together with a static method that configures any <code>MarkdownEditor<\/code> instance:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">MarkdownEditorAssetService<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><span style=\"color:#6A737D\">\/\/...<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">static<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">configureEditor<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#E1E4E8\"> $editor)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $editor<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">fileAttachmentsDisk<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">self::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">())<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">saveUploadedFileAttachmentUsing<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> (<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#79B8FF\">TemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\"> $file,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#E1E4E8\"> $component,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#79B8FF\">Model<\/span><span style=\"color:#E1E4E8\"> $record <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#E1E4E8\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">app<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">self::class<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">storeUploadedAttachment<\/span><span style=\"color:#E1E4E8\">($file, $component, $record);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t})<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentUrlUsing<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">fn<\/span><span style=\"color:#E1E4E8\"> (<\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#E1E4E8\"> $file)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">string<\/span><span style=\"color:#E1E4E8\"> =&gt; $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">url);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t}<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>The <code>saveUploadedFileAttachmentUsing<\/code> closure resolves the service through the container with <code>app(self::class)<\/code>. This keeps the static method free of constructor dependencies while still allowing the instance method to be testable.<\/p>\n<p>The <code>getFileAttachmentUrlUsing<\/code> callback receives the Asset model (the return value of your upload handler) and tells Filament what URL to insert into the markdown. Here it returns the stored URL directly, but this is where you could transform it.<\/p>\n<h2 id=\"using-it-in-a-form-schema\">Using it in a form schema<\/h2>\n<p>With the service built, applying it to a MarkdownEditor is one line:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">App\\Services\\MarkdownEditorAssetService<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Filament\\Forms\\Components\\MarkdownEditor<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">configureEditor<\/span><span style=\"color:#E1E4E8\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">make<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;body&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">columnSpanFull<\/span><span style=\"color:#E1E4E8\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">),<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>This works anywhere you'd put a <code>MarkdownEditor<\/code>: resource forms, page builder blocks, standalone Livewire forms. The static factory pattern means you don't change how you compose your schemas.<\/p>\n<h2 id=\"other-use-cases\">Other use cases<\/h2>\n<p>Intercepting file uploads opens up possibilities beyond just database tracking.<\/p>\n<p><strong>Image transformation URLs.<\/strong> Instead of returning the raw S3 URL, <code>getFileAttachmentUrlUsing<\/code> could return a URL through an image service like Imgix, Cloudinary, or Laravel's Glide package. Your stored files stay as originals while the URLs in your markdown point to optimised, resized versions:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentUrlUsing<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> (<\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#E1E4E8\"> $file)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">string<\/span><span style=\"color:#E1E4E8\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;https:\/\/your-domain.imgix.net\/&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">.<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">path <\/span><span style=\"color:#F97583\">.<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;?w=800&amp;auto=format&#39;<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p><strong>Async thumbnail generation.<\/strong> The upload handler could dispatch a job to generate thumbnails or run image optimisation in the background:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">$asset <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">create<\/span><span style=\"color:#E1E4E8\">([<\/span><span style=\"color:#F97583\">...<\/span><span style=\"color:#E1E4E8\">]);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#79B8FF\">GenerateThumbnails<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">dispatch<\/span><span style=\"color:#E1E4E8\">($asset);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $asset;<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p><strong>Cleanup.<\/strong> With every upload tracked in the database, you can find orphaned assets (those with no <code>attachable<\/code> link after a grace period) and delete both the record and the file from storage. No more mystery images accumulating on S3.<\/p>\n<h2 id=\"testing\">Testing<\/h2>\n<p>Testing the upload flow requires creating a <code>TemporaryUploadedFile<\/code>, which normally only exists inside a Livewire component lifecycle. The workaround is to spin up a minimal Livewire component in your test:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Component<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Features\\SupportFileUploads\\TemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Features\\SupportFileUploads\\WithFileUploads<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Livewire<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">protected<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">makeTemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">string<\/span><span style=\"color:#E1E4E8\"> $filename)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">TemporaryUploadedFile<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $component <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">new<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">extends<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">Component<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">WithFileUploads<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> $upload <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">render<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">string<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;&lt;div&gt;&lt;\/div&gt;&#39;<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    };<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $testComponent <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">test<\/span><span style=\"color:#E1E4E8\">($component<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $testComponent<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">set<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;upload&#39;<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#79B8FF\">UploadedFile<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">fake<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">image<\/span><span style=\"color:#E1E4E8\">($filename));<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $testComponent<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">get<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;upload&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>With that helper, tests are standard Laravel feature tests. Fake the storage disk, act as a user, call the service, and assert against the database:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">App\\Services\\MarkdownEditorAssetService<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">test_store_uploaded_attachment_persists_file_and_creates_asset<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">void<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">Storage<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">fake<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">());<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">actingAs<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">User<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">factory<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">create<\/span><span style=\"color:#E1E4E8\">());<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $service <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">app<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $component <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">make<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;body&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">fileAttachmentsDisk<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">())<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">fileAttachmentsDirectory<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;markdown-attachments&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $asset <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> $service<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">storeUploadedAttachment<\/span><span style=\"color:#E1E4E8\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">makeTemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;hero.png&#39;<\/span><span style=\"color:#E1E4E8\">),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        $component,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    );<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">Storage<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">disk<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">())<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">assertExists<\/span><span style=\"color:#E1E4E8\">($asset<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">path);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">assertDatabaseHas<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;assets&#39;<\/span><span style=\"color:#E1E4E8\">, [<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;id&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $asset<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">id,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;field&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;body&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">auth<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">id<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    ]);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<h2 id=\"wrapping-up\">Wrapping up<\/h2>\n<p>Filament's <code>MarkdownEditor<\/code> does a lot of the heavy lifting for file uploads. The three hook methods give you full control over where files go and what happens when they arrive. Wrapping that behaviour in a service keeps your form schemas clean and puts the logic in one testable place.<\/p>\n<p>The approach here tracks uploads in the database, but the pattern is the same whether you're generating thumbnails, transforming URLs, or enforcing upload policies. Override the upload handler, do your work, return something the URL resolver can use.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Tracking File Uploads from Filament's MarkdownEditor\">Email a comment<\/a><\/p>",
            "summary": "<p>The <code>MarkdownEditor<\/code> component that ships with <a href=\"https:\/\/filamentphp.com\">Filament<\/a> handles file uploads out of the box. Drop an image in, it gets stored to disk, and a URL is inserted into your markdown. Simple enough.<\/p>\n<p>But what happens to those files after that? There's no database record. No way to know which post an image belongs to. No audit trail of who uploaded what. If you delete a post, its images quietly become orphans sitting on your S3 bucket forever.<\/p>\n<p>I ran into this while building a Laravel package that uses Filament to manage blog content. I wanted every uploaded file tracked in the database, linked back to its parent post or page, and attributed to the user who uploaded it. The <code>MarkdownEditor<\/code> component has the hooks to make this work; you just need to wire them up.<\/p>\n<h2 id=\"the-hooks-filament-gives-you\">The hooks Filament gives you<\/h2>\n<p><code>MarkdownEditor<\/code> exposes three methods for customising file attachment behaviour:<\/p>\n<ul>\n<li><code>fileAttachmentsDisk()<\/code> sets which filesystem disk to store files on<\/li>\n<li><code>saveUploadedFileAttachmentUsing()<\/code> overrides the default upload handler<\/li>\n<li><code>getFileAttachmentUrlUsing()<\/code> controls how the inserted URL is resolved<\/li>\n<\/ul>\n<p>The default behaviour stores files and returns a URL. By overriding <code>saveUploadedFileAttachmentUsing<\/code>, you can intercept that upload and do whatever you need: save a database record, generate thumbnails, transform the URL through an image service, or all three.<\/p>\n<h2 id=\"the-asset-model\">The Asset model<\/h2>\n<p>First, a model to track uploads. Each record captures the file's storage location, metadata, and its relationship to the content it belongs to.<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Database\\Eloquent\\Model<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Database\\Eloquent\\Relations\\BelongsTo<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Database\\Eloquent\\Relations\\MorphTo<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">Asset<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">extends<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">Model<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">protected<\/span><span style=\"color:#E1E4E8\"> $fillable <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> [<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;directory&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;filename&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;mime_type&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;size&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;visibility&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;url&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;field&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;attachable_type&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;attachable_id&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    ];<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">protected<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">casts<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">array<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> [<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#9ECBFF\">&#39;size&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;integer&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        ];<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">attachable<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">MorphTo<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">morphTo<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">uploadedBy<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">BelongsTo<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">belongsTo<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">User<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>The <code>attachable<\/code> morph relationship means an Asset can belong to a Post, a Page, a Category, or any other model. The <code>field<\/code> column records which MarkdownEditor on the form triggered the upload, useful when a resource has more than one.<\/p>\n<p>The migration:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#79B8FF\">Schema<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">create<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;assets&#39;<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> (<\/span><span style=\"color:#79B8FF\">Blueprint<\/span><span style=\"color:#E1E4E8\"> $table) {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">id<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;directory&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullable<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;filename&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;mime_type&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullable<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">unsignedBigInteger<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;size&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullable<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;visibility&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">default<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;public&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">text<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;url&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">string<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;field&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">foreignId<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullable<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">constrained<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;users&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullOnDelete<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">nullableMorphs<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;attachable&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">timestamps<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $table<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">unique<\/span><span style=\"color:#E1E4E8\">([<\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\">]);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">});<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>The unique constraint on <code>['disk', 'path']<\/code> prevents duplicate records if the same file somehow gets processed twice.<\/p>\n<h2 id=\"building-the-service\">Building the service<\/h2>\n<p>The service needs to do two things: configure a <code>MarkdownEditor<\/code> instance with custom upload handling, and perform the actual upload-and-record-creation when a file comes in.<\/p>\n<p>Start with a constant for the storage disk. Hard-code a sensible default and change it when your environment needs something different:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">namespace<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">App\\Services<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Filament\\Forms\\Components\\MarkdownEditor<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">MarkdownEditorAssetService<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">private<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">const<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">ATTACHMENTS_DISK<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;s3&#39;<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">static<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">string<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">self::<\/span><span style=\"color:#79B8FF\">ATTACHMENTS_DISK<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>Next, the upload handler. This receives Livewire's <code>TemporaryUploadedFile<\/code>, stores it to disk, and creates the Asset record:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#6A737D\">\/\/...<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Database\\Eloquent\\Model<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Illuminate\\Support\\Facades\\Storage<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Features\\SupportFileUploads\\TemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">MarkdownEditorAssetService<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><span style=\"color:#6A737D\">\/\/...<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">storeUploadedAttachment<\/span><span style=\"color:#E1E4E8\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#79B8FF\">TemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\"> $file,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#E1E4E8\"> $component,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#79B8FF\">Model<\/span><span style=\"color:#E1E4E8\"> $record <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#E1E4E8\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t$diskName <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentsDiskName<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t$directory <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentsDirectory<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t$path <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">store<\/span><span style=\"color:#E1E4E8\">($directory, $diskName);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t$disk <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Storage<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">disk<\/span><span style=\"color:#E1E4E8\">($diskName);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#B392F0\">rescue<\/span><span style=\"color:#E1E4E8\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#F97583\">fn<\/span><span style=\"color:#E1E4E8\"> ()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">bool<\/span><span style=\"color:#E1E4E8\"> =&gt; $disk<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">setVisibility<\/span><span style=\"color:#E1E4E8\">($path, $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentsVisibility<\/span><span style=\"color:#E1E4E8\">()),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#B392F0\">report<\/span><span style=\"color:#E1E4E8\">: <\/span><span style=\"color:#79B8FF\">false<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">create<\/span><span style=\"color:#E1E4E8\">([<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $diskName,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;path&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $path,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;directory&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">pathinfo<\/span><span style=\"color:#E1E4E8\">($path, <\/span><span style=\"color:#79B8FF\">PATHINFO_DIRNAME<\/span><span style=\"color:#E1E4E8\">) <\/span><span style=\"color:#F97583\">!==<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;.&#39;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">pathinfo<\/span><span style=\"color:#E1E4E8\">($path, <\/span><span style=\"color:#79B8FF\">PATHINFO_DIRNAME<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;filename&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getClientOriginalName<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;mime_type&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getMimeType<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;size&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getSize<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;visibility&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentsVisibility<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;url&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $disk<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">url<\/span><span style=\"color:#E1E4E8\">($path),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;field&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $component<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getName<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">auth<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">id<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;attachable_type&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $record<\/span><span style=\"color:#F97583\">?-&gt;<\/span><span style=\"color:#E1E4E8\">exists <\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#E1E4E8\"> $record<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#9ECBFF\">&#39;attachable_id&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $record<\/span><span style=\"color:#F97583\">?-&gt;<\/span><span style=\"color:#E1E4E8\">exists <\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#E1E4E8\"> $record<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getKey<\/span><span style=\"color:#E1E4E8\">() <\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t]);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t}<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>A few things to note here. The method reads its configuration from the component itself (disk name, directory, visibility) rather than hardcoding values. This means individual MarkdownEditor fields can override settings if needed.<\/p>\n<p>The <code>rescue()<\/code> call around <code>setVisibility<\/code> is a pragmatic choice. Some storage drivers (local disk, for instance) don't support visibility settings and will throw. Wrapping it means the upload still succeeds.<\/p>\n<p>The <code>$record<\/code> parameter might be null. When a user creates a new post and drags an image in before saving, there's no database record yet. The Asset gets created without an <code>attachable<\/code> link. You'd then associate it after the parent record is saved, or leave it unlinked and clean up orphans periodically.<\/p>\n<p>Finally, wire it all together with a static method that configures any <code>MarkdownEditor<\/code> instance:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">MarkdownEditorAssetService<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><span style=\"color:#6A737D\">\/\/...<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t<\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">static<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">configureEditor<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#E1E4E8\"> $editor)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t<\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $editor<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">fileAttachmentsDisk<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">self::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">())<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">saveUploadedFileAttachmentUsing<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> (<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#79B8FF\">TemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\"> $file,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#E1E4E8\"> $component,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#F97583\">?<\/span><span style=\"color:#79B8FF\">Model<\/span><span style=\"color:#E1E4E8\"> $record <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#E1E4E8\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t\t\t<\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">app<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">self::class<\/span><span style=\"color:#E1E4E8\">)<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">storeUploadedAttachment<\/span><span style=\"color:#E1E4E8\">($file, $component, $record);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t})<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t\t\t\t<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentUrlUsing<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">fn<\/span><span style=\"color:#E1E4E8\"> (<\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#E1E4E8\"> $file)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">string<\/span><span style=\"color:#E1E4E8\"> =&gt; $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">url);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t}<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>The <code>saveUploadedFileAttachmentUsing<\/code> closure resolves the service through the container with <code>app(self::class)<\/code>. This keeps the static method free of constructor dependencies while still allowing the instance method to be testable.<\/p>\n<p>The <code>getFileAttachmentUrlUsing<\/code> callback receives the Asset model (the return value of your upload handler) and tells Filament what URL to insert into the markdown. Here it returns the stored URL directly, but this is where you could transform it.<\/p>\n<h2 id=\"using-it-in-a-form-schema\">Using it in a form schema<\/h2>\n<p>With the service built, applying it to a MarkdownEditor is one line:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">App\\Services\\MarkdownEditorAssetService<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Filament\\Forms\\Components\\MarkdownEditor<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">configureEditor<\/span><span style=\"color:#E1E4E8\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">make<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;body&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">columnSpanFull<\/span><span style=\"color:#E1E4E8\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">),<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>This works anywhere you'd put a <code>MarkdownEditor<\/code>: resource forms, page builder blocks, standalone Livewire forms. The static factory pattern means you don't change how you compose your schemas.<\/p>\n<h2 id=\"other-use-cases\">Other use cases<\/h2>\n<p>Intercepting file uploads opens up possibilities beyond just database tracking.<\/p>\n<p><strong>Image transformation URLs.<\/strong> Instead of returning the raw S3 URL, <code>getFileAttachmentUrlUsing<\/code> could return a URL through an image service like Imgix, Cloudinary, or Laravel's Glide package. Your stored files stay as originals while the URLs in your markdown point to optimised, resized versions:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">getFileAttachmentUrlUsing<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> (<\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#E1E4E8\"> $file)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">string<\/span><span style=\"color:#E1E4E8\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;https:\/\/your-domain.imgix.net\/&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">.<\/span><span style=\"color:#E1E4E8\"> $file<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">path <\/span><span style=\"color:#F97583\">.<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;?w=800&amp;auto=format&#39;<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p><strong>Async thumbnail generation.<\/strong> The upload handler could dispatch a job to generate thumbnails or run image optimisation in the background:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">$asset <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Asset<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">create<\/span><span style=\"color:#E1E4E8\">([<\/span><span style=\"color:#F97583\">...<\/span><span style=\"color:#E1E4E8\">]);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#79B8FF\">GenerateThumbnails<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">dispatch<\/span><span style=\"color:#E1E4E8\">($asset);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $asset;<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p><strong>Cleanup.<\/strong> With every upload tracked in the database, you can find orphaned assets (those with no <code>attachable<\/code> link after a grace period) and delete both the record and the file from storage. No more mystery images accumulating on S3.<\/p>\n<h2 id=\"testing\">Testing<\/h2>\n<p>Testing the upload flow requires creating a <code>TemporaryUploadedFile<\/code>, which normally only exists inside a Livewire component lifecycle. The workaround is to spin up a minimal Livewire component in your test:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Component<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Features\\SupportFileUploads\\TemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Features\\SupportFileUploads\\WithFileUploads<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire\\Livewire<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">protected<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">makeTemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#F97583\">string<\/span><span style=\"color:#E1E4E8\"> $filename)<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">TemporaryUploadedFile<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $component <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">new<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">class<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">extends<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">Component<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">WithFileUploads<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> $upload <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">null<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">render<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">string<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">            <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;&lt;div&gt;&lt;\/div&gt;&#39;<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    };<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $testComponent <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">Livewire<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">test<\/span><span style=\"color:#E1E4E8\">($component<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $testComponent<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">set<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;upload&#39;<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#79B8FF\">UploadedFile<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">fake<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">image<\/span><span style=\"color:#E1E4E8\">($filename));<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> $testComponent<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">get<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;upload&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>With that helper, tests are standard Laravel feature tests. Fake the storage disk, act as a user, call the service, and assert against the database:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">use<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">App\\Services\\MarkdownEditorAssetService<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">public<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">test_store_uploaded_attachment_persists_file_and_creates_asset<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">void<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">Storage<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">fake<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">());<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">actingAs<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">User<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">factory<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">create<\/span><span style=\"color:#E1E4E8\">());<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $service <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">app<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::class<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $component <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">MarkdownEditor<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">make<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;body&#39;<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">fileAttachmentsDisk<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">())<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">fileAttachmentsDirectory<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;markdown-attachments&#39;<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    $asset <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> $service<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">storeUploadedAttachment<\/span><span style=\"color:#E1E4E8\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">makeTemporaryUploadedFile<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;hero.png&#39;<\/span><span style=\"color:#E1E4E8\">),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        $component,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    );<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">Storage<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">disk<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">())<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">assertExists<\/span><span style=\"color:#E1E4E8\">($asset<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">path);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    <\/span><span style=\"color:#79B8FF\">$this<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">assertDatabaseHas<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">&#39;assets&#39;<\/span><span style=\"color:#E1E4E8\">, [<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;id&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> $asset<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#E1E4E8\">id,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;disk&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">MarkdownEditorAssetService<\/span><span style=\"color:#F97583\">::<\/span><span style=\"color:#B392F0\">attachmentDisk<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;field&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&#39;body&#39;<\/span><span style=\"color:#E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">        <\/span><span style=\"color:#9ECBFF\">&#39;uploaded_by&#39;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=&gt;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">auth<\/span><span style=\"color:#E1E4E8\">()<\/span><span style=\"color:#F97583\">-&gt;<\/span><span style=\"color:#B392F0\">id<\/span><span style=\"color:#E1E4E8\">(),<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    ]);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<h2 id=\"wrapping-up\">Wrapping up<\/h2>\n<p>Filament's <code>MarkdownEditor<\/code> does a lot of the heavy lifting for file uploads. The three hook methods give you full control over where files go and what happens when they arrive. Wrapping that behaviour in a service keeps your form schemas clean and puts the logic in one testable place.<\/p>\n<p>The approach here tracks uploads in the database, but the pattern is the same whether you're generating thumbnails, transforming URLs, or enforcing upload policies. Override the upload handler, do your work, return something the URL resolver can use.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Tracking File Uploads from Filament's MarkdownEditor\">Email a comment<\/a><\/p>",
            "date_published": "2026-03-27T00:00:00+10:00",
            "date_modified": "2026-03-27T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/106",
            "title": "Rebuilding my micro-blog with AI agents",
            "url": "https://philstephens.com/blog/rebuilding-my-micro-blog-with-ai-agents",
            "content_html": "<p>Over the past few months I've started using AI agents more regularly in my development workflow. At first it was mostly the usual things: asking questions, generating small snippets of code, or acting as a slightly more capable autocomplete. In many ways it simply replaced a lot of trips to search engines and documentation.<\/p>\n<p>But as AI has become the dominant talking point in software development, I realised I hadn't really tested what an agent-driven workflow actually feels like in practice.<\/p>\n<p>With the prospect of moving back into the workforce, I wanted to explore this properly. Not just out of curiosity, but as a way to think more concretely about how AI might realistically fit into a professional development workflow.<\/p>\n<p>So I decided to build something.<\/p>\n<h2 id=\"choosing-a-small-controlled-project\">Choosing a small, controlled project<\/h2>\n<p>The project I chose was to rebuild the micro-blog section of my personal website. It's already live over at <a href=\"https:\/\/moments.philstephens.com\">https:\/\/moments.philstephens.com<\/a>.<\/p>\n<p>It's small, well defined, and not mission critical. That makes it a good candidate for experimentation because there's very little risk if things go wrong.<\/p>\n<p>Instead of working primarily inside an IDE, I wanted to push the idea of agentic development as far as I reasonably could. The goal wasn't to blindly accept whatever the AI produced, but to treat the agent as a collaborator while I focused on shaping the architecture and reviewing the results.<\/p>\n<p>To keep things grounded, I deliberately chose a stack I know extremely well. The rebuild uses the <abbr title=\"Tailwind, Alpine, Laravel and Livewire\">TALL<\/abbr> stack. Familiar tools meant I could easily spot mistakes, refine prompts, and gradually tighten the guardrails I was giving the agent as the project progressed.<\/p>\n<p>The repository for the project lives here: <a href=\"https:\/\/github.com\/theprivateer\/moments\">https:\/\/github.com\/theprivateer\/moments<\/a><\/p>\n<h2 id=\"why-claude-code\">Why Claude Code?<\/h2>\n<p>For this project I decided to use <strong>Claude Code<\/strong> as the primary agent.<\/p>\n<p>The choice wasn't especially philosophical. At the time I started the project, Anthropic's Opus and Sonnet models were outperforming the comparable ChatGPT models in my own testing. Claude Code also offered a workflow that made it relatively easy to work outside a traditional IDE, which suited the experiment I wanted to run.<\/p>\n<p>The interesting part wasn't which model I chose. It was figuring out how to structure the interaction so the agent could work effectively without drifting too far from the design.<\/p>\n<p>That meant writing clearer prompts, defining rules for the codebase, and gradually refining the instructions the agent followed as the project evolved.<\/p>\n<h2 id=\"early-results\">Early results<\/h2>\n<p>It's still early days, but the base functionality is already there.<\/p>\n<p>At the moment the project exists as a full Laravel application. My longer-term plan is to convert it into a reusable package that can be installed into any Laravel project, in a similar way to how Statamic works.<\/p>\n<p>If that works out, it should make it easy to drop a micro-blog timeline into other projects without rebuilding the same functionality each time. There's still plenty of work to do before it reaches that point, but the foundations are in place.<\/p>\n<h2 id=\"productivity-versus-reality\">Productivity versus reality<\/h2>\n<p>One of the questions I had going into this experiment was whether working with an AI agent would genuinely make me faster.<\/p>\n<p>The honest answer is yes, although the benefits are uneven. When the problem is clearly defined and the boundaries are well understood, an agent can move surprisingly quickly. Boilerplate disappears, repetitive tasks shrink, and it becomes easy to explore small ideas without much friction.<\/p>\n<p>However, the moment something becomes architectural, ambiguous, or subtle, the human still needs to take the lead. Reviewing the output carefully is essential because small mistakes can compound quickly if they slip through.<\/p>\n<p>So while AI has definitely improved my productivity on this project, I'm not quite ready to uninstall my IDE just yet.<\/p>\n<h2 id=\"a-side-quest-building-a-small-ios-app\">A side quest: building a small iOS app<\/h2>\n<p>Alongside the web project, I also took the opportunity to try something slightly outside my usual comfort zone. I built a small iOS app that lets me interact with the micro-blog timeline from my phone.<\/p>\n<p>The app is intentionally simple. It allows me to post updates and browse the timeline, which is really all I need. The real value was in experimenting with a SwiftUI workflow while working with an AI agent.<\/p>\n<p>The code for that project is here: <a href=\"https:\/\/github.com\/theprivateer\/moments-ios\">https:\/\/github.com\/theprivateer\/moments-ios<\/a><\/p>\n<p>Working with Claude Code in a SwiftUI project turned out to be more productive than I expected, and it gave me the confidence to attempt something more ambitious.<\/p>\n<h2 id=\"the-next-project\">The next project<\/h2>\n<p>That experiment has already led directly to the next project.<\/p>\n<p>I'm currently building what I would consider a &quot;real&quot; app that I intend to publish on the App Store. To keep things manageable I've chosen to rebuild an older project with a very clear specification, although it's a step up in scope and complexity compared to the micro-blog app.<\/p>\n<p>If it works the way I hope, it should be a good test of whether an AI-assisted workflow holds up across a larger codebase.<\/p>\n<p>But that's a story for another post.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Rebuilding my micro-blog with AI agents\">Email a comment<\/a><\/p>",
            "summary": "<p>Over the past few months I've started using AI agents more regularly in my development workflow. At first it was mostly the usual things: asking questions, generating small snippets of code, or acting as a slightly more capable autocomplete. In many ways it simply replaced a lot of trips to search engines and documentation.<\/p>\n<p>But as AI has become the dominant talking point in software development, I realised I hadn't really tested what an agent-driven workflow actually feels like in practice.<\/p>\n<p>With the prospect of moving back into the workforce, I wanted to explore this properly. Not just out of curiosity, but as a way to think more concretely about how AI might realistically fit into a professional development workflow.<\/p>\n<p>So I decided to build something.<\/p>\n<h2 id=\"choosing-a-small-controlled-project\">Choosing a small, controlled project<\/h2>\n<p>The project I chose was to rebuild the micro-blog section of my personal website. It's already live over at <a href=\"https:\/\/moments.philstephens.com\">https:\/\/moments.philstephens.com<\/a>.<\/p>\n<p>It's small, well defined, and not mission critical. That makes it a good candidate for experimentation because there's very little risk if things go wrong.<\/p>\n<p>Instead of working primarily inside an IDE, I wanted to push the idea of agentic development as far as I reasonably could. The goal wasn't to blindly accept whatever the AI produced, but to treat the agent as a collaborator while I focused on shaping the architecture and reviewing the results.<\/p>\n<p>To keep things grounded, I deliberately chose a stack I know extremely well. The rebuild uses the <abbr title=\"Tailwind, Alpine, Laravel and Livewire\">TALL<\/abbr> stack. Familiar tools meant I could easily spot mistakes, refine prompts, and gradually tighten the guardrails I was giving the agent as the project progressed.<\/p>\n<p>The repository for the project lives here: <a href=\"https:\/\/github.com\/theprivateer\/moments\">https:\/\/github.com\/theprivateer\/moments<\/a><\/p>\n<h2 id=\"why-claude-code\">Why Claude Code?<\/h2>\n<p>For this project I decided to use <strong>Claude Code<\/strong> as the primary agent.<\/p>\n<p>The choice wasn't especially philosophical. At the time I started the project, Anthropic's Opus and Sonnet models were outperforming the comparable ChatGPT models in my own testing. Claude Code also offered a workflow that made it relatively easy to work outside a traditional IDE, which suited the experiment I wanted to run.<\/p>\n<p>The interesting part wasn't which model I chose. It was figuring out how to structure the interaction so the agent could work effectively without drifting too far from the design.<\/p>\n<p>That meant writing clearer prompts, defining rules for the codebase, and gradually refining the instructions the agent followed as the project evolved.<\/p>\n<h2 id=\"early-results\">Early results<\/h2>\n<p>It's still early days, but the base functionality is already there.<\/p>\n<p>At the moment the project exists as a full Laravel application. My longer-term plan is to convert it into a reusable package that can be installed into any Laravel project, in a similar way to how Statamic works.<\/p>\n<p>If that works out, it should make it easy to drop a micro-blog timeline into other projects without rebuilding the same functionality each time. There's still plenty of work to do before it reaches that point, but the foundations are in place.<\/p>\n<h2 id=\"productivity-versus-reality\">Productivity versus reality<\/h2>\n<p>One of the questions I had going into this experiment was whether working with an AI agent would genuinely make me faster.<\/p>\n<p>The honest answer is yes, although the benefits are uneven. When the problem is clearly defined and the boundaries are well understood, an agent can move surprisingly quickly. Boilerplate disappears, repetitive tasks shrink, and it becomes easy to explore small ideas without much friction.<\/p>\n<p>However, the moment something becomes architectural, ambiguous, or subtle, the human still needs to take the lead. Reviewing the output carefully is essential because small mistakes can compound quickly if they slip through.<\/p>\n<p>So while AI has definitely improved my productivity on this project, I'm not quite ready to uninstall my IDE just yet.<\/p>\n<h2 id=\"a-side-quest-building-a-small-ios-app\">A side quest: building a small iOS app<\/h2>\n<p>Alongside the web project, I also took the opportunity to try something slightly outside my usual comfort zone. I built a small iOS app that lets me interact with the micro-blog timeline from my phone.<\/p>\n<p>The app is intentionally simple. It allows me to post updates and browse the timeline, which is really all I need. The real value was in experimenting with a SwiftUI workflow while working with an AI agent.<\/p>\n<p>The code for that project is here: <a href=\"https:\/\/github.com\/theprivateer\/moments-ios\">https:\/\/github.com\/theprivateer\/moments-ios<\/a><\/p>\n<p>Working with Claude Code in a SwiftUI project turned out to be more productive than I expected, and it gave me the confidence to attempt something more ambitious.<\/p>\n<h2 id=\"the-next-project\">The next project<\/h2>\n<p>That experiment has already led directly to the next project.<\/p>\n<p>I'm currently building what I would consider a &quot;real&quot; app that I intend to publish on the App Store. To keep things manageable I've chosen to rebuild an older project with a very clear specification, although it's a step up in scope and complexity compared to the micro-blog app.<\/p>\n<p>If it works the way I hope, it should be a good test of whether an AI-assisted workflow holds up across a larger codebase.<\/p>\n<p>But that's a story for another post.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Rebuilding my micro-blog with AI agents\">Email a comment<\/a><\/p>",
            "date_published": "2026-03-13T00:00:00+10:00",
            "date_modified": "2026-03-13T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/105",
            "title": "Use Claude to automatically generate a commit message",
            "url": "https://philstephens.com/blog/use-claude-to-automatically-generate-a-commit-message",
            "content_html": "<p>My Git commits will never have &quot;WIP&quot; message again. Loving this simple-but-effective use-case for Claude:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">commit<\/span><span style=\"color:#E1E4E8\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  commitMessage<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#79B8FF\">$*<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  <\/span><span style=\"color:#B392F0\">git<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">add<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">.<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  <\/span><span style=\"color:#F97583\">if<\/span><span style=\"color:#E1E4E8\"> [ <\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\">$commitMessage<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;&quot;<\/span><span style=\"color:#E1E4E8\"> ]; <\/span><span style=\"color:#F97583\">then<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#6A737D\"># Start spinner in background (suppress job control messages)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       spinner<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#9ECBFF\">&quot;\u280b\u2819\u2839\u2838\u283c\u2834\u2826\u2827\u2807\u280f&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#F97583\">while<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">true<\/span><span style=\"color:#E1E4E8\">; <\/span><span style=\"color:#F97583\">do<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">         <\/span><span style=\"color:#F97583\">for<\/span><span style=\"color:#E1E4E8\"> (( i<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#79B8FF\">0<\/span><span style=\"color:#E1E4E8\">; i<\/span><span style=\"color:#F97583\">&lt;<\/span><span style=\"color:#E1E4E8\">${<\/span><span style=\"color:#F97583\">#<\/span><span style=\"color:#E1E4E8\">spinner}; i<\/span><span style=\"color:#F97583\">++<\/span><span style=\"color:#E1E4E8\"> )); <\/span><span style=\"color:#F97583\">do<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">           <\/span><span style=\"color:#79B8FF\">printf<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;\\r${<\/span><span style=\"color:#E1E4E8\">spinner<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\">$i<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\">1<\/span><span style=\"color:#9ECBFF\">} Generating commit message...&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">           <\/span><span style=\"color:#B392F0\">sleep<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">0.1<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">         <\/span><span style=\"color:#F97583\">done<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#F97583\">done<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     } &amp;<\/span><span style=\"color:#F97583\">!<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     spinner_pid<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#79B8FF\">$!<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#6A737D\"># Cleanup function for interrupt<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#B392F0\">cleanup<\/span><span style=\"color:#E1E4E8\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       { <\/span><span style=\"color:#79B8FF\">kill<\/span><span style=\"color:#E1E4E8\"> $spinner_pid; <\/span><span style=\"color:#79B8FF\">wait<\/span><span style=\"color:#E1E4E8\"> $spinner_pid; } <\/span><span style=\"color:#F97583\">2&gt;<\/span><span style=\"color:#E1E4E8\">\/dev\/null<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#79B8FF\">printf<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;\\r\\033[K&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#79B8FF\">trap<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">-<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">INT<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">1<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#79B8FF\">trap<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">cleanup<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">INT<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#6A737D\"># Get diff with size limit, include stat summary for context<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     diff_input<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\">$(<\/span><span style=\"color:#79B8FF\">echo<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;=== Summary ===&quot;<\/span><span style=\"color:#E1E4E8\"> &amp;&amp; <\/span><span style=\"color:#B392F0\">git<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">diff<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">--cached<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">--stat<\/span><span style=\"color:#E1E4E8\"> &amp;&amp; <\/span><span style=\"color:#79B8FF\">echo<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">-e<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;\\n=== Diff (truncated if large) ===&quot;<\/span><span style=\"color:#E1E4E8\"> &amp;&amp; <\/span><span style=\"color:#B392F0\">git<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">diff<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">--cached<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">|<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">head<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">-c<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">50000<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     commitMessage<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\">$(<\/span><span style=\"color:#79B8FF\">echo<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\">$diff_input<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">|<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">claude<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">--model<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">haiku<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">-p<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;Write a single-line commit message for this diff. Output ONLY the message, no quotes, no explanation, no markdown.&quot;<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#6A737D\"># Stop spinner and clear line<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#79B8FF\">trap<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">-<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">INT<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     { <\/span><span style=\"color:#79B8FF\">kill<\/span><span style=\"color:#E1E4E8\"> $spinner_pid; <\/span><span style=\"color:#79B8FF\">wait<\/span><span style=\"color:#E1E4E8\"> $spinner_pid; } <\/span><span style=\"color:#F97583\">2&gt;<\/span><span style=\"color:#E1E4E8\">\/dev\/null<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#79B8FF\">printf<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;\\r\\033[K&quot;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#B392F0\">git<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">commit<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">-m<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\">$commitMessage<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#F97583\">return<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  <\/span><span style=\"color:#F97583\">fi<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  <\/span><span style=\"color:#79B8FF\">eval<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;git commit -a -m &#39;${<\/span><span style=\"color:#E1E4E8\">commitMessage<\/span><span style=\"color:#9ECBFF\">}&#39;&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>Your mileage may vary depending on which model you use - I seem to get good enough results with Haiku 4.5, but feel free to change to <code>sonnet<\/code> or <code>opus<\/code> depending on your needs.<\/p>\n<p>All credit goes to <a href=\"https:\/\/freek.dev\/2978-how-to-automatically-generate-a-commit-message-using-claude\">Freek Van der Herten<\/a> - check out his post for a full breakdown of how it works.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Use Claude to automatically generate a commit message\">Email a comment<\/a><\/p>",
            "summary": "<p>My Git commits will never have &quot;WIP&quot; message again. Loving this simple-but-effective use-case for Claude:<\/p>\n<pre class=\"shiki\" style=\"background-color: #24292e\"><code><span class=\"line\"><span style=\"color:#F97583\">function<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">commit<\/span><span style=\"color:#E1E4E8\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  commitMessage<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#79B8FF\">$*<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  <\/span><span style=\"color:#B392F0\">git<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">add<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">.<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  <\/span><span style=\"color:#F97583\">if<\/span><span style=\"color:#E1E4E8\"> [ <\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\">$commitMessage<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;&quot;<\/span><span style=\"color:#E1E4E8\"> ]; <\/span><span style=\"color:#F97583\">then<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#6A737D\"># Start spinner in background (suppress job control messages)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       spinner<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#9ECBFF\">&quot;\u280b\u2819\u2839\u2838\u283c\u2834\u2826\u2827\u2807\u280f&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#F97583\">while<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">true<\/span><span style=\"color:#E1E4E8\">; <\/span><span style=\"color:#F97583\">do<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">         <\/span><span style=\"color:#F97583\">for<\/span><span style=\"color:#E1E4E8\"> (( i<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#79B8FF\">0<\/span><span style=\"color:#E1E4E8\">; i<\/span><span style=\"color:#F97583\">&lt;<\/span><span style=\"color:#E1E4E8\">${<\/span><span style=\"color:#F97583\">#<\/span><span style=\"color:#E1E4E8\">spinner}; i<\/span><span style=\"color:#F97583\">++<\/span><span style=\"color:#E1E4E8\"> )); <\/span><span style=\"color:#F97583\">do<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">           <\/span><span style=\"color:#79B8FF\">printf<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;\\r${<\/span><span style=\"color:#E1E4E8\">spinner<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\">$i<\/span><span style=\"color:#F97583\">:<\/span><span style=\"color:#E1E4E8\">1<\/span><span style=\"color:#9ECBFF\">} Generating commit message...&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">           <\/span><span style=\"color:#B392F0\">sleep<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">0.1<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">         <\/span><span style=\"color:#F97583\">done<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#F97583\">done<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     } &amp;<\/span><span style=\"color:#F97583\">!<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     spinner_pid<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#79B8FF\">$!<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#6A737D\"># Cleanup function for interrupt<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#B392F0\">cleanup<\/span><span style=\"color:#E1E4E8\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       { <\/span><span style=\"color:#79B8FF\">kill<\/span><span style=\"color:#E1E4E8\"> $spinner_pid; <\/span><span style=\"color:#79B8FF\">wait<\/span><span style=\"color:#E1E4E8\"> $spinner_pid; } <\/span><span style=\"color:#F97583\">2&gt;<\/span><span style=\"color:#E1E4E8\">\/dev\/null<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#79B8FF\">printf<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;\\r\\033[K&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#79B8FF\">trap<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">-<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">INT<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">       <\/span><span style=\"color:#F97583\">return<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">1<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     }<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#79B8FF\">trap<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">cleanup<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">INT<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#6A737D\"># Get diff with size limit, include stat summary for context<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     diff_input<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\">$(<\/span><span style=\"color:#79B8FF\">echo<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;=== Summary ===&quot;<\/span><span style=\"color:#E1E4E8\"> &amp;&amp; <\/span><span style=\"color:#B392F0\">git<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">diff<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">--cached<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">--stat<\/span><span style=\"color:#E1E4E8\"> &amp;&amp; <\/span><span style=\"color:#79B8FF\">echo<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">-e<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;\\n=== Diff (truncated if large) ===&quot;<\/span><span style=\"color:#E1E4E8\"> &amp;&amp; <\/span><span style=\"color:#B392F0\">git<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">diff<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">--cached<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">|<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">head<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">-c<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">50000<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     commitMessage<\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\">$(<\/span><span style=\"color:#79B8FF\">echo<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\">$diff_input<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#F97583\">|<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#B392F0\">claude<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">--model<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">haiku<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">-p<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;Write a single-line commit message for this diff. Output ONLY the message, no quotes, no explanation, no markdown.&quot;<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#6A737D\"># Stop spinner and clear line<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#79B8FF\">trap<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">-<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">INT<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     { <\/span><span style=\"color:#79B8FF\">kill<\/span><span style=\"color:#E1E4E8\"> $spinner_pid; <\/span><span style=\"color:#79B8FF\">wait<\/span><span style=\"color:#E1E4E8\"> $spinner_pid; } <\/span><span style=\"color:#F97583\">2&gt;<\/span><span style=\"color:#E1E4E8\">\/dev\/null<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#79B8FF\">printf<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;\\r\\033[K&quot;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#B392F0\">git<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">commit<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#79B8FF\">-m<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;<\/span><span style=\"color:#E1E4E8\">$commitMessage<\/span><span style=\"color:#9ECBFF\">&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">     <\/span><span style=\"color:#F97583\">return<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  <\/span><span style=\"color:#F97583\">fi<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  <\/span><span style=\"color:#79B8FF\">eval<\/span><span style=\"color:#E1E4E8\"> <\/span><span style=\"color:#9ECBFF\">&quot;git commit -a -m &#39;${<\/span><span style=\"color:#E1E4E8\">commitMessage<\/span><span style=\"color:#9ECBFF\">}&#39;&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre>\n<p>Your mileage may vary depending on which model you use - I seem to get good enough results with Haiku 4.5, but feel free to change to <code>sonnet<\/code> or <code>opus<\/code> depending on your needs.<\/p>\n<p>All credit goes to <a href=\"https:\/\/freek.dev\/2978-how-to-automatically-generate-a-commit-message-using-claude\">Freek Van der Herten<\/a> - check out his post for a full breakdown of how it works.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Use Claude to automatically generate a commit message\">Email a comment<\/a><\/p>",
            "date_published": "2026-03-12T00:00:00+10:00",
            "date_modified": "2026-03-12T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/104",
            "title": "What managing a backpacker hostel taught me about engineering management",
            "url": "https://philstephens.com/blog/what-managing-a-backpacker-hostel-taught-me-about-engineering-management",
            "content_html": "<p>About fifteen years ago I spent six months managing <a href=\"https:\/\/www.scotlandstophostels.com\/our-hostels\/inverness-student-hotel\">a backpacker hostel in the Highlands of Scotland<\/a>. The hostel was part of a wider chain with the head-office based in Edinburgh, more than 250 kilometres away. That distance mattered. Support was not immediate. Decisions stuck. If something went wrong, there was no quick escalation. You owned the outcome.<\/p>\n<p>The hostel catered to an international mix of travellers, each bringing different languages, cultures, and expectations. Some guests wanted warmth and conversation, others wanted efficiency and quiet. Everyone was budget conscious, which meant expectations were high and tolerance for friction was low. Small issues surfaced quickly.<\/p>\n<p>The team was small. A couple of front-of-house staff, casual cleaners, and me. There was no buffer layer. I spent about half my time on reception and the rest filling whatever gaps appeared. Cleaning rooms, dealing with maintenance issues, resolving booking mistakes, calming tired travellers arriving late and cold. You could not manage from a distance because the work was always right in front of you.<\/p>\n<p>Several times a week, we checked in bus tour groups, sometimes close to half the hostel's capacity arriving at once. If the check in process was slow or unclear, it was immediately obvious. Queues formed, stress levels rose, and staff felt it first. Any inefficiency was amplified.<\/p>\n<p>Then there were the moments you cannot plan for. One winter night - just days before Christmas - the brand new hot water system failed. In the Highlands. In December. With a full house. Head office was hundreds of kilometres away and options were limited. That night reinforced the value of staying calm, communicating clearly, and focusing on what could be done rather than what could not.<\/p>\n<p>We also had longterm residents alongside short-stay travellers. Over time, we built a family-like atmosphere -  especially important around Christmas when many guests were far from home. Shared meals and simple traditions made a difference. People were more patient, staff felt supported, and problems were easier to resolve because trust already existed.<\/p>\n<p>During this period I was closely involved in shaping the new in-house booking and property management system (which I would later build - a story for another time). I used it daily under pressure, which made its strengths and weaknesses impossible to ignore. I learned what information mattered at the front desk, what could wait, and what had to work every single time. Small design decisions had a direct impact on stress levels and guest experience.<\/p>\n<p>Looking back, the parallels with engineering management are clear.<\/p>\n<p>Engineering teams live inside the systems we give them. Processes, tools, and expectations shape behaviour whether we intend them to or not. When systems are clumsy or unclear, teams compensate. They invent workarounds and absorb friction, and over time that friction turns into stress.<\/p>\n<p>The bus tour check ins were an early lesson in scale. A process that works fine for a handful of people can fall apart under sudden load. Software behaves the same way. What seems acceptable in calm conditions often breaks down when pressure hits.<\/p>\n<p>The hot water failure taught another lesson that has stayed with me. When things go wrong, people do not expect perfection. They expect leadership. Clear communication, visible effort, and a sense that someone is paying attention. In engineering terms, this is good incident management.<\/p>\n<p>The atmosphere we built mattered as much as any process. When people felt cared for, small failures were forgiven. When staff felt supported, pressure was easier to handle. The same holds true in engineering teams. Trust changes how people respond when plans shift or systems fail.<\/p>\n<p>The biggest lesson was simple. Good engineering management is about flow, not control. Removing friction, staying close to the work, and designing systems that support people on hard days as well as easy ones.<\/p>\n<p>At the time, I was just trying to keep a remote hostel running through a Scottish winter. I did not realise I was learning lessons that would carry directly into leading engineering teams years later.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: What managing a backpacker hostel taught me about engineering management\">Email a comment<\/a><\/p>",
            "summary": "<p>About fifteen years ago I spent six months managing <a href=\"https:\/\/www.scotlandstophostels.com\/our-hostels\/inverness-student-hotel\">a backpacker hostel in the Highlands of Scotland<\/a>. The hostel was part of a wider chain with the head-office based in Edinburgh, more than 250 kilometres away. That distance mattered. Support was not immediate. Decisions stuck. If something went wrong, there was no quick escalation. You owned the outcome.<\/p>\n<p>The hostel catered to an international mix of travellers, each bringing different languages, cultures, and expectations. Some guests wanted warmth and conversation, others wanted efficiency and quiet. Everyone was budget conscious, which meant expectations were high and tolerance for friction was low. Small issues surfaced quickly.<\/p>\n<p>The team was small. A couple of front-of-house staff, casual cleaners, and me. There was no buffer layer. I spent about half my time on reception and the rest filling whatever gaps appeared. Cleaning rooms, dealing with maintenance issues, resolving booking mistakes, calming tired travellers arriving late and cold. You could not manage from a distance because the work was always right in front of you.<\/p>\n<p>Several times a week, we checked in bus tour groups, sometimes close to half the hostel's capacity arriving at once. If the check in process was slow or unclear, it was immediately obvious. Queues formed, stress levels rose, and staff felt it first. Any inefficiency was amplified.<\/p>\n<p>Then there were the moments you cannot plan for. One winter night - just days before Christmas - the brand new hot water system failed. In the Highlands. In December. With a full house. Head office was hundreds of kilometres away and options were limited. That night reinforced the value of staying calm, communicating clearly, and focusing on what could be done rather than what could not.<\/p>\n<p>We also had longterm residents alongside short-stay travellers. Over time, we built a family-like atmosphere -  especially important around Christmas when many guests were far from home. Shared meals and simple traditions made a difference. People were more patient, staff felt supported, and problems were easier to resolve because trust already existed.<\/p>\n<p>During this period I was closely involved in shaping the new in-house booking and property management system (which I would later build - a story for another time). I used it daily under pressure, which made its strengths and weaknesses impossible to ignore. I learned what information mattered at the front desk, what could wait, and what had to work every single time. Small design decisions had a direct impact on stress levels and guest experience.<\/p>\n<p>Looking back, the parallels with engineering management are clear.<\/p>\n<p>Engineering teams live inside the systems we give them. Processes, tools, and expectations shape behaviour whether we intend them to or not. When systems are clumsy or unclear, teams compensate. They invent workarounds and absorb friction, and over time that friction turns into stress.<\/p>\n<p>The bus tour check ins were an early lesson in scale. A process that works fine for a handful of people can fall apart under sudden load. Software behaves the same way. What seems acceptable in calm conditions often breaks down when pressure hits.<\/p>\n<p>The hot water failure taught another lesson that has stayed with me. When things go wrong, people do not expect perfection. They expect leadership. Clear communication, visible effort, and a sense that someone is paying attention. In engineering terms, this is good incident management.<\/p>\n<p>The atmosphere we built mattered as much as any process. When people felt cared for, small failures were forgiven. When staff felt supported, pressure was easier to handle. The same holds true in engineering teams. Trust changes how people respond when plans shift or systems fail.<\/p>\n<p>The biggest lesson was simple. Good engineering management is about flow, not control. Removing friction, staying close to the work, and designing systems that support people on hard days as well as easy ones.<\/p>\n<p>At the time, I was just trying to keep a remote hostel running through a Scottish winter. I did not realise I was learning lessons that would carry directly into leading engineering teams years later.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: What managing a backpacker hostel taught me about engineering management\">Email a comment<\/a><\/p>",
            "date_published": "2026-02-09T00:00:00+10:00",
            "date_modified": "2026-02-09T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/103",
            "title": "Why I am choosing engineering management now",
            "url": "https://philstephens.com/blog/why-i-am-choosing-engineering-management-now",
            "content_html": "<p>I'm <a href=\"https:\/\/philstephens.com\/blog\/the-next-chapter\">back on the job market<\/a>, and over the past few weeks I have been having a lot of conversations with recruiters, hiring managers, and engineering leaders. Different companies, different roles, different contexts, but many of the same questions.<\/p>\n<p><em>Why now? Why management? What does leadership mean to you?<\/em><\/p>\n<p>Answering those questions has forced me to slow down and take stock of how I got here.<\/p>\n<hr \/>\n<p>I have been building software for over twenty years. During that time, my role has shifted back and forth between senior engineer, technical lead, and team leadership.<\/p>\n<p>Looking only at job titles over the past decade does not always tell that full story. In several roles, I was already doing much of the work typically associated with an engineering manager or even a head of engineering. I was responsible for delivery, cross-team coordination, mentoring, and technical direction, just without the formal recognition or title.<\/p>\n<p>There were periods where I was leading teams, shaping delivery, and spending most of my time on people and planning. There were also periods where changes in company structure or priorities pulled me back into hands-on development. At the time, some of those shifts felt like setbacks - but in hindsight they have added depth.<\/p>\n<p>Spending more time back in the code has kept me grounded in the realities of delivery. It reinforced empathy for engineers under pressure and sharpened my sense of what actually helps teams ship.<\/p>\n<p>Those same detours also gave me deeper exposure to product management and business analysis than I might not have otherwise have had. I've spent time close to requirements, stakeholders, trade-offs, and constraints. I've seen first-hand how product decisions land with engineering teams, and how technical realities can push back on product ambition. This wasn't always planned, but it has shaped how I think about leadership.<\/p>\n<hr \/>\n<p>One of the benefits of these recent conversations is that they have surfaced patterns.<\/p>\n<p>When I talk about the work I have enjoyed most, it is rarely about individual technical wins. It is about creating clarity, improving how teams make decisions, and helping engineers grow in confidence and judgment.<\/p>\n<p>When I talk about the problems that frustrate me, they are rarely about technology. They are about misalignment, churn, and avoidable stress.<\/p>\n<h2 id=\"so-quotwhy-nowquot\">So &quot;Why now?&quot;<\/h2>\n<p>Choosing engineering management now feels less like a change in direction and more like committing to the work I have been circling for years.<\/p>\n<p>I am not moving away from technical work because I am tired of it. I still care deeply about code quality, system design, and sound engineering decisions. What has changed is where I believe my experience has the most leverage.<\/p>\n<p>I want to spend my time building environments where good work can happen reliably, not just occasionally.<\/p>\n<p>I want to be accountable for teams, not just systems. I want to work on how decisions are made, how priorities are set, and how engineers are supported as they grow. I want to shape environments where good work happens consistently, rather than stepping in when things are already under pressure.<\/p>\n<p>Engineering management comes with less visible output and more responsibility for outcomes that I do not directly control. It also offers the kind of leverage I am looking for at this stage of my career.<\/p>\n<aside class=\"callout primary\">\n\tIf you are looking for an engineering manager with a deep background in solving problems, building teams and getting projects back on track, feel free to <a href=\"https:\/\/philstephens.com\/resume\">download my resume here<\/a> and <a href=\"https:\/\/philstephens.com\/contact\">get in touch<\/a> - I'm getting really good at talking about my journey, and the benefits I bring!\n<\/aside>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Why I am choosing engineering management now\">Email a comment<\/a><\/p>",
            "summary": "<p>I'm <a href=\"https:\/\/philstephens.com\/blog\/the-next-chapter\">back on the job market<\/a>, and over the past few weeks I have been having a lot of conversations with recruiters, hiring managers, and engineering leaders. Different companies, different roles, different contexts, but many of the same questions.<\/p>\n<p><em>Why now? Why management? What does leadership mean to you?<\/em><\/p>\n<p>Answering those questions has forced me to slow down and take stock of how I got here.<\/p>\n<hr \/>\n<p>I have been building software for over twenty years. During that time, my role has shifted back and forth between senior engineer, technical lead, and team leadership.<\/p>\n<p>Looking only at job titles over the past decade does not always tell that full story. In several roles, I was already doing much of the work typically associated with an engineering manager or even a head of engineering. I was responsible for delivery, cross-team coordination, mentoring, and technical direction, just without the formal recognition or title.<\/p>\n<p>There were periods where I was leading teams, shaping delivery, and spending most of my time on people and planning. There were also periods where changes in company structure or priorities pulled me back into hands-on development. At the time, some of those shifts felt like setbacks - but in hindsight they have added depth.<\/p>\n<p>Spending more time back in the code has kept me grounded in the realities of delivery. It reinforced empathy for engineers under pressure and sharpened my sense of what actually helps teams ship.<\/p>\n<p>Those same detours also gave me deeper exposure to product management and business analysis than I might not have otherwise have had. I've spent time close to requirements, stakeholders, trade-offs, and constraints. I've seen first-hand how product decisions land with engineering teams, and how technical realities can push back on product ambition. This wasn't always planned, but it has shaped how I think about leadership.<\/p>\n<hr \/>\n<p>One of the benefits of these recent conversations is that they have surfaced patterns.<\/p>\n<p>When I talk about the work I have enjoyed most, it is rarely about individual technical wins. It is about creating clarity, improving how teams make decisions, and helping engineers grow in confidence and judgment.<\/p>\n<p>When I talk about the problems that frustrate me, they are rarely about technology. They are about misalignment, churn, and avoidable stress.<\/p>\n<h2 id=\"so-quotwhy-nowquot\">So &quot;Why now?&quot;<\/h2>\n<p>Choosing engineering management now feels less like a change in direction and more like committing to the work I have been circling for years.<\/p>\n<p>I am not moving away from technical work because I am tired of it. I still care deeply about code quality, system design, and sound engineering decisions. What has changed is where I believe my experience has the most leverage.<\/p>\n<p>I want to spend my time building environments where good work can happen reliably, not just occasionally.<\/p>\n<p>I want to be accountable for teams, not just systems. I want to work on how decisions are made, how priorities are set, and how engineers are supported as they grow. I want to shape environments where good work happens consistently, rather than stepping in when things are already under pressure.<\/p>\n<p>Engineering management comes with less visible output and more responsibility for outcomes that I do not directly control. It also offers the kind of leverage I am looking for at this stage of my career.<\/p>\n<aside class=\"callout primary\">\n\tIf you are looking for an engineering manager with a deep background in solving problems, building teams and getting projects back on track, feel free to <a href=\"https:\/\/philstephens.com\/resume\">download my resume here<\/a> and <a href=\"https:\/\/philstephens.com\/contact\">get in touch<\/a> - I'm getting really good at talking about my journey, and the benefits I bring!\n<\/aside>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Why I am choosing engineering management now\">Email a comment<\/a><\/p>",
            "date_published": "2026-02-05T00:00:00+10:00",
            "date_modified": "2026-02-05T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/102",
            "title": "Vibe-coding and reinventing the wheel",
            "url": "https://philstephens.com/blog/vibe-coding-and-reinventing-the-wheel",
            "content_html": "<p>I came across a LinkedIn post today that stuck with me.<\/p>\n<p>A CEO shared how he had used <del>Clawdbot<\/del> <del>Moltbot<\/del> <a href=\"https:\/\/openclaw.ai\">OpenClaw<\/a> to create a small macOS app. He has a simple goal - to able to easily insert emojis into text without his hands leaving the keyboard. He opted for typing Slack-style emoji shortcuts like <code>:emoji:<\/code> and have them expand into actual emojis. He was upfront about it being a niche use case. It only saved him a few minutes a day. Still, he was clearly proud of the process. Over the course of a morning, in between meetings, he iterated on the idea using the self-hosted chatbot and ended up with something that worked.<\/p>\n<p>The comments were full of praise. People loved the initiative - a few even asked if he could share the app.<\/p>\n<p>Buried a little deeper in the thread, though, someone pointed out that macOS already has a built-in emoji picker, accessible via a keyboard shortcut. That alone makes the whole exercise questionable if the goal is pure productivity.<\/p>\n<p>But there is a more important detail that never really landed in the discussion.<\/p>\n<p>There is already an app that does exactly what he built. It's called <a href=\"https:\/\/matthewpalmer.net\/rocket\/\">Rocket<\/a>. It has been around for years. I use it every single day. You type <code>:fire:<\/code> or <code>:thumbsup:<\/code> and it expands to an emoji in any app. It's fast, polished, and solved.<\/p>\n<p>That's where this story stopped being charming and started raising a bigger question for me. Why are we so quick to reach for AI to re-solve problems that already have good, boring, well-tested solutions?<\/p>\n<p>I don't think this is about saving time. In this case, the AI-assisted approach objectively took longer. A quick search would have turned up Rocket in under a minute. Instead, he spent a morning iterating, plus however many Claude API tokens went up in smoke along the way. Ironically, even ChatGPT would have pointed him straight to the existing app and saved the effort entirely (I checked).<\/p>\n<p><img src=\"https:\/\/assets.philstephens.com\/3BDiPKWtKWc2gmrnZ27ACvFfyfN6TDHepST4vCNY.png\" alt=\"A screenshot of a ChatGPT prompt and response\" \/><\/p>\n<p>So what's really going on here?<\/p>\n<p>Part of it might be laziness, but not the obvious kind. Not &quot;I don't want to do the work&quot; but &quot;I don't want to stop and look around&quot;. There's a subtle friction to researching existing tools. It feels slower than just building something, especially when AI makes building feel playful and low-risk.<\/p>\n<p>I think there's also a bit of developer hubris mixed in. The quiet assumption that rebuilding something yourself is inherently better, or at least more interesting, than adopting what already exists. We've all felt it. The pull to reinvent the wheel because we can, not because we should.<\/p>\n<p>To be clear, I'm not anti-experimentation. Hacking on small tools for fun or learning is great. Using AI as a thinking partner can be genuinely powerful. But there's a difference between experimenting and mistaking novelty for progress.<\/p>\n<p>As builders and leaders, part of our job is knowing when not to build. When to pause, scan the landscape, and recognise that someone else already solved this problem years ago. Often better than we will in a single morning.<\/p>\n<p>AI lowers the cost of creation, which is exciting. But it doesn't remove the need for judgment. If anything, it makes that judgment more important. Otherwise, we risk spending more time, more money, and more energy enthusiastically rebuilding things we already have.<\/p>\n<p><strong>P.S. If you haven't already, you should <a href=\"https:\/\/matthewpalmer.net\/rocket\/\">download Rocket<\/a>. You can even support the indie-developer by <a href=\"https:\/\/store.matthewpalmer.net\/rocket-pro\">upgrading to the Pro version<\/a> for $10.<\/strong><\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Vibe-coding and reinventing the wheel\">Email a comment<\/a><\/p>",
            "summary": "<p>I came across a LinkedIn post today that stuck with me.<\/p>\n<p>A CEO shared how he had used <del>Clawdbot<\/del> <del>Moltbot<\/del> <a href=\"https:\/\/openclaw.ai\">OpenClaw<\/a> to create a small macOS app. He has a simple goal - to able to easily insert emojis into text without his hands leaving the keyboard. He opted for typing Slack-style emoji shortcuts like <code>:emoji:<\/code> and have them expand into actual emojis. He was upfront about it being a niche use case. It only saved him a few minutes a day. Still, he was clearly proud of the process. Over the course of a morning, in between meetings, he iterated on the idea using the self-hosted chatbot and ended up with something that worked.<\/p>\n<p>The comments were full of praise. People loved the initiative - a few even asked if he could share the app.<\/p>\n<p>Buried a little deeper in the thread, though, someone pointed out that macOS already has a built-in emoji picker, accessible via a keyboard shortcut. That alone makes the whole exercise questionable if the goal is pure productivity.<\/p>\n<p>But there is a more important detail that never really landed in the discussion.<\/p>\n<p>There is already an app that does exactly what he built. It's called <a href=\"https:\/\/matthewpalmer.net\/rocket\/\">Rocket<\/a>. It has been around for years. I use it every single day. You type <code>:fire:<\/code> or <code>:thumbsup:<\/code> and it expands to an emoji in any app. It's fast, polished, and solved.<\/p>\n<p>That's where this story stopped being charming and started raising a bigger question for me. Why are we so quick to reach for AI to re-solve problems that already have good, boring, well-tested solutions?<\/p>\n<p>I don't think this is about saving time. In this case, the AI-assisted approach objectively took longer. A quick search would have turned up Rocket in under a minute. Instead, he spent a morning iterating, plus however many Claude API tokens went up in smoke along the way. Ironically, even ChatGPT would have pointed him straight to the existing app and saved the effort entirely (I checked).<\/p>\n<p><img src=\"https:\/\/assets.philstephens.com\/3BDiPKWtKWc2gmrnZ27ACvFfyfN6TDHepST4vCNY.png\" alt=\"A screenshot of a ChatGPT prompt and response\" \/><\/p>\n<p>So what's really going on here?<\/p>\n<p>Part of it might be laziness, but not the obvious kind. Not &quot;I don't want to do the work&quot; but &quot;I don't want to stop and look around&quot;. There's a subtle friction to researching existing tools. It feels slower than just building something, especially when AI makes building feel playful and low-risk.<\/p>\n<p>I think there's also a bit of developer hubris mixed in. The quiet assumption that rebuilding something yourself is inherently better, or at least more interesting, than adopting what already exists. We've all felt it. The pull to reinvent the wheel because we can, not because we should.<\/p>\n<p>To be clear, I'm not anti-experimentation. Hacking on small tools for fun or learning is great. Using AI as a thinking partner can be genuinely powerful. But there's a difference between experimenting and mistaking novelty for progress.<\/p>\n<p>As builders and leaders, part of our job is knowing when not to build. When to pause, scan the landscape, and recognise that someone else already solved this problem years ago. Often better than we will in a single morning.<\/p>\n<p>AI lowers the cost of creation, which is exciting. But it doesn't remove the need for judgment. If anything, it makes that judgment more important. Otherwise, we risk spending more time, more money, and more energy enthusiastically rebuilding things we already have.<\/p>\n<p><strong>P.S. If you haven't already, you should <a href=\"https:\/\/matthewpalmer.net\/rocket\/\">download Rocket<\/a>. You can even support the indie-developer by <a href=\"https:\/\/store.matthewpalmer.net\/rocket-pro\">upgrading to the Pro version<\/a> for $10.<\/strong><\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Vibe-coding and reinventing the wheel\">Email a comment<\/a><\/p>",
            "date_published": "2026-01-30T00:00:00+10:00",
            "date_modified": "2026-01-30T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/101",
            "title": "Embracing technology to protect my time",
            "url": "https://philstephens.com/blog/embracing-technology-to-protect-my-time",
            "content_html": "<p>This year, I'm deliberately rethinking my relationship with technology, and AI sits right at the centre of that shift. Not because I'm chasing novelty, but because the pace and volume of information in everyday life have crossed a threshold where old approaches are no longer enough.<\/p>\n<p>I'm moving back into employment and looking at more management-focused roles, which brings a different rhythm to the day. At the same time, life outside work is just as demanding. I have two school-aged children with busy schedules, extra-curricular activities, and all the coordination that comes with them. Between work, home, and the background noise of modern life, there's a constant stream of information that needs attention.<\/p>\n<p>For a long time, my response to that pressure was to look for simplicity. I've read the books, tried the systems, and experimented with productivity frameworks designed around focus, calm, and reduced cognitive load. They're thoughtful ideas, but most assume conditions that rarely exist in the real world. Long stretches of uninterrupted time, minimal interruptions, and predictable schedules aren't luxuries that management roles or family life often provide.<\/p>\n<p>That mismatch is part of what's changed my thinking. Rather than trying to force idealised systems onto a complex reality, I'm more interested in tools that can help me operate effectively within it.<\/p>\n<p>Until recently, AI wasn't something I felt comfortable leaning on outside of work. There was too much hype, too much loose thinking, and an uncomfortable narrative around replacing human judgement. It felt more distracting than helpful. But that stance has softened as the technology has matured and the conversation has become more grounded.<\/p>\n<p>I'm not interested in grand claims about artificial general intelligence. What I am interested in are practical capabilities that already exist. Tools that can summarise information, organise inputs, surface patterns, and reduce the background noise that competes for attention. When used with intent and clear boundaries, large language models can act as support rather than substitution.<\/p>\n<p>The key issue for me is agency. I want to use AI to offload the mechanical parts of managing information, not the thinking that actually matters. Drafting, summarising, exploring options, and connecting dots are all areas where AI can be helpful, especially when time is fragmented. Decision-making, judgement, and responsibility stay firmly human.<\/p>\n<p>That shift in perspective has made me look again at the tools I already use every day, particularly within the Apple ecosystem. Notes, Reminders, Calendar, Shortcuts, and Mail are already central to how I manage work and home life. The opportunity now is in how intelligence gets layered into those tools, rather than bolted on as something separate.<\/p>\n<p>Automation and AI start to overlap here in useful ways. Small, dependable workflows that capture information automatically, structure it consistently, and make it easier to retrieve later reduce the mental bookkeeping that builds up over time. When combined with AI that can help interpret and summarise that information, the overall system becomes far more resilient to interruption and context switching.<\/p>\n<p>This is where the announcement about Apple partnering with Google to use Gemini to power Apple Intelligence becomes particularly interesting. Apple's cautious approach, especially around privacy and on-device processing, suggests a focus on assistive intelligence rather than attention-grabbing features. If AI shows up quietly in the tools I already rely on, supporting everyday tasks across work and home, that's where it has real value.<\/p>\n<p>None of this is about optimisation for its own sake or trying to extract more output from every hour. The goal is protection. Protecting time, energy, and attention in a life that is full by default. If AI can help reduce friction, clarify priorities, and make it easier to stay present, then it earns its place.<\/p>\n<p>As I work through this shift, I plan to share the tools, methods, and workflows I adopt, particularly where AI genuinely helps and where it falls short. I'm interested in what holds up under real-world conditions, not theoretical ideals. If you've found ways to use AI that support your thinking without replacing it, I'd genuinely like to hear about them. Comparing notes feels like a useful way to navigate what's changing, and I'm looking forward to the conversations that follow.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Embracing technology to protect my time\">Email a comment<\/a><\/p>",
            "summary": "<p>This year, I'm deliberately rethinking my relationship with technology, and AI sits right at the centre of that shift. Not because I'm chasing novelty, but because the pace and volume of information in everyday life have crossed a threshold where old approaches are no longer enough.<\/p>\n<p>I'm moving back into employment and looking at more management-focused roles, which brings a different rhythm to the day. At the same time, life outside work is just as demanding. I have two school-aged children with busy schedules, extra-curricular activities, and all the coordination that comes with them. Between work, home, and the background noise of modern life, there's a constant stream of information that needs attention.<\/p>\n<p>For a long time, my response to that pressure was to look for simplicity. I've read the books, tried the systems, and experimented with productivity frameworks designed around focus, calm, and reduced cognitive load. They're thoughtful ideas, but most assume conditions that rarely exist in the real world. Long stretches of uninterrupted time, minimal interruptions, and predictable schedules aren't luxuries that management roles or family life often provide.<\/p>\n<p>That mismatch is part of what's changed my thinking. Rather than trying to force idealised systems onto a complex reality, I'm more interested in tools that can help me operate effectively within it.<\/p>\n<p>Until recently, AI wasn't something I felt comfortable leaning on outside of work. There was too much hype, too much loose thinking, and an uncomfortable narrative around replacing human judgement. It felt more distracting than helpful. But that stance has softened as the technology has matured and the conversation has become more grounded.<\/p>\n<p>I'm not interested in grand claims about artificial general intelligence. What I am interested in are practical capabilities that already exist. Tools that can summarise information, organise inputs, surface patterns, and reduce the background noise that competes for attention. When used with intent and clear boundaries, large language models can act as support rather than substitution.<\/p>\n<p>The key issue for me is agency. I want to use AI to offload the mechanical parts of managing information, not the thinking that actually matters. Drafting, summarising, exploring options, and connecting dots are all areas where AI can be helpful, especially when time is fragmented. Decision-making, judgement, and responsibility stay firmly human.<\/p>\n<p>That shift in perspective has made me look again at the tools I already use every day, particularly within the Apple ecosystem. Notes, Reminders, Calendar, Shortcuts, and Mail are already central to how I manage work and home life. The opportunity now is in how intelligence gets layered into those tools, rather than bolted on as something separate.<\/p>\n<p>Automation and AI start to overlap here in useful ways. Small, dependable workflows that capture information automatically, structure it consistently, and make it easier to retrieve later reduce the mental bookkeeping that builds up over time. When combined with AI that can help interpret and summarise that information, the overall system becomes far more resilient to interruption and context switching.<\/p>\n<p>This is where the announcement about Apple partnering with Google to use Gemini to power Apple Intelligence becomes particularly interesting. Apple's cautious approach, especially around privacy and on-device processing, suggests a focus on assistive intelligence rather than attention-grabbing features. If AI shows up quietly in the tools I already rely on, supporting everyday tasks across work and home, that's where it has real value.<\/p>\n<p>None of this is about optimisation for its own sake or trying to extract more output from every hour. The goal is protection. Protecting time, energy, and attention in a life that is full by default. If AI can help reduce friction, clarify priorities, and make it easier to stay present, then it earns its place.<\/p>\n<p>As I work through this shift, I plan to share the tools, methods, and workflows I adopt, particularly where AI genuinely helps and where it falls short. I'm interested in what holds up under real-world conditions, not theoretical ideals. If you've found ways to use AI that support your thinking without replacing it, I'd genuinely like to hear about them. Comparing notes feels like a useful way to navigate what's changing, and I'm looking forward to the conversations that follow.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Embracing technology to protect my time\">Email a comment<\/a><\/p>",
            "date_published": "2026-01-16T00:00:00+10:00",
            "date_modified": "2026-01-16T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/100",
            "title": "The Next Chapter",
            "url": "https://philstephens.com/blog/the-next-chapter",
            "content_html": "<p>It's been almost seven months since I stepped away from <a href=\"https:\/\/rexsoftware.com\">Rex Software<\/a>. Over that time, I've taken the chance to reset, learn, and spend more time with my family. It's been invaluable, but now I'm ready to dive back in.<\/p>\n<p>Software building still fascinates me. There's nothing quite like getting hands-on, creating something that helps people, and seeing it come to life. That part of the job has always been incredibly rewarding.<\/p>\n<p>What I've missed most, though, is technical leadership. I love creating environments where developers feel confident to experiment, learn, and have fun while shipping solid, reliable systems. Guiding the architecture of platforms - big and small - mentoring developers, and connecting people has always been a central part of my professional passion.<\/p>\n<p>In 2026, I'm looking to advance my career in technical leadership. I'm seeking engineering manager roles, or technical\/team lead positions with a clear path toward engineering management. I'm not rushing into the first opportunity - I want something long-term, not just a stepping-stone.<\/p>\n<p>I'm excited to see what the next chapter holds and to continue building software that makes a difference while helping teams thrive.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: The Next Chapter\">Email a comment<\/a><\/p>",
            "summary": "<p>It's been almost seven months since I stepped away from <a href=\"https:\/\/rexsoftware.com\">Rex Software<\/a>. Over that time, I've taken the chance to reset, learn, and spend more time with my family. It's been invaluable, but now I'm ready to dive back in.<\/p>\n<p>Software building still fascinates me. There's nothing quite like getting hands-on, creating something that helps people, and seeing it come to life. That part of the job has always been incredibly rewarding.<\/p>\n<p>What I've missed most, though, is technical leadership. I love creating environments where developers feel confident to experiment, learn, and have fun while shipping solid, reliable systems. Guiding the architecture of platforms - big and small - mentoring developers, and connecting people has always been a central part of my professional passion.<\/p>\n<p>In 2026, I'm looking to advance my career in technical leadership. I'm seeking engineering manager roles, or technical\/team lead positions with a clear path toward engineering management. I'm not rushing into the first opportunity - I want something long-term, not just a stepping-stone.<\/p>\n<p>I'm excited to see what the next chapter holds and to continue building software that makes a difference while helping teams thrive.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: The Next Chapter\">Email a comment<\/a><\/p>",
            "date_published": "2026-01-13T00:00:00+10:00",
            "date_modified": "2026-01-13T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/99",
            "title": "In \/ Out",
            "url": "https://philstephens.com/blog/in-out-2026",
            "content_html": "<p>2026 for me, will aim to be:<\/p>\n<h2 id=\"in\">In<\/h2>\n<ul>\n<li>Saunas<\/li>\n<li>Networking<\/li>\n<li>Self-awareness<\/li>\n<li>Leading<\/li>\n<li>Donating blood<\/li>\n<li>Running<\/li>\n<li>Cycling<\/li>\n<li>Self-care<\/li>\n<li>AI<\/li>\n<\/ul>\n<h2 id=\"out\">Out<\/h2>\n<ul>\n<li>Imposter syndrome<\/li>\n<li>Doom-scrolling<\/li>\n<li>FOMO<\/li>\n<li>Comfort zone<\/li>\n<\/ul>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: In \/ Out\">Email a comment<\/a><\/p>",
            "summary": "<p>2026 for me, will aim to be:<\/p>\n<h2 id=\"in\">In<\/h2>\n<ul>\n<li>Saunas<\/li>\n<li>Networking<\/li>\n<li>Self-awareness<\/li>\n<li>Leading<\/li>\n<li>Donating blood<\/li>\n<li>Running<\/li>\n<li>Cycling<\/li>\n<li>Self-care<\/li>\n<li>AI<\/li>\n<\/ul>\n<h2 id=\"out\">Out<\/h2>\n<ul>\n<li>Imposter syndrome<\/li>\n<li>Doom-scrolling<\/li>\n<li>FOMO<\/li>\n<li>Comfort zone<\/li>\n<\/ul>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: In \/ Out\">Email a comment<\/a><\/p>",
            "date_published": "2026-01-01T00:00:00+10:00",
            "date_modified": "2026-01-01T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/98",
            "title": "Farewell Rex",
            "url": "https://philstephens.com/blog/farewell-rex",
            "content_html": "<p>After 3 years and 9 months I have decided to move on from Rex. It has been an exciting roller-coaster ride, seeing the company change and grow over such a relatively short period of time.<\/p>\n<p>Rather than jumping straight into a new role I'm going to take a break. Over the next few months I'll be spending more time with the kids, working through a never-ending list of household to-do's, and giving myself space to figure out what my next professional adventure is going to look like.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Farewell Rex\">Email a comment<\/a><\/p>",
            "summary": "<p>After 3 years and 9 months I have decided to move on from Rex. It has been an exciting roller-coaster ride, seeing the company change and grow over such a relatively short period of time.<\/p>\n<p>Rather than jumping straight into a new role I'm going to take a break. Over the next few months I'll be spending more time with the kids, working through a never-ending list of household to-do's, and giving myself space to figure out what my next professional adventure is going to look like.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Farewell Rex\">Email a comment<\/a><\/p>",
            "date_published": "2025-06-20T00:00:00+10:00",
            "date_modified": "2025-06-20T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/97",
            "title": "Supporting Creators",
            "url": "https://philstephens.com/blog/supporting-creators",
            "content_html": "<p>I follow quite a few independent content creators via RSS, email newsletter and podcast. I am increasingly getting greater value from these than I do from more mainstream sources, so I have decided to do the right thing and start supporting some financially.<\/p>\n<p>Whilst I would love to be able to support all of the content creators I follow that just isn't feasible for me. Instead I am going to be hand-picking a few different creators, with a view to adding one or two more over the next few months.<\/p>\n<p>My first round of subscription dollars goes to the <a href=\"https:\/\/patreon.com\/techwontsaveus\">Tech Won\u2019t Save Us<\/a> podcast and <a href=\"https:\/\/www.joanwestenberg.com\">Joan Westenberg<\/a>.<\/p>\n<p>Both represent independent critical perspectives of the tech landscape and its impacts on society. They also both provide the majority of their content free of charge so I recommend you check them out.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Supporting Creators\">Email a comment<\/a><\/p>",
            "summary": "<p>I follow quite a few independent content creators via RSS, email newsletter and podcast. I am increasingly getting greater value from these than I do from more mainstream sources, so I have decided to do the right thing and start supporting some financially.<\/p>\n<p>Whilst I would love to be able to support all of the content creators I follow that just isn't feasible for me. Instead I am going to be hand-picking a few different creators, with a view to adding one or two more over the next few months.<\/p>\n<p>My first round of subscription dollars goes to the <a href=\"https:\/\/patreon.com\/techwontsaveus\">Tech Won\u2019t Save Us<\/a> podcast and <a href=\"https:\/\/www.joanwestenberg.com\">Joan Westenberg<\/a>.<\/p>\n<p>Both represent independent critical perspectives of the tech landscape and its impacts on society. They also both provide the majority of their content free of charge so I recommend you check them out.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Supporting Creators\">Email a comment<\/a><\/p>",
            "date_published": "2025-05-07T00:00:00+10:00",
            "date_modified": "2025-05-07T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/96",
            "title": "I ran 10k",
            "url": "https://philstephens.com/blog/i-ran-10k",
            "content_html": "<p>Last night I surprised myself by running 10km non-stop for the first time.<\/p>\n<p>I was running laps of the <a href=\"https:\/\/www.parkrun.com.au\/northlakes\/course\/\">North Lakes Parkrun<\/a> course whilst my daughter was attending a session at the local YMCA, and only intended on matching my previous best of ~6.5km, which is four laps of the lake. However at the end of the fourth lap I just kept going... and going.<\/p>\n<p>I only started running for the first time about a year ago, and was pretty on-and-off towards the end of 2024, so I\u2019m really happy with my progress.<\/p>\n<p>I\u2019m registered to run the 10km event at the <a href=\"https:\/\/goldcoastmarathon.com.au\">Gold Coast Marathon<\/a> in July, and then the <a href=\"https:\/\/www.visitmoretonbayregion.com.au\/j2j\">Jetty2Jetty<\/a> half-marathon a couple of weeks later.<\/p>\n<p>Whilst it is still 12 weeks out, it is good to know that I <em>can<\/em> run the 10km distance without too much trouble and can now knuckle down and concentrate on training for the half-marathon distance.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: I ran 10k\">Email a comment<\/a><\/p>",
            "summary": "<p>Last night I surprised myself by running 10km non-stop for the first time.<\/p>\n<p>I was running laps of the <a href=\"https:\/\/www.parkrun.com.au\/northlakes\/course\/\">North Lakes Parkrun<\/a> course whilst my daughter was attending a session at the local YMCA, and only intended on matching my previous best of ~6.5km, which is four laps of the lake. However at the end of the fourth lap I just kept going... and going.<\/p>\n<p>I only started running for the first time about a year ago, and was pretty on-and-off towards the end of 2024, so I\u2019m really happy with my progress.<\/p>\n<p>I\u2019m registered to run the 10km event at the <a href=\"https:\/\/goldcoastmarathon.com.au\">Gold Coast Marathon<\/a> in July, and then the <a href=\"https:\/\/www.visitmoretonbayregion.com.au\/j2j\">Jetty2Jetty<\/a> half-marathon a couple of weeks later.<\/p>\n<p>Whilst it is still 12 weeks out, it is good to know that I <em>can<\/em> run the 10km distance without too much trouble and can now knuckle down and concentrate on training for the half-marathon distance.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: I ran 10k\">Email a comment<\/a><\/p>",
            "date_published": "2025-04-11T00:00:00+10:00",
            "date_modified": "2025-04-11T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/95",
            "title": "I\u2019ve turned off Apple Intelligence",
            "url": "https://philstephens.com/blog/ive-turned-off-apple-intelligence",
            "content_html": "<p>Today I turned off <strong>Apple Intelligence<\/strong> on all of my devices.<\/p>\n<p>Apart from the fact this initial release has been widely panned (with hints in their marketing at <a href=\"https:\/\/notes.ghed.in\/posts\/2024\/selfishness-in-ai\/\">appealing to sociopaths<\/a>) I just don't use any of the tools, and the notification summaries add no value to me. I won't be missing anything.<\/p>\n<p>This is not an anti-Apple stance. Like a lot of what Apple does, this has just been a swing and a miss for me - and that\u2019s okay. If and when it genuinely gets better I may turn it back on, but for now, I'm good.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: I\u2019ve turned off Apple Intelligence\">Email a comment<\/a><\/p>",
            "summary": "<p>Today I turned off <strong>Apple Intelligence<\/strong> on all of my devices.<\/p>\n<p>Apart from the fact this initial release has been widely panned (with hints in their marketing at <a href=\"https:\/\/notes.ghed.in\/posts\/2024\/selfishness-in-ai\/\">appealing to sociopaths<\/a>) I just don't use any of the tools, and the notification summaries add no value to me. I won't be missing anything.<\/p>\n<p>This is not an anti-Apple stance. Like a lot of what Apple does, this has just been a swing and a miss for me - and that\u2019s okay. If and when it genuinely gets better I may turn it back on, but for now, I'm good.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: I\u2019ve turned off Apple Intelligence\">Email a comment<\/a><\/p>",
            "date_published": "2025-02-07T00:00:00+10:00",
            "date_modified": "2025-02-07T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/94",
            "title": "More processing power isn\u2019t necessarily better",
            "url": "https://philstephens.com/blog/more-processing-power-isnt-necessarily-better",
            "content_html": "<p>At work we use a unified development environment in Docker. This allows us to simulate all of the interconnected services and workloads of our production Kubernetes clusters, which is pretty awesome.<\/p>\n<p>Unfortunately, to-date the virtualisation on Apple\u2019s M-series chips hasn\u2019t been amazing, causing some performance bottlenecks. This is often most felt on cold-starts of the virtual environment, seeding and running tests, but can also be seen during certain requests to the backend and longer-running jobs. To be clear, the performance of our production environments is not a problem - the local Docker environment simply magnifies where there might be issues to the point we can experience a slower request first-hand.<\/p>\n<p>Personally I have always embraced this friction - it is a constant reminder to program for efficient processing and not to rely on raw power and auto-scaling to make up for inefficiency.<\/p>\n<p>A couple of weeks ago I was upgraded from my original M1 MacBook Pro (brand new out of the box) that I was given when I first started at the company, to a MacBook Pro with an M4 Pro chip.<\/p>\n<p>After the first cold start of our Docker environment I thought that perhaps it had failed somewhere along the way - it was so quick! The M4 Pro is a beast - arguably more powerful than I need - and it breezes through workloads that my previous M1 chugged through at a more leisurely pace.<\/p>\n<p>The performance bottlenecks that were more apparent on my M1 are now free-flowing. The previous friction that I had become accustomed to is gone.<\/p>\n<p>Whilst I am extremely grateful for the upgrade, I now need to be more mindful of performance and avoiding \u2018lazy\u2019 programming.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: More processing power isn\u2019t necessarily better\">Email a comment<\/a><\/p>",
            "summary": "<p>At work we use a unified development environment in Docker. This allows us to simulate all of the interconnected services and workloads of our production Kubernetes clusters, which is pretty awesome.<\/p>\n<p>Unfortunately, to-date the virtualisation on Apple\u2019s M-series chips hasn\u2019t been amazing, causing some performance bottlenecks. This is often most felt on cold-starts of the virtual environment, seeding and running tests, but can also be seen during certain requests to the backend and longer-running jobs. To be clear, the performance of our production environments is not a problem - the local Docker environment simply magnifies where there might be issues to the point we can experience a slower request first-hand.<\/p>\n<p>Personally I have always embraced this friction - it is a constant reminder to program for efficient processing and not to rely on raw power and auto-scaling to make up for inefficiency.<\/p>\n<p>A couple of weeks ago I was upgraded from my original M1 MacBook Pro (brand new out of the box) that I was given when I first started at the company, to a MacBook Pro with an M4 Pro chip.<\/p>\n<p>After the first cold start of our Docker environment I thought that perhaps it had failed somewhere along the way - it was so quick! The M4 Pro is a beast - arguably more powerful than I need - and it breezes through workloads that my previous M1 chugged through at a more leisurely pace.<\/p>\n<p>The performance bottlenecks that were more apparent on my M1 are now free-flowing. The previous friction that I had become accustomed to is gone.<\/p>\n<p>Whilst I am extremely grateful for the upgrade, I now need to be more mindful of performance and avoiding \u2018lazy\u2019 programming.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: More processing power isn\u2019t necessarily better\">Email a comment<\/a><\/p>",
            "date_published": "2025-02-03T00:00:00+10:00",
            "date_modified": "2025-02-03T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        },
        {
            "id": "https://philstephens.com/93",
            "title": "Write Simply",
            "url": "https://philstephens.com/blog/write-simply",
            "content_html": "<p>I\u2019ll admit I\u2019ve been struggling to build my writing habit over the last few weeks. I\u2019ve realised that the issue has generally been three-fold:<\/p>\n<ul>\n<li>Finding something interesting - <em>for others<\/em> - to write about in the first place;<\/li>\n<li>I might get a glimpse of an idea, an unformed thought. I\u2019ll add it to my <code>Drafts<\/code> folder without a great deal of context and invariably wonder where I was going with it (or that the idea is so vast or vague that it is hard to know where to start or how to structure it);<\/li>\n<li>I\u2019m focussing too much on making my writing \u2018publish-worthy\u2019 when, to be honest, I\u2019m not much of a writer.<\/li>\n<\/ul>\n<p>I\u2019ve put a lot of effort into ensuring that the writing and publishing flow for my new site is <em>technically<\/em> as frictionless as possible - however it has <em>mentally<\/em> become a chore. What to do?<\/p>\n<p>I\u2019ve realised that at the end of the day I\u2019m writing for me, not an imagined audience. I don\u2019t want to force it - I\u2019ve decided to just write and worry less about editing. If I get a big idea, I need to break it down into smaller chunks to make it more approachable. If I get a fragment of an idea, I just need to put it out there - there is nothing that stops me from coming back and building on it later.<\/p>\n<p>I also want to be open to more opportunities for short form writing such as daily \/ weekly journaling.<\/p>\n<p>By giving myself permission to be less-polished in my writing - <strong>writing simply<\/strong> - I\u2019m hoping the perceived roadblocks will start to drop away and the whole process of writing will become easier.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Write Simply\">Email a comment<\/a><\/p>",
            "summary": "<p>I\u2019ll admit I\u2019ve been struggling to build my writing habit over the last few weeks. I\u2019ve realised that the issue has generally been three-fold:<\/p>\n<ul>\n<li>Finding something interesting - <em>for others<\/em> - to write about in the first place;<\/li>\n<li>I might get a glimpse of an idea, an unformed thought. I\u2019ll add it to my <code>Drafts<\/code> folder without a great deal of context and invariably wonder where I was going with it (or that the idea is so vast or vague that it is hard to know where to start or how to structure it);<\/li>\n<li>I\u2019m focussing too much on making my writing \u2018publish-worthy\u2019 when, to be honest, I\u2019m not much of a writer.<\/li>\n<\/ul>\n<p>I\u2019ve put a lot of effort into ensuring that the writing and publishing flow for my new site is <em>technically<\/em> as frictionless as possible - however it has <em>mentally<\/em> become a chore. What to do?<\/p>\n<p>I\u2019ve realised that at the end of the day I\u2019m writing for me, not an imagined audience. I don\u2019t want to force it - I\u2019ve decided to just write and worry less about editing. If I get a big idea, I need to break it down into smaller chunks to make it more approachable. If I get a fragment of an idea, I just need to put it out there - there is nothing that stops me from coming back and building on it later.<\/p>\n<p>I also want to be open to more opportunities for short form writing such as daily \/ weekly journaling.<\/p>\n<p>By giving myself permission to be less-polished in my writing - <strong>writing simply<\/strong> - I\u2019m hoping the perceived roadblocks will start to drop away and the whole process of writing will become easier.<\/p>\n<p><a href=\"mailto:hello@philstephens.com?subject=Comment: Write Simply\">Email a comment<\/a><\/p>",
            "date_published": "2025-02-02T00:00:00+10:00",
            "date_modified": "2025-02-02T00:00:00+10:00",
            "authors": [{ "name": "Phil Stephens" }],
            "tags": [  ]
        }        
    ]
}
