Somewhere in the first few weeks of building, I started asking a question that changed how I learn:
What would Taylor Otwell do?
Taylor Otwell is the creator of Laravel. Asking what he’d do forces the AI to stop giving me the “technically correct” answer and start giving me the Laravel answer — the idiomatic, convention-following, YAGNI-respecting answer that works with the framework instead of against it.
I call it WWTD — like WWJD (“What Would Jesus Do?”), the 90s bracelet phenomenon, but for the church of Laravel. Pronounced “dub-dub-td” if you’re feeling brave.
I’ve now used it in 49 separate coding sessions. What started as a casual question became a structured system that runs automatically on every Laravel decision I make. Here’s how it evolved, and seven real examples of what it looks like in practice.
Table of contents
Open Table of contents
- How WWTD evolved
- Seven real examples
- 1. Keep relationship names plural
- 2. Prefer explicit query constraints over global scopes
- 3. Use
e()to escape output, notstrip_tags()for security - 4. Use Flux components as they’re designed
- 5. Use
Str::random()— don’t reinvent the framework - 6. Validate with
#[Validate]attributes in Livewire - 7. YAGNI — just remove it
- Why WWTD works
- WWTD as a Laravel Skill
How WWTD evolved
It started simple. I’d be mid-conversation with Claude about a code decision and just ask:
what would taylor otwell do?
Then I started combining it with other checks:
what is the laravel convention? what would taylor otwell do?
is this really needed? does laravel not project protections out of the box for xss like this? what are best practises? what would taylor otwell do?
Before long, WWTD became the final step in a structured investigation template I use for any non-trivial Laravel question:
1. Explain in detail but using simple terms for a non-developer
2. Investigate 3 possible solutions
3. Pick your suggested solution and explain why
4. Check official Laravel documentation
5. Check the laravel/framework on GitHub
6. Check for perspectives from well-known Laravel community members
7. Answer my favourite question — what would Taylor Otwell do?
Eventually I found myself typing it so often that I told Claude to add it to its memory:
When I ask a Laravel or app-related question, please could you check “What would Taylor Otwell do?” and explain your answer in the response.
Now it runs every Laravel question through that lens automatically. I don’t even have to ask anymore.
Seven real examples
1. Keep relationship names plural
What I asked:
what would taylor otwell do?
The situation: My Idea model had two methods — star() and stars() — that both returned the exact same morphMany relationship. The singular star() was used in 3 files, the plural stars() was used elsewhere. I was going through code quality issues and asked whether to keep one or the other.
What Taylor would do: Keep stars() — the plural form — because Laravel convention for hasMany and morphMany relationships is to use plural names. Then update the 3 call sites that used star().
The fix:
// Before: two methods returning the same thing
public function stars(): MorphMany
{
return $this->morphMany(Star::class, 'starable');
}
public function star()
{
return $this->morphMany(Star::class, 'starable');
}
// After: one method, following the convention
public function stars(): MorphMany
{
return $this->morphMany(Star::class, 'starable');
}
All call sites were updated from $idea->star()->where(...) to $idea->stars()->where(...).
The takeaway: Naming conventions aren’t just style. When every morphMany relationship is plural, anyone reading the code immediately knows what it returns without checking the method body. The duplicate existed because two people wrote the same thing at different times — nobody cleaned it up.
2. Prefer explicit query constraints over global scopes
What I asked:
what is the laravel convention? what would taylor otwell do?
The situation: During a performance audit, Claude flagged that StrategyModel::with('answers') was loading answers for every team and filtering in PHP. I asked it to explain more — and it turned out to be a false alarm. The TeamStrategy model already had a global scope filtering by current_team_id, so the eager load was already constrained.
But the question of how it was constrained was still interesting.
What Taylor would do: Prefer explicit query constraints over global scopes:
// The global scope approach (already in the codebase)
// Works, but applies invisibly everywhere
protected static function boot()
{
parent::boot();
static::addGlobalScope('team', function (Builder $builder) {
if (auth()->user()) {
$builder->where('team_id', auth()->user()->current_team_id);
}
});
}
// What Taylor would prefer — explicit and visible
StrategyModel::with(['answers' => fn($q) => $q->where('team_id', auth()->user()->current_team_id)])
->orderBy('order')
->get();
The takeaway: Global scopes are convenient but they apply invisibly to every query on the model. That means anyone reading StrategyModel::with('answers') has no idea the answers are being filtered unless they also know about the global scope. Explicit constraints make the intent visible at the call site. In this case the code worked fine, so nothing was changed — but I understood why the explicit version is generally preferred.
3. Use e() to escape output, not strip_tags() for security
What I asked:
is this really needed? does laravel not project protections out of the box for xss like this? what are best practises? what would taylor otwell do?
The situation: I received a security audit flagging two XSS vulnerabilities — one in search result highlighting, one in user comments. In both cases, my Blade templates used {!! !!} (raw HTML output) to render formatted content. Claude immediately tried to fix them, but I pushed back — doesn’t Laravel handle this automatically?
What Taylor would do: Laravel does protect against XSS out of the box — {{ }} in Blade auto-escapes everything via htmlspecialchars(). But {!! !!} is Blade’s deliberate escape hatch that says “render this raw.” When you use it, you take responsibility for sanitising the inputs yourself.
The idiomatic pattern is to escape first with e(), then construct your controlled HTML:
// Before: user input goes directly into raw HTML
$model->highlightedName = str_ireplace(
$this->search,
"<strong>{$this->search}</strong>",
$model->name
);
// After: escape inputs, then construct safe HTML
$escapedName = e($model->name);
$escapedSearch = e($this->search);
$model->highlightedName = str_ireplace(
$escapedSearch,
"<strong>{$escapedSearch}</strong>",
$escapedName
);
Taylor wouldn’t rely on strip_tags() for security — it’s a well-known PHP footgun that strips tags but leaves attributes on allowed tags intact (meaning <a onclick="alert('XSS')"> slips right through).
The takeaway: Laravel’s XSS protection is excellent — as long as you don’t opt out of it. When you need raw HTML (for highlights, mentions, markdown), the pattern is always the same: escape everything with e() first, then build your controlled HTML, then use {!! !!} on the safe result.
4. Use Flux components as they’re designed
What I asked:
hang on - which method would you recommend and why? which feels more fluxui / laravel native? what would taylor otwell do and why?
The situation: I needed to add a “Share” action to a reports page dropdown menu. Claude had built a solution using a <livewire:share> component with a hidden prop and $dispatchTo events to bridge the dropdown to the share modal. But the ideas page in my app already did this differently — the dropdown triggered a <flux:modal> directly at the page level.
I asked which approach was better and why.
What Taylor would do: Use the Flux components as they’re designed. <flux:dropdown> triggers <flux:modal> — that’s the pattern. The <livewire:share> component was built before Flux was adopted, and it showed. Making it work inside a Flux menu required workarounds (hidden props, cross-component event wiring) that fought the framework rather than working with it.
<!-- Approach A (Taylor's way): Flux dropdown triggers Flux modal directly -->
<flux:menu.item icon="share"
x-on:click="$wire.selectItem({{ $report->id }})
.then(() => $flux.modal('share-modal').show())">
Share
</flux:menu.item>
<!-- Approach B (the workaround): hidden Livewire component + event dispatch -->
<livewire:share type="report" :id="$report->id" hidden />
<flux:menu.item icon="share"
x-on:click="$dispatchTo('share', 'openShare')">
Share
</flux:menu.item>
The takeaway: When you adopt a component library, use it the way it’s designed. Hidden props, event bridges, and cross-component wiring are signs you’re fighting the framework. A small amount of code duplication is worth it if the alternative is a fragile workaround. The minor duplication of a generateShareLink() method was far less costly than the complexity of making a pre-Flux component work inside a Flux context.
5. Use Str::random() — don’t reinvent the framework
What I asked:
what would taylor otwell do and why?
The situation: The security audit flagged weak token generation. The app had a custom getRandomString() method on a UtilityServiceProvider that used mt_rand() (not cryptographically secure). This method was duplicated across 6 files — some called the provider, others had their own private copy.
What Taylor would do: Use Str::random() and delete everything else. Two reasons:
- Don’t reinvent what the framework gives you. Laravel already has a purpose-built, tested, cryptographically secure random string generator. Writing your own is code you have to maintain for no benefit.
- A ServiceProvider is not a utility class. Putting a static helper method on a ServiceProvider goes against what ServiceProviders are for. They exist to register bindings and bootstrap services. The fact that
register()andboot()were both empty told you the class was being misused.
// Before: 8 lines, duplicated 3 times, insecure
$hash = UtilityServiceProvider::getRandomString(30);
// After: 1 line, zero duplication, secure
$hash = Str::random(30);
The takeaway: Before writing a helper method, check if Laravel already has one. Str::random(), Str::uuid(), Str::slug() — the Str and Arr classes are full of utilities that do exactly what your custom code does, but tested and maintained by the framework. This is Laravel’s whole philosophy: don’t reinvent what the framework gives you.
6. Validate with #[Validate] attributes in Livewire
What I asked:
what are laravel best practises here? what do the official docs suggest and what would taylor do?
The situation: A Livewire form component for email subscriptions had no server-side validation — it relied entirely on HTML type="email" and required attributes, which anyone can bypass with browser dev tools. The component also had a hardcoded team_id = 3 fallback and a redundant database query.
What Taylor would do: Use Livewire’s #[Validate] attribute on the property and call $this->validate() at the start of the action method:
// Before: no server-side validation
public $email;
public function store()
{
// goes straight to database work...
}
// After: idiomatic Livewire validation
use Livewire\Attributes\Validate;
#[Validate('required|email')]
public $email;
public function store()
{
$this->validate();
// only reaches here if validation passed
}
Livewire offers three ways to define validation rules — #[Validate] attributes, a rules() method, and form objects. For a simple case like one email field, the attribute is the right choice. A rules() method or form object would be over-engineering it.
The takeaway: Browser-side validation (HTML attributes, JavaScript) is for UX — it gives users instant feedback. Server-side validation is for security — it’s the line that actually stops bad data. You always need both. The #[Validate] attribute makes adding server-side rules so easy there’s no excuse to skip it.
7. YAGNI — just remove it
What I asked:
integrated metrics aren’t actually used right now, and likely wont be for another month or so at which point we’ll do some refactoring. can we just comment out the update-metrics-from-api event for now? seems like a cleaner/easier fix? what would taylor otwell do?
The situation: When moving a campaign to the “Analysing” stage, there was a brief flash of an “Updating metrics…” overlay. The click handler was dispatching an update-metrics-from-api event, which triggered a server round-trip to fetch integration metrics — even though integration metrics weren’t being used yet. Claude proposed a complex fix: a new updateMetricsIfNeeded() method with conditional logic to only dispatch the event when integration metrics existed.
I pushed back. The feature wasn’t in use and wouldn’t be for a month.
What Taylor would do: The simplest thing that works. YAGNI — You Aren’t Gonna Need It.
<!-- Before: dispatches an event that does nothing except cause a visual glitch -->
<x-buttons.link x-on:click="
$wire.set('stage', '{{ $this->nextStage() }}');
$wire.$parent.set('stage', '{{ $this->nextStage() }}');
$dispatch('update-metrics-from-api');">
Move to {{ $this->nextStage() }}
</x-buttons.link>
<!-- After: just remove the dispatch -->
<x-buttons.link x-on:click="
$wire.set('stage', '{{ $this->nextStage() }}');
$wire.$parent.set('stage', '{{ $this->nextStage() }}');">
Move to {{ $this->nextStage() }}
</x-buttons.link>
One line removed. No new methods, no conditional logic, no over-engineering. When integration metrics are ready, you add the dispatch back.
The takeaway: When an AI suggests a complex solution to a simple problem, push back. The best code change is often the smallest one. Don’t build infrastructure for features that don’t exist yet — you’ll refactor anyway when the requirements become clear.
Why WWTD works
Looking back at these seven examples, the prompt consistently does three things:
-
It forces convention over cleverness. Laravel has opinions about how things should be done — plural relationship names,
Str::random()over custom helpers,#[Validate]over manual checking. WWTD surfaces those conventions instead of letting the AI give you a technically correct but non-idiomatic answer. -
It favours simplicity. Taylor’s philosophy is that the framework should do the heavy lifting so your code stays simple. Every time I asked the question, the answer was simpler than what was already in the codebase — fewer lines, less duplication, less custom code.
-
It teaches you why, not just what. Knowing that
Str::random()is better thanmt_rand()is useful. Understanding that a ServiceProvider isn’t a utility class — that’s the kind of knowledge that changes how you think about code structure.
WWTD as a Laravel Skill
I’ve packaged WWTD as a Laravel Boost skill that you can drop into any project. Save the following as .ai/skills/wwtd/SKILL.md in your Laravel project:
---
name: wwtd
description: >-
Apply "What Would Taylor Otwell Do?" (WWTD) as a decision-making lens
for Laravel architecture, conventions, and code quality questions.
Activate when the user asks about best practices, naming conventions,
code structure, or when choosing between multiple approaches.
---
# WWTD — What Would Taylor Otwell Do?
## When to activate
Use this skill when the user:
- Asks about Laravel conventions or best practices
- Is choosing between multiple implementation approaches
- Wants to know the idiomatic Laravel way to do something
- Asks "what would Taylor do?" or "WWTD"
## Core principles
When answering through the WWTD lens, prioritise these values
(derived from Taylor Otwell's philosophy and Laravel's design):
1. **Convention over configuration.** Laravel has opinions. Follow them.
Plural relationship names, `snake_case` columns, resourceful controllers.
2. **Use what the framework gives you.** Before writing a helper, check if
Laravel already has one (`Str::random()`, `e()`, `data_get()`, etc).
3. **Simplicity wins.** The best solution is usually the one with the fewest
lines, the least abstraction, and no premature optimisation.
4. **YAGNI.** Don't build for hypothetical future requirements. Remove unused
code rather than commenting it out.
5. **Explicit over magic.** Prefer explicit query constraints over global
scopes. Prefer `$fillable` over `$guarded = []`. Make intent visible at
the call site.
6. **Work with the framework, not against it.** If a solution requires
workarounds, hidden props, or event bridges — you're fighting Laravel.
Step back and use the components as designed.
## Response format
When this skill is activated, structure your response as:
1. **The situation** — what the user is trying to do
2. **What Taylor would do** — the idiomatic Laravel answer, with reasoning
3. **The fix** — concrete before/after code if applicable
4. **The takeaway** — what principle this teaches about Laravel
## Anti-patterns to flag
- Custom utility classes that duplicate framework helpers
- `$guarded = []` on models (use explicit `$fillable` instead)
- Global scopes where explicit constraints would be clearer
- `strip_tags()` for security (use `e()` for escaping)
- ServiceProviders used as utility classes
- Complex conditional logic for features that don't exist yet
- Hidden Livewire components bridging UI frameworks
## Investigation template
For non-trivial questions, follow this structured approach:
1. Explain the issue in simple terms
2. Investigate 2–3 possible solutions
3. Pick a recommendation with clear pros and cons
4. Check the official Laravel documentation
5. What would Taylor Otwell do — and why?
If your project uses Laravel Boost, it will be picked up automatically when you run boost:update. It also works with Claude Code (add it to .claude/skills/wwtd/SKILL.md), Cursor (.cursor/rules/), or any AI agent that reads markdown instruction files.
You don’t need to know everything Taylor Otwell knows. You just need to keep asking what he’d do.
The prompt works because Laravel is an opinionated framework built by someone with strong, consistent opinions. When you ask what the creator would do, you’re really asking: what does this framework want me to do? And that’s the fastest way to stop fighting it and start learning it.