Skip to content
Merged

Dev #40

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e0d2238
Add Laravel 13 to CI workflow matrix
nicobleiler Apr 14, 2026
8298329
chore: update Trivy action to version 0.36.0 in workflow
nicobleiler Apr 30, 2026
2e18b78
chore: remove php82 support from Rector configuration
nicobleiler Apr 30, 2026
f84b910
refactor: fully_qualified_strict_types
nicobleiler Apr 30, 2026
78e0f01
chore: rename phpstan configuration file
nicobleiler Apr 30, 2026
ea55cb1
chore: update .gitignore to include phpstan.neon
nicobleiler Apr 30, 2026
8142fc3
fix: add missing type annotation for words array in loadBundledEff me…
nicobleiler Apr 30, 2026
1f0a069
fix: add missing type annotations for word list and excluded words in…
nicobleiler Apr 30, 2026
baeb52e
feat: add ConfigException for validation and update PassphraseService…
nicobleiler Apr 30, 2026
782815f
feat: enhance ConfigException for better validation messages and upda…
nicobleiler Apr 30, 2026
aeb829d
fix: reorder scripts in composer.json for consistency
nicobleiler Apr 30, 2026
e054418
feat: add validation tests for non-integer num_words, non-string word…
nicobleiler Apr 30, 2026
f5df767
refactor: Refactored cachedEff for better typing
nicobleiler Apr 30, 2026
7fe0dd8
refactor: update PassphraseGenerator singleton to use Application typ…
nicobleiler Apr 30, 2026
d23edc7
fix: update PHPStan level from 9 to 10 for improved analysis
nicobleiler Apr 30, 2026
47fc756
fix: remove unused NewlineBetweenClassLikeStmtsRector from configuration
nicobleiler Apr 30, 2026
adabe5a
fix: update orchestra/testbench requirement to support version 11.0
nicobleiler Apr 30, 2026
416f6b5
fix: enable platform automerge for dev branch in Renovate configuration
nicobleiler Apr 30, 2026
2af0b76
chore(deps): update dependency ubuntu to v24
renovate[bot] Apr 30, 2026
1ebb0eb
Merge pull request #34 from nicobleiler/renovate/ubuntu-24.x
nicobleiler Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ concurrency:

jobs:
quality:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
shell: bash
Expand Down Expand Up @@ -64,16 +64,14 @@ jobs:
matrix:
os: ${{ fromJSON(github.ref_name == 'master' && '["ubuntu-22.04", "windows-2022"]' || '["ubuntu-22.04"]') }}
php: ["8.2", "8.3", "8.4", "8.5"]
laravel: ["11", "12"]
laravel: ["11", "12", "13"]
exclude:
- php: 8.4
laravel: 11
- php: 8.5
laravel: 11
- php: 8.2
laravel: 12
- php: 8.3
laravel: 12
laravel: 13

name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }}

Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/trivy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v6

- name: Run Trivy vulnerability scanner (all severities for SARIF)
uses: aquasecurity/trivy-action@0.34.1
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: fs
scan-ref: .
Expand All @@ -39,13 +39,15 @@ jobs:
ignore-unfixed: true

- name: Upload Trivy scan results to GitHub Security tab
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
if: ${{ github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name ==
github.repository }}
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy-results.sarif

- name: Fail build on HIGH/CRITICAL vulnerabilities
uses: aquasecurity/trivy-action@0.34.1
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: fs
scan-ref: .
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
*.swo
.DS_Store
Thumbs.db
.phpstan/cache/
.phpstan/cache/
phpstan.neon
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"ext-mbstring": "*"
},
"require-dev": {
"orchestra/testbench": "^9.0 || ^10.0",
"orchestra/testbench": "^9.0 || ^10.0 || ^11.0",
"phpunit/phpunit": "^11.4 || ^12.0 || ^13.0",
"phpstan/phpstan": "^2.1",
"laravel/pint": "^1.27",
Expand Down Expand Up @@ -52,8 +52,8 @@
"scripts": {
"analyse": "phpstan analyse --no-progress --memory-limit=1G",
"lint": [
"pint",
"rector"
"rector",
"pint"
],
"test:lint": [
"pint --test",
Expand All @@ -63,8 +63,8 @@
"test:types": "phpstan",
"test": [
"@test:lint",
"@test:unit",
"@test:types"
"@test:types",
"@test:unit"
],
"bench": "vendor/bin/phpbench run --report=providers",
"bench:baseline": "vendor/bin/phpbench run --tag=baseline --report=providers",
Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon → phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
parameters:
level: 8
level: 10
paths:
- src
treatPhpDocTypesAsCertain: false
Expand Down
4 changes: 1 addition & 3 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

declare(strict_types=1);

use Rector\CodingStyle\Rector\ClassLike\NewlineBetweenClassLikeStmtsRector;
use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector;
use Rector\Config\RectorConfig;
use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
Expand All @@ -15,7 +14,6 @@
->withSkip([
ReadOnlyPropertyRector::class,
EncapsedStringsToSprintfRector::class,
NewlineBetweenClassLikeStmtsRector::class,
])
->withPreparedSets(
deadCode: true,
Expand All @@ -24,4 +22,4 @@
typeDeclarations: true,
earlyReturn: true,
)
->withPhpSets(php82: true);
->withPhpSets();
4 changes: 2 additions & 2 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
"digest"
],
"automerge": true,
"platformAutomerge": false,
"platformAutomerge": true,
"automergeType": "pr",
"matchBaseBranches": [
"dev"
]
}
]
}
}
40 changes: 40 additions & 0 deletions src/Exceptions/ConfigException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace NicoBleiler\Passphrase\Exceptions;

