From 98bafc784b18abb4ecb08e59dd9731d400ea21ec Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Thu, 15 Oct 2026 15:15:58 +0200 Subject: [PATCH] feat: Naming::headline(), the words of a name as Str::headline() writes them --- CHANGELOG.md | 4 ++++ src/Support/Naming.php | 19 +++++++++++++++++++ tests/Unit/NamingTest.php | 4 ++++ 3 files changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e1421..935827b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `Naming::headline()`, the words of a name with a capital each, as Laravel's `Str::headline()` writes them, for the packages that label a role or a binding source from its class name + ## [0.1.0] - 2026-10-12 A first version for the framework's next release to build on; 1.0.0 comes with that release. diff --git a/src/Support/Naming.php b/src/Support/Naming.php index 0640640..fd83d74 100644 --- a/src/Support/Naming.php +++ b/src/Support/Naming.php @@ -38,6 +38,25 @@ public static function kebab(string $value): string return self::snake($value, '-'); } + /** + * The value as words with a capital each, as Laravel's Str::headline() + * writes it: `EventManager`, `event_manager` and `event-manager` give + * `Event Manager`. + */ + public static function headline(string $value): string + { + $words = mb_split('\s+', $value) ?: [$value]; + $title = static fn (string $word): string => mb_convert_case($word, MB_CASE_TITLE, 'UTF-8'); + + $parts = count($words) > 1 + ? array_map($title, $words) + : array_map($title, preg_split('/(?=\p{Lu})/u', implode('_', $words), -1, PREG_SPLIT_NO_EMPTY) ?: []); + + $collapsed = str_replace(['-', '_', ' '], '_', implode('_', $parts)); + + return implode(' ', array_filter(explode('_', $collapsed))); + } + /** * The class name without its namespace. */ diff --git a/tests/Unit/NamingTest.php b/tests/Unit/NamingTest.php index 5866e68..7ac4497 100644 --- a/tests/Unit/NamingTest.php +++ b/tests/Unit/NamingTest.php @@ -33,6 +33,10 @@ expect(Naming::kebab($name))->toBe(Str::kebab($name)); })->with('names'); +it('writes headlines as Str::headline() does', function (string $name): void { + expect(Naming::headline($name))->toBe(Str::headline($name)); +})->with('names')->with(['kebab-case' => 'event-manager', 'snake with digits' => 'event_manager_2', 'several words' => 'Event manager role', 'empty' => '']); + it('strips the namespace as class_basename() does', function (): void { expect(Naming::classBasename('App\\Cms\\PostTypes\\Book'))->toBe(class_basename('App\\Cms\\PostTypes\\Book')) ->and(Naming::classBasename('Book'))->toBe('Book');