A curated index of coding tips posted by @PovilasKorop (Laravel Daily) on X, with the code from each tip's screenshot transcribed into copy-pasteable text.
- Tips collected: 83
- Date range: 2026-03-20 → 2026-06-30
- Categories (tips can span more than one): Laravel (69) · Eloquent (9) · Filament (13) · Livewire (3) · PHP (6) · Testing (4)
- Source: pulled from his X timeline via the X API v2 (
/2/users/:id/tweets); code transcribed from the attached images.
⚠️ Coverage note: This covers ~3.5 months (2026-03-20 → 2026-06-30), not the full 6 months requested. The X user-timeline API caps at a user's ~3,200 most recent posts; because he posts/replies very frequently, that window only reaches back to mid-March 2026. Reaching the full 6 months would require Pro-tier full-archive search (/2/tweets/search/all), which is not enabled on this API plan.Code is transcribed from screenshots (OCR-assisted); spot-check against the linked tweet for anything you copy into production.
- 1. Extract URL parts with parse_url()
- 2. Typed route constraints without regex
- 3. Whitelist dynamic SQL with backed enums
- 4. Clean up side effects in deleting event
- 5. Group orWhere inside a closure
- 6. Mass delete skips model events
- 7. Eloquent accessors returning value objects
- 8. Filament tables UX video
- 9. FilaCheck navigation-badge-not-cached rule
- 10. Share validation rules with a trait
- 11. Use a short-lived cache key for repeats
- 12. Enums with multiple helper methods
- 13. Declare @props to keep models out of attributes
- 14. Cache keys as reusable model constants
- 15. Query Builder bypasses Soft Deletes
- 16. Run raw SQL files in seeders
- 17. Video: 3 Filament performance tips
- 18. Use findOrFail to hide record existence
- 19. Course: Practical Laravel Security
- 20. LazilyRefreshDatabase over RefreshDatabase
- 21. Return JSON for all API exceptions
- 22. Video: laravel-lang attack and Composer security
- 23. Get data out of DB::transaction()
- 24. Custom relationship methods with built-in filtering
- 25. Select relationship columns with with()
- 26. Create child records via relationship
- 27. Idempotent migrations with hasTable/hasColumn
- 28. Validate route params with where/whereIn
- 29. Strip query params with fullUrlWithoutQuery()
- 30. Throw validation errors manually
- 31. Debug queries with dd() and ddRawSql()
- 32. Factory states speak business language
- 33. Auto-capitalize translation variables
- 34. Round timestamps with setSeconds/setMinutes
- 35. Obfuscate sensitive data with Str::mask()
- 36. Redirect to page section with withFragment()
- 37. Environment-specific Blade with @production and @env
- 38. Context7 MCP for third-party package docs
- 39. This Week in Laravel newsletter
- 40. Stack Rule::unique() across multiple tables
- 41. whereBelongsTo() for relationship queries
- 42. Format big numbers with Number::abbreviate()
- 43. Filament filter indicators with indicateUsing()
- 44. Conditional aggregates with whenCounted()
- 45. Return 204 with response()->noContent()
- 46. Enforce multi-tenancy beyond global scopes
- 47. Prefer classical structures over Laravel "magic"
- 48. React to specific changes with wasChanged() + getOriginal()
- 49. Preview FilaCheck fixes with --dry-run
- 50. Build dynamic Filament Tabs from config
- 51. Conditional required() accepts a boolean
- 52. Shareable filtered URLs with #[Url]
- 53. Spatie laravel-pdf supports five drivers
- 54. Disable Filament stats widget polling
- 55. Bulk actions with their own form
- 56. Use Validator inside Artisan commands
- 57. match throws where switch falls through
- 58. Pest datasets with ->with()
- 59. Nested match expressions
- 60. Name relations clearly (inviter, not user)
- 61. Dispatch Events After the Transaction
- 62. Automate "laravel new" With Flags
- 63. Wait for Three Uses Before Extracting a Trait
- 64. Move Listener Logic Out of Controllers
- 65. One Event, Multiple Listeners
- 66. Weekly Laravel Newsletter Links
- 67. Don't Extract an Action for Simple CRUD
- 68. All "laravel new" Installer Options
- 69. Pest Architecture Tests for Boundaries
- 70. Stringable DTOs for Class Strings
- 71. Typed Config Objects Instead of Arrays
- 72. Generate a Unique Slug With Suffix
- 73. Auto-Detect JS Framework From package.json
- 74. Observer isDirty() for Smart Cleanup
- 75. One Action, Many Entry Points
- 76. Environment-Aware Caching
- 77. Block Dangerous Saves With Model::booted()
- 78. Persist Filament Wizard Step in URL
- 79. Encrypt and JSON-Encode With One Cast
- 80. Prevent Destructive DB Commands in Production
- 81. Environment-Aware Password Defaults
- 82. Search All Polymorphic Types With whereHasMorph()
- 83. Fill Chart Date Gaps With CarbonPeriod
2026-06-30 · PHP · View tweet
PHP tip.
Stop using substr() and explode() to pull pieces out of a URL string.
PHP's parse_url() can hand you just the part you want.
Pass PHP_URL_PATH for the path or PHP_URL_HOST for the domain, and it deals with query strings and ports for you.
// Before:
$path = explode('?',
str_replace('https://site.com', '', $url))[0];
// ==================
// After:
$path = parse_url($url, PHP_URL_PATH); // "/blog/post"
$host = parse_url($url, PHP_URL_HOST); // "site.com"
// Same-domain check:
$same = parse_url($a, PHP_URL_HOST)
=== parse_url($b, PHP_URL_HOST);2026-06-29 · Laravel · View tweet
Laravel tip.
Typed route constraints: whereNumber(), whereUuid(), whereAlpha()
You don't need regex to check route values.
Laravel has helpers: whereNumber(), whereUuid(), whereAlpha(), whereAlphaNumeric(), whereIn().
Wrong id returns 404 BEFORE controller.
// Before:
Route::get('/email/{id}', [EmailController::class, 'show'])
->where('id', '[0-9]+');
// =============================
// After:
Route::get('/email/{id}', [EmailController::class, 'show'])
->whereNumber('id');
Route::get('/user/{uuid}', ...)->whereUuid('uuid');
Route::get('/post/{slug}', ...)->whereAlphaNumeric('slug');2026-06-27 · Laravel · View tweet
Laravel tip: Whitelist dynamic SQL with backed enums.
If users can choose an aggregate for reports, do not pass random strings into SQL.
A backed enum gives you a tiny whitelist for dynamic COUNT, SUM, AVG, MIN, and MAX.
// Before
$aggregate = request('aggregate', 'count');
$value = Order::query()
->selectRaw(strtoupper($aggregate).'(*) as value')
->value('value');
// ---------------------------
// After
enum Aggregate: string
{
case COUNT = 'count';
case SUM = 'sum';
case AVG = 'avg';
case MIN = 'min';
case MAX = 'max';
}
$aggregate = Aggregate::from(request('aggregate', 'count'));
$value = Order::query()
->selectRaw(strtoupper($aggregate->value).'(total) as value')
->value('value');2026-06-26 · Laravel · View tweet
Laravel tip.
Clean up side effects with the model deleting event.
Don't put file cleanup code in every controller.
Put it once in the MODEL using the booted() method.
Now, when you delete the row anywhere, its files are deleted too. The model cleans up after itself.
// Before:
public function destroy($id) {
$email = EmailMessage::findOrFail($id);
Storage::delete($email->attachment_path);
$email->delete();
}
// =============================
// After:
class EmailMessage extends Model
{
protected static function booted() void
{
static::deleting(function (EmailMessage $email) {
Storage::delete($email->attachment_path);
});
}
}2026-06-25 · Eloquent, Laravel · View tweet
Laravel tip.
Common Eloquent mistake: when you mix where() and orWhere(), the filter can give wrong results.
Put the OR part inside a FUNCTION.
Then it means (a OR b) AND active, which is what you want.
// Before:
Email::where('subject', 'like', "%$s%")
->orWhere('body', 'like', "%$s%")
->where('active', true)
->get();
// subject LIKE x OR body LIKE x AND active = 1
// =============================
// After:
Email::where(function ($q) use ($s) {
$q->where('subject', 'like', "%$s%")
->orWhere('body', 'like', "%$s%");
})
->where('active', true)
->get();
// (subject OR body) AND active = 12026-06-23 · Eloquent, Laravel · View tweet
Laravel/Eloquent tip. Mass delete skips model events.
Model::query()->delete() does NOT fire deleting/deleted events or touch observers.
If your model cleans up files or relations on delete, mass delete silently skips it.
Loop with chunkById() instead.
// Before:
EmailMessage::query()->delete();
// deleting event never fires -> files orphaned
// =============================
// After:
EmailMessage::query()->chunkById(200, function ($emails) {
foreach ($emails as $email) {
$email->delete(); // fires the deleting hook
}
});2026-06-15 · Eloquent, Laravel · View tweet
Laravel tip. Did you know Eloquent accessors can return tiny value objects?
Example: Instead of repeating fallback logic like “name or email” in every Blade/view/service, wrap related columns once and call intention-revealing methods.
// Before: fallback logic leaks everywhere
{{ $email->fromName ?: $email->fromEmail }}
<img
alt="{{ $email->fromName ?: $email->fromEmail }}"
title="{{ $email->fromName ?: $email->fromEmail }}"
>
$from = $email->fromName ?: $email->fromEmail;// After: model exposes a small value object
class Email extends Model
{
public function from(): Attribute
{
return Attribute::get(
fn () => new EmailAddress($this->fromEmail, $this->fromName),
);
}
}
class EmailAddress
{
public function __construct(
public readonly ?string $email = null,
public readonly ?string $name = null,
) {}
public function getNameOrEmail(): string
{
return $this->name ?: ($this->email ?? '');
}
}
// Blade
{{ $email->from->getNameOrEmail() }}
// Service
$fromName = $email->from->getNameOrEmail();2026-06-09 · Filament · View tweet
My new video on Filament Daily channel.
Filament Tables: 2 Tips for Much Better UX https://t.co/PisFWMB8gX
[YouTube thumbnail: a Filament table with "Stage" and "Progress" columns — rows show green dot ratings and orange progress bars (100% and 80%) — next to a man pointing, with overlaid text "Make Tables More Useful".]
2026-06-09 · Filament · View tweet
New in my FilaCheck Pro!
Performance check navigation-badge-not-cached: warns if there's getNavigationBadge() without caching
https://t.co/mqo5HorFik
Based on my recent YouTube video "3 Filament Performance Tips You May Not Know" https://t.co/ODIBt1nwEi
[Screenshot: FilaCheck Pro feature page — "19 additional rules for performance optimization, security, best practices, and UX suggestions — on top of the 16 free rules." A "NEW" arrow points at the navigation-badge-not-cached rule.]
Performance Rules
- too-many-columns — Warns when tables have more than 10 columns
- large-option-list-searchable — Suggests
->searchable()for lists with 10+ options - heavy-closure-in-format-state — Detects database queries inside
formatStateUsing()closures that cause N+1 issues - stats-widget-polling-not-disabled — Warns when
StatsOverviewWidgetuses the default 5-second polling interval - navigation-badge-not-cached — Warns when
getNavigationBadge()is not cached using theCachefacade orcache()helper
Security Rules
- file-upload-missing-accepted-file-types
Best Practices Rules
- custom-theme-needed — Detects Tailwind CSS usage in Blade without a custom theme configured
- unnecessary-unique-ignore-record — Detects
->unique(ignoreRecord: true)— default in Filament v4 (auto-fixable) - string-icon-instead-of-enum — Use
Heroicon::Pencilenum instead of string icons (auto-fixable) - string-font-weight-instead-of-enum — Use
FontWeight::Boldenum instead of strings (auto-fixable) - deprecated-notification-action-namespace — Detects old notification Action namespace (auto-fixable)
- file-upload-missing-max-size — Warns when
FileUploadis missingmaxSize()
[Screenshot: GitHub release notes — "How it works" for the rule.]
Adds a new non-fixable Performance rule, navigation-badge-not-cached, that warns when a Filament resource/page declares public static function getNavigationBadge(): ?string without caching its value.
The navigation badge is rendered on every page load for every panel navigation item. When the badge value comes from a database query (e.g. static::getModel()::count()) and isn't cached, it adds an avoidable query to each request. This rule nudges developers to wrap the value in a cache call.
How it works
- Triggers on a
staticmethod namedgetNavigationBadge. - Recursively walks the method body and considers the value cached if it finds either:
- the Cache facade — any static call on
Cache(matched on the class name, soCache::remember(),Cache::flexible(),Cache::rememberForever(), etc. all count), or - the cache() helper — e.g.
cache()->remember(...).
- the Cache facade — any static call on
- If neither is present, it emits a
warningpointing at the method.
The check is method-agnostic by design: it asks "does this touch the cache at all?" rather than whitelisting a single method like remember(), so it won't need updating as new cache methods appear.
2026-06-05 · Laravel · View tweet
Laravel tip. Share repeating validation rules with a TRAIT.
Same email/name rules in registration, profile update, and admin? Put them in a trait and reuse.
// BEFORE:
// CreateUser
'email' => ['required', 'email', 'max:255',
Rule::unique(User::class)],
// UpdateProfile — almost identical, easy to drift out of sync
'email' => ['required', 'email', 'max:255',
Rule::unique(User::class)->ignore($id)],
// =====================
// AFTER:
trait ProfileValidationRules
{
protected function emailRules(?int $userId = null): array
{
return [
'required', 'email', 'max:255',
$userId === null
? Rule::unique(User::class)
: Rule::unique(User::class)->ignore($userId),
];
}
}
// anywhere
'email' => $this->emailRules($user?->id),2026-06-04 · Laravel · View tweet
Laravel tip. Some DB operations happen REPEATEDLY within short time.
Example: tracking QR scans / page views? Someone can scan/visit same page within minutes.
Don't query the DB to check "did this IP already count?".
Drop a short-lived cache key. It's fast and self-expiring.
// =======
// BEFORE:
// =======
$alreadyScanned = QrScan::where('property_id', $property->id)
->where('ip_address', $request->ip())
->where('scanned_at', '>=', now()->subMinutes(30))
->exists();
if (! $alreadyScanned) {
QrScan::create([...]);
}
// ======
// AFTER:
// ======
$key = "qr-scan:{$property->id}:{$request->ip()}";
if (Cache::has($key)) {
return;
}
Cache::put($key, true, now()->addMinutes(30));
QrScan::create([...]);2026-06-03 · Laravel, PHP · View tweet
PHP/Laravel tips about Enums.
An Enum isn't just a list of values. it's a class.
Pack it with MULTIPLE helper methods: label(), flag(), icon(), color()...
So ALL the logic lives in one place instead of scattered match() blocks across your Blade files.
// WITHOUT Enums: Plain string column — logic leaks everywhere
@if ($property->default_language === 'de') 🇩🇪 German
@elseif ($property->default_language === 'fr') 🇫🇷 French
@endif
```
// WITH Enum: cast it 'default_language' => Language::class
enum Language: string
{
case German = 'de';
case French = 'fr';
public function label(): string
{
return match ($this) {
self::German => 'German',
self::French => 'French',
};
}
public function flag(): string
{
return match ($this) {
self::German => '🇩🇪',
self::French => '🇫🇷',
};
}
public function nativeName(): string { /* ... */ }
public function isRtl(): bool { /* ... */ }
}
// ===============================
// Blade: clean, and every helper just works
{{ $property->default_language->flag() }}
{{ $property->default_language->label() }}2026-06-02 · Laravel · View tweet
Laravel tip.
Why @props in Blade components matters.
Forget to declare a passed-in model, and {{ $attributes->merge() }} happily serializes the ENTIRE model into your HTML.
Bloated HTML size + data leak.
@props pulls it out of $attributes.
// The model gets bound as an attribute
<x-forms.input :model="$myModel" class="font-bold" />
// resources/views/components/forms/input.blade.php
// BEFORE: NO PROPS
<div {{ $attributes->merge(['class' => 'rounded border']) }}>
<input type="text" />
</div>
// RESULT HTML:
<div class="rounded border font-bold"
model="{"id":22,"client_id":19,"provider_id":14,
"updated_at":"2026-05-22T06:43:45.000000Z",
"created_by_user_id":160}">
<input type="text" />
</div>
// resources/views/components/forms/input.blade.php
// AFTER: WITH PROPS
@props(['model'])
<div {{ $attributes->merge(['class' => 'rounded border']) }}>
<input type="text" />
</div>
// CLEAN HTML:
<div class="rounded border font-bold">
<input type="text" />
</div>2026-06-01 · Laravel · View tweet
Laravel tip. Cache key names as REUSABLE constants.
Don't put cache key names as raw strings.
If someone MISTYPES 'products.count' somewhere, cache invalidation breaks.
And you won't get an error. Just stale data forever.
Make it a constant on the model. Use it everywhere.
// app/Models/Product.php
class Product extends Model
{
public const NAV_BADGE_CACHE_KEY = 'products.count';
public static function cachedCount(): int
{
return Cache::remember(
self::NAV_BADGE_CACHE_KEY,
now()->addSeconds(60),
fn (): int => self::count(),
);
}
}// app/Observers/ProductObserver.php
class ProductObserver
{
public function created(Product $product): void
{
Cache::forget(Product::NAV_BADGE_CACHE_KEY);
}
public function deleted(Product $product): void
{
Cache::forget(Product::NAV_BADGE_CACHE_KEY);
}
public function restored(Product $product): void
{
Cache::forget(Product::NAV_BADGE_CACHE_KEY);
}
public function forceDeleted(Product $product): void
{
Cache::forget(Product::NAV_BADGE_CACHE_KEY);
}
}2026-05-29 · Eloquent, Laravel · View tweet
Laravel tip: reminder to be cautious about Soft Deletes.
Laravel Query Builder bypasses SoftDeletes trait entirely and returns ALL records.
Use Eloquent models instead, or manually filter.
The withTrashed() method helps when you need deleted records included.
// WRONG: Query Builder ignores soft deletes
$posts = DB::table('posts')->select('title', 'content')->get();
// RIGHT: Eloquent respects soft deletes by default
$posts = Post::select('title', 'content')->get();
// INCLUDE TRASHED: Retrieve soft-deleted posts too
$posts = Post::select('title', 'content')->withTrashed()->get();2026-05-28 · Laravel · View tweet
Laravel tip.
You have SQL export from the DB, and want to turn into Laravel seeders?
Instead of manually converting SQL insert statements into Laravel seeders, you can execute raw SQL files directly using DB::unprepared() in your seeder class.
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class ProductSeeder extends Seeder
{
public function run(): void
{
$sql = file_get_contents(
database_path('seeders/sql/products.sql'));
DB::unprepared($sql);
}
}2026-05-27 · Filament · View tweet
My new video on Filament Daily channel.
3 @filamentphp Performance Tips You May Not Know https://t.co/ODIBt1nwEi
[Video thumbnail: Filament sidebar "Catalog" group showing "Products" with a badge "100000" and "Categories", overlaid with the text "How To Make It FASTER?" next to a photo of the presenter]
2026-05-27 · Laravel · View tweet
Laravel tip.
Combine whereBelongsTo() + findOrFail() to hide EXISTENCE of the record.
It may be considered a security measure: returning 403 leaks "this resource exists, you just can't see it."
With 404, attackers can't distinguish "doesn't exist" from "not yours."
// BEFORE:
$habit = Habit::findOrFail($id);
abort_unless($habit->user_id === $request->user()->id, 403);
// AFTER:
$habit = Habit::query()
->whereBelongsTo($request->user())
->findOrFail($id); // → 404 for both "missing" and "not yours"2026-05-26 · Laravel · View tweet
I talk more on such security tips/preventions in my newest course on Laravel Daily.
Practical Laravel Security: Packages, Secrets, Supply-Chain Attacks https://t.co/A3ie8mObkF
7 lessons, 43 min text-based course, so you can skim quickly and stop on what's relevant for you
[Screenshot: "Course Curriculum" page listing the lessons]
Course Curriculum
1 Intro: The New Reality
New "Level" of Laravel Security: laravel-lang Attack Example
2 Practical Checklist: How to Protect
Best Composer Habits for Safety
How to Secure Secret Keys and Passwords
Secure Local Development: Protect Your Machine
3 Incident Response: What To Do When Attacked
How To Know If You're Affected
Emergency Response Checklist: What To Do?
What Secrets Need Rotation (and in what order)
2026-05-26 · Laravel, Testing · View tweet
Laravel tip for Pest testing.
LazilyRefreshDatabase is almost always better than RefreshDatabase.
RefreshDatabase runs migrations for EVERY test, even those that don't touch the DB.
The lazy version only kicks in the first time the connection is used.
// tests/Pest.php - BEFORE:
pest()->extend(TestCase::class)
->use(RefreshDatabase::class)
->in('Feature');
// ================================
// tests/Pest.php - AFTER:
pest()->extend(TestCase::class)
->use(LazilyRefreshDatabase::class)
->in('Feature');2026-05-25 · Laravel, PHP · View tweet
Laravel tip. Make your API return JSON for ALL exceptions.
By default Laravel renders HTML error pages even for API routes if the client forgot the Accept: application/json header.
Fix it once in bootstrap/app.php.
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request, Throwable $e): bool =>
$request->is('api/*') || $request->expectsJson()
);
})2026-05-25 · Laravel · View tweet
My new video on Laravel Daily channel.
IMPORTANT: "laravel-lang" Attack and Composer Security Tips https://t.co/gcpoMbiQiU
[Video thumbnail: terminal showing "> composer update" with a red stop-hand sign and the text "New Security Reality" for Laravel Devs next to a photo of the presenter]
2026-05-22 · Laravel · View tweet
Laravel tip.
Need to get data out of a DB::transaction()?
Try returning directly from the closure, or pass a variable by reference with the ampersand operator.
// OPTION 1: Return value from transaction
$post = DB::transaction(function () use ($postData) {
$post = Post::create($postData);
$post->tags()->sync($postData['tags']);
return $post;
});
// OPTION 2: Modify variable passed by reference
$post = null;
DB::transaction(function () use ($postData, &$post) {
$post = Post::create($postData);
$post->tags()->sync($postData['tags']);
});2026-05-21 · Laravel · View tweet
Laravel tip.
Instead of repeating complex Laravel query logic in your Controllers, define custom relationship methods in MODELS, with built-in filtering and sorting.
Cleaner to read, more reusable code.
class User extends Model
{
public function orders()
{
return $this->hasMany(Order::class);
}
public function completedOrders()
{
return $this->hasMany(Order::class) // or ->orders()
->where('status', 'completed')
->orderByDesc('completed_at');
}
}
// BEFORE: query logic scattered in controller
$user = User::find($id);
$orders = $user->orders()
->where('status', 'completed')
->orderByDesc('completed_at')
->get();
// AFTER: dedicated relationship method
$user = User::find($id);
$orders = $user->completedOrders()->get();2026-05-20 · Eloquent, Laravel · View tweet
Laravel tip.
Eloquent with() method accepts a COLUMN selection syntax.
It prevents unnecessary data from being fetched when loading relationships, boosting your application's performance.
// Inefficient: loads entire related table
$products = Product::with('reviews')
->limit(10)
->get();
// Better: load only required columns from reviews
$products = Product::with('reviews:id,product_id,rating,text')
->limit(10)
->get();
// This reduces database load and API response sizes2026-05-19 · Eloquent, Laravel · View tweet
Laravel tip.
Eloquent relationships like hasMany let you create child records without touching foreign keys.
Just call create() on the relationship and you're done.
Personal preference, of course, you may prefer the "typical" way of just setting the foreign key DB value.
$team = Team::find(3);
// BEFORE: manually provide the team_id
$member = Member::create([
'team_id' => $team->id,
'name' => 'John Doe',
'role' => 'Developer',
]);
// AFTER: leverage the relationship
$member = $team->members()->create([
'name' => 'John Doe',
'role' => 'Developer',
]);2026-05-18 · Laravel · View tweet
Laravel tip for DB migrations.
Schema has hasTable() and hasColumn() methods that let you safely add database structures without errors on re-runs.
Perfect for migrations that need to be idempotent and handle PARTIAL deployments gracefully.
use Illuminate\Support\Facades\Schema;
public function up(): void
{
// Example 1: Only add the column if it doesn't exist
if (! Schema::hasColumn('users', 'phone_verified_at')) {
Schema::table('users', function (Blueprint $table) {
$table->timestamp('phone_verified_at')->nullable();
$table->index('phone_verified_at');
});
}
}
public function up(): void
{
// Example 2: Only add the table if it doesn't exist
if (! Schema::hasTable('products')) {
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
}2026-05-15 · Laravel · View tweet
Laravel tip.
Some URL parameters may (should?) be validated even before they reach Controllers.
In the routes file.
Combine where() regex patterns and whereIn() values in route definitions.
// Multi-language documentation routes
Route::prefix('docs')->group(function () {
Route::get('{lang}/{page}', [DocController::class, 'show'])
->whereIn('lang', ['en', 'es', 'de', 'ja'])
->where('page', '[a-z0-9\\-]+');
});
// Username and status filtering
Route::get('profile/@{username}/posts/{status}', [ProfileController::class, 'posts'])
->where('username', '[a-zA-Z0-9_]{3,20}')
->whereIn('status', ['published', 'draft', 'archived']);2026-05-14 · Laravel · View tweet
Laravel tip.
Did you know Laravel provides fullUrlWithoutQuery() to generate URLs while excluding specific query parameters.
Perfect for pagination links, filtering, or cleaning up URL state.
// When building filter/sort links in your view
// Current URL: https://example.com/users?page=2&role=admin&verified=true
// Create a link that resets pagination but keeps other filters
$resetPageUrl = request()->fullUrlWithoutQuery('page');
// https://example.com/users?role=admin&verified=true
// Or remove multiple parameters at once
$freshUrl = request()->fullUrlWithoutQuery(['page', 'sort', 'order']);
// https://example.com/users?role=admin&verified=true2026-05-13 · Laravel · View tweet
Laravel tip.
If you want to mimic the validation error somewhere outside of Form Request, create validation error responses "manually".
Throw ValidationException::withMessages() with your custom messages.
use Illuminate\Validation\ValidationException;
public function createInvite(Request $request)
{
if (User::where('email', $request->email)->exists()) {
throw ValidationException::withMessages([
'email' => 'This user has already been invited.',
]);
}
}2026-05-12 · Eloquent, Laravel · View tweet
Laravel tip on SQL query debugging.
Did you know you can append ->dd() to any Eloquent query instead of ->get()?
Or, even better: did you know about ->ddRawSql() method?
Screenshots from Artisan Tinker.
[Screenshot: Artisan Tinker REPL session]
> User::where('id', 1)->dd();
"select * from "users" where "id" = ?" // vendor/lar…
array:1 [
0 => 1
] // vendor/laravel/framework/src/Illuminate/Databas…
> User::where('id', 1)->ddRawSql();
"select * from "users" where "id" = 1" // vendor/lar…2026-05-11 · Laravel · View tweet
Laravel tip.
Code readability is very important, especially in the age of AI.
So, with Factories, create states to make them speak Business Language.
A factory state like published() is tiny, but it makes tests read like product requirements instead of database setup.
// Before
$job = JobListing::factory()->create([
'status' => 'published',
'published_at' => now(),
'deadline' => now()->addMonth(),
]);
// ==============================
// After
class JobListingFactory extends Factory
{
public function published(): static
{
return $this->state(fn (array $attributes) => [
'status' => JobStatus::Published,
'published_at' => now(),
'deadline' => now()->addMonth(),
]);
}
}
// Test
$job = JobListing::factory()
->for($company)
->published()
->create();2026-05-08 · Laravel · View tweet
Laravel tip.
Laravel translation files support automatic capitalization using :Variable or :VARIABLE syntax in your lang folder.
Just pass your variables normally and Laravel handles the rest.
// lang/en/emails.php
return [
'greeting' => 'Hello :NAME :SURNAME,',
];
$user = ['name' => 'jane', 'surname' => 'doe'];
__('emails.greeting', $user)
// Result: "Hello JANE DOE,"2026-05-07 · Laravel · View tweet
Laravel tip.
Use Carbon's setSeconds(0) and setMinutes(0) to round timestamps down to minutes or hours.
Clean way to normalize dates without manual datetime manipulation.
// Round timestamps for database queries
$query = User::where(
'last_active',
'>=',
now()->setSeconds(0)->setMinutes(0)
)->get();
// Or in your model
class User extends Model
{
public function getHourlySessionTime()
{
return $this->created_at->setMinutes(0)->setSeconds(0);
}
}2026-05-06 · Laravel · View tweet
Laravel tip.
Str::mask() method makes it easy to obfuscate sensitive information by replacing characters with asterisks or any other character you choose.
Docs: https://t.co/xbkFiosidj
use Illuminate\Support\Str;
$string = Str::mask('taylor@example.com', '*', 3);
// tay***************
$string = Str::mask('taylor@example.com', '*', -15, 3);
// tay***@example.com2026-05-05 · Laravel · View tweet
Laravel tip.
Use withFragment() to navigate users to specific page sections when redirecting.
Perfect for jumping to form fields, tabs, or comments after form submission.
// Using withFragment() for form validation errors
if ($validator->fails()) {
return redirect()
->route('post.edit', $post)
->withFragment('comments')
->withErrors($validator);
}
// RESULTING URL (example)
// https://example.com/posts/1/edit#comments2026-05-04 · Laravel · View tweet
Laravel tip.
Control what displays in your Blade templates using @production and @env() directives.
Perfect for showing analytics, ads, or debug info only in specific environments.
@env('local', 'testing')
<div class="debug-bar">
<!-- Development tools -->
</div>
@endenv
@production
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js">
</script>
@endproduction2026-05-03 · Laravel · View tweet
Laravel tip.
Working with AI agents on Laravel projects?
Laravel Boost is great for the FIRST-PARTY package search-docs.
But the reason why you also need Context7 MCP is that it has the docs for many more package.
Like Wirechat package, in this case.
[Screenshot: AI agent terminal output showing a Context7 MCP query-docs call for the Wirechat package, returning a code example. Visible text (some lines cut off at the right edge):]
- Benchmark Score: 77.9
• Called
└ context7.query-docs({"libraryId":"/wirechat/wirechat","query":"How does namu/wirechat define
Laravel Livewire chat pages, especially for /chats and /chats/{id}, so they can be render
Defines a custom chat panel for customer support with specific authentication, attachment, th
configurations. This uses the Wirechat PanelProvider class and returns a configured Panel
```php
<?php
namespace App\Providers\WirechatPanels;
use Wirechat\Wirechat\Panel;
2026-05-02 · Filament, Laravel · View tweet
This Week in Laravel: Next.js, Filament Tips, and More https://t.co/vumKHxahEe
Tradition of my weekly Thursday newsletter, published also as a free article on the website.
[Screenshot: "From Laravel Community" section of the newsletter article. Visible text:]
From Laravel Community
I just released my first open source project - Spectacular - a functional specification tool built in Laravel : r/laravel
reddit.com
Like most side projects, this was born out of frustration. As a developer, I hated getting vague requirements scattered over Basecamp, Jira, Slack...
Scramble 0.13.21 – Laravel API documentation generator update
reddit.com
JSON:API support, toResource and toResourceCollection support, Laravel Query Builder improvements
I built scout-postgres
reddit.com
I just released scout-postgres, an open-source Laravel package I built. It is a Postgres-native Laravel Scout engine that uses PostgreSQL.
2026-05-01 · Laravel · View tweet
Laravel tip.
In validation rules, you can stack multiple Rule::unique() validators to check uniqueness across DIFFERENT tables simultaneously.
Perfect for preventing duplicate emails EVERYWHERE.
class UpdateProfileRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => [
'required',
'email:rfc,dns',
Rule::unique(Profile::class, 'email')->ignore($this->user()->id),
Rule::unique(InvitationModel::class, 'email'),
],
];
}
public function messages(): array
{
return [
'email.unique' => 'Email is already in use or has a pending invitation.',
];
}
}2026-04-30 · Eloquent, Laravel · View tweet
Laravel tip: whereBelongsTo()
Tiny readability win for those who like that style: let Eloquent express relationships instead of repeating _id columns everywhere.
Even shorter sentences if combined with Eloquent scopes.
Of course, it's a personal preference.
// Before
Order::query()
->where('user_id', $user->id)
->where('book_id', $book->id)
->where('status', 'paid')
->exists();
// After
Order::query()
->whereBelongsTo($user)
->whereBelongsTo($book)
->paid()
->exists();2026-04-29 · Filament, Laravel · View tweet
Laravel tip.
Want to show a number like "123.4K" instead of "123 400"?
There's a helper class Number, with Number::abbreviate() function.
Here's an example from one of my latest Filament projects.
More in the docs: https://t.co/F9rS2WSZTy
[Screenshot: two "Activity overview" dashboard panels compared. Top, labeled "without Number::abbreviate()": Tokens 7457147561, Token value $4660.7175, Requests 202668, Sessions 4881. Bottom, labeled "with Number::abbreviate()": Tokens 7.5B, Token value $4.7K, Requests 202.7K, Sessions 4.9K.]
// BEFORE: just original numbers
return [
'tokens' => (int) ($totals->tokens ?? 0),
'token_value' => '$'.(float) ($totals->token_value ?? 0),
'requests' => (int) ($totals->requests ?? 0),
'sessions' => (int) ($totals->sessions ?? 0),
];
// ================================================
// AFTER: with Number::abbreviate()
return [
'tokens' => Number::abbreviate((int) ($totals->tokens ?? 0), precision: 1),
'token_value' => '$'.Number::abbreviate((float) ($totals->token_value ?? 0), precision: 1),
'requests' => Number::abbreviate((int) ($totals->requests ?? 0), precision: 1),
'sessions' => Number::abbreviate((int) ($totals->sessions ?? 0), precision: 1),
];2026-04-29 · Filament · View tweet
Filament tip.
For custom table filters, don't forget to add active indicators, with indicateUsing().
Otherwise users can filter a table and not know why records disappeared.
// Before
Filter::make('paid_at')
->schema([
DatePicker::make('from'),
DatePicker::make('until'),
])
->query(fn (Builder $query, array $data): Builder => $query
->when($data['from'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('paid_at', '>=', $date))
->when($data['until'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('paid_at', '<=', $date)))
// ============================================================
// After
Filter::make('paid_at')
->schema([
DatePicker::make('from'),
DatePicker::make('until'),
])
->query(fn (Builder $query, array $data): Builder => $query
->when($data['from'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('paid_at', '>=', $date))
->when($data['until'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('paid_at', '<=', $date)))
->indicateUsing(fn (array $data): array => [
...(($data['from'] ?? null) ? [Indicator::make('Paid from '.$data['from'])->removeField('from')] : []),
...(($data['until'] ?? null) ? [Indicator::make('Paid until '.$data['until'])->removeField('until')] : []),
]),[Screenshot: Filament Orders table with the date filter applied but no active-filter indicator; annotation "without indicateUsing() users may FORGET about the applied filter".]
[Screenshot: same Filament Orders table now showing "Active filters: Paid from 2026-04-22, Paid until 2026-04-30" chips; annotation "with indicateUsing()".]
2026-04-28 · Eloquent, Laravel · View tweet
Laravel tip.
In Eloquent API resources, whenCounted() allows you to CONDITIONALLY output relationship aggregates.
The attribute is SKIPPED entirely if the relationship count wasn't eager loaded, avoiding null values.
// API Resource:
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'total_orders' => $this->whenCounted('orders'), // <-----
'total_spent' => $this->whenCounted('purchases'), // <-----
'email' => $this->email,
];
}
// Controller:
public function index()
{
$customers = Customer::query()
->withCount(['orders', 'purchases'])
// ^ 'total_orders' and 'total_spent' will be returned
// only if this `withCount()` is present
->get();
return CustomerResource::collection($customers);
}2026-04-27 · Laravel · View tweet
Laravel tip.
When your API action completes successfully but doesn't need to return data, use response()->noContent() to send a 204 HTTP status.
public function deleteUser(User $user)
{
$user->delete();
return response()->noContent();
// ^ Same as return response()->json(null, 204);
}2026-04-23 · Laravel · View tweet
Laravel tip: multi-tenancy.
For simple multi-tenancy in a single DB with no packages, it's enough to have ->where('company_id') in a Global Scope, right?
Wrong.
What devs often forget is to take care of:
- Validation rules
- Permissions/policy
- Raw DB queries
// ❌ Only filtering queries (not enough)
Vessel::where('company_id', auth()->user()->company_id)->get();
// =====================================
// ✅ Also enforce tenancy in VALIDATION
'vessel_id' => [
'required',
Rule::exists('vessels', 'id')
->where('company_id', auth()->user()->company_id),
];
// =====================================
// ✅ Also enforce via AUTHORIZATION layer
Gate::authorize('view', $vessel);
class VesselPolicy
{
public function view(User $user, Vessel $vessel): bool
{
return $user->company_id === $vessel->company_id;
}
public function update(User $user, Vessel $vessel): bool
{
return $user->company_id === $vessel->company_id
&& $user->hasRole('company-admin');
}
}
// =====================================
// ✅ Always enforce tenancy in RAW QUERIES
DB::select("SELECT * FROM vessels
WHERE id = ? AND company_id = ?", [$id, auth()->user()->company_id]);2026-04-21 · Laravel · View tweet
Cool tip, but I disagree here.
Laravel "magic" functions are cool and make the code shorter, but way less readable for devs not that familiar with Laravel.
Same old "try-catch", "if-else" and other CLASSICAL structures are clear for devs with ANY language/framework background.
2026-04-20 · Laravel · View tweet
Laravel tip for Observers.
wasChanged() + getOriginal(): react only to SPECIFIC field changes.
Check exactly which field changed and grab its old value.
// Before:
// Observer fires on EVERY update — even name typo fixes
// app/Observers/RequestObserver.php
public function updated(Request $request): void
{
// Always sends notification, even if only title changed
$request->customer->notify(new RequestUpdated($request));
ActivityLogger::log($request, 'updated');
}
// ====================================================
// After:
public function updated(Request $request): void
{
if ($request->wasChanged('status')) {
$oldStatus = $request->getOriginal('status');
ActivityLogger::logStatusChanged($request, $oldStatus, $request->status->value);
$request->customer->notify(
new RequestStatusChanged($request, $oldStatus, $request->status)
);
}
if ($request->wasChanged('agent_id')) {
$request->agent?->notify(new RequestAssigned($request));
}
// Title typo fix? Nothing happens. As it should.
}2026-04-19 · Filament · View tweet
Filament tip about my FilaCheck package.
Did you know about --dry-run flag?
It will show the actual code diff, without performing it, yet.
Call it a "rehearsal" or "try-out".
Read more about FilaCheck: https://t.co/bBA6txq1E1
[Terminal output: running vendor/bin/filacheck --dry-run, showing a proposed code change diff.]
povilas@povilass MacBook Pro ~/Herd/request %
> vendor/bin/filacheck --dry-run
Scanning: /Users/povilas/Herd/request/app/Filament
............................F......xx.
✓ unnecessary-unique-ignore-record (Best Practices) (proposed)
app/Filament/Resources/Users/Schemas/UserForm.php
Line 27: The `ignoreRecord: true` parameter is now the default in Filament v4. (proposed)
→ Remove `ignoreRecord: true` as it's the default behavior. See: https://filamentphp.com/docs/5
Proposed file changes:
@@ line 27 @@
- →unique(ignoreRecord: true)
+ →unique()2026-04-16 · Filament · View tweet
Filament tip: dynamic Tabs.
You can build form schema from components with foreach/collect from config values.
Including Tabs with their own input fields inside.
Example from my multi-language project, video on my YouTube here: https://t.co/qdHRlds2Fi
// config/locales.php
return [
'supported' => [
'en' => ['name' => 'English', 'native' => 'English', 'flag' => '🇺🇸'],
'es' => ['name' => 'Spanish', 'native' => 'Español', 'flag' => '🇪🇸'],
'de' => ['name' => 'German', 'native' => 'Deutsch', 'flag' => '🇩🇪'],
// ... other locales
],
];
// ========================
// Filament Resource Form
Tabs::make('Translations')
->tabs(
collect(config('locales.supported'))->map( // <-- Collection from Config
fn (array $locale, string $code) => Tab::make("{$locale['flag']} {$locale['name']}")
->schema([
TextInput::make("title.{$code}")
->label('Title')
->required($code === 'en')
->live(onBlur: true)
->afterStateUpdated(function (?string $state, Set $set) use ($code) {
$set("slug.{$code}", Str::slug($state ?? ''));
}),
TextInput::make("slug.{$code}")
->label('Slug')
->required($code === 'en'),
// ... other inputs[Screenshot: Filament "Edit Tour" form with language tabs — English, Spanish, German, Chinese, Japanese — English selected, showing required Title "Machu Picchu Trek" and Slug "machu-picchu-trek".]
2026-04-15 · Filament · View tweet
Filament tip: conditional required(). This function may accept a boolean.
Let's say you have a multi-language form, and title/slug are required only for the main language.
Then you have a loop through all locales, and each input has ->required($code === 'en') or similar.
// These inputs are in a loop/collection
// where $code is en/de/es/...
TextInput::make("title.{$code}")
->required($code === 'en')
->label('Title'),
TextInput::make("slug.{$code}")
->required($code === 'en')
->label('Slug'),[Screenshot: English tab selected — Title field shows a required asterisk and value "Welcome to TravelSite".]
[Screenshot: Spanish tab selected — Title field has no required asterisk and value "Bienvenido a TravelSite".]
2026-04-15 · Livewire · View tweet
Livewire tip: shareable links with #[Url]
Add #[Url] to a Livewire property and it auto-syncs to the query string. Users can now bookmark or share filtered views.
// Before:
class KanbanBoard extends Page
{
public ?int $projectId = null;
// User selects a project filter...
// Refreshes page → filter is gone
// Can't share the filtered URL with a teammate
}
// ===================================
// After:
use Livewire\Attributes\Url;
class KanbanBoard extends Page
{
#[Url]
public ?int $projectId = null;
// URL auto-updates to ?projectId=5
// Refresh → filter preserved ✅
// Share link → teammate sees same view ✅
}[Screenshot: Kanban board "Office Tower BIM Modeling" with columns To Do, In Progress, Review; browser URL bar reads .../admin/kanban?projectId=1.]
2026-04-14 · Laravel · View tweet
Laravel tip. Did you know that @spatie_be laravel-pdf package supports FIVE drivers?
- browsershot (default)
- cloudflare (remote API)
- dompdf (old classic, weak modern CSS/JS support)
- gotenberg (Docker-based)
- weasyprint (good for printing)
Docs: https://t.co/mjGpw52cxu
[Documentation screenshot: "Selecting a Driver" — the driver option in config/laravel-pdf.php determines which PDF generation backend to use. Supported values: browsershot, cloudflare, dompdf, gotenberg, weasyprint.]
'driver' => env('LARAVEL_PDF_DRIVER', 'browsershot'),2026-04-14 · Filament · View tweet
Filament tip.
Did you know that Dashboard Widgets like Stats will auto-refresh data from server every 5 seconds?
That may be convenient, but often it's unnecessary server request and performance issue.
My package FilaCheck will detect it. Read more: https://t.co/NbPVz6Mrbz
[Screenshot: Filament dashboard with stats cards (Total Projects, Revenue, etc.) and browser DevTools Network panel showing repeated livewire "update" requests every few seconds.]
[Documentation screenshot: "Live updating stats (polling)" — stats overview widgets refresh every 5 seconds by default; override $pollingInterval to change or disable it.]
protected ?string $pollingInterval = '10s';protected ?string $pollingInterval = null;[Terminal output: running vendor/bin/filacheck flagging a stats-widget-polling issue.]
povilas@povilass MacBook Pro ~/Herd/bpo %
> vendor/bin/filacheck
Scanning: /Users/povilas/Herd/bpo/app/Filament
..............x.........x.x..x........xx.
× action-in-bulk-action-group (Best Practices)
app/Filament/Resources/Invoices/Tables/InvoicesTable.php
Line 59: `Action::make()` is used inside `toolbarActions()`. Use `BulkAction::make()` instead.
→ Replace `Action::make()` with `BulkAction::make()`. See: https://filamentphp.com/docs/5.x/tables/actions#bulk-actions
× stats-widget-polling-not-disabled (Performance)
app/Filament/Widgets/ProjectStatsOverview.php
Line 11: StatsOverviewWidget polls every 5 seconds by default, which can hammer your server with unnecessary requests.
→ Add `protected ?string $pollingInterval = null;` to disable automatic polling, or set it to a longer interval. See: https://filamentphp.com/docs/5.x/widgets/stats-overview#live-updating-stats-polling
app/Filament/Widgets/FinancialOverview.php
Line 10: StatsOverviewWidget polls every 5 seconds by default, which can hammer your server with unnecessary requests.
→ Add `protected ?string $pollingInterval = null;` to disable automatic polling, or set it to a longer interval. See: https://filamentphp.com/docs/5.x/widgets/stats-overview#live-updating-stats-polling2026-04-13 · Filament · View tweet
Filament tip. Bulk actions can be mini workflows, not just "delete selected"
They can have their own form (schema).
That means one action can collect input, operate on selected records, and show a success notification.
[Screenshot: Participants table with "Bulk actions" dropdown open showing Send Email, Send SMS, Delete selected; 2 records selected.]
[Screenshot: "Send Bulk Email" confirmation modal with required Subject and Message Body fields, Cancel and Confirm buttons.]
BulkAction::make('sendMessage')
->schema([
Select::make('channel')
->options(['email' => 'Email', 'sms' => 'SMS'])
->required(),
TextInput::make('subject')
->visible(fn (callable $get): bool => $get('channel') === 'email'),
Textarea::make('body')->required(),
])
->action(function (Collection $records, array $data): void {
$records->each(fn ($participant) => $participant->notify(
new BulkParticipantMessage(
subject: $data['subject'] ?? $data['body'],
body: $data['body'],
channel: $data['channel'],
)
));
});2026-04-13 · Laravel · View tweet
Laravel tip.
You can use Laravel Validator class not only in Controllers.
Here's an example in Artisan command class.
For complex validation, instead of a bunch of if-statements, you may use Validator.
class CreateServerCommand extends Command
{
public function handle()
{
$data = [
'driver' => $this->option('driver'),
'host' => $this->ask('Host?'),
'port' => $this->ask('Port?'),
'database' => $this->ask('Database name?'),
];
$validator = Validator::make($data, [
'driver' => 'required|in:mysql,pgsql,sqlite',
'host' => 'required|string|max:255',
'port' => 'required|integer|between:1,65535',
'database' => 'required|regex:/^[a-zA-Z0-9_]+$/',
]);
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
$this->error($error);
}
return 1;
}
}
}2026-04-10 · PHP · View tweet
PHP tip: slight difference between match and switch.
If you forget to add the default option, switch without default DOES NOT THROW ERRORS when nothing matches. It just "silently" falls through.
The operator match without default THROWS AN ERROR.
switch ($driver) {
case 'mysql':
$result = "mysql:host={$config['host']};port={$config['port']}";
case 'pgsql':
$result = "pgsql:host={$config['host']};port={$config['port']}";
// Added 'sqlsrv' support last month but forgot this switch
// No error — just returns null silently
}
// ===================
$result = match ($driver) {
'mysql' => "mysql:host={$config['host']};port={$config['port']}",
'pgsql' => "pgsql:host={$config['host']};port={$config['port']}",
'sqlsrv' => "sqlsrv:Server={$config['host']},{$config['port']}",
'sqlite' => "sqlite:{$config['database']}",
// Any unhandled driver throws UnhandledMatchError
// You'll know immediately, not after a mystery bug
};2026-04-09 · Laravel, Testing · View tweet
Laravel tip: Pest ->with() Datasets vs Repeating Tests
Stop copy-pasting the same test 10 times with different inputs.
Pest's ->with() runs one test against many datasets.
Each case gets its own name in the output.
// Before:
it('converts users to User', function () {
expect(tableToModel('users'))->toBe('User');
});
it('converts teams to Team', function () {
expect(tableToModel('teams'))->toBe('Team');
});
it('converts team_members to TeamMember', function () {
expect(tableToModel('team_members'))->toBe('TeamMember');
});
// ==============================
// After:
it('converts table names to model names', function (string $table, string $expected) {
expect(tableToModel($table))->toBe($expected);
})->with([
'users' => ['users', 'User'],
'teams' => ['teams', 'Team'],
'team_members' => ['team_members', 'TeamMember'],
]);2026-04-08 · PHP · View tweet
PHP tip.
Did you know that match can be NESTED?
function mapComponent(Column $col): string {
return match ($col->ui_hint) {
'password', 'email' => 'TextInput',
'toggle' => 'Toggle',
default => match ($col->type) {
'boolean' => 'Toggle',
'text' => 'Textarea',
'datetime' => 'DateTimePicker',
default => 'TextInput',
},
};
}2026-04-07 · Laravel · View tweet
Laravel tip.
Sometimes foreign keys deserve more clear relation names than defaults.
Field invited_by becoming $teamInvitation->user() is vague.
Use ->inviter() for clarity, even if the relationship is to User model.
// BEFORE:
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'invited_by');
}
// =============================
// AFTER:
public function inviter(): BelongsTo
{
return $this->belongsTo(User::class, 'invited_by');
}2026-04-06 · Laravel · View tweet
Laravel tip. Dispatch Events AFTER the Transaction, Not Inside.
Use a &$shouldDispatch reference variable to flag inside the transaction, then dispatch outside it.
Listeners will never see uncommitted data.
// =========================
// Before:
// =========================
DB::transaction(function () use ($lead) {
$lead->update(['pipeline_stage' => 'quoted']);
$lead->payments()->create([...]);
// BAD: listener runs before transaction commits
// If transaction rolls back, listener already sent an email
LeadQuoted::dispatch($lead);
});
// =========================
// After:
// =========================
$shouldDispatch = false;
DB::transaction(function () use ($lead, &$shouldDispatch) {
$lead->update(['pipeline_stage' => 'quoted']);
$lead->payments()->create([...]);
$shouldDispatch = true;
});
// Only fires after transaction is committed
if ($shouldDispatch) {
LeadQuoted::dispatch($lead);
}2026-04-06 · Livewire, Laravel, Testing · View tweet
Laravel tip.
You're doing a lot ot "laravel new" for testing and hate answering wizard questions every time?
You can automate this. Provide the flags and values upfront.
Example: "laravel new project --livewire --database=sqlite --pest --npm --no-boost"
No questions asked.
povilas@Povilass-MacBook-Pro ~/Herd %
> laravel new laravel --livewire --database=sqlite --pest --npm --no-boost
# [ASCII art: LARAVEL logo]
Creating a "laravel/livewire-starter-kit" project at "./laravel"
Installing laravel/livewire-starter-kit (dev-main b3ae1b8e557e54bd303caac0a502c4288c5230ac)
- Downloading laravel/livewire-starter-kit (dev-main b3ae1b8)
- Installing laravel/livewire-starter-kit (dev-main b3ae1b8): Extracting archive
Created project in /Users/povilas/Herd/laravel
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies
Lock file operations: 115 installs, 0 updates, 0 removals
- Locking bacon/bacon-qr-code (v3.1.1)
- Locking brick/math (0.14.8)
- Locking carbonphp/carbon-doctrine-types (3.2.0)
- Locking dasprid/enum (1.0.7)
- Locking dflydev/dot-access-data (v3.0.3)
- Locking doctrine/inflector (2.1.0)
- Locking doctrine/lexer (3.0.1)
- Locking dragonmantank/cron-expression (v3.6.0)
- Locking egulias/email-validator (4.0.4)
- Locking fakerphp/faker (v1.24.1)
- Locking filp/whoops (2.18.4)
- Locking fruitcake/php-cors (v1.4.0)
- Locking graham-campbell/result-type (v1.1.4)
- Locking guzzlehttp/guzzle (7.10.0)
- Locking guzzlehttp/promises (2.3.0)
- Locking guzzlehttp/psr7 (2.9.0)
- Locking guzzlehttp/uri-template (v1.0.5)2026-04-05 · Laravel, PHP · View tweet
Laravel/PHP tip: when to use Traits?
My personal rule of thumb: Don't extract a trait after seeing code duplicated twice. Wait for THREE uses.
More examples in my newly-updated course lesson: "Repeating Responses: BaseController or Traits?" https://t.co/Xri14oXDZH
// Extracted after being needed in Tag, Article, AND Thread:
trait HasSlug
{
public static function findBySlug(string $slug): self
{
return static::where('slug', $slug)->firstOrFail();
}
public function getRouteKeyName(): string
{
return 'slug';
}
protected static function bootHasSlug(): void
{
static::creating(function ($model) {
$model->slug = Str::slug($model->title);
});
}
}
// Used across 3+ models:
class Article extends Model { use HasSlug; }
class Thread extends Model { use HasSlug; }
class Tag extends Model { use HasSlug; }2026-04-04 · Laravel · View tweet
This tip comes from my newly-updated text-based course "How to structure Laravel 13 projects".
This specific lesson is called "Events and Listeners: When and How?" https://t.co/fVt8qmKSra
// Example from Laravel Skeleton
//
// Another example of events/listeners comes from Laravel itself.
//
// In the Controller, we don't need to send email verification notifications because it is already
// handled by Laravel's `Registered` event and `SendEmailVerificationNotification` listener.
//
// But we can create another Listener to send more notifications: for example, notify admins about something.
//
// First, create a Listener and register it in the `EventServiceProvider`:
php artisan make:listener NewUserSendAdminNotifications --event=NewUserRegistered
// Now, in the `NewUserSendAdminNotifications` listener, in the `handle()` method, we move the
// code from the Controller. And you can access the `User` from the `$event` using `$event->user`:
class NewUserSendAdminNotifications
{
public function handle(NewUserRegistered $event)
{
$admins = User::where('is_admin', 1)->get();
Notification::send($admins, new AdminNewUserNotification($event->user));
}
}2026-04-04 · Laravel · View tweet
Laravel tip: when events/listeners are REALLY beneficial?
The real power of Laravel Events: one event, multiple listeners.
This real-world example from laravel(dot)io fires ONE event when a reply is created: FOUR different things happen automatically.
// One event dispatched:
ReplyWasCreated::dispatch($reply);
// Four independent listeners react:
// 1. Mark thread as having new activity
// 2. Send notification to thread author
// 3. Auto-subscribe mentioned users
// 4. Notify mentioned users
// Adding a 5th reaction later?
// Just create a new Listener. Zero existing code touched.2026-04-03 · Laravel · View tweet
This Week in Laravel: v13 Teams and Best-Practices AI Skill https://t.co/V26Q1POEBk
My weekly newsletter with community links/news.
But I notice by clicks that devs are not THAT interested in NEWS. They prefer TOOLS and TIPS.
Maybe time to re-think my newsletter content.
[Screenshot: newsletter link list with three entries]
Laravel Updates on X: "Most Laravel bugs come from hidden arrays."
x.com
DTOs force clarity. You know exactly what data is expected, and your code becomes safer to change.
Freek Van der Herten on X: "We just released Scotty, a simple, beautiful SSH task runner."
x.com
Run deploy scripts from your terminal and watch every step as it happens. Real-
time output, step counters, elapsed time, and a summary when it's done.
Barry vd. Heuvel on X: "I added a @laravelphp Boost skill + cli to Laravel Debugbar to help you fix issues and optimize queries"
x.com
It can: - Browse latest requests - See collected data and show details - Show overview
of queries (with duplicates) - Query details (backtrace and even EXPLAIN results)
2026-04-03 · Laravel · View tweet
This tip comes from my latest updated course on Laravel structure, from the NEW added lesson called "When NOT to Extract: Avoiding Over-Engineering": https://t.co/tFjCU79WPL
// You Don't Need an Action for Simple CRUD
//
// If your Controller method is just this:
public function store(StoreUserRequest $request)
{
$user = User::create($request->validated());
return redirect()->route('users.index');
}
// That's fine. You don't need a `CreateUserAction` class for this. The logic is two lines, it's clear, it's
// not reused anywhere. An Action class would add a file, an import, and indirection — for zero benefit.
//
// Extract when there's a reason to extract:
// - The logic is complex (more than a few lines)
// - The logic is reused from multiple places
// - The logic has side effects that need testing2026-04-02 · Laravel · View tweet
Laravel tip.
When installing with "laravel new", look how many different options you can choose, also avoiding wizard questions along the way.
For example, you can try "laravel new project --bun" instead of npm.
All list here: https://t.co/KJ8iUuQNsq
// installer / src / NewCommand.php
protected function configure()
{
$this
->setName('new')
->setDescription('Create a new Laravel application')
->addArgument('name', InputArgument::REQUIRED)
->addOption('dev', null, InputOption::VALUE_NONE, 'Install the latest "development" release')
->addOption('git', null, InputOption::VALUE_NONE, 'Initialize a Git repository')
->addOption('branch', null, InputOption::VALUE_REQUIRED, 'The branch that should be created for a new repository', $this->defaultB…
->addOption('github', null, InputOption::VALUE_OPTIONAL, 'Create a new repository on GitHub', false)
->addOption('organization', null, InputOption::VALUE_REQUIRED, 'The GitHub organization to create the new repository for')
->addOption('database', null, InputOption::VALUE_REQUIRED, 'The database driver your application will use. Possible values are: '.…
->addOption('react', null, InputOption::VALUE_NONE, 'Install the React Starter Kit')
->addOption('svelte', null, InputOption::VALUE_NONE, 'Install the Svelte Starter Kit')
->addOption('vue', null, InputOption::VALUE_NONE, 'Install the Vue Starter Kit')
->addOption('livewire', null, InputOption::VALUE_NONE, 'Install the Livewire Starter Kit')
->addOption('livewire-class-components', null, InputOption::VALUE_NONE, 'Generate stand-alone Livewire class components')
->addOption('workos', null, InputOption::VALUE_NONE, 'Use WorkOS for authentication')
->addOption('teams', null, InputOption::VALUE_NONE, 'Install team support')
->addOption('no-authentication', null, InputOption::VALUE_NONE, 'Do not generate authentication scaffolding')
->addOption('pest', null, InputOption::VALUE_NONE, 'Install the Pest testing framework')
->addOption('phpunit', null, InputOption::VALUE_NONE, 'Install the PHPUnit testing framework')
->addOption('npm', null, InputOption::VALUE_NONE, 'Install and build NPM dependencies')
->addOption('pnpm', null, InputOption::VALUE_NONE, 'Install and build NPM dependencies via PNPM')
->addOption('bun', null, InputOption::VALUE_NONE, 'Install and build NPM dependencies via Bun')
->addOption('yarn', null, InputOption::VALUE_NONE, 'Install and build NPM dependencies via Yarn')
->addOption('boost', null, InputOption::VALUE_NONE, 'Install Laravel Boost to improve AI assisted coding')
->addOption('no-boost', null, InputOption::VALUE_NONE, 'Skip Laravel Boost installation')
->addOption('using', null, InputOption::VALUE_OPTIONAL, 'Install a custom starter kit from a community maintained package')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces install even if the directory already exists');
}2026-04-02 · Filament, Laravel, Testing · View tweet
Laravel tip: Pest architecture tests for boundaries.
You can test architecture, not just behavior. Great for keeping, for example, Filament resources from leaking into domain code.
Read more in the Pest docs: https://t.co/hJ8tjkPkJl
// Architecture Tests
// Test boundaries, not just behavior
// Keep domain layer clean
arch('domain != Filament')
->expect('App\\Domain')
->not->toUse(
'App\\Filament\\Resources'
);
// Support stays framework-light
arch('support stays light')
->expect('App\\Support')
->not->toUse('App\\Http')
->not->toUse('App\\Filament');
// Enforce strict types everywhere
arch('strict types')
->expect('App')
->toUseStrictTypes();2026-04-01 · Laravel · View tweet
Laravel tip: stringable DTOs for class fragments
If an object's whole job is "be a valid class string", make it Stringable.
// Stringable DTOs
// If an object's job is "be a class string", make it Stringable
// Before: manual string concatenation
$classes = trim(
$font['family']
.' '.$font['color']
.' '.$font['size']
);
// ====================
// After: a Stringable value object
final readonly class TextStyle
implements Stringable
{
public function __construct(
public string $family,
public string $color,
public string $size,
) {}
public function __toString(): string
{
return implode(' ', array_filter([
$this->family,
$this->color,
$this->size,
]));
}
}
// Usage: just cast to string anywhere
$style = new TextStyle(
'font-serif', 'text-slate-900', 'text-2xl'
);
return "<h1 class=\"{$style}\">Revenue</h1>";2026-03-31 · Laravel · View tweet
Laravel tip: Typed config objects instead of giant arrays.
Non-standard but clean: use readonly DTOs and enums for config, so bad values fail earlier and IDE support gets much better.
// Typed Config Objects
// Replace loose config arrays with readonly DTOs + enums
// Before:
// config/reports.php
return [
'theme' => 'dark',
'show_filters' => true,
'layout' => 'grid',
];
$layout = config('reports.layout')
=== 'grid' ? 'grid-cols-3' : 'grid-cols-1';
// ====================
// After: readonly DTO + enum
enum ReportLayout: string
{
case Grid = 'grid';
case List = 'list';
}
final readonly class ReportsConfig
{
public function __construct(
public string $theme = 'dark',
public bool $showFilters = true,
public ReportLayout $layout
= ReportLayout::Grid,
) {}
}
// Usage: IDE autocomplete + exhaustive match
$config = config('reports.ui',
new ReportsConfig());
$layout = match ($config->layout) {
ReportLayout::Grid => 'grid-cols-3',
ReportLayout::List => 'grid-cols-1',
};2026-03-30 · Laravel · View tweet
Laravel tip.
Need to generate a UNIQUE slug for team name / username / article, checking the existing ones?
So, "john-smith", but next would be "john-smith-1", "john-smith-2", etc?
Laravel starter kits TEAMS feature has an interesting implementation: https://t.co/vQeeT6HUX5
// app/Concerns/GeneratesUniqueTeamSlugs.php
protected static function generateUniqueTeamSlug(string $name, ?int $excludeId = null): string
{
$defaultSlug = Str::slug($name);
$query = static::withTrashed()
->where(function ($query) use ($defaultSlug) {
$query->where('slug', $defaultSlug)
->orWhere('slug', 'like', $defaultSlug.'-%');
});
if ($excludeId) {
$query->where('id', '!=', $excludeId);
}
$existingSlugs = $query->pluck('slug');
$maxSuffix = $existingSlugs
->map(function (string $slug) use ($defaultSlug): ?int {
if ($slug === $defaultSlug) {
return 0;
} elseif (preg_match('/^'.preg_quote($defaultSlug, '/').'-(\d+)$/', $slug, $matches)) {
return (int) $matches[1];
}
return null;
})
->filter(fn (?int $suffix) => $suffix !== null)
->max() ?? 0;
return $existingSlugs->isEmpty()
? $defaultSlug
: $defaultSlug.'-'.($maxSuffix + 1);
}2026-03-30 · Laravel · View tweet
Laravel tip.
Writing a Laravel package with JS adapters? Auto-detect the user's framework from package.json in your install command.
Don't make them manually choose React/Vue/Svelte when you can just READ it.
// Don't make users choose — just read package.json
public function handle(): int
{
$adapter = $this->detectJsFramework();
if ($adapter) {
$this->info("Detected {$adapter[0]}");
$this->line(
" import from adapters/{$adapter[1]}"
);
}
return self::SUCCESS;
}
private function detectJsFramework(): ?array
{
$pkg = json_decode(
File::get(base_path('package.json')),
true
) ?? [];
$deps = ($pkg['dependencies'] ?? [])
+ ($pkg['devDependencies'] ?? []);
return match (true) {
isset($deps['react']) => ['React', 'react'],
isset($deps['vue']) => ['Vue', 'vue'],
isset($deps['svelte']) => ['Svelte', 'svelte'],
default => null,
};
}2026-03-29 · Laravel · View tweet
Laravel tip: Observer isDirty() for SMART cleanup.
In Observers, check if specific column changed with isDirty() and get old value with getRawOriginal().
Comes from my updated Laravel structure course: lesson "Before Saving: Mutator or Observer?" https://t.co/u3fq9s5Y1Q
class AlbumObserver
{
public function updating(Album $album): void
{
// Only delete old cover if it actually changed
if ($album->isDirty('cover')) {
$oldCover = $album->getRawOriginal('cover');
Storage::delete($oldCover);
}
}
public function deleted(Album $album): void
{
// Clean up all files when album is deleted
Storage::delete($album->cover);
Storage::delete($album->thumbnail);
}
}2026-03-28 · Livewire, Laravel · View tweet
Laravel tip: One Action, Many Entry Points
Extracting logic into Actions/Services to call the SAME logic from everywhere: Controller, Artisan command, Livewire, Job, etc.
This comes from the lesson in my course "How to structure Laravel 13 projects": https://t.co/pBmcFcXWhh
// Laravel tip: One Action, Many Entry Points
class CreateUserAction
{
public function handle(array $data): User
{
// All your business logic lives here
}
}
// Web Controller
app(CreateUserAction::class)->handle($request->validated());
// Artisan Command (CSV import)
app(CreateUserAction::class)->handle($row);
// Livewire Component
app(CreateUserAction::class)->handle($this->form);
// Scheduled Job (nightly sync)
app(CreateUserAction::class)->handle($apiData);2026-03-27 · Laravel · View tweet
Laravel Tip: Environment-Aware Caching
Stop flushing cache every time you edit a config locally. Check app()->environment('local') and skip caching entirely in dev.
Production gets rememberForever().
Best of both worlds.
// Before:
protected function translations(string $locale): array
{
return Cache::remember(
"translations_{$locale}",
3600,
fn () => $this->loadTranslations($locale)
);
// Dev: "why aren't my changes showing?!"
}
// ==================
// After:
protected function translations(string $locale): array
{
// Local: always fresh, no cache headaches
if (app()->environment('local')) {
return $this->loadTranslations($locale);
}
// Prod: cached forever, invalidated on deploy
return Cache::rememberForever(
"translations_{$locale}",
fn () => $this->loadTranslations($locale)
);
}2026-03-26 · Laravel · View tweet
Laravel tip.
Did you know you can use Model::booted() to throw ValidationException and block dangerous saves?
Prevent admins from disabling their own account or removing the last active admin: at the model level, not just in forms.
// Before: validation only in controller
public function update(Request $request)
{
if ($admin->id === auth()->id
&& !$request->is_active) {
return back()->withErrors([...
}
$admin->update($request->validated());
}
// Jobs, CLI, API can still bypass this!
// ==================
// After: enforced everywhere automatically
protected static function booted(): void
{
static::saving(function (self $admin) {
if ($admin->isCurrentAdmin()
&& !$admin->is_active) {
throw ValidationException::withMessages([
'is_active' => 'Cannot disable yourself.'
]);
}
if ($admin->isLastActiveAdmin()) {
throw ValidationException::withMessages([
'is_active' => 'Last admin must remain.'
]);
}
});
}
// Forms, API, CLI, jobs — all protected2026-03-25 · Filament · View tweet
Filament tip.
@filamentphp wizards can persist the current step in the URL query string, with persistStepInQueryString().
Users can bookmark mid-flow, refresh without losing their place, and share direct links to step 3.
// Before: resets to step 1 on refresh
Wizard::make([
Step::make('Scope')
->schema([...]),
Step::make('Cadence')
->schema([...]),
Step::make('Connection')
->schema([...]),
])
// User on step 3, hits refresh... back to 1
// ==================
// After: step survives refresh & is shareable
Wizard::make([
Step::make('Scope')
->schema([...]),
Step::make('Cadence')
->schema([...]),
Step::make('Connection')
->schema([...]),
])
->persistStepInQueryString('step')
->skippable()
// URL: /create?step=connection
// Refresh? Still on step 3
// Share the link? Opens on step 32026-03-25 · Laravel · View tweet
Laravel tip.
Did you know Laravel can encrypt AND json_encode a column with a single cast?
No manual Crypt::encrypt() calls.
Secrets stored encrypted at rest, decoded to arrays automatically.
// BEFORE
// Saving secrets manually:
$check->secret_config =
Crypt::encryptString(
json_encode($data)
);
// Reading secrets manually:
$data = json_decode(
Crypt::decryptString(
$check->secret_config
), true);
// AFTER
protected function casts(): array
{
return [
'config' => 'array',
'secret_config' => 'encrypted:array',
];
}
// Just use it like a normal array:
$check->secret_config = ['token' => 'sk-...'];
$token = $check->secret_config['token'];2026-03-23 · Laravel · View tweet
Laravel tip.
Did you know one line in your AppServiceProvider can prevent accidental migrate:fresh or db:wipe in production?
// Before:
// Nothing stops you from running:
// php artisan migrate:fresh --seed
// ...on production. Goodbye data.
// After:
// AppServiceProvider
use Illuminate\Support\Facades\DB;
public function boot(): void
{
DB::prohibitDestructiveCommands(
app()->isProduction(),
);
}
// Now in production:
// php artisan migrate:fresh
// ❌ Prohibited destructive command2026-03-22 · Laravel · View tweet
Laravel tip. Set strict password rules in production but skip them in development.
Password::defaults() accepts a closure — return null for local dev and full rules for production.
Defined once in AppServiceProvider.
// Before:
// In every Form Request / validation:
'password' => [
'required',
'string',
Password::min(12)
->mixedCase()
->numbers()
->symbols()
->uncompromised(),
'confirmed',
],
// ==============================
// After:
// AppServiceProvider::boot() — set once:
Password::defaults(fn (): ?Password => app()->isProduction()
? Password::min(12)
->mixedCase()
->letters()
->numbers()
->symbols()
->uncompromised()
: null,
);
// Every Form Request — just reference defaults:
'password' => ['required', 'string', Password::default(), 'confirmed'],2026-03-21 · Laravel · View tweet
Laravel tip: whereHasMorph().
Did you know you can search across ALL polymorphic types in one query? No switch statements, no union queries.
// Before:
// Manual approach: check type, query the right table
if ($report->reportable_type === Listing::class) {
$results = Report::whereHas('reportable', fn ($q) =>
$q->where('title', 'like', "%{$search}%")
);
}
// What about BulletinPost? Another if block...
// =================================
// After:
Report::whereHasMorph(
'reportable',
[Listing::class, BulletinPost::class],
fn (Builder $query) => $query->where('title', 'like', "%{$search}%")
)->get();
// Searches title in BOTH listings AND bulletin_posts tables
// Laravel generates the correct JOINs for each morph type2026-03-20 · Laravel · View tweet
Laravel tip.
Did you know CarbonPeriod can generate every date in a range?
No more missing days in your charts.
// Before:
// Query only returns days WITH data — gaps in chart
$data = User::selectRaw('DATE(created_at) as date, count(*) as total')
->groupBy('date')
->pluck('total', 'date');
// Result: ['Mar 1' => 5, 'Mar 3' => 2] — Mar 2 is missing!
// =================================
// After:
$counts = User::selectRaw('DATE(created_at) as date, count(*) as total')
->where('created_at', '>=', now()->subDays(29))
->groupBy('date')
->pluck('total', 'date');
foreach (CarbonPeriod::create(now()->subDays(29), now()) as $date) {
$labels[] = $date->format('M j');
$data[] = $counts[$date->format('Y-m-d')] ?? 0;
}
// Result: ['Mar 1' => 5, 'Mar 2' => 0, 'Mar 3' => 2]