Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
TableRenderer
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 6
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 prepareColumns
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 prepareRows
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 getSortIconClass
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildSortUrl
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2declare( strict_types = 1 );
3
4/**
5 * TableRenderer.php
6 *
7 * This file is part of the Codex PHP library, which provides a PHP-based interface for creating
8 * UI components consistent with the Codex design system.
9 *
10 * The `TableRenderer` class leverages the `TemplateParser` and `Sanitizer` utilities to ensure the
11 * component object is rendered according to Codex design system standards.
12 *
13 * @category Renderer
14 * @package  Codex\Renderer
15 * @since    0.1.0
16 * @author   Doğu Abaris <abaris@null.net>
17 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
18 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
19 */
20
21namespace Wikimedia\Codex\Renderer;
22
23use InvalidArgumentException;
24use UnexpectedValueException;
25use Wikimedia\Codex\Component\HtmlSnippet;
26use Wikimedia\Codex\Component\Table;
27use Wikimedia\Codex\Contract\Component;
28use Wikimedia\Codex\Contract\ILocalizer;
29use Wikimedia\Codex\Contract\Renderer;
30use Wikimedia\Codex\ParamValidator\ParamDefinitions;
31use Wikimedia\Codex\ParamValidator\ParamValidator;
32use Wikimedia\Codex\ParamValidator\ParamValidatorCallbacks;
33use Wikimedia\Codex\Parser\TemplateParser;
34use Wikimedia\Codex\Utility\Sanitizer;
35
36/**
37 * TableRenderer is responsible for rendering the HTML markup
38 * for a Table component using a Mustache template.
39 *
40 * This class uses the `TemplateParser` and `Sanitizer` utilities to manage
41 * the template rendering process, ensuring that the component object's HTML
42 * output adheres to the Codex design system's standards.
43 *
44 * @category Renderer
45 * @package  Codex\Renderer
46 * @since    0.1.0
47 * @author   Doğu Abaris <abaris@null.net>
48 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
49 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
50 */
51class TableRenderer extends Renderer {
52
53    /**
54     * Constructor to initialize the TableRenderer with necessary dependencies.
55     *
56     * @since 0.1.0
57     * @param Sanitizer $sanitizer The sanitizer instance for cleaning user-provided data and HTML attributes.
58     * @param TemplateParser $templateParser The template parser instance for rendering Mustache templates.
59     * @param ILocalizer $localizer The localizer instance for i18n messages.
60     * @param ParamValidator $paramValidator The parameter validator instance to validate query parameters.
61     * @param ParamValidatorCallbacks $paramValidatorCallbacks The callbacks instance for fetching validated parameters.
62     */
63    public function __construct(
64        Sanitizer $sanitizer,
65        private readonly TemplateParser $templateParser,
66        private readonly ILocalizer $localizer,
67        private readonly ParamValidator $paramValidator,
68        private readonly ParamValidatorCallbacks $paramValidatorCallbacks
69    ) {
70        parent::__construct( $sanitizer );
71    }
72
73    /**
74     * Renders the HTML for a table component.
75     *
76     * Uses the provided Table component to generate HTML markup adhering to the Codex design system.
77     *
78     * @since 0.1.0
79     * @param Component $component The Table object to render.
80     * @return string The rendered HTML string for the component.
81     */
82    public function render( Component $component ): string {
83        if ( !$component instanceof Table ) {
84            throw new InvalidArgumentException( "Expected instance of Table, got " . get_class( $component ) );
85        }
86
87        $pager = $component->getPager();
88        $tableData = [
89            'id' => $this->sanitizer->sanitizeText( $component->getId() ),
90            'showVerticalBorders' => $component->getShowVerticalBorders(),
91            'useRowHeaders' => $component->getUseRowHeaders(),
92            'paginationPosition' => $component->getPaginationPosition(),
93            'totalRows' => $component->getTotalRows(),
94            'caption' => $component->getCaption(),
95            'columns' => $this->prepareColumns( $component ),
96            'rows' => $this->prepareRows( $component ),
97            'hideCaption' => $component->getHideCaption(),
98            'headerContent-html' => $this->sanitizer->sanitizeText( $component->getHeaderContent() ?? '' ),
99            'hasData' => (bool)count( $component->getData() ),
100            'noDataMessage' => count( $component->getData() ) === 0 ?
101                $this->localizer->msg( 'cdx-table-no-data-message' ) : '',
102            'pager' => $pager ? $pager->getHtml() : '',
103            'extraClasses' => $this->getExtraClasses( $component->getAttributes() ),
104            'attributes' => $this->getOtherAttributes( $component->getAttributes() ),
105            'footer-html' => $this->sanitizer->sanitizeText( $component->getFooter() ?? '' ),
106        ];
107        return $this->templateParser->processTemplate( 'table', $tableData );
108    }
109
110    /**
111     * Prepares the column data for rendering in the Mustache template.
112     *
113     * This method takes the columns defined in the Table component and processes them into an array
114     * format suitable for rendering in the table. It handles sorting options, alignment, and the correct
115     * icon for the sorting direction.
116     *
117     * @since 0.1.0
118     * @param Table $table The Table object containing column definitions.
119     * @return array The processed columns ready for rendering.
120     */
121    private function prepareColumns( Table $table ): array {
122        $columns = [];
123        foreach ( $table->getColumns() as $column ) {
124            $isCurrentSortColumn = $table->getCurrentSortColumn() === $column['id'];
125            $columns[] = [
126                'id' => $column['id'],
127                'label-html' => $this->sanitizer->sanitizeText( $column['label'] ),
128                'align' => $column['align'] ?? '',
129                'sortable' => !empty( $column['sortable'] ),
130                'isCurrentSort' => $isCurrentSortColumn,
131                'sortUrl' => $this->buildSortUrl( $table, $column['id'] ),
132                'sortIconClass' => $this->getSortIconClass( $table, $isCurrentSortColumn ),
133            ];
134        }
135
136        return $columns;
137    }
138
139    /**
140     * Prepares the row data for rendering in the Mustache template.
141     *
142     * This method processes the data provided in the Table component and matches it with the defined columns.
143     * Each row is prepared as an array of columns with their respective cell data and alignment settings.
144     *
145     * @since 0.1.0
146     * @param Table $table The Table object containing row data.
147     * @return array The processed rows ready for rendering.
148     */
149    private function prepareRows( Table $table ): array {
150        $rows = [];
151        foreach ( $table->getData() as $row ) {
152            $rowData = [];
153            foreach ( $table->getColumns() as $column ) {
154                $id = $row[$column['id']] ?? '';
155                if ( !$id instanceof HtmlSnippet ) {
156                    $id = (string)$id;
157                }
158                $cellData = $this->sanitizer->sanitizeText( $id );
159                $align = $column['align'] ?? '';
160                $rowData[] = [
161                    'cellData-html' => $cellData,
162                    'align' => $align,
163                ];
164            }
165            $rows[] = [ 'columns' => $rowData ];
166        }
167
168        return $rows;
169    }
170
171    /**
172     * Determines the appropriate CSS class for the sort icon based on the current sort state.
173     *
174     * If the column is the currently sorted column, it returns the correct ascending or descending sort icon class.
175     * Otherwise, it returns the unsorted icon class.
176     *
177     * @since 0.1.0
178     * @param Table $table The Table object.
179     * @param bool $isCurrentSortColumn Whether the column is currently sorted.
180     * @return string The CSS class for the sort icon.
181     */
182    private function getSortIconClass( Table $table, bool $isCurrentSortColumn ): string {
183        if ( $isCurrentSortColumn ) {
184            return $table->getCurrentSortDirection() === Table::SORT_ASCENDING ? 'cdx-table__table__sort-icon--asc'
185                : 'cdx-table__table__sort-icon--desc';
186        }
187
188        return 'cdx-table__table__sort-icon--unsorted';
189    }
190
191    /**
192     * Builds the URL for sorting the table by a specific column.
193     *
194     * This method constructs the sort URL by adjusting the query parameters to reflect the new sort column
195     * and direction (ascending or descending).
196     *
197     * @since 0.1.0
198     * @param Table $table The Table object.
199     * @param string $columnId The ID of the column to sort by.
200     * @return string The generated URL for sorting by the specified column.
201     */
202    private function buildSortUrl( Table $table, string $columnId ): string {
203        $definitions = ParamDefinitions::getDefinitionsForContext( 'table' );
204
205        foreach ( $definitions as $param => $rules ) {
206            try {
207                $this->paramValidator->validateValue(
208                    $param,
209                    $this->paramValidatorCallbacks->getValue(
210                        $param,
211                        $rules[ParamValidator::PARAM_DEFAULT],
212                        []
213                    ),
214                    $rules
215                );
216            } catch ( UnexpectedValueException $e ) {
217                throw new InvalidArgumentException( "Invalid value for parameter '$param': " . $e->getMessage() );
218            }
219        }
220
221        $isAscending = $table->getCurrentSortDirection() === Table::SORT_ASCENDING;
222        return '?' . http_build_query( [
223            // Start with all current URL parameters to preserve them
224            ...$this->paramValidatorCallbacks->getAllParams(),
225            // Override only the sort-specific parameters
226            'offset' => $this->paramValidatorCallbacks->getValue( 'offset', '', [] ),
227            'limit' => $this->paramValidatorCallbacks->getValue( 'limit', 5, [] ),
228            'sort' => $columnId,
229            'asc' => $isAscending ? '' : 1,
230            'desc' => $isAscending ? 1 : '',
231        ] );
232    }
233}