Skip to content

Add DataColumn::$encodeContent #266

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

Merged
merged 5 commits into from
Mar 26, 2025
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
24 changes: 20 additions & 4 deletions src/Column/DataColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Yiisoft\Yii\DataView\Column;

use Stringable;
use Yiisoft\Validator\RuleInterface;
use Yiisoft\Yii\DataView\Column\Base\DataContext;
use Yiisoft\Yii\DataView\Filter\Factory\FilterFactoryInterface;
Expand All @@ -14,11 +15,18 @@
*
* A simple data column definition refers to an attribute in the GridView's data provider.
*
* @psalm-type ContentCallable = callable(array|object, DataContext): string|Stringable|int|float
* @psalm-type FilterEmptyCallable = callable(mixed $value): bool
* @psalm-type BodyAttributesCallable = callable(array|object,DataContext): array
* @psalm-type BodyAttributesCallable = callable(array|object, DataContext): array
*/
final class DataColumn implements ColumnInterface
{
/**
* @var callable|float|int|string|Stringable|null
* @psalm-var string|Stringable|int|float|ContentCallable|null
*/
public readonly mixed $content;

/**
* Function to determine if a filter value should be considered empty.
*
Expand Down Expand Up @@ -79,8 +87,13 @@ final class DataColumn implements ColumnInterface
* @param array|callable $bodyAttributes HTML attributes for the body cells. Can be a callable that returns attributes.
* The callable signature is: `function(array|object $data, DataContext $context): array`.
* @param bool $withSorting Whether this column is sortable.
* @param mixed $content Custom content for data cells. Can be a callable with signature:
* `function(array|object $data, DataContext $context): string|Stringable`.
* @param callable|float|int|string|Stringable|null $content Custom content for data cells. Can be a callable with signature:
* `function(array|object $data, DataContext $context): string|Stringable|int|float`.
* @param bool|null $encodeContent Whether to HTML-encode the cell content. Supported values:
* - `null`: stringable objects implementing {@see NoEncodeStringableInterface} aren't encoded,
* everything else is encoded (default behavior);
* - `true`: any content is encoded, regardless of type;
* - `false`: nothing is encoded, use with caution and only for trusted content.
* @param string|null $dateTimeFormat Format string for datetime values (e.g., 'Y-m-d H:i:s').
* @param array|bool|FilterWidget $filter Filter configuration. Can be:
* - `false` (disabled)
Expand All @@ -106,6 +119,7 @@ final class DataColumn implements ColumnInterface
* @param string|null $bodyClass Additional CSS class for the body cells.
*
* @psalm-param array|BodyAttributesCallable $bodyAttributes
* @psalm-param string|Stringable|int|float|ContentCallable|null $content
* @psalm-param bool|array<array-key,string|array<array-key,string>>|FilterWidget $filter
* @psalm-param RuleInterface[]|RuleInterface|null $filterValidation
* @psalm-param bool|FilterEmptyCallable|null $filterEmpty
Expand All @@ -120,7 +134,8 @@ public function __construct(
public readonly array $headerAttributes = [],
public readonly mixed $bodyAttributes = [],
public readonly bool $withSorting = true,
public readonly mixed $content = null,
string|Stringable|int|float|callable|null $content = null,
public bool|null $encodeContent = null,
public readonly ?string $dateTimeFormat = null,
public readonly bool|array|FilterWidget $filter = false,
public readonly string|FilterFactoryInterface|null $filterFactory = null,
Expand All @@ -131,6 +146,7 @@ public function __construct(
public readonly ?string $headerClass = null,
public readonly ?string $bodyClass = null,
) {
$this->content = $content;
$this->filterEmpty = $filterEmpty;
}

Expand Down
56 changes: 31 additions & 25 deletions src/Column/DataColumnRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

use DateTimeInterface;
use Psr\Container\ContainerInterface;
use Stringable;
use Yiisoft\Arrays\ArrayHelper;
use Yiisoft\Data\Reader\FilterInterface;
use Yiisoft\Html\Html;
use Yiisoft\Html\NoEncodeStringableInterface;
use Yiisoft\Validator\EmptyCondition\NeverEmpty;
use Yiisoft\Validator\EmptyCondition\WhenEmpty;
use Yiisoft\Validator\ValidatorInterface;
Expand Down Expand Up @@ -195,17 +197,14 @@ public function renderBody(ColumnInterface $column, Cell $cell, DataContext $con
{
/** @var DataColumn $column This annotation is for IDE only */

$contentSource = $column->content;

if ($contentSource !== null) {
$content = (string)(is_callable($contentSource) ? $contentSource($context->data, $context) : $contentSource);
} elseif ($column->property !== null) {
$value = ArrayHelper::getValue($context->data, $column->property);
$value = $this->castToString($value, $column);
$content = Html::encode($value);
if ($column->content === null) {
$content = $this->extractValueFromData($column, $context);
} elseif (is_callable($column->content)) {
$content = ($column->content)($context->data, $context);
} else {
$content = '';
$content = $column->content;
}
$content = $this->encodeContent($content, $column->encodeContent);

if (is_callable($column->bodyAttributes)) {
/** @var array $attributes Remove annotation after fix https://github.com/vimeo/psalm/issues/11062 */
Expand Down Expand Up @@ -258,16 +257,25 @@ private function normalizeFilterEmpty(bool|callable $value): callable
return $value;
}

/**
* Cast a value to string with type-specific handling.
*
* @param mixed $value The value to cast.
* @param DataColumn $column The column containing format settings.
*
* @return string The string representation of the value.
*/
private function castToString(mixed $value, DataColumn $column): string
public function getOrderProperties(ColumnInterface $column): array
{
/** @var DataColumn $column This annotation is for IDE only */

if ($column->property === null) {
return [];
}

return [$column->property => $column->field ?? $column->property];
}

private function extractValueFromData(DataColumn $column, DataContext $context): string|Stringable
{
if ($column->property === null) {
return '';
}

$value = ArrayHelper::getValue($context->data, $column->property);

if ($value === null) {
return '';
}
Expand All @@ -276,17 +284,15 @@ private function castToString(mixed $value, DataColumn $column): string
return $value->format($column->dateTimeFormat ?? $this->dateTimeFormat);
}

return (string) $value;
return $value instanceof Stringable ? $value : (string) $value;
}

public function getOrderProperties(ColumnInterface $column): array
private function encodeContent(string|Stringable|int|float $content, ?bool $encode): string
{
/** @var DataColumn $column This annotation is for IDE only */
$encode ??= !$content instanceof NoEncodeStringableInterface;

if ($column->property === null) {
return [];
}
$contentAsString = (string) $content;

return [$column->property => $column->field ?? $column->property];
return $encode ? Html::encode($contentAsString) : $contentAsString;
}
}
73 changes: 73 additions & 0 deletions tests/Column/DataColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

