Issues (13)

src/BaseListView.php (2 issues)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\DataView;
6
7
use Yiisoft\Data\Paginator\KeysetPaginator;
8
use Yiisoft\Data\Paginator\OffsetPaginator;
9
use Yiisoft\Data\Paginator\PaginatorInterface;
10
use Yiisoft\Data\Reader\ReadableDataInterface;
11
use Yiisoft\Definitions\Exception\CircularReferenceException;
12
use Yiisoft\Definitions\Exception\InvalidConfigException;
13
use Yiisoft\Definitions\Exception\NotInstantiableException;
14
use Yiisoft\Factory\NotFoundException;
15
use Yiisoft\Html\Tag\Div;
16
use Yiisoft\Html\Tag\Td;
17
use Yiisoft\Router\UrlGeneratorInterface;
18
use Yiisoft\Translator\CategorySource;
19
use Yiisoft\Translator\IdMessageReader;
20
use Yiisoft\Translator\IntlMessageFormatter;
21
use Yiisoft\Translator\SimpleMessageFormatter;
22
use Yiisoft\Translator\Translator;
23
use Yiisoft\Translator\TranslatorInterface;
24
use Yiisoft\Widget\Widget;
25
use Yiisoft\Yii\DataView\Exception\DataReaderNotSetException;
26
27
abstract class BaseListView extends Widget
28
{
29
    /**
30
     * A name for {@see CategorySource} used with translator ({@see TranslatorInterface}) by default.
31
     */
32
    public const DEFAULT_TRANSLATION_CATEGORY = 'yii-dataview';
33
34
    /**
35
     * @var TranslatorInterface A translator instance used for translations of messages. If it was not set
36
     * explicitly in the constructor, a default one created automatically in {@see createDefaultTranslator()}.
37
     */
38
    private TranslatorInterface $translator;
39
40
    private array $attributes = [];
41
    protected ?string $emptyText = null;
42
    private array $emptyTextAttributes = [];
43
    private string $header = '';
44
    private array $headerAttributes = [];
45
    private string $layout = "{header}\n{toolbar}";
46
    private string $layoutGridTable = "{items}\n{summary}\n{pager}";
47
    private string $pagination = '';
48
    protected ?ReadableDataInterface $dataReader = null;
49
    protected array $sortLinkAttributes = [];
50
    private ?string $summary = null;
51
    private array $summaryAttributes = [];
52
    private string $toolbar = '';
53
    protected array $urlArguments = [];
54
    protected array $urlQueryParameters = [];
55
    private bool $withContainer = true;
56
57 99
    public function __construct(
58
        TranslatorInterface|null $translator = null,
59
        private UrlGeneratorInterface|null $urlGenerator = null,
60
        private string $translationCategory = self::DEFAULT_TRANSLATION_CATEGORY,
61
    ) {
62 99
        $this->translator = $translator ?? $this->createDefaultTranslator();
63
    }
64
65
    /**
66
     * Renders the data models.
67
     *
68
     * @return string the rendering result.
69
     */
70
    abstract protected function renderItems(): string;
71
72
    /**
73
     * Returns a new instance with the HTML attributes. The following special options are recognized.
74
     *
75
     * @param array $values Attribute values indexed by attribute names.
76
     */
77 1
    public function attributes(array $values): static
78
    {
79 1
        $new = clone $this;
80 1
        $new->attributes = $values;
81
82 1
        return $new;
83
    }
84
85
    /**
86
     * Return a new instance with the empty text.
87
     *
88
     * @param string $emptyText the HTML content to be displayed when {@see dataProvider} does not have any data.
89
     *
90
     * The default value is the text "No results found." which will be translated to the current application language.
91
     *
92
     * {@see notShowOnEmpty()}
93
     * {@see emptyTextAttributes()}
94
     */
95 2
    public function emptyText(?string $emptyText): static
96
    {
97 2
        $new = clone $this;
98 2
        $new->emptyText = $emptyText;
99
100 2
        return $new;
101
    }
102
103
    /**
104
     * Returns a new instance with the HTML attributes for the empty text.
105
     *
106
     * @param array $values Attribute values indexed by attribute names.
107
     */
108 1
    public function emptyTextAttributes(array $values): static
109
    {
110 1
        $new = clone $this;
111 1
        $new->emptyTextAttributes = $values;
112
113 1
        return $new;
114
    }
115
116 93
    public function getDataReader(): ReadableDataInterface
117
    {
118 93
        if ($this->dataReader === null) {
119 1
            throw new DataReaderNotSetException();
120
        }
121
122 92
        return $this->dataReader;
123
    }
124
125 1
    public function getUrlGenerator(): UrlGeneratorInterface
126
    {
127 1
        if ($this->urlGenerator === null) {
128 1
            throw new Exception\UrlGeneratorNotSetException();
129
        }
130
131
        return $this->urlGenerator;
132
    }
133
134
    /**
135
     * Return new instance with the header for the grid.
136
     *
137
     * @param string $value The header of the grid.
138
     *
139
     * {@see headerAttributes}
140
     */
141 3
    public function header(string $value): self
142
    {
143 3
        $new = clone $this;
144 3
        $new->header = $value;
145
146 3
        return $new;
147
    }
148
149
    /**
150
     * Return new instance with the HTML attributes for the header.
151
     *
152
     * @param array $values Attribute values indexed by attribute names.
153
     */
154 1
    public function headerAttributes(array $values): self
155
    {
156 1
        $new = clone $this;
157 1
        $new->headerAttributes = $values;
158
159 1
        return $new;
160
    }
161
162
    /**
163
     * Returns a new instance with the id of the grid view, detail view, or list view.
164
     *
165
     * @param string $value The id of the grid view, detail view, or list view.
166
     */
167 85
    public function id(string $value): static
168
    {
169 85
        $new = clone $this;
170 85
        $new->attributes['id'] = $value;
171
172 85
        return $new;
173
    }
174
175
    /**
176
     * Returns a new instance with the layout of the grid view, and list view.
177
     *
178
     * @param string $value The template that determines how different sections of the grid view, list view. Should be
179
     * organized.
180
     *
181
     * The following tokens will be replaced with the corresponding section contents:
182
     *
183
     * - `{header}`: The header section.
184
     * - `{toolbar}`: The toolbar section.
185
     */
186 2
    public function layout(string $value): static
187
    {
188 2
        $new = clone $this;
189 2
        $new->layout = $value;
190
191 2
        return $new;
192
    }
193
194
    /**
195
     * Returns a new instance with the layout grid table.
196
     *
197
     * @param string $value The layout that determines how different sections of the grid view, list view. Should be
198
     * organized.
199
     *
200
     * The following tokens will be replaced with the corresponding section contents:
201
     *
202
     * - `{items}`: The items section.
203
     * - `{summary}`: The summary section.
204
     * - `{pager}`: The pager section.
205
     */
206 3
    public function layoutGridTable(string $value): static
207
    {
208 3
        $new = clone $this;
209 3
        $new->layoutGridTable = $value;
210
211 3
        return $new;
212
    }
213
214
    /**
215
     * Returns a new instance with the pagination of the grid view, detail view, or list view.
216
     *
217
     * @param string $value The pagination of the grid view, detail view, or list view.
218
     */
219 4
    public function pagination(string $value): static
220
    {
221 4
        $new = clone $this;
222 4
        $new->pagination = $value;
223
224 4
        return $new;
225
    }
226
227
    /**
228
     * Returns a new instance with the paginator interface of the grid view, detail view, or list view.
229
     *
230
     * @param ReadableDataInterface $dataReader The paginator interface of the grid view, detail view, or list view.
231
     */
232 93
    public function dataReader(ReadableDataInterface $dataReader): static
233
    {
234 93
        $new = clone $this;
235 93
        $new->dataReader = $dataReader;
236 93
        return $new;
237
    }
238
239
    /**
240
     * Return new instance with the HTML attributes for widget link sort.
241
     *
242
     * @param array $values Attribute values indexed by attribute names.
243
     */
244 1
    public function sortLinkAttributes(array $values): static
245
    {
246 1
        $new = clone $this;
247 1
        $new->sortLinkAttributes = $values;
248
249 1
        return $new;
250
    }
251
252
    /**
253
     * Returns a new instance with the summary of the grid view, detail view, and list view.
254
     *
255
     * @param string $value the HTML content to be displayed as the summary of the list view.
256
     *
257
     * If you do not want to show the summary, you may set it with an empty string.
258
     *
259
     * The following tokens will be replaced with the corresponding values:
260
     *
261
     * - `{begin}`: the starting row number (1-based) currently being displayed.
262
     * - `{end}`: the ending row number (1-based) currently being displayed.
263
     * - `{count}`: the number of rows currently being displayed.
264
     * - `{totalCount}`: the total number of rows available.
265
     * - `{page}`: the page number (1-based) current being displayed.
266
     * - `{pageCount}`: the number of pages available.
267
     */
268 1
    public function summary(?string $value): static
269
    {
270 1
        $new = clone $this;
271 1
        $new->summary = $value;
272
273 1
        return $new;
274
    }
275
276
    /**
277
     * Returns a new instance with the HTML attributes for summary of grid view, detail view, and list view.
278
     *
279
     * @param array $values Attribute values indexed by attribute names.
280
     */
281 1
    public function summaryAttributes(array $values): static
282
    {
283 1
        $new = clone $this;
284 1
        $new->summaryAttributes = $values;
285
286 1
        return $new;
287
    }
288
289
    /**
290
     * Return new instance with toolbar content.
291
     *
292
     * @param string $value The toolbar content.
293
     *
294
     * @psalm-param array<array-key,array> $toolbar
295
     */
296 1
    public function toolbar(string $value): self
297
    {
298 1
        $new = clone $this;
299 1
        $new->toolbar = $value;
300
301 1
        return $new;
302
    }
303
304
    /**
305
     * Return a new instance with arguments of the route.
306
     *
307
     * @param array $value Arguments of the route.
308
     */
309 1
    public function urlArguments(array $value): static
310
    {
311 1
        $new = clone $this;
312 1
        $new->urlArguments = $value;
313
314 1
        return $new;
315
    }
316
317
    /**
318
     * Return a new instance with query parameters of the route.
319
     *
320
     * @param array $value The query parameters of the route.
321
     */
322 1
    public function urlQueryParameters(array $value): static
323
    {
324 1
        $new = clone $this;
325 1
        $new->urlQueryParameters = $value;
326
327 1
        return $new;
328
    }
329
330
    /**
331
     * Returns a new instance whether container is enabled or not.
332
     *
333
     * @param bool $value Whether container is enabled or not.
334
     */
335 1
    public function withContainer(bool $value = true): static
336
    {
337 1
        $new = clone $this;
338 1
        $new->withContainer = $value;
339
340 1
        return $new;
341
    }
342
343 7
    protected function renderEmpty(int $colspan): Td
344
    {
345 7
        $emptyTextAttributes = $this->emptyTextAttributes;
346 7
        $emptyTextAttributes['colspan'] = $colspan;
347
348 7
        $emptyText = $this->translator->translate(
349 7
            $this->emptyText ?? 'No results found.',
350 7
            category: $this->translationCategory
351 7
        );
352
353 7
        return Td::tag()->attributes($emptyTextAttributes)->content($emptyText);
354
    }
355
356
    /**
357
     * @throws InvalidConfigException
358
     * @throws NotFoundException
359
     * @throws NotInstantiableException
360
     * @throws CircularReferenceException
361
     */
362
    protected function renderLinkSorter(string $attribute, string $label): string
363
    {
364
        $dataReader = $this->getDataReader();
365
        if (!$dataReader instanceof PaginatorInterface) {
366
            return '';
367
        }
368
369
        $sort = $dataReader->getSort();
370
        if ($sort === null) {
371
            return '';
372
        }
373
374
        $linkSorter = $dataReader instanceof OffsetPaginator
375
            ? LinkSorter::widget()->currentPage($dataReader->getCurrentPage())
0 ignored issues
show
The method currentPage() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\DataView\LinkSorter. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

375
            ? LinkSorter::widget()->/** @scrutinizer ignore-call */ currentPage($dataReader->getCurrentPage())
Loading history...
376
            : LinkSorter::widget();
377
378
        return $linkSorter
379
            ->attribute($attribute)
0 ignored issues
show
The method attribute() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\DataView\LinkSorter. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

379
            ->/** @scrutinizer ignore-call */ attribute($attribute)
Loading history...
380
            ->attributes($sort->getCriteria())
381
            ->directions($sort->getOrder())
382
            ->iconAscClass('bi bi-sort-alpha-up')
383
            ->iconDescClass('bi bi-sort-alpha-down')
384
            ->label($label)
385
            ->linkAttributes($this->sortLinkAttributes)
386
            ->pageSize($dataReader->getPageSize())
387
            ->urlArguments($this->urlArguments)
388
            ->urlQueryParameters($this->urlQueryParameters)
389
            ->render();
390
    }
391
392 94
    public function render(): string
393
    {
394 94
        if ($this->dataReader === null) {
395 2
            throw new DataReaderNotSetException();
396
        }
397
398 92
        return $this->renderGrid();
399
    }
400
401
    /**
402
     * @psalm-return array<array-key, array|object>
403
     */
404 92
    protected function getItems(): array
405
    {
406 92
        $data = $this->getDataReader()->read();
407 92
        return is_array($data) ? $data : iterator_to_array($data);
408
    }
409
410 90
    private function renderPagination(): string
411
    {
412 90
        $dataReader = $this->getDataReader();
413 90
        if (!$dataReader instanceof PaginatorInterface) {
414
            return '';
415
        }
416
417 90
        return $dataReader->isRequired() ? $this->pagination : '';
418
    }
419
420 90
    private function renderSummary(): string
421
    {
422 90
        if ($this->getDataReader() instanceof KeysetPaginator) {
423 2
            return '';
424
        }
425
426
        /** @var OffsetPaginator $paginator */
427 88
        $paginator = $this->getDataReader();
428
429 88
        $data = iterator_to_array($paginator->read());
430 88
        $pageCount = count($data);
431
432 88
        if ($pageCount <= 0) {
433 6
            return '';
434
        }
435
436 82
        $summary = $this->translator->translate(
437 82
            $this->summary ?? 'Page <b>{currentPage}</b> of <b>{totalPages}</b>',
438 82
            [
439 82
                'currentPage' => $paginator->getCurrentPage(),
440 82
                'totalPages' => $paginator->getTotalPages(),
441 82
            ],
442 82
            $this->translationCategory,
443 82
        );
444
445 82
        return Div::tag()->attributes($this->summaryAttributes)->content($summary)->encode(false)->render();
446
    }
447
448 92
    private function renderGrid(): string
449
    {
450 92
        $attributes = $this->attributes;
451 92
        $contentGrid = '';
452
453 92
        if ($this->layout !== '') {
454 91
            $contentGrid = trim(
455 91
                strtr($this->layout, ['{header}' => $this->renderHeader(), '{toolbar}' => $this->toolbar])
456 91
            );
457
        }
458
459 92
        return match ($this->withContainer) {
460 92
            true => trim(
461 92
                $contentGrid . PHP_EOL . Div::tag()
462 92
                    ->attributes($attributes)
463 92
                    ->content(PHP_EOL . $this->renderGridTable() . PHP_EOL)
464 92
                    ->encode(false)
465 92
                    ->render()
466 92
            ),
467 92
            false => trim($contentGrid . PHP_EOL . $this->renderGridTable()),
468 92
        };
469
    }
470
471 92
    private function renderGridTable(): string
472
    {
473 92
        return trim(
474 92
            strtr(
475 92
                $this->layoutGridTable,
476 92
                [
477 92
                    '{header}' => $this->renderHeader(),
478 92
                    '{toolbar}' => $this->toolbar,
479 92
                    '{items}' => $this->renderItems(),
480 92
                    '{summary}' => $this->renderSummary(),
481 92
                    '{pager}' => $this->renderPagination(),
482 92
                ],
483 92
            )
484 92
        );
485
    }
486
487 92
    private function renderHeader(): string
488
    {
489 92
        return match ($this->header) {
490 92
            '' => '',
491 92
            default => Div::tag()
492 92
                ->attributes($this->headerAttributes)
493 92
                ->content($this->header)
494 92
                ->encode(false)
495 92
                ->render(),
496 92
        };
497
    }
498
499
    /**
500
     * Creates default translator to use if {@see $translator} was not set explicitly in the constructor. Depending on
501
     * "intl" extension availability, either {@see IntlMessageFormatter} or {@see SimpleMessageFormatter} is used as
502
     * formatter.
503
     *
504
     * @return Translator Translator instance used for translations of messages.
505
     */
506 93
    private function createDefaultTranslator(): Translator
507
    {
508 93
        $categorySource = new CategorySource(
509 93
            $this->translationCategory,
510 93
            new IdMessageReader(),
511 93
            extension_loaded('intl') ? new IntlMessageFormatter() : new SimpleMessageFormatter(),
512 93
        );
513 93
        $translator = new Translator();
514 93
        $translator->addCategorySources($categorySource);
515
516 93
        return $translator;
517
    }
518
}
519