33[ ![ Latest Version] ( https://img.shields.io/packagist/v/nicobleiler/php-passphrase.svg )] ( https://packagist.org/packages/nicobleiler/php-passphrase )
44[ ![ Downloads] ( https://img.shields.io/packagist/dt/nicobleiler/php-passphrase.svg )] ( https://packagist.org/packages/nicobleiler/php-passphrase )
55[ ![ PHP Version] ( https://img.shields.io/packagist/php-v/nicobleiler/php-passphrase.svg )] ( https://packagist.org/packages/nicobleiler/php-passphrase )
6+ [ ![ Code Size] ( https://img.shields.io/github/languages/code-size/nicobleiler/php-passphrase )] ( https://github.com/nicobleiler/php-passphrase )
7+ [ ![ Wordlist Size] ( https://img.shields.io/github/size/nicobleiler/php-passphrase/resources/wordlists/eff_large_wordlist.php?label=wordlist )] ( https://github.com/nicobleiler/php-passphrase/blob/master/resources/wordlists/eff_large_wordlist.php )
68[ ![ CI] ( https://github.com/nicobleiler/php-passphrase/actions/workflows/test.yml/badge.svg )] ( https://github.com/nicobleiler/php-passphrase/actions/workflows/test.yml )
79[ ![ License] ( https://img.shields.io/packagist/l/nicobleiler/php-passphrase.svg )] ( LICENSE )
810
@@ -110,7 +112,7 @@ echo $generator->generate(); // deterministic output
110112
111113| Parameter | Type | Default | Description |
112114| ---| ---| ---| ---|
113- | ` numWords ` | ` ?int ` | ` 3 ` | Number of words (3–20 ). ` null ` uses instance/config default. |
115+ | ` numWords ` | ` ?int ` | ` 3 ` | Number of words (minimum of 3 ). ` null ` uses instance/config default. |
114116| ` wordSeparator ` | ` ?string ` | ` '-' ` | Character(s) between words. ` null ` uses instance/config default. |
115117| ` capitalize ` | ` ?bool ` | ` false ` | Capitalize the first letter of each word. ` null ` uses instance/config default. |
116118| ` includeNumber ` | ` ?bool ` | ` false ` | Append a random digit (0–9) to one random word. ` null ` uses instance/config default. |
@@ -139,6 +141,10 @@ return [
139141 // null = bundled EFF long word list (7,776 words)
140142 // Or provide your own word list as a PHP array of strings
141143 'word_list' => null,
144+
145+ // Optional words to remove from the active word list
146+ // Works with both bundled EFF and custom word_list values
147+ 'excluded_words' => [],
142148];
143149```
144150
@@ -153,6 +159,9 @@ Provide `word_list` as a PHP array of strings:
153159``` php
154160// config/passphrase.php
155161'word_list' => ['correct', 'horse', 'battery', 'staple'],
162+
163+ // Optionally remove specific words from the active list
164+ 'excluded_words' => ['horse'],
156165```
157166
158167Or load it from a dedicated PHP file:
@@ -193,10 +202,49 @@ You can also publish the bundled EFF word list to your resources folder:
193202php artisan vendor:publish --tag=passphrase-wordlists
194203```
195204
205+ This is optional and mainly useful if you want a local copy to inspect or customize. By default, the package reads the bundled EFF list directly.
206+
196207## How It Works
197208
198209The generation algorithm mirrors [ Bitwarden's Rust implementation] ( https://sdk-api-docs.bitwarden.com/src/bitwarden_generators/passphrase.rs.html ) :
199210
211+ ### Sequence diagram
212+
213+ ``` mermaid
214+ sequenceDiagram
215+ participant Client
216+ participant ServiceProvider
217+ participant PassphraseGenerator
218+ participant Randomizer
219+ participant WordList
220+
221+ Client->>ServiceProvider: Boot (Laravel)
222+ ServiceProvider->>WordList: Create (singleton)
223+ ServiceProvider->>PassphraseGenerator: Create with Randomizer(Secure)
224+ ServiceProvider->>PassphraseGenerator: setDefaults(config values)
225+ activate PassphraseGenerator
226+ PassphraseGenerator-->>ServiceProvider: self (fluent)
227+ deactivate PassphraseGenerator
228+
229+ Client->>PassphraseGenerator: generate() [no params]
230+ activate PassphraseGenerator
231+ PassphraseGenerator->>PassphraseGenerator: Use instance defaults
232+ PassphraseGenerator->>Randomizer: getInt() [select words]
233+ activate Randomizer
234+ Randomizer-->>PassphraseGenerator: random indices
235+ deactivate Randomizer
236+ PassphraseGenerator->>WordList: wordAt(index)
237+ activate WordList
238+ WordList-->>PassphraseGenerator: word string
239+ deactivate WordList
240+ PassphraseGenerator->>Randomizer: getInt() [digit if needed]
241+ activate Randomizer
242+ Randomizer-->>PassphraseGenerator: random digit
243+ deactivate Randomizer
244+ PassphraseGenerator-->>Client: passphrase string
245+ deactivate PassphraseGenerator
246+ ```
247+
200248## Testing
201249
202250``` bash
@@ -219,40 +267,46 @@ The test suite includes tests modeled after Bitwarden's own test cases:
219267- EFF word list integrity
220268- Laravel integration (service provider, facade, config defaults)
221269
222- ## Performance (2026-02-14 02:41:47 MEZ )
270+ ## Performance (2026-02-16 )
223271
224272These benchmarks were run on a local Ryzen 9 5950X machine running Windows 11 with PHP 8.5.0.
225273
226- In warm-run benchmarks at similar entropy targets, php-passphrase is ~ 4.4× faster than genphrase/genphrase and ~ 1333× faster than martbock/laravel-diceware (based on mean time per generation).
227-
228- In cold-run benchmarks (startup + first generation), php-passphrase is ~ 5.6× faster than genphrase/genphrase and ~ 12.8× faster than martbock/laravel-diceware (based on mean time per generation).
274+ In this run the available benchmark providers were: ` php-passphrase ` , ` genphrase/genphrase ` , ` martbock/laravel-diceware ` , ` random_bytes ` , ` Illuminate\\Support\\Str::random ` , and ` Illuminate\\Support\\Str::password ` (providers are included automatically when their packages are installed).
229275
230276> ** Note on cold runs:** ` benchGenerateCold ` includes setup and first-use initialization (autoloading, object construction, and initial word-list work). Cold-run ` rstdev ` is therefore expected to be higher and should be interpreted as startup-cost signal, not steady-state throughput.
231277
232- ```
278+ ``` text
233279benchGenerateCold
234- +----------------+-----------------------------------------------------+------+-----+-----------+-----------+---------+----------+
235- | benchmark | set | revs | its | mem_peak | mode | mean | rstdev |
236- +----------------+-----------------------------------------------------+------+-----+-----------+-----------+---------+----------+
237- | ProvidersBench | php-passphrase (EFF 5 words, ~64.6 bits) | 1 | 20 | 1.614mb | 127.847μs | 320.5μs | ±241.90% |
238- | ProvidersBench | genphrase/genphrase (65-bit target, diceware) | 1 | 20 | 1.364mb | 1.654ms | 1.806ms | ±27.58% |
239- | ProvidersBench | martbock/laravel-diceware (EFF 5 words, ~64.6 bits) | 1 | 20 | 957.688kb | 3.608ms | 4.106ms | ±35.28% |
240- | ProvidersBench | random_bytes(8) hex (~64 bits) | 1 | 20 | 493.784kb | 7.74μs | 8.8μs | ±21.14% |
241- | ProvidersBench | Illuminate\Support\Str::random(11) (~65.5 bits) | 1 | 20 | 493.8kb | 175.25μs | 241μs | ±79.83% |
242- +----------------+-----------------------------------------------------+------+-----+-----------+-----------+---------+----------+
280+ +----------------+--------------------------------------------------------------------+------+-----+-----------+-----------+----------+----------+
281+ | benchmark | set | revs | its | mem_peak | mode | mean | rstdev |
282+ +----------------+--------------------------------------------------------------------+------+-----+-----------+-----------+----------+----------+
283+ | ProvidersBench | php-passphrase (EFF 5 words, ~64.6 bits) | 1 | 20 | 1.612mb | 331.431μs | 504μs | ±141.06% |
284+ | ProvidersBench | genphrase/genphrase (65-bit target, diceware) | 1 | 20 | 1.366mb | 1.662ms | 3.788ms | ±240.86% |
285+ | ProvidersBench | martbock/laravel-diceware (EFF 5 words, ~64.6 bits) | 1 | 20 | 958.76kb | 3.68ms | 4.745ms | ±82.04% |
286+ | ProvidersBench | random_bytes(8) hex (~64 bits) | 1 | 20 | 494.856kb | 9.818μs | 11.6μs | ±35.59% |
287+ | ProvidersBench | Illuminate\Support\Str::random(11) (~65.5 bits) | 1 | 20 | 494.872kb | 182.63μs | 245.95μs | ±77.25% |
288+ | ProvidersBench | Illuminate\Support\Str::password(10) (default options, ~64.6 bits) | 1 | 20 | 1.143mb | 921.507μs | 1.371ms | ±132.20% |
289+ +----------------+--------------------------------------------------------------------+------+-----+-----------+-----------+----------+----------+
243290
244291benchGenerateWarm
245- +----------------+-----------------------------------------------------+------+-----+-----------+---------+---------+--------+
246- | benchmark | set | revs | its | mem_peak | mode | mean | rstdev |
247- +----------------+-----------------------------------------------------+------+-----+-----------+---------+---------+--------+
248- | ProvidersBench | php-passphrase (EFF 5 words, ~64.6 bits) | 100 | 20 | 494.048kb | 1.596μs | 1.612μs | ±3.79% |
249- | ProvidersBench | genphrase/genphrase (65-bit target, diceware) | 100 | 20 | 1.363mb | 6.956μs | 7.091μs | ±6.42% |
250- | ProvidersBench | martbock/laravel-diceware (EFF 5 words, ~64.6 bits) | 100 | 20 | 508.944kb | 2.161ms | 2.149ms | ±2.87% |
251- | ProvidersBench | random_bytes(8) hex (~64 bits) | 100 | 20 | 494.04kb | 0.125μs | 0.125μs | ±7.38% |
252- | ProvidersBench | Illuminate\Support\Str::random(11) (~65.5 bits) | 100 | 20 | 494.056kb | 0.56μs | 0.565μs | ±3.86% |
253- +----------------+-----------------------------------------------------+------+-----+-----------+---------+---------+--------+
292+ +----------------+--------------------------------------------------------------------+------+-----+-----------+---------+----------+---------+
293+ | benchmark | set | revs | its | mem_peak | mode | mean | rstdev |
294+ +----------------+--------------------------------------------------------------------+------+-----+-----------+---------+----------+---------+
295+ | ProvidersBench | php-passphrase (EFF 5 words, ~64.6 bits) | 100 | 20 | 495.12kb | 1.353μs | 1.406μs | ±14.18% |
296+ | ProvidersBench | genphrase/genphrase (65-bit target, diceware) | 100 | 20 | 1.364mb | 6.715μs | 6.829μs | ±3.74% |
297+ | ProvidersBench | martbock/laravel-diceware (EFF 5 words, ~64.6 bits) | 100 | 20 | 510.016kb | 2.099ms | 2.073ms | ±2.68% |
298+ | ProvidersBench | random_bytes(8) hex (~64 bits) | 100 | 20 | 495.112kb | 0.125μs | 0.132μs | ±24.62% |
299+ | ProvidersBench | Illuminate\Support\Str::random(11) (~65.5 bits) | 100 | 20 | 495.128kb | 0.532μs | 0.563μs | ±16.54% |
300+ | ProvidersBench | Illuminate\Support\Str::password(10) (default options, ~64.6 bits) | 100 | 20 | 587.672kb | 11.86μs | 11.927μs | ±2.81% |
301+ +----------------+--------------------------------------------------------------------+------+-----+-----------+---------+----------+---------+
254302```
255303
304+ Relative comparisons (mean times):
305+
306+ - Cold run: ` php-passphrase ` (504 μs) is ~ 7.5× faster than ` genphrase ` (3.788 ms) and ~ 9.4× faster than ` martbock/laravel-diceware ` (4.745 ms).
307+ - Warm run: ` php-passphrase ` (1.406 μs) is ~ 4.9× faster than ` genphrase ` (6.829 μs) and ~ 1,475× faster than ` martbock/laravel-diceware ` (2.073 ms).
308+
309+ These values are environment-dependent; run ` composer bench ` locally if you need numbers for a different machine or PHP version.
256310## Benchmarking
257311
258312Run the benchmark suite with:
@@ -283,10 +337,55 @@ vendor/bin/phpbench run --report=providers --iterations=40 --revs=5 --retry-thre
283337Compared providers:
284338
285339- ` php-passphrase ` with EFF 5 words (~ 64.6 bits)
286- - ` genphrase/genphrase ` with a 65-bit target on diceware mode
287- - ` martbock/laravel-diceware ` with EFF 5 words (~ 64.6 bits)
288340- ` random_bytes(8) ` (~ 64 bits)
289341- ` Illuminate\\Support\\Str::random(11) ` (~ 65.5 bits)
342+ - ` Illuminate\\Support\\Str::password(10) ` with default options (~ 64.6 bits)
343+
344+ Optional providers can be added ` composer require --dev genphrase/genphrase martbock/laravel-diceware ` and will be included in the benchmark suite if present:
345+
346+ - ` genphrase/genphrase ` with a 65-bit target on diceware mode
347+ - ` martbock/laravel-diceware ` with EFF 5 words (~ 64.6 bits)
348+
349+ The ` eurosat7/random ` package cannot currently be required directly via Composer VCS because its upstream ` composer.json ` has no valid package ` name ` .
350+
351+ Use a local package-repository override in your ` composer.json ` instead:
352+
353+ ``` json
354+ {
355+ "repositories" : [
356+ {
357+ "type" : " package" ,
358+ "package" : {
359+ "name" : " eurosat7/random" ,
360+ "version" : " dev-main" ,
361+ "source" : {
362+ "type" : " git" ,
363+ "url" : " https://github.com/eurosat7/random.git" ,
364+ "reference" : " main"
365+ },
366+ "autoload" : {
367+ "psr-4" : {
368+ "Eurosat7\\ Random\\ " : " src/"
369+ }
370+ },
371+ "require" : {
372+ "php" : " >=8.2"
373+ }
374+ }
375+ }
376+ ]
377+ }
378+ ```
379+
380+ Then run:
381+
382+ ``` bash
383+ composer require --dev eurosat7/random:dev-main
384+ ```
385+
386+ When installed, the benchmark also includes:
387+
388+ - ` eurosat7/random ` via ` Eurosat7\Random\Generator::password(10) ` (~ 64+ bits)
290389
291390Baseline and comparison runs:
292391
0 commit comments