namespace Yiisoft\Yii\DataView\Tests\Column;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Yiisoft\Data\Reader\Iterable\IterableDataReader;
use Yiisoft\Definitions\Exception\CircularReferenceException;
use Yiisoft\Definitions\Exception\InvalidConfigException;
use Yiisoft\Definitions\Exception\NotInstantiableException;
use Yiisoft\Factory\NotFoundException;
use Yiisoft\Html\NoEncode;
use Yiisoft\Yii\DataView\Column\DataColumn;
use Yiisoft\Yii\DataView\GridView;
use Yiisoft\Yii\DataView\Tests\Support\Assert;
Expand Down Expand Up @@ -779,4 +782,74 @@ public function testContentWithNullValue(): void
$this->assertStringContainsString('<td>N/A</td>', $output);
$this->assertStringContainsString('<td>Mary</td>', $output);
}

public static function dataEncodeContent(): array
{
return [
['John &gt;', null, null],
['John &gt;', null, true],
['John >', null, false],
['123', 123, null],
['123', 123, true],
['123', 123, false],
['20.12', 20.12, null],
['20.12', 20.12, true],
['20.12', 20.12, false],
['1 &gt; 2', '1 > 2', null],
['1 &gt; 2', '1 > 2', true],
['1 > 2', '1 > 2', false],
['1 > 2', NoEncode::string('1 > 2'), null],
['1 &gt; 2', NoEncode::string('1 > 2'), true],
['1 > 2', NoEncode::string('1 > 2'), false],
['1 &gt; 2', static fn() => '1 > 2', null],
['1 &gt; 2', static fn() => '1 > 2', true],
['1 > 2', static fn() => '1 > 2', false],
['1 > 2', static fn() => NoEncode::string('1 > 2'), null],
['1 &gt; 2', static fn() => NoEncode::string('1 > 2'), true],
['1 > 2', static fn() => NoEncode::string('1 > 2'), false],
];
}

#[DataProvider('dataEncodeContent')]
public function testEncodeContent(string $expected, mixed $content, ?bool $encodeContent): void
{
$output = GridView::widget()
->columns(
new DataColumn(
'name',
content: $content,
encodeContent: $encodeContent,
),
)
->dataReader(
new IterableDataReader([
['id' => 1, 'name' => 'John >'],
])
)
->render();

$this->assertStringContainsString($expected, $output);
}

