lightmaker/complexify-bundle
A Symfony bundle that integrates Complexify password-strength checking into forms, providing configurable server-side validation and complexity rules to help enforce stronger passwords consistently across your application.
This is a Symfony 2 bundle, not a Laravel package — so "getting started" means deciding whether you're consuming the bundle or the algorithm. In 2016-era code the bundle is thin: a DI extension registering a complexify service, a Twig/asset hookup for jquery.complexify.js, and (usually) a validator constraint. The valuable part for a Laravel app is the server-side scoring class plus the JS meter.
Step 1 — check whether it can even install.
composer require lightmaker/complexify-bundle --dry-run
The bundle declares symfony/framework-bundle: ~2.x and friends. Modern Laravel ships Symfony 6/7 components (http-foundation, console, process), so the solver will usually refuse. Don't fight it. If it does resolve, you've likely downgraded Symfony internals you actually need — revert.
Step 2 — read the bundle in this order (clone it, or browse GitHub):
DependencyInjection/ — what service IDs and parameters it registers (complexify, complexify.min_complexity, ban-list path).Resources/config/services.xml — constructor arguments, especially the dictionary/ban-list file.Complexify class itself — this is Michael Crumley's PHP port, a single self-contained file with evaluateSecurity($password) returning something like ->valid and ->complexity (0–100).Resources/views/ and Resources/public/js/ — the Twig template and the jQuery plugin you'll copy into resources/js/vendor/.Validator/ or Form/ — how the bundle turned the score into a constraint; you'll rewrite this as a Laravel rule.Step 3 — first use case: registration + password change. Live strength meter on the client, hard floor on the server. That's the whole day-to-day value.
Step 4 — extract, don't depend. Copy the single algorithm class into app/Support/Complexify.php (keep the MIT header) or install the upstream mcrumley/php-complexify package directly. Treat the bundle as a reference implementation, not a dependency: last release 2016, zero dependents, zero stars means nobody is patching it for you.
Pattern A — the extracted service as a container singleton.
// app/Support/Complexify.php (copied from the bundle, MIT header retained)
namespace App\Support;
class Complexify
{
/** @var array<string,true> */
private array $banned = [];
public function __construct(?string $banListPath = null)
{
$path = $banListPath ?? resource_path('data/common-passwords.txt');
if (is_readable($path)) {
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$this->banned[strtolower(trim($line))] = true;
}
}
}
/** @return object{valid: bool, complexity: float} */
public function evaluateSecurity(string $password): object
{
// bundle's algorithm: length, character classes, repeats, dictionary hits
}
}
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(Complexify::class, fn () => new Complexify(
banListPath: config('auth.complexify.ban_list'),
));
}
Bind it as a singleton so the ban list is parsed once per request/worker, not once per field.
Pattern B — a Laravel validation rule wrapping it.
namespace App\Rules;
use App\Support\Complexify;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class ComplexEnough implements ValidationRule
{
public function __construct(
private int $minComplexity = 50,
private int $minLength = 10,
) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value) || $value === '') {
return; // let 'required' / 'string' report it
}
if (mb_strlen($value) < $this->minLength) {
$fail('The :attribute must be at least '.$this->minLength.' characters.');
return;
}
$result = app(Complexify::class)->evaluateSecurity($value);
if (! $result->valid || $result->complexity < $this->minComplexity) {
$fail('The :attribute is too easy to guess. Try a longer passphrase.');
}
}
}
$request->validate([
'password' => ['required', 'string', 'max:255', 'confirmed', new ComplexEnough(minComplexity: 50)],
]);
Pattern C — client meter, wired through Vite/Mix. Copy jquery.complexify.js out of the bundle's Resources/public/ into resources/js/vendor/, then:
{{-- resources/views/auth/partials/password.blade.php --}}
<div>
<label for="password">Password</label>
<input id="password" name="password" type="password"
data-complexify data-min-complexity="50"
@error('password') aria-invalid="true" aria-describedby="password-error" @enderror>
<div class="meter" aria-hidden="true"><span id="password-meter"></span></div>
<p id="password-strength" aria-live="polite"></p>
@error('password')<p id="password-error">{{ $message }}</p>@enderror
</div>
// resources/js/app.js
import $ from 'jquery';
import './vendor/jquery.complexify';
$(function () {
const $input = $('[data-complexify]');
if (!$input.length) return;
const $meter = $('#password-meter');
const $label = $('#password-strength');
const min = Number($input.data('min-complexity') ?? 50);
$input.complexify({ minimumChars: 10, strengthScaleFactor: 1 }, (valid, complexity) => {
const pct = Math.round(complexity);
$meter.css('width', `${pct}%`);
$label.text(pct >= min ? `Strong (${pct}%)` : `Too weak (${pct}%)`);
});
});
Pattern D — keep both sides honest. Use the same minimum (50 here), the same minimum length, and ideally the same ban list on both sides. The JS plugin's dictionary and the PHP port's dictionary drift; if you can't pin them, treat the client score as a hint only and let the server be the single source of truth.
Pattern E — test it like any other rule.
public function test_common_passwords_are_rejected(): void
{
$this->post('/register', [
'name' => 'Ada',
'email' => '[email protected]',
'password' => 'Password1',
'password_confirmation' => 'Password1',
])->assertSessionHasErrors('password');
}
public function test_passphrases_are_accepted(): void
{
$password = 'correct horse battery staple';
$this->post('/register', [
'name' => 'Ada',
'email' => '[email protected]',
'password' => $password,
'password_confirmation' => $password,
])->assertRedirect('/dashboard');
}
Pattern F — only then hash. Validate, then Hash::make($password). Never score the hash, and never store the score.
--dry-run before touching composer.json, and be prepared to abandon the bundle entirely.vendor/ dependency.Password::min(10)->letters()->numbers()->uncompromised() covers length, classes, and breach checking via HIBP. complexify adds a client-side entropy meter and a small dictionary — that's the marginal value. If you're on Laravel 10/11, consider zxcvbn-php for the server rule and a maintained meter on the client; the upgrade is usually a few hours and buys you a dictionary that isn't from 2014.minComplexity so high that correct horse battery staple fails — you'll train users to pick P@ssw0rd1! instead.file() the dictionary on every evaluateSecurity() call. Parse once in the constructor and register the class as a singleton; in Octane this matters more, not less.max:255 for the input is fine, but bcrypt truncates at 72 bytes. Validate max length explicitly so a 200-char passphrase doesn't get silently truncated into something weaker. Argon2id avoids the issue.config.yml keys (complexify.min_complexity, dictionary path) don't map to Laravel config; recreate them under config/auth.php or a dedicated config/complexify.php and reference them from the rule constructor so thresholds are tunable without code edits.Constraint.aria-live="polite" on the strength label and aria-describedby from the input to the error are the two things people forget; the meter <div> should be aria-hidden.How can I help you explore Laravel packages today?