Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor CamelCaseToSeparator Filter #186

Merged
merged 4 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions docs/book/v3/migration/v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,51 @@ The impact of the removal of these aliases will not affect you if you use a FQCN

### Changes to Individual Filters

#### `CamelCaseToDash`
gsteel marked this conversation as resolved.
Show resolved Hide resolved

The following methods have been removed:

- `setOptions`
- `getOptions`
- `isOptions`
- `setSeparator`
- `getSeparator`

The constructor now only accepts an associative array of [documented options](../word.md#camelCaseToDash).

The filter will now treat numbers as a word boundary.
For example `ThisHas4Words` will filter to `This-Has-4-Words`

#### `CamelCaseToSeparator`

The following methods have been removed:

- `setOptions`
- `getOptions`
- `isOptions`
- `setSeparator`
- `getSeparator`

The constructor now only accepts an associative array of [documented options](../word.md#camelCaseToSeparator).

The filter will now treat numbers as a word boundary.
For example `ThisHas4Words` with the default separator will filter to `This Has 4 Words`

#### `CamelCaseToUnderscore`

The following methods have been removed:

- `setOptions`
- `getOptions`
- `isOptions`
- `setSeparator`
- `getSeparator`

The constructor now only accepts an associative array of [documented options](../word.md#camelCaseToUnderscore).

The filter will now treat numbers as a word boundary.
For example `ThisHas4Words` will filter to `This_Has_4_Words`

#### `DashToSeparator`

The following methods have been removed:
Expand Down
15 changes: 13 additions & 2 deletions docs/book/v3/word.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ to filtering word strings.

## CamelCaseToDash

TIP: **New Behaviour since Version 3**
The filter will now treat numbers as a word boundary.
For example `ThisHas4Words` will filter to `This-Has-4-Words`.

This filter modifies a given string such that `CamelCaseWords` are converted to `Camel-Case-Words`.

### Supported Options
Expand All @@ -23,6 +27,10 @@ The above example returns `This-Is-My-Content`.

## CamelCaseToSeparator

TIP: **New Behaviour since Version 3**
The filter will now treat numbers as a word boundary.
For example `ThisHas4Words` with the default separator will filter to `This Has 4 Words`

This filter modifies a given string such that `CamelCaseWords` are converted to `Camel Case Words`.

### Supported Options
Expand All @@ -35,8 +43,7 @@ The following options are supported for `Laminas\Filter\Word\CamelCaseToSeparato
### Basic Usage

```php
$filter = new Laminas\Filter\Word\CamelCaseToSeparator(':');
// or new Laminas\Filter\Word\CamelCaseToSeparator(array('separator' => ':'));
$filter = new Laminas\Filter\Word\CamelCaseToSeparator(['separator' => ':']);

print $filter->filter('ThisIsMyContent');
```
Expand All @@ -55,6 +62,10 @@ The above example returns `This Is My Content`.

## CamelCaseToUnderscore

TIP: **New Behaviour since Version 3**
The filter will now treat numbers as a word boundary.
For example `ThisHas4Words` will filter to `This_Has_4_Words`

This filter modifies a given string such that `CamelCaseWords` are converted to
`Camel_Case_Words`.

Expand Down
8 changes: 4 additions & 4 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -961,15 +961,15 @@
</file>
<file src="test/Word/CamelCaseToSeparatorTest.php">
<PossiblyUnusedMethod>
<code><![CDATA[camelCasedWordsProvider]]></code>
<code><![CDATA[returnNonStringScalarValues]]></code>
<code><![CDATA[returnUnfilteredDataProvider]]></code>
</PossiblyUnusedMethod>
</file>
<file src="test/Word/CamelCaseToUnderscoreTest.php">
<MixedAssignment>
<code><![CDATA[$filtered]]></code>
<code><![CDATA[$filtered]]></code>
</MixedAssignment>
<PossiblyUnusedMethod>
<code><![CDATA[camelCasedWordsProvider]]></code>
</PossiblyUnusedMethod>
</file>
<file src="test/Word/DashToSeparatorTest.php">
<PossiblyUnusedMethod>
Expand Down
24 changes: 13 additions & 11 deletions src/Word/CamelCaseToDash.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@

namespace Laminas\Filter\Word;

/**
* @psalm-type Options = array{
* separator?: string,
* ...
* }
* @template TOptions of Options
* @extends CamelCaseToSeparator<TOptions>
*/
final class CamelCaseToDash extends CamelCaseToSeparator
use Laminas\Filter\FilterInterface;

/** @implements FilterInterface<string|array<array-key, string|mixed>> */
final class CamelCaseToDash implements FilterInterface
{
public function __construct()
public function filter(mixed $value): mixed
{
$filter = new CamelCaseToSeparator(['separator' => '-']);

return $filter->filter($value);
}

public function __invoke(mixed $value): mixed
{
parent::__construct('-');
return $this->filter($value);
}
}
64 changes: 39 additions & 25 deletions src/Word/CamelCaseToSeparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,57 @@

namespace Laminas\Filter\Word;

use Closure;
use Laminas\Stdlib\StringUtils;
use Laminas\Filter\FilterInterface;
use Laminas\Filter\ScalarOrArrayFilterCallback;

use function preg_replace;
use function implode;
use function preg_split;

use const PREG_SPLIT_DELIM_CAPTURE;
use const PREG_SPLIT_NO_EMPTY;

/**
* @psalm-type Options = array{
* separator?: string,
* ...
* }
* @template TOptions of Options
* @extends AbstractSeparator<TOptions>
* @implements FilterInterface<string|array<array-key, string|mixed>>
*/
class CamelCaseToSeparator extends AbstractSeparator
final class CamelCaseToSeparator implements FilterInterface
{
public function filter(mixed $value): mixed
private readonly string $separator;

/** @param Options $options */
public function __construct(array $options = [])
{
return self::applyFilterOnlyToStringableValuesAndStringableArrayValues(
$value,
Closure::fromCallable([$this, 'filterNormalizedValue'])
);
$this->separator = $options['separator'] ?? ' ';
}

public function __invoke(mixed $value): mixed
{
return $this->filter($value);
}

/**
* @param string|string[] $value
* @return string|string[]
*/
private function filterNormalizedValue(string|array $value): string|array
public function filter(mixed $value): mixed
{
if (StringUtils::hasPcreUnicodeSupport()) {
$pattern = ['#(?<=(?:\p{Lu}))(\p{Lu}\p{Ll})#', '#(?<=(?:\p{Ll}|\p{Nd}))(\p{Lu})#'];
$replacement = [$this->separator . '\1', $this->separator . '\1'];
} else {
$pattern = ['#(?<=(?:[A-Z]))([A-Z]+)([A-Z][a-z])#', '#(?<=(?:[a-z0-9]))([A-Z])#'];
$replacement = ['\1' . $this->separator . '\2', $this->separator . '\1'];
}

return preg_replace($pattern, $replacement, $value);
$pattern = <<<REGEXP
/
(
(?:\p{Lu}\p{Ll}+) # Upper followed by lower
|
(?:\p{Lu}+(?!\p{Ll})) # Upper not followed by lower
|
(?:\p{N}+) # Runs of numbers
)
/ux
REGEXP;

return ScalarOrArrayFilterCallback::applyRecursively(
$value,
fn (string $input): string => implode(
$this->separator,
preg_split($pattern, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY),
)
);
}
}
24 changes: 13 additions & 11 deletions src/Word/CamelCaseToUnderscore.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@

namespace Laminas\Filter\Word;

/**
* @psalm-type Options = array{
* separator?: string,
* ...
* }
* @template TOptions of Options
* @extends CamelCaseToSeparator<TOptions>
*/
final class CamelCaseToUnderscore extends CamelCaseToSeparator
use Laminas\Filter\FilterInterface;

/** @implements FilterInterface<string|array<array-key, string|mixed>> */
final class CamelCaseToUnderscore implements FilterInterface
{
public function __construct()
public function filter(mixed $value): mixed
{
$filter = new CamelCaseToSeparator(['separator' => '_']);

return $filter->filter($value);
}

public function __invoke(mixed $value): mixed
{
parent::__construct('_');
return $this->filter($value);
}
}
45 changes: 37 additions & 8 deletions test/Word/CamelCaseToSeparatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,53 @@ public function testFilterSeparatesCamelCasedWordsWithSpacesByDefault(): void
self::assertSame('Camel Cased Words', $filtered);
}

public function testFilterSeparatesCamelCasedWordsWithProvidedSeparator(): void
/** @return list<array{string, string}> */
public static function camelCasedWordsProvider(): array
{
$string = 'CamelCasedWords';
$filter = new CamelCaseToSeparatorFilter(':-#');
$filtered = $filter($string);
return [
['SomeCamelCase', 'Some-Camel-Case'],
['Some12With5Numbers', 'Some-12-With-5-Numbers'],
['SomePDFInText', 'Some-PDF-In-Text'],
['123LeadingNumbers', '123-Leading-Numbers'],
['ItIs2016', 'It-Is-2016'],
['What-If', 'What---If'],
['ASingleLetterB', 'A-Single-Letter-B'],
['some_snake_case', 'some_snake_case'],
['Title_Snake_Case', 'Title-_-Snake-_-Case'],
['lower-with-dash', 'lower-with-dash'],
['FFS!', 'FFS-!'],
['WithA😃', 'With-A-😃'],
['PDF123', 'PDF-123'],
['EmojiInThe🤞Middle', 'Emoji-In-The-🤞-Middle'],
['12345', '12345'],
['123A', '123-A'],
['A123', 'A-123'],
['War&Peace', 'War-&-Peace'],
['lowerThenTitleCase', 'lower-Then-Title-Case'],
['123lower', '123-lower'],
['lower123', 'lower-123'],
['ItIsÜber', 'It-Is-Über'],
['SømeThing', 'Søme-Thing'],
];
}

self::assertNotEquals($string, $filtered);
self::assertSame('Camel:-#Cased:-#Words', $filtered);
#[DataProvider('camelCasedWordsProvider')]
public function testFilterSeparatesCamelCasedWordsWithProvidedSeparator(string $input, string $expected): void
{
$filter = new CamelCaseToSeparatorFilter(['separator' => '-']);
$filtered = $filter($input);

self::assertSame($expected, $filtered);
}

public function testFilterSeperatesMultipleUppercasedLettersAndUnderscores(): void
{
$string = 'TheseAre_SOME_CamelCASEDWords';
$filter = new CamelCaseToSeparatorFilter('_');
$filter = new CamelCaseToSeparatorFilter(['separator' => '_']);
$filtered = $filter($string);

self::assertNotEquals($string, $filtered);
self::assertSame('These_Are_SOME_Camel_CASED_Words', $filtered);
self::assertSame('These_Are___SOME___Camel_CASED_Words', $filtered);
}

public function testFilterSupportArray(): void
Expand Down
39 changes: 13 additions & 26 deletions test/Word/CamelCaseToUnderscoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,28 @@
namespace LaminasTest\Filter\Word;

use Laminas\Filter\Word\CamelCaseToUnderscore as CamelCaseToUnderscoreFilter;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class CamelCaseToUnderscoreTest extends TestCase
{
public function testFilterSeparatesCamelCasedWordsWithUnderscores(): void
/** @return list<array{string, string}> */
public static function camelCasedWordsProvider(): array
{
$string = 'CamelCasedWords';
$filter = new CamelCaseToUnderscoreFilter();
$filtered = $filter($string);

self::assertNotEquals($string, $filtered);
self::assertSame('Camel_Cased_Words', $filtered);
return [
['CamelCasedWords', 'Camel_Cased_Words'],
['PaTitle', 'Pa_Title'],
['Pa2Title', 'Pa_2_Title'],
['Pa2aTitle', 'Pa_2_a_Title'],
];
}

public function testFilterSeparatingNumbersToUnderscore(): void
#[DataProvider('camelCasedWordsProvider')]
public function testFilterSeparatesCamelCasedWordsWithUnderscores(string $input, string $expected): void
{
$string = 'PaTitle';
$filter = new CamelCaseToUnderscoreFilter();
$filtered = $filter($string);

self::assertNotEquals($string, $filtered);
self::assertSame('Pa_Title', $filtered);

$string = 'Pa2Title';
$filter = new CamelCaseToUnderscoreFilter();
$filtered = $filter($string);

self::assertNotEquals($string, $filtered);
self::assertSame('Pa2_Title', $filtered);

$string = 'Pa2aTitle';
$filter = new CamelCaseToUnderscoreFilter();
$filtered = $filter($string);
$filtered = $filter($input);

self::assertNotEquals($string, $filtered);
self::assertSame('Pa2a_Title', $filtered);
self::assertSame($expected, $filtered);
}
}