liammills wrote a comment+100 XP
1mo ago
For those setting up the API while having PHP 8.5+ installed on your system, you will likely get deprecation warnings relating to the Laravel vendor package for the database file (and these deprecation warnings will be returned alongside the API data). For the purposes of this course I just disabled deprecation warnings altogether by writing the below code inside of my /boostrap/app.php file before the return statement.
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
Obviously never do this for a real environment, but for just quickly setting up the API for this course it's handy.
liammills liked a comment+100 XP
1mo ago
** Warning **
Do not just use ->markdown() on its own this is prome to XSS (Cross-Site-Scripting) if you was to put <img src="#" onmouseover="alert('hacked');" /> in your idea description or worse a user was, when they hover over the image, an alert will show. Instead use:
`return Attribute::get(
fn ($value, $attributes) => new HtmlString(str($attributes['description'])->markdown([
'html_input' => 'escape',
'allow_unsafe_links' => false,
'max_nesting_level' => 5,
])));`
liammills wrote a comment+100 XP
1mo ago
When using step.completed ? '1' : '0' for the x-model for steps Alpine was throwing a Invalid left-hand side in assignment error (although the code still worked on submission). Wanting to remove this error I instead set x-model to just be step.completed.
This then meant that when the form was submitted the type of completed was defaulting to a string boolean "true" or "false" and therefore wasn't passing validation. The only appropriate work around I've found was to use prepareForValidation within my IdeaRequest class and rebuild the steps array. Posting this here in case someone else encounters the same issue.
class IdeaRequest extends FormRequest
{
public function authorize(): bool { ... }
public function rules(): array { ... }
protected function prepareForValidation(): void
{
if ($this->has('steps')) {
$steps = [];
foreach ($this->steps as $index => $step) {
$steps[$index]['description'] = $step['description'];
$steps[$index]['completed'] = filter_var($step['completed'], FILTER_VALIDATE_BOOLEAN);
}
$this->merge([
'steps' => $steps
]);
}
}
}
liammills wrote a comment+100 XP
2mos ago
A little nicety with Tailwind is being able to style an element based on its previous sibling using the peer class. This lets us style the text with a strike-through without relying on additional PHP logic, instead just using the aria-checked value of the button as the source of truth.
<button
@class([
'btn btn-square btn-sm peer',
'btn-success' => $step->completed
])
type="submit"
role="checkbox"
aria-checked="{{ $step->completed ? 'true' : 'false' }}"
>
@if ($step->completed)
<x-icons.check />
@endif
</button>
<p class="peer-aria-checked:line-through peer-aria-checked:text-neutral-content">
{{ $step->description }}
</p>
liammills wrote a comment+100 XP
2mos ago
@rudysacostacrousset You can also wrap this inside of a try/catch block to flash an error response if needed.
public function store(StoreIdeaRequest $request): RedirectResponse
{
try {
DB::transaction(function () use ($request): void
{
$idea = Auth::user()->ideas()->create($request->safe()->except('steps'));
$idea->steps()->createMany(
collect($request->steps)->map(fn ($step) => ['description' => $step])
);
});
return to_route('idea.index')->with('success', 'Idea created!');
} catch (\Exception $e) {
// Show the error message.
return to_route('idea.index')->with('error', $e->getMessage());
// ...or show a more generic, user-friendly error message.
return to_route('idea.index')->with('error', 'Idea could not be created.');
}
}
liammills liked a comment+100 XP
2mos ago
In the controller, I modified the logic for storing an idea a little bit. I believe that in this case it is good practice to use database transactions because we depend on two queries working correctly: creating the Idea and creating each of its steps.
If one of them fails, the whole operation should be rolled back to keep the database consistent.
public function store(StoreIdeaRequest $request)
{
DB::transaction(function() use ($request): void {
$idea = Auth::user()->ideas()->create($request->safe()->except('steps'));
$steps = collect($request->safe()->input('steps', []))
->map(fn($step) => ['description' => $step])
->all();
if ($steps !== []) {
$idea->steps()->createMany($steps);
}
});
return to_route('idea.index')
->with('success', 'Idea created!');
}
liammills liked a comment+100 XP
2mos ago
For verifying that the request status exists in the IdeaStatus enum, PHP has a nice built in method that Claude Code showed me:
$status = IdeaStatus::tryFrom($request->status ?? '');
It will return the value if it exists or null otherwise.
liammills wrote a comment+100 XP
2mos ago
For those who are bothered by having the "all" filter button appear unselected when the status value in the query string is not within the allowed set of enums, you may find the below code useful to include in your IdeaController.
Thanks to @jacobodonnell in this thread with the tryForm suggestion, which the below code builds upon.
$status = $request->status;
// This returns null if $status is not within the allowed enums.
$status = Status::tryFrom($status ?? '');
// If the `status` query param is invalid or missing (set as null in the line above), then "all" is used.
$current = $status?->value ?? 'all';
$ideas = Auth::user()
->ideas()
->when($status, fn ($query, $status) => $query->where('status', $status))
->get();
return view('idea.index', [
'ideas' => $ideas,
'statuses' => Status::cases(),
'status_counts' => Idea::statusCounts($user),
'current' => $current,
]);
Passing current into your blade view, you can then conditionally style your filter button as needed, for example:
<div>
<a
href="{{ route('idea.index') }}"
@class([
'button',
'active' => 'all' === $current,
])
>
All
</a>
@foreach ($statuses as $status)
<a
href="{{ route('idea.index', ['status' => $status->value]) }}"
@class([
'button',
'active' => $status->value === $current,
])
>
{{ $status->label() }}
</a>
@endforeach
</div>