Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
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
84 changes: 63 additions & 21 deletions src/View/StringTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Cake\Core\InstanceConfigTrait;
use Cake\Utility\Hash;
use InvalidArgumentException;
use function Cake\Core\deprecationWarning;
use function Cake\Core\h;

/**
Expand Down Expand Up @@ -344,45 +345,86 @@ protected function _formatAttribute(string $key, mixed $value, bool $escape = tr
}

/**
* Adds a class and returns a unique list either in array or space separated
* Merges two sets of CSS classes into a unique array.
*
* @param array<string, mixed>|string|null $input The array or string to add the class to
* @param array<string>|string|false|null $newClass the new class or classes to add
* @param string $useIndex if you are inputting an array with an element other than default of 'class'.
* @return array<string, string>|string|null
* Accepts class lists as arrays or space-separated strings and returns
* a unique, indexed array of all classes.
*
* @param array<string>|string $existing The existing class(es).
* @param array<string>|string $new The new class(es) to merge.
* @return array<string> A unique array of merged classes.
Copy link
Member

@ADmad ADmad Oct 28, 2025

Choose a reason for hiding this comment

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

Doesn't the old method return string if the 1st arg is string? If so we can retain the same. We can use conditional return type based on argument type which keeps the return type deterministic for static analyzers.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, thats why the old code needed (array) casting.
These methods indeed are the 6.x code I recommended moving forward.
So those are already "clean" from BC requirements.
For the backport you are right, we probably should consider more BC approach.

Feel free to add a commit, not quite sure how that conditional works.

*/
public function addClassNames(array|string $existing, array|string $new): array
{
if (!is_array($existing)) {
$existing = $existing !== '' ? explode(' ', $existing) : [];
}
if (!is_array($new)) {
$new = $new !== '' ? explode(' ', $new) : [];
}

return array_values(array_unique(array_merge($existing, $new)));
}