#[TestWith(['1 > 2', null])]
#[TestWith(['1 &gt; 2', true])]
#[TestWith(['1 > 2', false])]
public function testEncodeContentWithNoEncodeInData(string $expected, ?bool $encodeContent): void
{
$output = GridView::widget()
->columns(
new DataColumn(
'name',
encodeContent: $encodeContent,
),
)
->dataReader(
new IterableDataReader([
['id' => 1, 'name' => NoEncode::string('1 > 2')],
])
)
->render();

$this->assertStringContainsString($expected, $output);
}
}
12 changes: 6 additions & 6 deletions tests/DetailView/Bootstrap5Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ public function testRender(): void
DetailView::widget()
->attributes(['class' => 'container'])
->fields(
new Datafield('id', label: 'Id'),
new Datafield('login'),
new Datafield('created_at', label: 'Created At'),
new DataField('id', label: 'Id'),
new DataField('login'),
new DataField('created_at', label: 'Created At'),
)
->fieldListAttributes(['class' => 'row flex-column justify-content-center align-items-center'])
->data(
Expand Down Expand Up @@ -101,9 +101,9 @@ public function testRenderWithTable(): void
DetailView::widget()
->attributes(['class' => 'table table-success table-striped'])
->fields(
new Datafield('id', label: 'Id'),
new Datafield('login'),
new Datafield('created_at', label: 'Created At'),
new DataField('id', label: 'Id'),
new DataField('login'),
new DataField('created_at', label: 'Created At'),
)
->data(
[
Expand Down
2 changes: 1 addition & 1 deletion tests/DetailView/ExceptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ public function testDataEmpty(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The "data" must be set.');
DetailView::widget()->fields(new Datafield('id'))->data([])->render();
DetailView::widget()->fields(new DataField('id'))->data([])->render();
}
}
36 changes: 18 additions & 18 deletions tests/Field/DataFieldTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ public function testLabelAttributes(): void
HTML,
DetailView::widget()
->fields(
new Datafield('id'),
new Datafield('username'),
new Datafield('isAdmin', labelAttributes: ['class' => 'test-class']),
new DataField('id'),
new DataField('username'),
new DataField('isAdmin', labelAttributes: ['class' => 'test-class']),
)
->data(['id' => 1, 'username' => 'admin', 'isAdmin' => true])
->render(),
Expand Down Expand Up @@ -91,9 +91,9 @@ public function testLabelTag(): void
HTML,
DetailView::widget()
->fields(
new Datafield('id'),
new Datafield('username'),
new Datafield('isAdmin', labelTag: 'p'),
new DataField('id'),
new DataField('username'),
new DataField('isAdmin', labelTag: 'p'),
)
->data(['id' => 1, 'username' => 'admin', 'isAdmin' => true])
->render(),
Expand Down Expand Up @@ -129,8 +129,8 @@ public function testValueAttributeWithClosure(): void
HTML,
DetailView::widget()
->fields(
new Datafield('id'),
new Datafield('username'),
new DataField('id'),
new DataField('username'),
new DataField(
'total',
valueAttributes: static fn (array $data) => $data['total'] > 10
Expand Down Expand Up @@ -171,8 +171,8 @@ public function testValueWithDataArray(): void
HTML,
DetailView::widget()
->fields(
new Datafield('id'),
new Datafield('username'),
new DataField('id'),
new DataField('username'),
new DataField('status', value: static fn (array $data): string => $data['status'] ? 'yes' : 'no')
)
->data(['id' => 1, 'username' => 'tests 1', 'status' => true])
Expand Down Expand Up @@ -215,8 +215,8 @@ public function testValueWithDataObject(): void
HTML,
DetailView::widget()
->fields(
new Datafield('id'),
new Datafield('username'),
new DataField('id'),
new DataField('username'),
new DataField('status', value: static fn (object $data): string => $data->status ? 'yes' : 'no')
)
->data($dataObject)
Expand Down Expand Up @@ -253,9 +253,9 @@ public function testValueInt(): void
HTML,
DetailView::widget()
->fields(
new Datafield('id'),
new Datafield('username'),
new Datafield('isAdmin', value: 1),
new DataField('id'),
new DataField('username'),
new DataField('isAdmin', value: 1),
)
->data(['id' => 1, 'username' => 'guess', 'isAdmin' => false])
->valueFalse('no')
Expand Down Expand Up @@ -292,9 +292,9 @@ public function testValueTag(): void
HTML,
DetailView::widget()
->fields(
new Datafield('id'),
new Datafield('username'),
new Datafield('isAdmin', valueTag: 'p'),
new DataField('id'),
new DataField('username'),
new DataField('isAdmin', valueTag: 'p'),
)
->data(['id' => 1, 'username' => 'admin', 'isAdmin' => true])
->render(),
Expand Down
4 changes: 2 additions & 2 deletions tests/Support/TestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private function createOffsetPaginator(
int $pageSize,
int $currentPage = 1,
bool $sort = false
): OffSetPaginator {
): OffsetPaginator {
$data = new IterableDataReader($data);

if ($sort) {
Expand All @@ -59,7 +59,7 @@ private function createOffsetPaginator(
return (new OffsetPaginator($data))->withToken(PageToken::next((string) $currentPage))->withPageSize($pageSize);
}

private function createKeysetPaginator(array $data, int $pageSize): KeySetPaginator
private function createKeysetPaginator(array $data, int $pageSize): KeysetPaginator
{
$data = (new IterableDataReader($data))
->withSort(Sort::any()->withOrder(['id' => 'asc', 'name' => 'asc']));
Expand Down
Loading