Skip to content

Commit

Permalink
Improve action column (extract url creating, move default values to r…
Browse files Browse the repository at this point in the history
…enderer) (#142)
  • Loading branch information
vjik authored Dec 2, 2023
1 parent 6f023de commit 3495a4b
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 141 deletions.
8 changes: 8 additions & 0 deletions config/di.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@

declare(strict_types=1);

use Yiisoft\Definitions\Reference;
use Yiisoft\Translator\CategorySource;
use Yiisoft\Translator\IdMessageReader;
use Yiisoft\Translator\IntlMessageFormatter;
use Yiisoft\Translator\Message\Php\MessageSource;
use Yiisoft\Translator\SimpleMessageFormatter;
use Yiisoft\Yii\DataView\Column\ActionColumnRenderer;
use Yiisoft\Yii\DataView\YiiRouter\ActionColumnUrlCreator;

/** @var array $params */

return [
ActionColumnRenderer::class => [
'__construct()' => [
'defaultUrlCreator' => Reference::to(ActionColumnUrlCreator::class),
],
],
'yii.dataview.categorySource' => [
'definition' => static function () use ($params): CategorySource {
$reader = class_exists(MessageSource::class)
Expand Down
36 changes: 27 additions & 9 deletions src/Column/ActionColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,54 @@
namespace Yiisoft\Yii\DataView\Column;

use Closure;
use Yiisoft\Yii\DataView\Column\Base\DataContext;

/**
* `ActionColumn` is a column for the {@see GridView} widget that displays buttons for viewing and manipulating
* the items.
*
* @psalm-type UrlCreator = callable(string,DataContext):string
*/
final class ActionColumn implements ColumnInterface
{
/**
* @var UrlCreator|null
*/
private $urlCreator;

/**
* @param ?string $primaryKey The primary key of the data to be used to generate an URL automatically.
* @param ?callable $urlCreator A callback that creates a button URL using the specified data information.
*
* @psalm-param UrlCreator|null $urlCreator
* @psalm-param array<string,Closure> $buttons
* @psalm-param array<string,bool|Closure> $visibleButtons
* @psalm-param array<string,bool|Closure>|null $visibleButtons
*/
public function __construct(
public readonly string $primaryKey = 'id',
public readonly string $template = "{view}\n{update}\n{delete}",
public readonly ?string $routeName = null,
public readonly array $urlParamsConfig = [],
public readonly ?array $urlArguments = null,
public readonly array $urlQueryParameters = [],
public readonly ?Closure $urlCreator = null,
public readonly ?string $primaryKey = null,
public readonly ?string $template = null,
public readonly mixed $urlConfig = null,
?callable $urlCreator = null,
public readonly ?string $header = null,
public readonly ?string $footer = null,
public readonly mixed $content = null,
public readonly array $buttons = [],
public readonly array $visibleButtons = [],
public readonly ?array $visibleButtons = null,
public readonly array $columnAttributes = [],
public readonly array $headerAttributes = [],
public readonly array $bodyAttributes = [],
public readonly array $footerAttributes = [],
private readonly bool $visible = true,
) {
$this->urlCreator = $urlCreator;
}

/**
* @psalm-return UrlCreator|null
*/
public function getUrlCreator(): ?callable
{
return $this->urlCreator;
}

public function isVisible(): bool
Expand Down
146 changes: 66 additions & 80 deletions src/Column/ActionColumnRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,70 @@

use Closure;
use InvalidArgumentException;
use LogicException;
use Yiisoft\Html\Html;
use Yiisoft\Router\CurrentRoute;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Yii\DataView\Column\Base\Cell;
use Yiisoft\Yii\DataView\Column\Base\GlobalContext;
use Yiisoft\Yii\DataView\Column\Base\DataContext;

use function is_object;

/**
* @psalm-import-type UrlCreator from ActionColumn
*/
final class ActionColumnRenderer implements ColumnRendererInterface
{
/**
* @var UrlCreator|null
*/
private $defaultUrlCreator;

/**
* @psalm-var array<string,Closure>
*/
private readonly array $defaultButtons;

/**
* @psalm-param UrlCreator|null $defaultUrlCreator
* @psalm-param array<string,Closure> $defaultButtons
*/
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly CurrentRoute $currentRoute,
?callable $defaultUrlCreator = null,
private readonly string $defaultTemplate = "{view}\n{update}\n{delete}",
?array $defaultButtons = null,
) {
$this->defaultUrlCreator = $defaultUrlCreator;

$this->defaultButtons = $defaultButtons ?? [
'view' => static fn(string $url): string => Html::a(
Html::span('🔎'),
$url,
[
'name' => 'view',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'View',
],
)->render(),
'update' => static fn(string $url): string => Html::a(
Html::span(''),
$url,
[
'name' => 'update',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'Update',
],
)->render(),
'delete' => static fn(string $url): string => Html::a(
Html::span(''),
$url,
[
'name' => 'delete',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'Delete',
],
)->render(),
];
}

public function renderColumn(ColumnInterface $column, Cell $cell, GlobalContext $context): Cell
Expand Down Expand Up @@ -51,7 +100,7 @@ public function renderBody(ColumnInterface $column, Cell $cell, DataContext $con
if ($contentSource !== null) {
$content = (string)(is_callable($contentSource) ? $contentSource($context->data, $context) : $contentSource);
} else {
$buttons = empty($column->buttons) ? $this->getDefaultButtons() : $column->buttons;
$buttons = empty($column->buttons) ? $this->defaultButtons : $column->buttons;
$content = preg_replace_callback(
'/{([\w\-\/]+)}/',
function (array $matches) use ($column, $buttons, $context): string {
Expand All @@ -67,13 +116,13 @@ function (array $matches) use ($column, $buttons, $context): string {
) &&
isset($buttons[$name])
) {
$url = $this->createUrl($column, $name, $context->data, $context->key);
$url = $this->createUrl($name, $context);
return (string)$buttons[$name]($url);
}

return '';
},
$column->template
$column->template ?? $this->defaultTemplate
);
$content = trim($content);
}
Expand All @@ -95,40 +144,17 @@ public function renderFooter(ColumnInterface $column, Cell $cell, GlobalContext
return $cell->addAttributes($column->footerAttributes);
}

private function createUrl(ActionColumn $column, string $action, array|object $data, mixed $key): string
private function createUrl(string $action, DataContext $context): string
{
if ($column->urlCreator !== null) {
return (string) ($column->urlCreator)($action, $data, $key);
}

$primaryKey = $column->primaryKey;
$routeName = $column->routeName;
/** @var ActionColumn $column */
$column = $context->column;

if ($primaryKey !== '') {
$key = (is_object($data) ? $data->$primaryKey : $data[$primaryKey]) ?? $key;
$urlCreator = $column->getUrlCreator() ?? $this->defaultUrlCreator;
if ($urlCreator === null) {
throw new LogicException('Do not set URL creator.');
}

$currentRouteName = $this->currentRoute->getName() ?? '';

$route = $routeName === null
? $currentRouteName . '/' . $action
: $routeName . '/' . $action;

$urlParamsConfig = array_merge(
$column->urlParamsConfig,
is_array($key) ? $key : [$primaryKey => $key]
);

if ($column->urlArguments !== null) {
/** @psalm-var array<string,string> */
$urlArguments = array_merge($column->urlArguments, $urlParamsConfig);
$urlQueryParameters = [];
} else {
$urlArguments = [];
$urlQueryParameters = array_merge($column->urlQueryParameters, $urlParamsConfig);
}

return $this->urlGenerator->generate($route, $urlArguments, $urlQueryParameters);
return $urlCreator($action, $context);
}

private function isVisibleButton(
Expand All @@ -140,7 +166,7 @@ private function isVisibleButton(
): bool {
$visibleButtons = $column->visibleButtons;

if (empty($visibleButtons)) {
if ($visibleButtons === null) {
return true;
}

Expand All @@ -153,46 +179,6 @@ private function isVisibleButton(
return $visibleValue($data, $key, $index);
}

/**
* Initializes the default button rendering callback for single button.
* @psalm-return array<string,Closure>
*/
private function getDefaultButtons(): array
{
return [
'view' => static fn(string $url): string => Html::a(
Html::span('🔎'),
$url,
[
'name' => 'view',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'View',
],
)->render(),
'update' => static fn(string $url): string => Html::a(
Html::span(''),
$url,
[
'name' => 'update',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'Update',
],
)->render(),
'delete' => static fn(string $url): string => Html::a(
Html::span(''),
$url,
[
'name' => 'delete',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'Delete',
],
)->render(),
];
}

/**
* @psalm-assert ActionColumn $column
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Column/Base/DataContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class DataContext
public function __construct(
public readonly ColumnInterface $column,
public readonly array|object $data,
public readonly mixed $key,
public readonly int|string $key,
public readonly int $index,
) {
}
Expand Down
5 changes: 1 addition & 4 deletions src/Column/CheckboxColumnRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@ public function renderBody(ColumnInterface $column, Cell $cell, DataContext $con
}

if (!array_key_exists('value', $inputAttributes)) {
$key = $context->key;
$value = is_array($key)
? json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
: (string)$key;
$value = $context->key;
}

$input = Html::checkbox($name, $value, $inputAttributes);
Expand Down
5 changes: 1 addition & 4 deletions src/Column/RadioColumnRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ public function renderBody(ColumnInterface $column, Cell $cell, DataContext $con
}

if (!array_key_exists('value', $inputAttributes)) {
$key = $context->key;
$value = is_array($key)
? json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
: (string)$key;
$value = $context->key;
}

$input = Html::radio($name, $value, $inputAttributes);
Expand Down
55 changes: 55 additions & 0 deletions src/YiiRouter/ActionColumnUrlCreator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\DataView\YiiRouter;

use LogicException;
use Yiisoft\Router\CurrentRoute;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Yii\DataView\Column\ActionColumn;
use Yiisoft\Yii\DataView\Column\Base\DataContext;

use function is_object;

final class ActionColumnUrlCreator
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly CurrentRoute $currentRoute,
private readonly string $defaultPrimaryKey = 'id',
private readonly bool $defaultPrimaryKeyPlace = UrlConfig::ARGUMENTS,
) {
}

public function __invoke(string $action, DataContext $context): string
{
/** @var ActionColumn $column */
$column = $context->column;

$config = $column->urlConfig ?? new UrlConfig();
if (!$config instanceof UrlConfig) {
throw new LogicException(self::class . ' supports ' . UrlConfig::class . ' only.');
}

$primaryKey = $column->primaryKey ?? $this->defaultPrimaryKey;
$primaryKeyPlace = $config->primaryKeyPlace ?? $this->defaultPrimaryKeyPlace;

$primaryKeyValue = is_object($context->data)
? $context->data->$primaryKey
: $context->data[$primaryKey];

/** @psalm-suppress PossiblyNullOperand We guess that current route is matched. */
$route = ($config->baseRouteName ?? $this->currentRoute->getName()) . '/' . $action;

$arguments = $config->arguments;
$queryParameters = $config->queryParameters;
if ($primaryKeyPlace === UrlConfig::ARGUMENTS) {
$arguments = array_merge($arguments, [$primaryKey => (string)$primaryKeyValue]);
} else {
$queryParameters = array_merge($queryParameters, [$primaryKey => (string)$primaryKeyValue]);
}

return $this->urlGenerator->generate($route, $arguments, $queryParameters);
}
}
Loading

0 comments on commit 3495a4b

Please sign in to comment.