/**
* Adds CSS classes to an attribute array.
*
* Merges the provided classes with any existing classes in the specified key
* and returns the updated attribute array with unique class values.
*
* Deprecated: Passing a non-array as first argument is deprecated.
* Pass an attributes array instead.
* Deprecated: Returning a non-array value is deprecated.
* The method will only return arrays in the future.
*
* @param array<string, mixed>|string|null $input The attribute array to add classes to.
* @param array<string>|string|false|null $newClass The class(es) to add.
* @param string $useIndex The array key to use. Defaults to 'class'.
* @return array<string, string>|string|null The updated attribute array.
*/
public function addClass(
mixed $input,
array|string|false|null $newClass,
string $useIndex = 'class',
): array|string|null {
if (!is_array($input)) {
deprecationWarning(
'5.3.0',
'Passing a non-array as first argument to `StringTemplate::addClass()` is deprecated. ' .
'Pass an attributes array instead.',
);
}

// NOOP
if (!$newClass) {
return $input;
}

if (is_array($input)) {
$class = Hash::get($input, $useIndex, []);
} else {
$class = $input;
$input = [];
}

// Convert and sanitize the inputs
if (!is_array($class)) {
if (is_string($class) && !empty($class)) {
$class = explode(' ', $class);
if (!is_array($input)) {
if (is_string($input) && $input !== '') {
$class = explode(' ', $input);
} else {
$class = [];
}
}

if (is_string($newClass)) {
$newClass = explode(' ', $newClass);
if (is_string($newClass)) {
$newClass = explode(' ', $newClass);
}

$class = array_unique(array_merge($class, $newClass));

deprecationWarning(
'5.3.0',
'Returning a non-array value from `StringTemplate::addClass()` is deprecated. ' .
'The method will only return arrays in the future.',
);

return Hash::insert([], $useIndex, $class);
}

$class = array_unique(array_merge($class, $newClass));
$existingClasses = Hash::get($input, $useIndex, []);
$mergedClasses = $this->addClassNames($existingClasses, $newClass);

return Hash::insert($input, $useIndex, $class);
return Hash::insert($input, $useIndex, $mergedClasses);
}
}
2 changes: 1 addition & 1 deletion src/View/Widget/MultiCheckboxWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ protected function _renderInput(array $checkbox, ContextInterface $context): str

if ($checkbox['checked']) {
$selectedClass = $this->_templates->format('selectedClass', []);
$labelAttrs = (array)$this->_templates->addClass($labelAttrs, $selectedClass);
$labelAttrs = (array)$this->_templates->addClass((array)$labelAttrs, $selectedClass);
}

$label = $this->_label->render($labelAttrs, $context);
Expand Down
2 changes: 1 addition & 1 deletion src/View/Widget/RadioWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ protected function _renderInput(

if (!is_bool($data['label']) && isset($radio['checked']) && $radio['checked']) {
$selectedClass = $this->_templates->format('selectedClass', []);
$data['label'] = $this->_templates->addClass($data['label'], $selectedClass);
$data['label'] = $this->_templates->addClass((array)$data['label'], $selectedClass);
}

$radio['disabled'] = $this->_isDisabled($radio, $data['disabled']);
Expand Down
150 changes: 120 additions & 30 deletions tests/TestCase/View/StringTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,58 +304,148 @@ public function testPushPopTemplates(): void
}

/**
* Test addClass method newClass parameter
*
* Tests null, string, array and false for `input`
* Test addClass method with array input (new behavior).
*/
public function testAddClassMethodNewClass(): void
public function testAddClassWithArray(): void
{
$result = $this->template->addClass([], 'new_class');
$this->assertEquals($result, ['class' => ['new_class']]);
// Test adding a single class to empty array
$result = $this->template->addClass([], 'new-class');
$this->assertSame(['class' => ['new-class']], $result);

// Test adding array of classes to empty array
$result = $this->template->addClass([], ['class-one', 'class-two']);
$this->assertSame(['class' => ['class-one', 'class-two']], $result);

// Test adding to existing classes
$result = $this->template->addClass(['class' => ['existing']], 'new-class');
$this->assertSame(['class' => ['existing', 'new-class']], $result);

// Test with custom key
$result = $this->template->addClass([], 'custom-class', 'custom-key');
$this->assertSame(['custom-key' => ['custom-class']], $result);

// Test preserves other attributes
$attrs = ['id' => 'test', 'class' => ['current']];
$result = $this->template->addClass($attrs, 'new-class');
$this->assertSame(['id' => 'test', 'class' => ['current', 'new-class']], $result);

// Test with string existing classes
$attrs = ['class' => 'foo bar'];
$result = $this->template->addClass($attrs, 'baz');
$this->assertSame(['class' => ['foo', 'bar', 'baz']], $result);

// Test uniqueness
$attrs = ['class' => ['duplicate']];
$result = $this->template->addClass($attrs, 'duplicate');
$this->assertSame(['class' => ['duplicate']], $result);
}

$result = $this->template->addClass([], ['new_class']);
$this->assertEquals($result, ['class' => ['new_class']]);
/**
* Test addClassNames method with various input types.
*/
public function testAddClassNames(): void
{
// Test merging two arrays
$result = $this->template->addClassNames(['existing'], ['new']);
$this->assertSame(['existing', 'new'], $result);

// Test merging string with array
$result = $this->template->addClassNames('existing', ['new']);
$this->assertSame(['existing', 'new'], $result);
Comment on lines +349 to +354
Copy link
Member

Choose a reason for hiding this comment

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

I think this is going in a better direction from an ergonomics point of view. I'll try to make sometime this weekend to try out my suggestion as well.


$result = $this->template->addClass([], false);
$this->assertEquals($result, []);
// Test merging array with string
$result = $this->template->addClassNames(['existing'], 'new');
$this->assertSame(['existing', 'new'], $result);

$result = $this->template->addClass([], null);
$this->assertEquals($result, []);
// Test merging two strings
$result = $this->template->addClassNames('existing', 'new');
$this->assertSame(['existing', 'new'], $result);

$result = $this->template->addClass(null, null);
$this->assertNull($result);
// Test merging space-separated string classes
$result = $this->template->addClassNames('class-one class-two', 'class-three class-four');
$this->assertSame(['class-one', 'class-two', 'class-three', 'class-four'], $result);

// Test uniqueness - duplicates should be removed
$result = $this->template->addClassNames(['duplicate'], ['duplicate']);
$this->assertSame(['duplicate'], $result);

// Test empty strings
$result = $this->template->addClassNames('', 'new');
$this->assertSame(['new'], $result);

$result = $this->template->addClassNames('existing', '');
$this->assertSame(['existing'], $result);

// Test both empty
$result = $this->template->addClassNames('', '');
$this->assertSame([], $result);

// Test empty arrays
$result = $this->template->addClassNames([], []);
$this->assertSame([], $result);

// Test maintains numeric indexing
$result = $this->template->addClassNames(['one', 'two'], ['three']);
$this->assertSame([0, 1, 2], array_keys($result));
}

/**
* Test addClass method input (currentClass) parameter
* Test addClass method with deprecated string input
*
* Tests null, string, array, false and object
* @deprecated Tests deprecated addClass method with string input
*/
public function testAddClassMethodCurrentClass(): void
public function testAddClassMethodDeprecatedStringInput(): void
{
$result = $this->template->addClass(['class' => ['current']], 'new_class');
$this->assertEquals($result, ['class' => ['current', 'new_class']]);
$this->expectDeprecationMessageMatches(
'/Passing a non-array as first argument to `StringTemplate::addClass\(\)` is deprecated/',
function (): void {
$result = $this->template->addClass(null, null);
$this->assertNull($result);
},
);
}

$result = $this->template->addClass('', 'new_class');
$this->assertEquals($result, ['class' => ['new_class']]);
/**
* Test addClass method with deprecated non-array inputs
*
* Tests null, string, false and object as first parameter
*
* @deprecated Tests deprecated addClass method with non-array input
*/
public function testAddClassMethodCurrentClass(): void
{
$this->expectDeprecationMessageMatches(
'/Passing a non-array as first argument to `StringTemplate::addClass\(\)` is deprecated/',
function (): void {
$result = $this->template->addClass('', 'new_class');
$this->assertEquals($result, ['class' => ['new_class']]);

$result = $this->template->addClass(null, 'new_class');
$this->assertEquals($result, ['class' => ['new_class']]);
$result = $this->template->addClass(null, 'new_class');
$this->assertEquals($result, ['class' => ['new_class']]);

$result = $this->template->addClass(false, 'new_class');
$this->assertEquals($result, ['class' => ['new_class']]);
$result = $this->template->addClass(false, 'new_class');
$this->assertEquals($result, ['class' => ['new_class']]);

$result = $this->template->addClass(new stdClass(), 'new_class');
$this->assertEquals($result, ['class' => ['new_class']]);
$result = $this->template->addClass(new stdClass(), 'new_class');
$this->assertEquals($result, ['class' => ['new_class']]);
},
);
}

/**
* Test addClass method string parameter, it should fallback to string
*
* @deprecated Tests deprecated addClass method with string input
*/
public function testAddClassMethodFallbackToString(): void
{
$result = $this->template->addClass('current', 'new_class');
$this->assertEquals($result, ['class' => ['current', 'new_class']]);
$this->expectDeprecationMessageMatches(
'/Passing a non-array as first argument to `StringTemplate::addClass\(\)` is deprecated/',
function (): void {
$result = $this->template->addClass('current', 'new_class');
$this->assertEquals($result, ['class' => ['current', 'new_class']]);
},
);
}

/**
Expand All @@ -370,7 +460,7 @@ public function testAddClassMethodUnique(): void
/**
* Test addClass method useIndex param
*
* Tests for useIndex being the default, 'my_class' and false
* Tests for useIndex being the default and 'my_class'
*/
public function testAddClassMethodUseIndex(): void
{
Expand Down
Loading