Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 4.0.0 under development

- Chg #221: Throw `LogicException` in `Tag::id()` when id is empty string (@razvbir)
- Chg #234: Remove tag attributes sorting (@FrankiFixx)

## 3.13.0 March 13, 2026

Expand Down
6 changes: 5 additions & 1 deletion UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ application when you upgrade the package from one version to another.

## Upgrade from 3.x

- `Tag::id()` now throws `LogicException` when an empty string is passed. Check your code for places where you call
- `Tag::id()` now throws `LogicException` when an empty string is passed. Check your code for places where you call
`Tag::id()` and make sure you are not passing an empty string.
- HTML tag attributes are no longer sorted by `Html::renderTagAttributes()`. Previously, attributes were reordered
according to a predefined priority list (`type`, `id`, `class`, `name`, `value`, etc.). Now attributes are rendered
in the order they are set. If your code or tests depend on a specific attribute order in the rendered HTML, you need
to update them.
49 changes: 0 additions & 49 deletions src/Html.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,45 +105,6 @@
*/
final class Html
{
/**
* The preferred order of attributes in a tag. This mainly affects the order of the attributes that are
* rendered by {@see renderTagAttributes()}.
*/
private const ATTRIBUTE_ORDER = [
'type',
'id',
'class',
'name',
'value',

'href',
'loading',
'src',
'srcset',
'form',
'action',
'method',

'selected',
'checked',
'readonly',
'disabled',
'multiple',

'size',
'maxlength',
'minlength',
'width',
'height',
'rows',
'cols',

'alt',
'title',
'rel',
'media',
];

/**
* List of tag attributes that should be specially handled when their values are of array type.
* In particular, if the value of the `data` attribute is `['name' => 'xyz', 'age' => 13]`, two attributes will be
Expand Down Expand Up @@ -1680,16 +1641,6 @@
*/
public static function renderTagAttributes(array $attributes): string
{
if (count($attributes) > 1) {
$sorted = [];
foreach (self::ATTRIBUTE_ORDER as $name) {
if (isset($attributes[$name])) {
$sorted[$name] = $attributes[$name];
}
}
$attributes = array_merge($sorted, $attributes);
}

$html = '';
/**
* @var string $name
Expand All @@ -1704,11 +1655,11 @@
/** @psalm-var array<array-key, scalar[]|string|Stringable|null> $value */
foreach ($value as $n => $v) {
if (!isset($v)) {
continue;

Check warning on line 1658 in src/Html.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "Continue_": @@ @@ /** @psalm-var array<array-key, scalar[]|string|Stringable|null> $value */ foreach ($value as $n => $v) { if (!isset($v)) { - continue; + break; } $fullName = "$name-$n"; if (in_array($fullName, self::ATTRIBUTES_WITH_CONCATENATED_VALUES, true)) {
}
$fullName = "$name-$n";
if (in_array($fullName, self::ATTRIBUTES_WITH_CONCATENATED_VALUES, true)) {
$html .= self::renderAttribute(

Check warning on line 1662 in src/Html.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.5-ubuntu-latest

Escaped Mutant for Mutator "Assignment": @@ @@ } $fullName = "$name-$n"; if (in_array($fullName, self::ATTRIBUTES_WITH_CONCATENATED_VALUES, true)) { - $html .= self::renderAttribute( + $html = self::renderAttribute( $fullName, self::encodeAttribute( is_array($v) ? implode(' ', $v) : $v,
$fullName,
self::encodeAttribute(
is_array($v) ? implode(' ', $v) : $v,
Expand Down
80 changes: 40 additions & 40 deletions tests/HtmlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public function testMeta(): void
{
$this->assertSame('<meta>', Html::meta()->render());
$this->assertSame(
'<meta id="main" name="keywords" content="yii">',
'<meta name="keywords" content="yii" id="main">',
Html::meta(['name' => 'keywords', 'content' => 'yii', 'id' => 'main'])->render(),
);
}
Expand All @@ -156,15 +156,15 @@ public function testLink(): void
public function testCssFile(): void
{
$this->assertSame(
'<link href="http://example.com" rel="stylesheet">',
'<link rel="stylesheet" href="http://example.com">',
Html::cssFile('http://example.com')->render(),
);
$this->assertSame(
'<link href rel="stylesheet">',
'<link rel="stylesheet" href>',
Html::cssFile('')->render(),
);
$this->assertSame(
'<link id="main" href="http://example.com" rel="stylesheet">',
'<link rel="stylesheet" href="http://example.com" id="main">',
Html::cssFile('http://example.com', ['id' => 'main'])->render(),
);
}
Expand All @@ -180,7 +180,7 @@ public function testJavaScriptFile(): void
Html::javaScriptFile('')->render(),
);
$this->assertSame(
'<script id="main" src="http://example.com"></script>',
'<script src="http://example.com" id="main"></script>',
Html::javaScriptFile('http://example.com', ['id' => 'main'])->render(),
);
}
Expand Down Expand Up @@ -208,7 +208,7 @@ public function testMailto(): void
Html::mailto('contact me', 'info@example.com')->render(),
);
$this->assertSame(
'<a id="contact" href="mailto:info@example.com">contact me</a>',
'<a href="mailto:info@example.com" id="contact">contact me</a>',
Html::mailto('contact me', 'info@example.com', ['id' => 'contact'])->render(),
);
}
Expand Down Expand Up @@ -387,15 +387,15 @@ public function testTextInput(): void
public function testColorInput(): void
{
$this->assertSame('<input type="color">', Html::color()->render());
$this->assertSame('<input type="color" name>', Html::color('')->render());
$this->assertSame('<input type="color" value>', Html::color(null, '')->render());
$this->assertSame('<input type="color" name="test">', Html::color('test')->render());
$this->assertSame('<input name type="color">', Html::color('')->render());
$this->assertSame('<input value type="color">', Html::color(null, '')->render());
$this->assertSame('<input name="test" type="color">', Html::color('test')->render());
$this->assertSame(
'<input type="color" name="test" value="#ff0000">',
'<input name="test" value="#ff0000" type="color">',
Html::color('test', '#ff0000')->render(),
);
$this->assertSame(
'<input type="color" name="test" value="#ff0000" required>',
'<input name="test" value="#ff0000" required type="color">',
Html::color('test', '#ff0000', ['required' => true])->render(),
);
}
Expand All @@ -411,7 +411,7 @@ public function testHiddenInput(): void
Html::hiddenInput('test', '43')->render(),
);
$this->assertSame(
'<input type="hidden" id="ABC" name="test" value="43">',
'<input type="hidden" name="test" value="43" id="ABC">',
Html::hiddenInput('test', '43', ['id' => 'ABC'])->render(),
);
}
Expand All @@ -435,63 +435,63 @@ public function testPasswordInput(): void
public function testFile(): void
{
$this->assertSame('<input type="file">', Html::file()->render());
$this->assertSame('<input type="file" name>', Html::file('')->render());
$this->assertSame('<input type="file" value>', Html::file(null, '')->render());
$this->assertSame('<input type="file" name="test">', Html::file('test')->render());
$this->assertSame('<input name type="file">', Html::file('')->render());
$this->assertSame('<input value type="file">', Html::file(null, '')->render());
$this->assertSame('<input name="test" type="file">', Html::file('test')->render());
$this->assertSame(
'<input type="file" name="test" value="43">',
'<input name="test" value="43" type="file">',
Html::file('test', '43')->render(),
);
$this->assertSame(
'<input type="file" class="photo" name="test" value="43">',
'<input name="test" value="43" class="photo" type="file">',
Html::file('test', '43', ['class' => 'photo'])->render(),
);
}

public function testRadio(): void
{
$this->assertSame('<input type="radio">', Html::radio()->render());
$this->assertSame('<input type="radio" name>', Html::radio('')->render());
$this->assertSame('<input type="radio" value>', Html::radio(null, '')->render());
$this->assertSame('<input type="radio" name="test">', Html::radio('test')->render());
$this->assertSame('<input name type="radio">', Html::radio('')->render());
$this->assertSame('<input value type="radio">', Html::radio(null, '')->render());
$this->assertSame('<input name="test" type="radio">', Html::radio('test')->render());
$this->assertSame(
'<input type="radio" name="test" value="43">',
'<input name="test" value="43" type="radio">',
Html::radio('test', '43')->render(),
);
$this->assertSame(
'<input type="radio" name="test" value="43" readonly>',
'<input name="test" value="43" readonly type="radio">',
Html::radio('test', '43', ['readonly' => true])->render(),
);
}

public function testCheckbox(): void
{
$this->assertSame('<input type="checkbox">', Html::checkbox()->render());
$this->assertSame('<input type="checkbox" name>', Html::checkbox('')->render());
$this->assertSame('<input type="checkbox" value>', Html::checkbox(null, '')->render());
$this->assertSame('<input type="checkbox" name="test">', Html::checkbox('test')->render());
$this->assertSame('<input name type="checkbox">', Html::checkbox('')->render());
$this->assertSame('<input value type="checkbox">', Html::checkbox(null, '')->render());
$this->assertSame('<input name="test" type="checkbox">', Html::checkbox('test')->render());
$this->assertSame(
'<input type="checkbox" name="test" value="43">',
'<input name="test" value="43" type="checkbox">',
Html::checkbox('test', '43')->render(),
);
$this->assertSame(
'<input type="checkbox" name="test" value="43" readonly>',
'<input name="test" value="43" readonly type="checkbox">',
Html::checkbox('test', '43', ['readonly' => true])->render(),
);
}

public function testRange(): void
{
$this->assertSame('<input type="range">', Html::range()->render());
$this->assertSame('<input type="range" name>', Html::range('')->render());
$this->assertSame('<input type="range" value>', Html::range(null, '')->render());
$this->assertSame('<input type="range" name="test">', Html::range('test')->render());
$this->assertSame('<input name type="range">', Html::range('')->render());
$this->assertSame('<input value type="range">', Html::range(null, '')->render());
$this->assertSame('<input name="test" type="range">', Html::range('test')->render());
$this->assertSame(
'<input type="range" name="test" value="43">',
'<input name="test" value="43" type="range">',
Html::range('test', '43')->render(),
);
$this->assertSame(
'<input type="range" name="test" value="43" readonly>',
'<input name="test" value="43" readonly type="range">',
Html::range('test', '43', ['readonly' => true])->render(),
);
}
Expand Down Expand Up @@ -534,9 +534,9 @@ public function testCheckboxList(): void
$this->assertSame(
'<input type="hidden" name="test" value="0">' . "\n"
. '<div id="main">' . "\n"
. '<label><input type="checkbox" name="test[]" value="1"> One</label>' . "\n"
. '<label><input type="checkbox" name="test[]" value="2" checked> Two</label>' . "\n"
. '<label><input type="checkbox" name="test[]" value="5" checked> Five</label>' . "\n"
. '<label><input name="test[]" value="1" type="checkbox"> One</label>' . "\n"
. '<label><input name="test[]" value="2" checked type="checkbox"> Two</label>' . "\n"
. '<label><input name="test[]" value="5" checked type="checkbox"> Five</label>' . "\n"
. '</div>',
Html::checkboxList('test')
->items([1 => 'One', 2 => 'Two', 5 => 'Five'])
Expand All @@ -552,9 +552,9 @@ public function testRadioList(): void
$this->assertSame(
'<input type="hidden" name="test" value="0">' . "\n"
. '<div id="main">' . "\n"
. '<label><input type="radio" name="test" value="1"> One</label>' . "\n"
. '<label><input type="radio" name="test" value="2" checked> Two</label>' . "\n"
. '<label><input type="radio" name="test" value="5"> Five</label>' . "\n"
. '<label><input name="test" value="1" type="radio"> One</label>' . "\n"
. '<label><input name="test" value="2" checked type="radio"> Two</label>' . "\n"
. '<label><input name="test" value="5" type="radio"> Five</label>' . "\n"
. '</div>',
Html::radioList('test')
->items([1 => 'One', 2 => 'Two', 5 => 'Five'])
Expand Down Expand Up @@ -708,7 +708,7 @@ public function testLi(): void
$this->assertSame('<li><span>Hello</span></li>', Html::li(Html::span('Hello'))->render());

$this->assertSame(
'<li id="item-1" class="item">Content</li>',
'<li class="item" id="item-1">Content</li>',
Html::li('Content', ['class' => 'item', 'id' => 'item-1'])->render(),
);
$this->assertSame('<li class="empty"></li>', Html::li(attributes: ['class' => 'empty'])->render());
Expand Down Expand Up @@ -859,7 +859,7 @@ public static function dataRenderTagAttributes(): array
[' class="first second"', ['class' => ['first', 'second']]],
['', ['class' => []]],
[' style="width: 100px; height: 200px;"', ['style' => ['width' => '100px', 'height' => '200px']]],
[' name="position" value="42"', ['value' => 42, 'name' => 'position']],
[' value="42" name="position"', ['value' => 42, 'name' => 'position']],
[
' id="x" class="a b" data-a="1" data-b="2" style="width: 100px;" any=\'[1,2]\'',
[
Expand Down
Loading
Loading