Generate HTML Password Rules in Laravel 13.9.0 — Plus What’s New from 13.7 to 13.9
One new method, but its impact is immediately felt on every registration form you build.

Generate HTML Password Rules in Laravel 13.9.0 — Plus What’s New from 13.7 to 13.9
One new method, but its impact is immediately felt on every registration form you build.
You’ve seen this play out before. A user tries to sign up, their password manager generates a long, complex password, they submit the form — and get back an error: “Password must contain at least one uppercase letter and a symbol.” They go back, try to generate a new one, submit again, another error. A frustration loop that shouldn’t exist.
This isn’t a hard design problem. It’s a communication problem — your application has a specific set of password requirements, but browsers and password managers have no idea what those requirements are until the user has already submitted the wrong thing. They’re forced to trial-and-error against validation rules that could have been communicated upfront.
Laravel 13.9.0 ships an elegant fix for this through Password::toPasswordRulesString() — a method that converts your PHP password validation rules into an HTML passwordrules attribute, so browsers and password managers like Safari, 1Password, and Bitwarden can read your app's password policy and automatically generate a compliant password on the first try.
But that’s not the only thing worth knowing. The three releases spanning 13.7.0, 13.8.0, and 13.9.0 each bring features that are genuinely useful if you’re actively building with Laravel. In this article, we’ll work through all of them from the ground up, with code examples you can adapt immediately.
Background: What Is the passwordrules Attribute?
Before jumping into the implementation, it’s worth understanding the context.
passwordrules is an HTML specification introduced by Apple that lets browsers and password managers read an application's password policy and automatically suggest a password that already satisfies all the requirements — no back-and-forth with validation errors required.
The specification is supported by Safari, 1Password, Bitwarden, and most modern password managers. When a user focuses a password input during registration, the password manager reads this attribute and generates a valid password right away — not a random one that might not meet your requirements.
Before Laravel 13.9.0, implementing this meant manually writing the attribute string yourself — which created a duplication problem between your PHP validation rules and your HTML, with the very real risk of them drifting out of sync every time you changed the password policy.
Now, both are driven from the same source automatically.
Laravel 13.9.0 — Password::toPasswordRulesString()
How It Works
The toPasswordRulesString() method converts a Password instance into a passwordrules attribute string for HTML inputs. Each method on the Password rule maps to a specific rule token:
// min(n) → minlength: n
// max(n) → maxlength: n
// letters() → required: lower
// mixedCase() → required: lower; required: upper
// numbers() → required: digit
// symbols() → required: special
// Note: uncompromised() has no passwordrules equivalent
// and is not included in the output
Here are the outputs for the most common combinations, verified against the merged tests:
Password::min(8)->toPasswordRulesString();
// 'minlength: 8;'
Password::min(8)->max(64)->toPasswordRulesString();
// 'minlength: 8; maxlength: 64;'
Password::min(8)->letters()->toPasswordRulesString();
// 'minlength: 8; required: lower;'
Password::min(8)->mixedCase()->toPasswordRulesString();
// 'minlength: 8; required: lower; required: upper;'
Password::min(12)->max(64)->mixedCase()->numbers()->symbols()->toPasswordRulesString();
// 'minlength: 12; maxlength: 64; required: lower; required: upper; required: digit; required: special;'
The Right Implementation — Single Source of Truth
The most practical usage is pairing it with Password::defaults() so the same policy you define in AppServiceProvider drives both server-side validation and the browser's password suggestions at the same time.
First, define your default password policy:
// app/Providers/AppServiceProvider.php
use Illuminate\Validation\Rules\Password;
public function boot(): void
{
Password::defaults(function () {
return Password::min(12)
->max(64)
->mixedCase()
->numbers()
->symbols()
->uncompromised(); // server-side only - not included in passwordrules output
});
}
Use it in a form request for validation:
// app/Http/Requests/RegisterRequest.php
use Illuminate\Validation\Rules\Password;
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
];
}
And in the Blade view — a single line does the job:
<!-- resources/views/auth/register.blade.php -->
<input
type="password"
name="password"
id="password"
autocomplete="new-password"
passwordrules="{{ Password::defaults()->toPasswordRulesString() }}"
required
/>
The rendered HTML attribute looks like this:
<input
type="password"
name="password"
autocomplete="new-password"
passwordrules="minlength: 12; maxlength: 64; required: lower; required: upper; required: digit; required: special;"
required
/>
When a user focuses this field in a supporting browser or password manager, they’ll be offered a generated password that already satisfies all of your rules — no validation error loop.
Using It with Livewire
If you’re using Livewire, the integration stays clean:
// app/Livewire/Auth/Register.php
use Illuminate\Validation\Rules\Password;
use Livewire\Component;
class Register extends Component
{
public string $name = '';
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
public function register(): void
{
$this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
]);
// create user...
}
public function render(): \Illuminate\View\View
{
return view('livewire.auth.register', [
'passwordRules' => Password::defaults()->toPasswordRulesString(),
]);
}
}
<!-- resources/views/livewire/auth/register.blade.php -->
<input
type="password"
wire:model="password"
autocomplete="new-password"
passwordrules="{{ $passwordRules }}"
/>
Custom Policy per Context
Not every form needs the same policy. For example, your settings page’s change-password form might enforce stricter rules than standard registration:
// Standard policy for registration
$registrationPolicy = Password::min(8)->letters()->numbers();
// Stricter policy for password reset or change-password flows
$strictPolicy = Password::min(12)->max(64)->mixedCase()->numbers()->symbols();
// In views:
// <input passwordrules="{{ $registrationPolicy->toPasswordRulesString() }}" />
// <input passwordrules="{{ $strictPolicy->toPasswordRulesString() }}" />
This is particularly valuable for financial apps or enterprise tools where different areas of the app have different security requirements.
Laravel 13.8.0 — Queue-Wide Inspection Methods
Before 13.8.0, checking jobs across all your queues meant calling each one individually:
// ❌ BEFORE — requires a separate call per queue name
$defaultReserved = Queue::reservedJobs('default');
$emailReserved = Queue::reservedJobs('emails');
$highReserved = Queue::reservedJobs('high');
// and so on for every queue you have...
Laravel 13.8.0 adds allReservedJobs(), allDelayedJobs(), and allPendingJobs() to retrieve jobs across every queue in a single call:
// ✅ AFTER — one call covers all queues
use Illuminate\Support\Facades\Queue;
// All jobs currently being processed across all queues
$reservedJobs = Queue::allReservedJobs();
// All delayed jobs across all queues
$delayedJobs = Queue::allDelayedJobs();
// All pending jobs waiting to be processed
$pendingJobs = Queue::allPendingJobs();
// Each item is an InspectedJob with:
// - uuid
// - name
// - attempts
// - createdAt
foreach ($reservedJobs as $job) {
echo "{$job->name} | attempts: {$job->attempts}";
}
This is especially useful during deployments — you can confirm no jobs are actively running before stopping workers:
// Deployment check before stopping workers
public function handle(): void
{
$activeJobs = Queue::allReservedJobs();
if ($activeJobs->isNotEmpty()) {
$this->warn("Still {$activeJobs->count()} active jobs running. Waiting...");
return;
}
$this->info('All queues are clear. Safe to stop workers.');
}
Worker Pause and Resume Events
Two new events — WorkerPausing and WorkerResuming — are now dispatched when a queue worker receives SIGUSR2 or SIGCONT signals respectively. This gives you visibility into worker state transitions, which is useful for deployment logging or alerting:
// app/Providers/AppServiceProvider.php
use Illuminate\Queue\Events\WorkerPausing;
use Illuminate\Queue\Events\WorkerResuming;
Event::listen(WorkerPausing::class, function () {
Log::info('Queue worker pausing - deployment in progress.');
});
Event::listen(WorkerResuming::class, function () {
Log::info('Queue worker resumed - deployment complete.');
});
assertSessionMissingInput() for Testing
assertSessionMissingInput() is now available on TestResponse as the counterpart to the existing assertSessionHasInput(). It accepts a single field name or an array:
// tests/Feature/RegisterTest.php
// Before: could only assert session HAS input
$response->assertSessionHasInput('email');
// Now: can also assert session is MISSING input
$response->assertSessionMissingInput('password'); // single field
$response->assertSessionMissingInput([ // multiple fields
'credit_card',
'cvv',
'pin',
]);
This pairs naturally with security tests — verifying that sensitive fields like passwords, CVVs, and PINs are never flashed back to the session after a failed form submission.
Laravel 13.7.0 — Interruptible Jobs and the @fonts Directive
Interruptible Jobs
Queued jobs can now implement the Interruptible interface to respond when a worker receives a signal like SIGTERM. This lets long-running jobs clean up state or set a stop flag before the worker exits — instead of being killed mid-execution.
// app/Jobs/ProcessLargeDatasetJob.php
use Illuminate\Contracts\Queue\Interruptible;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessLargeDatasetJob implements ShouldQueue, Interruptible
{
use Queueable;
private bool $shouldStop = false;
public function interrupt(): void
{
// Called when the worker receives SIGTERM
$this->shouldStop = true;
Log::info('ProcessLargeDatasetJob: interrupt received, will stop after this batch.');
}
public function handle(): void
{
$records = DataRecord::where('processed', false)->cursor();
foreach ($records as $record) {
// Check before every iteration
if ($this->shouldStop) {
Log::info('Job stopped cleanly after interrupt signal.');
break;
}
$this->processRecord($record);
}
}
}
Without Interruptible, the job gets killed outright when SIGTERM arrives — which can leave data in an inconsistent state, especially for jobs that process records in batches or write to multiple tables. With this interface, you control exactly where the job stops.
@fonts Blade Directive
A new @fonts directive and Vite::fonts() method were added for rendering font preload links and inline styles. The feature reads font manifests generated by the Vite plugin and supports selective family loading:
<!-- resources/views/layouts/app.blade.php -->
<head>
{{-- Before: manually writing preload tags for each font file --}}
{{-- <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> --}}
{{-- <link rel="preload" href="/fonts/inter-bold.woff2" as="font" type="font/woff2" crossorigin> --}}
{{-- After: one directive handles everything --}}
@fonts
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
This renders all preload <link> tags and an inline <style> block with every @font-face rule and CSS variable from the font manifest. No more maintaining a manual font preload list every time you add or change a font in your Vite config.
Other Notable Additions in 13.9.0
Beyond toPasswordRulesString(), a handful of smaller but useful features landed in 13.9.0:
PendingDispatch Conditionable
PendingDispatch now implements the Conditionable trait, adding when() and unless() methods. This lets you configure dispatched jobs inline without wrapping the dispatch call in conditionals:
// ❌ BEFORE — verbose if/else around the dispatch
$job = SendPersonalDetailsToFraudDetectionTool::dispatch($customer);
if ($customer->hasSufficientPersonalDetails()) {
$job->withoutDelay();
}
// ✅ AFTER - inline, readable
SendPersonalDetailsToFraudDetectionTool::dispatch($customer)
->when(
$customer->hasSufficientPersonalDetails(),
fn ($job) => $job->withoutDelay()
);
// Or with unless():
SendPersonalDetailsToFraudDetectionTool::dispatch($customer)
->unless(
$customer->hasSufficientPersonalDetails(),
fn ($job) => $job->delay(180)
);
PreparesForDispatch Interface
The new PreparesForDispatch interface adds a prepareForDispatch() method that runs before a job is pushed to the queue. Returning false cancels the dispatch entirely — useful for deduplication or skipping work that's no longer needed:
// app/Jobs/SyncPodcastsJob.php
use Illuminate\Contracts\Queue\PreparesForDispatch;
class SyncPodcastsJob implements ShouldQueue, PreparesForDispatch
{
use Queueable;
public function __construct(public array $podcastIds) {}
public function prepareForDispatch(): bool
{
// Deduplicate IDs before the job enters the queue
$this->podcastIds = array_unique($this->podcastIds);
// Cancel dispatch entirely if there's nothing to sync
return count($this->podcastIds) > 0;
}
public function handle(): void
{
foreach ($this->podcastIds as $id) {
// sync podcast...
}
}
}
foreignUuidFor Schema Helper
A new foreignUuidFor() method on schema blueprints gives you a cleaner, model-aware API for UUID foreign key columns:
// ❌ BEFORE — verbose, manual column and reference definition
$table->uuid('user_id');
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
// ✅ AFTER - one line, inferred from the model
$table->foreignUuidFor(User::class)->constrained()->cascadeOnDelete();
// Automatically infers:
// - column name (user_id from model User)
// - table name (users)
// - referenced primary key column (id)
Concurrency::run() Timeout Support
Concurrency::run() now accepts a timeout parameter for the process driver. Previously, there was no way to customize the 60-second default timeout directly from Concurrency::run():
// Before: stuck with the 60-second default
Concurrency::run([
fn () => longRunningTask(),
]);
// After: set a custom timeout in seconds
Concurrency::run([
fn () => longRunningTask(),
fn () => anotherLongTask(),
], timeout: 300); // 5 minutes
Note: the fork driver is unchanged because spatie/fork doesn't support task timeouts, and the sync driver keeps its existing inline behavior.
What Changed from 13.7 to 13.9 — Quick Reference
If you’re evaluating whether to update, here’s a concise breakdown of each release’s focus:
Laravel 13.7.0 — Developer experience and runtime stability:
Interruptibleinterface for graceful job shutdown on worker signals@fontsdirective for automatic Vite font preload optimizationSortDirectionenum support in Collections- Bulk JSON path assertions for testing
Laravel 13.8.0 — Observability and testing:
allReservedJobs(),allDelayedJobs(),allPendingJobs()for queue-wide inspectionWorkerPausingandWorkerResumingevents for deployment visibilityassertSessionMissingInput()for security-focused testingSortDirectionenum support in the query builder
Laravel 13.9.0 — UX and queue improvements:
Password::toPasswordRulesString()for HTML passwordrules attributes ⭐PendingDispatchconditionable withwhen()andunless()PreparesForDispatchinterface for pre-dispatch hooksforeignUuidFor()schema helper for UUID foreign keysConcurrency::run()timeout support- Cloud queue metrics for Laravel Cloud deployments
How to Update
If you’re already on Laravel 13.x, updating to 13.9.0 is a single command:
composer update laravel/framework
No breaking changes — every feature across these three releases is additive and fully backward compatible. Run your test suite after updating to confirm everything is clean:
php artisan test
If you’re still on Laravel 12 and considering the jump to 13, make sure your server is running PHP 8.3 or higher first — that’s the minimum requirement for Laravel 13.
Closing
Password::toPasswordRulesString() is a textbook example of the philosophy that makes Laravel worth using: solving real user-facing problems with minimal ceremony on the developer side. One method, automatic synchronization between server-side validation and client-side hints, and a noticeably smoother registration experience for your users.
But the more interesting thing is the pattern across all three releases. Each version brings something that feels like “why didn’t this exist before?” — Interruptible jobs for clean shutdowns, queue-wide inspection in a single call, when() and unless() directly in the dispatch chain. Small individually. Significant when you run into one at 2 AM debugging a production issue.
Update, try toPasswordRulesString() on your next registration form, and see the difference for yourself.
메타데이터
- post_id
- 83d911fb569f
- slug
- generate-html-password-rules-in-laravel-13-9-0-plus-whats-new-from-13-7-to-13-9-83d911fb569f
- url
- https://medium.com/@developerawam/generate-html-password-rules-in-laravel-13-9-0-plus-whats-new-from-13-7-to-13-9-83d911fb569f
- canonical_url
- https://medium.com/@developerawam/generate-html-password-rules-in-laravel-13-9-0-plus-whats-new-from-13-7-to-13-9-83d911fb569f
- author_url
- https://medium.com/@developerawam
- status
- ok
- fetched_at
- 2026-06-09 15:37:30