use RuntimeException;

class ConfigException extends RuntimeException
{
public static function invalidNumWords(): self
{
return new self('Number of words config must be an integer');
}

public static function invalidWordSeparator(): self
{
return new self('Word separator config must be a string');
}

public static function invalidCapitalize(): self
{
return new self('Capitalize config must be a boolean');
}

public static function invalidIncludeNumber(): self
{
return new self('Include number config must be a boolean');
}

public static function invalidWordList(): self
{
return new self('Word list config must be an array of strings');
}

public static function invalidExcludedWords(): self
{
return new self('Excluded words config must be an array of strings');
}
}
10 changes: 0 additions & 10 deletions src/Exceptions/WordListException.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@

class WordListException extends RuntimeException
{
public static function invalidConfigType(): self
{
return new self('Word list config must be an array of strings');
}

public static function fileNotFound(string $path): self
{
return new self("Word list file not found: {$path}");
Expand All @@ -37,9 +32,4 @@ public static function invalidExcludedWordsType(): self
{
return new self('Excluded words must contain only strings');
}

public static function invalidExcludedWordsConfigType(): self
{
return new self('Excluded words config must be an array of strings');
}
}
43 changes: 33 additions & 10 deletions src/PassphraseServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,65 @@

namespace NicoBleiler\Passphrase;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use NicoBleiler\Passphrase\Exceptions\WordListException;
use NicoBleiler\Passphrase\Exceptions\ConfigException;

class PassphraseServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(__DIR__.'/../config/passphrase.php', 'passphrase');

$this->app->singleton(WordList::class, function (): \NicoBleiler\Passphrase\WordList {
$this->app->singleton(WordList::class, function (): WordList {
$wordList = config('passphrase.word_list');
$excludedWords = config('passphrase.excluded_words', []);

if (! is_array($excludedWords)) {
throw WordListException::invalidExcludedWordsConfigType();
throw ConfigException::invalidExcludedWords();
}

/** @var array<string> $excludedWords */
if ($wordList !== null) {
if (! is_array($wordList)) {
throw WordListException::invalidConfigType();
throw ConfigException::invalidWordList();
}

/** @var array<string> $wordList */

return WordList::fromArray($wordList)->excludeWords($excludedWords);
Comment on lines 21 to 33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate word_list/excluded_words element types in provider.

Line 21 and Line 27 only validate that values are arrays. Arrays containing non-strings currently bypass this layer and fail later with WordListException, which breaks the new config-validation exception contract.

🔧 Proposed fix
         if (! is_array($excludedWords)) {
             throw ConfigException::invalidExcludedWords();
         }
+        foreach ($excludedWords as $excludedWord) {
+            if (! is_string($excludedWord)) {
+                throw ConfigException::invalidExcludedWords();
+            }
+        }

         /** `@var` array<string> $excludedWords */
         if ($wordList !== null) {
             if (! is_array($wordList)) {
                 throw ConfigException::invalidWordList();
             }
+            foreach ($wordList as $configuredWord) {
+                if (! is_string($configuredWord)) {
+                    throw ConfigException::invalidWordList();
+                }
+            }

             /** `@var` array<string> $wordList */

As per coding guidelines: "Keep error handling explicit with package exception types where appropriate".

🧰 Tools
🪛 PHPMD (2.15.0)

[error] 22-22: Avoid using static access to class '\NicoBleiler\Passphrase\Exceptions\ConfigException' in method 'register'. (undefined)

(StaticAccess)


[error] 28-28: Avoid using static access to class '\NicoBleiler\Passphrase\Exceptions\ConfigException' in method 'register'. (undefined)

(StaticAccess)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/PassphraseServiceProvider.php` around lines 21 - 33, The provider
currently only checks that $excludedWords and $wordList are arrays, allowing
non-string elements to slip through and later raise WordListException; update
the validation in PassphraseServiceProvider to ensure every element is a string
before calling WordList::fromArray — for example, after confirming
is_array($excludedWords) and is_array($wordList) validate with
array_filter/is_string (or a simple loop) and if any element is not a string
throw ConfigException::invalidExcludedWords() or
ConfigException::invalidWordList() respectively so the config-validation
exception contract is preserved.

}

return WordList::eff()->excludeWords($excludedWords);
});

$this->app->singleton(PassphraseGenerator::class, function ($app): \NicoBleiler\Passphrase\PassphraseGenerator {
$generator = new PassphraseGenerator($app->make(WordList::class));
$this->app->singleton(PassphraseGenerator::class, function (Application $app): PassphraseGenerator {
$numWords = config('passphrase.num_words', PassphraseGenerator::DEFAULT_NUM_WORDS);
if (! is_int($numWords)) {
throw ConfigException::invalidNumWords();
}

$wordSeparator = config('passphrase.word_separator', PassphraseGenerator::DEFAULT_WORD_SEPARATOR);
if (! is_string($wordSeparator)) {
throw ConfigException::invalidWordSeparator();
}

$capitalize = config('passphrase.capitalize', PassphraseGenerator::DEFAULT_CAPITALIZE);
if (! is_bool($capitalize)) {
throw ConfigException::invalidCapitalize();
}

$includeNumber = config('passphrase.include_number', PassphraseGenerator::DEFAULT_INCLUDE_NUMBER);
if (! is_bool($includeNumber)) {
throw ConfigException::invalidIncludeNumber();
}

$generator = new PassphraseGenerator($app->make(WordList::class));
$generator->setDefaults(
numWords: (int) config('passphrase.num_words', PassphraseGenerator::DEFAULT_NUM_WORDS),
wordSeparator: (string) config('passphrase.word_separator', PassphraseGenerator::DEFAULT_WORD_SEPARATOR),
capitalize: (bool) config('passphrase.capitalize', PassphraseGenerator::DEFAULT_CAPITALIZE),
includeNumber: (bool) config('passphrase.include_number', PassphraseGenerator::DEFAULT_INCLUDE_NUMBER),
numWords: $numWords,
wordSeparator: $wordSeparator,
capitalize: $capitalize,
includeNumber: $includeNumber,
);

return $generator;
Expand Down
8 changes: 5 additions & 3 deletions src/WordList.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class WordList

private ?float $entropyPerWord = null;

/** Caches the parsed EFF word list so the file is only read once per process, regardless of how many derived lists (e.g. with excluded words) are created from it. */
private static ?self $cachedEff = null;

/**
* @param string[] $words
*/
Expand All @@ -29,9 +32,7 @@ private function __construct(array $words)
*/
public static function eff(): self
{
static $cachedEff = null;

return $cachedEff ??= self::loadBundledEff();
return self::$cachedEff ??= self::loadBundledEff();
}

/**
Expand All @@ -51,6 +52,7 @@ private static function loadBundledEff(): self
throw WordListException::empty();
}

/** @var array<string> $words */
return self::fromArray($words);
}

Expand Down
50 changes: 47 additions & 3 deletions tests/LaravelIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace NicoBleiler\Passphrase\Tests;

use NicoBleiler\Passphrase\Exceptions\WordListException;
use NicoBleiler\Passphrase\Exceptions\ConfigException;
use NicoBleiler\Passphrase\Facades\Passphrase;
use NicoBleiler\Passphrase\PassphraseGenerator;
use NicoBleiler\Passphrase\PassphraseServiceProvider;
Expand Down Expand Up @@ -125,7 +125,7 @@ public function test_non_array_word_list_config_throws_clear_message(): void

$this->refreshServiceProvider();

$this->expectExceptionObject(WordListException::invalidConfigType());
$this->expectExceptionObject(ConfigException::invalidWordList());

$this->app->make(WordList::class);
}
Expand All @@ -139,7 +139,7 @@ public function test_non_array_excluded_words_config_throws_clear_message(): voi

$this->refreshServiceProvider();

$this->expectExceptionObject(WordListException::invalidExcludedWordsConfigType());
$this->expectExceptionObject(ConfigException::invalidExcludedWords());

$this->app->make(WordList::class);
}
Expand Down Expand Up @@ -212,6 +212,50 @@ public function test_config_include_number_is_applied(): void
$this->assertMatchesRegularExpression('/\d/', $result, 'Expected passphrase to contain a digit');
}

public function test_non_integer_num_words_config_throws(): void
{
config(['passphrase.num_words' => 'three']);

$this->refreshServiceProvider();

$this->expectExceptionObject(ConfigException::invalidNumWords());

$this->app->make(PassphraseGenerator::class);
}

public function test_non_string_word_separator_config_throws(): void
{
config(['passphrase.word_separator' => 42]);

$this->refreshServiceProvider();

$this->expectExceptionObject(ConfigException::invalidWordSeparator());

$this->app->make(PassphraseGenerator::class);
}

public function test_non_boolean_capitalize_config_throws(): void
{
config(['passphrase.capitalize' => 'yes']);

$this->refreshServiceProvider();

$this->expectExceptionObject(ConfigException::invalidCapitalize());

$this->app->make(PassphraseGenerator::class);
}

public function test_non_boolean_include_number_config_throws(): void
{
config(['passphrase.include_number' => 1]);

$this->refreshServiceProvider();

$this->expectExceptionObject(ConfigException::invalidIncludeNumber());

$this->app->make(PassphraseGenerator::class);
}

public function test_target_entropy_bits_with_excluded_words_config(): void
{
$words = ['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', 'golf', 'hotel'];
Expand Down