xref: /plugin/struct/meta/AggregationTable.php (revision 07993756737e69247e6a2b15c689ba4f83661a7c)
1*07993756SAndreas Gohr<?php
2*07993756SAndreas Gohr
3*07993756SAndreas Gohrnamespace plugin\struct\meta;
4*07993756SAndreas Gohr
5*07993756SAndreas Gohrclass AggregationTable {
6*07993756SAndreas Gohr
7*07993756SAndreas Gohr    /**
8*07993756SAndreas Gohr     * @var string the page id of the page this is rendered to
9*07993756SAndreas Gohr     */
10*07993756SAndreas Gohr    protected $id;
11*07993756SAndreas Gohr    /**
12*07993756SAndreas Gohr     * @var string the Type of renderer used
13*07993756SAndreas Gohr     */
14*07993756SAndreas Gohr    protected $mode;
15*07993756SAndreas Gohr    /**
16*07993756SAndreas Gohr     * @var \Doku_Renderer the DokuWiki renderer used to create the output
17*07993756SAndreas Gohr     */
18*07993756SAndreas Gohr    protected $renderer;
19*07993756SAndreas Gohr    /**
20*07993756SAndreas Gohr     * @var SearchConfig the configured search - gives access to columns etc.
21*07993756SAndreas Gohr     */
22*07993756SAndreas Gohr    protected $searchConfig;
23*07993756SAndreas Gohr
24*07993756SAndreas Gohr    /**
25*07993756SAndreas Gohr     * @var Column[] the list of columns to be displayed
26*07993756SAndreas Gohr     */
27*07993756SAndreas Gohr    protected $columns;
28*07993756SAndreas Gohr
29*07993756SAndreas Gohr    /**
30*07993756SAndreas Gohr     * @var  Value[][] the search result
31*07993756SAndreas Gohr     */
32*07993756SAndreas Gohr    protected $result;
33*07993756SAndreas Gohr
34*07993756SAndreas Gohr    /**
35*07993756SAndreas Gohr     * @var int number of all results
36*07993756SAndreas Gohr     */
37*07993756SAndreas Gohr    protected $resultCount;
38*07993756SAndreas Gohr
39*07993756SAndreas Gohr    /**
40*07993756SAndreas Gohr     * @var array for summing up columns
41*07993756SAndreas Gohr     */
42*07993756SAndreas Gohr    protected $sums;
43*07993756SAndreas Gohr
44*07993756SAndreas Gohr    /**
45*07993756SAndreas Gohr     * @todo we might be able to get rid of this helper and move this to SearchConfig
46*07993756SAndreas Gohr     * @var \helper_plugin_struct_config
47*07993756SAndreas Gohr     */
48*07993756SAndreas Gohr    protected $helper;
49*07993756SAndreas Gohr
50*07993756SAndreas Gohr    /**
51*07993756SAndreas Gohr     * Initialize the Aggregation renderer and executes the search
52*07993756SAndreas Gohr     *
53*07993756SAndreas Gohr     * You need to call @see render() on the resulting object.
54*07993756SAndreas Gohr     *
55*07993756SAndreas Gohr     * @param string $id
56*07993756SAndreas Gohr     * @param string $mode
57*07993756SAndreas Gohr     * @param \Doku_Renderer $renderer
58*07993756SAndreas Gohr     * @param SearchConfig $searchConfig
59*07993756SAndreas Gohr     */
60*07993756SAndreas Gohr    public function __construct($id, $mode, \Doku_Renderer $renderer, SearchConfig $searchConfig) {
61*07993756SAndreas Gohr        $this->id = $id;
62*07993756SAndreas Gohr        $this->mode = $mode;
63*07993756SAndreas Gohr        $this->renderer = $renderer;
64*07993756SAndreas Gohr        $this->searchConfig = $searchConfig;
65*07993756SAndreas Gohr        $this->data = $searchConfig->getConf();
66*07993756SAndreas Gohr        $this->columns = $searchConfig->getColumns();
67*07993756SAndreas Gohr
68*07993756SAndreas Gohr        $this->result = $this->searchConfig->execute();
69*07993756SAndreas Gohr        $this->resultCount = $this->searchConfig->getCount();
70*07993756SAndreas Gohr        $this->helper = plugin_load('helper', 'struct_config');
71*07993756SAndreas Gohr    }
72*07993756SAndreas Gohr
73*07993756SAndreas Gohr    /**
74*07993756SAndreas Gohr     * Create the table on the renderer
75*07993756SAndreas Gohr     */
76*07993756SAndreas Gohr    public function render() {
77*07993756SAndreas Gohr        // table open
78*07993756SAndreas Gohr        $this->startScope();
79*07993756SAndreas Gohr        $this->showActiveFilters();
80*07993756SAndreas Gohr        $this->renderer->table_open();
81*07993756SAndreas Gohr
82*07993756SAndreas Gohr        // header
83*07993756SAndreas Gohr        $this->renderer->tablethead_open();
84*07993756SAndreas Gohr        $this->buildColumnHeaders();
85*07993756SAndreas Gohr        $this->addDynamicFilters();
86*07993756SAndreas Gohr        $this->renderer->tablethead_close();
87*07993756SAndreas Gohr
88*07993756SAndreas Gohr        if($this->resultCount) {
89*07993756SAndreas Gohr            // actual data
90*07993756SAndreas Gohr            $this->renderRows();
91*07993756SAndreas Gohr
92*07993756SAndreas Gohr            // footer
93*07993756SAndreas Gohr            $this->summarize();
94*07993756SAndreas Gohr            $this->addLimitControls();
95*07993756SAndreas Gohr        } else {
96*07993756SAndreas Gohr            // nothing found
97*07993756SAndreas Gohr            $this->nullRow();
98*07993756SAndreas Gohr        }
99*07993756SAndreas Gohr
100*07993756SAndreas Gohr        // table close
101*07993756SAndreas Gohr        $this->renderer->table_close();
102*07993756SAndreas Gohr        $this->finishScope();
103*07993756SAndreas Gohr    }
104*07993756SAndreas Gohr
105*07993756SAndreas Gohr    /**
106*07993756SAndreas Gohr     * Adds additional info to document and renderer in XHTML mode
107*07993756SAndreas Gohr     *
108*07993756SAndreas Gohr     * @see finishScope()
109*07993756SAndreas Gohr     */
110*07993756SAndreas Gohr    protected function startScope() {
111*07993756SAndreas Gohr        if($this->mode != 'xhtml') return;
112*07993756SAndreas Gohr
113*07993756SAndreas Gohr        // wrapping div
114*07993756SAndreas Gohr        $this->renderer->doc .= "<div class=\"structaggregation\">";
115*07993756SAndreas Gohr
116*07993756SAndreas Gohr        // unique identifier for this aggregation
117*07993756SAndreas Gohr        $this->renderer->info['struct_table_hash'] = md5(var_export($this->data, true));
118*07993756SAndreas Gohr    }
119*07993756SAndreas Gohr
120*07993756SAndreas Gohr    /**
121*07993756SAndreas Gohr     * Closes the table and anything opened in startScope()
122*07993756SAndreas Gohr     *
123*07993756SAndreas Gohr     * @see startScope()
124*07993756SAndreas Gohr     */
125*07993756SAndreas Gohr    protected function finishScope() {
126*07993756SAndreas Gohr        if($this->mode != 'xhtml') return;
127*07993756SAndreas Gohr
128*07993756SAndreas Gohr        // wrapping div
129*07993756SAndreas Gohr        $this->renderer->doc .= '</div>';
130*07993756SAndreas Gohr
131*07993756SAndreas Gohr        // remove identifier from renderer again
132*07993756SAndreas Gohr        if(isset($this->renderer->info['struct_table_hash'])) {
133*07993756SAndreas Gohr            unset($this->renderer->info['struct_table_hash']);
134*07993756SAndreas Gohr        }
135*07993756SAndreas Gohr    }
136*07993756SAndreas Gohr
137*07993756SAndreas Gohr    /**
138*07993756SAndreas Gohr     * Displays info about the currently applied filters
139*07993756SAndreas Gohr     */
140*07993756SAndreas Gohr    protected function showActiveFilters() {
141*07993756SAndreas Gohr        if($this->mode != 'xhtml') return;
142*07993756SAndreas Gohr        $dynamic = $this->searchConfig->getDynamicParameters();
143*07993756SAndreas Gohr        $filters = $dynamic->getFilters();
144*07993756SAndreas Gohr        if(!$filters) return;
145*07993756SAndreas Gohr
146*07993756SAndreas Gohr        $fltrs = array();
147*07993756SAndreas Gohr        foreach($filters as $column => $filter) {
148*07993756SAndreas Gohr            list($comp, $value) = $filter;
149*07993756SAndreas Gohr
150*07993756SAndreas Gohr            if(strpos($comp, '~') !== false) {
151*07993756SAndreas Gohr                if(strpos($comp, '!~') !== false) {
152*07993756SAndreas Gohr                    $comparator_value = '!~' . str_replace('%', '*', $value);
153*07993756SAndreas Gohr                } else {
154*07993756SAndreas Gohr                    $comparator_value = '~' . str_replace('%', '', $value);
155*07993756SAndreas Gohr                }
156*07993756SAndreas Gohr                $fltrs[] = $column . $comparator_value;
157*07993756SAndreas Gohr            } else {
158*07993756SAndreas Gohr                $fltrs[] = $column . $comp . $value;
159*07993756SAndreas Gohr            }
160*07993756SAndreas Gohr        }
161*07993756SAndreas Gohr
162*07993756SAndreas Gohr        $this->renderer->doc .= '<div class="filter">';
163*07993756SAndreas Gohr        $this->renderer->doc .= '<h4>' . sprintf($this->helper->getLang('tablefilteredby'), hsc(implode(' & ', $fltrs))) . '</h4>';
164*07993756SAndreas Gohr        $this->renderer->doc .= '<div class="resetfilter">';
165*07993756SAndreas Gohr        $this->renderer->internallink($this->id, $this->helper->getLang('tableresetfilter'));
166*07993756SAndreas Gohr        $this->renderer->doc .= '</div>';
167*07993756SAndreas Gohr        $this->renderer->doc .= '</div>';
168*07993756SAndreas Gohr    }
169*07993756SAndreas Gohr
170*07993756SAndreas Gohr    /**
171*07993756SAndreas Gohr     * Shows the column headers with links to sort by column
172*07993756SAndreas Gohr     */
173*07993756SAndreas Gohr    protected function buildColumnHeaders() {
174*07993756SAndreas Gohr        $this->renderer->tablerow_open();
175*07993756SAndreas Gohr
176*07993756SAndreas Gohr        // additional column for row numbers
177*07993756SAndreas Gohr        if($this->data['rownumbers']) {
178*07993756SAndreas Gohr            $this->renderer->tableheader_open();
179*07993756SAndreas Gohr            $this->renderer->cdata('#');
180*07993756SAndreas Gohr            $this->renderer->tableheader_close();
181*07993756SAndreas Gohr        }
182*07993756SAndreas Gohr
183*07993756SAndreas Gohr        // show all headers
184*07993756SAndreas Gohr        foreach($this->data['headers'] as $num => $header) {
185*07993756SAndreas Gohr            $column = $this->columns[$num];
186*07993756SAndreas Gohr
187*07993756SAndreas Gohr            // use field label if no header was set
188*07993756SAndreas Gohr            if(blank($header)) {
189*07993756SAndreas Gohr                if(is_a($column, 'plugin\struct\meta\PageColumn')) {
190*07993756SAndreas Gohr                    $header = $this->helper->getLang('pagelabel'); // @todo this could be part of PageColumn::getTranslatedLabel
191*07993756SAndreas Gohr                } else if(is_a($column, 'plugin\struct\meta\Column')) {
192*07993756SAndreas Gohr                    $header = $column->getTranslatedLabel();
193*07993756SAndreas Gohr                } else {
194*07993756SAndreas Gohr                    $header = 'column ' . $num; // this should never happen
195*07993756SAndreas Gohr                }
196*07993756SAndreas Gohr            }
197*07993756SAndreas Gohr
198*07993756SAndreas Gohr            // simple mode first
199*07993756SAndreas Gohr            if($this->mode != 'xhtml') {
200*07993756SAndreas Gohr                $this->renderer->tableheader_open();
201*07993756SAndreas Gohr                $this->renderer->cdata($header);
202*07993756SAndreas Gohr                $this->renderer->tableheader_close();
203*07993756SAndreas Gohr                continue;
204*07993756SAndreas Gohr            }
205*07993756SAndreas Gohr
206*07993756SAndreas Gohr            // still here? create custom header for more flexibility
207*07993756SAndreas Gohr
208*07993756SAndreas Gohr            // width setting
209*07993756SAndreas Gohr            $width = '';
210*07993756SAndreas Gohr            if(isset($data['widths'][$num]) && $data['widths'][$num] != '-') {
211*07993756SAndreas Gohr                $width = ' style="width: ' . $data['widths'][$num] . ';"';
212*07993756SAndreas Gohr            }
213*07993756SAndreas Gohr
214*07993756SAndreas Gohr            // sort indicator and link
215*07993756SAndreas Gohr            $sortclass = '';
216*07993756SAndreas Gohr            $sorts = $this->searchConfig->getSorts();
217*07993756SAndreas Gohr            $dynamic = $this->searchConfig->getDynamicParameters();
218*07993756SAndreas Gohr            if(isset($sorts[$column->getFullQualifiedLabel()])) {
219*07993756SAndreas Gohr                list(, $currentSort) = $sorts[$column->getFullQualifiedLabel()];
220*07993756SAndreas Gohr                if($currentSort[1]) {
221*07993756SAndreas Gohr                    $sortclass = 'sort-down';
222*07993756SAndreas Gohr                    $dynamic->setSort($column, false);
223*07993756SAndreas Gohr                } else {
224*07993756SAndreas Gohr                    $sortclass = 'sort-up';
225*07993756SAndreas Gohr                }
226*07993756SAndreas Gohr            }
227*07993756SAndreas Gohr            $dynamic->setSort($column, true);
228*07993756SAndreas Gohr            $link = wl($this->id, $dynamic->getURLParameters());
229*07993756SAndreas Gohr
230*07993756SAndreas Gohr            // output XHTML header
231*07993756SAndreas Gohr            $this->renderer->doc .= "<th $width >";
232*07993756SAndreas Gohr            $this->renderer->doc .= '<a href="' . $link . '" class="' . $sortclass . '" title="' . $this->helper->getLang('sort') . '">' . hsc($header) . '</a>';
233*07993756SAndreas Gohr            $this->renderer->doc .= '</th>';
234*07993756SAndreas Gohr        }
235*07993756SAndreas Gohr
236*07993756SAndreas Gohr        $this->renderer->tablerow_close();
237*07993756SAndreas Gohr    }
238*07993756SAndreas Gohr
239*07993756SAndreas Gohr    /**
240*07993756SAndreas Gohr     * Add input fields for dynamic filtering
241*07993756SAndreas Gohr     */
242*07993756SAndreas Gohr    protected function addDynamicFilters() {
243*07993756SAndreas Gohr        if($this->mode != 'xhtml') return;
244*07993756SAndreas Gohr        if(!$this->data['dynfilters']) return;
245*07993756SAndreas Gohr
246*07993756SAndreas Gohr        $this->renderer->doc .= '<tr class="dataflt">';
247*07993756SAndreas Gohr
248*07993756SAndreas Gohr        // add extra column for row numbers
249*07993756SAndreas Gohr        if($this->data['rownumbers']) {
250*07993756SAndreas Gohr            $this->renderer->doc .= '<th></th>';
251*07993756SAndreas Gohr        }
252*07993756SAndreas Gohr
253*07993756SAndreas Gohr        // each column gets a form
254*07993756SAndreas Gohr        foreach($this->columns as $column) {
255*07993756SAndreas Gohr            $this->renderer->doc .= '<th>';
256*07993756SAndreas Gohr            {
257*07993756SAndreas Gohr                $form = new \Doku_Form(array('method' => 'GET', 'action' => wl($this->id)));
258*07993756SAndreas Gohr
259*07993756SAndreas Gohr                // current value
260*07993756SAndreas Gohr                $dynamic = $this->searchConfig->getDynamicParameters();
261*07993756SAndreas Gohr                $filters = $dynamic->getFilters();
262*07993756SAndreas Gohr                if(isset($filters[$column->getFullQualifiedLabel()])) {
263*07993756SAndreas Gohr                    list(, $current) = $filters[$column->getFullQualifiedLabel()];
264*07993756SAndreas Gohr                    $dynamic->removeFilter($column);
265*07993756SAndreas Gohr                } else {
266*07993756SAndreas Gohr                    $current = '';
267*07993756SAndreas Gohr                }
268*07993756SAndreas Gohr
269*07993756SAndreas Gohr                // Add current request params
270*07993756SAndreas Gohr                $params = $dynamic->getURLParameters();
271*07993756SAndreas Gohr                foreach($params as $key => $val) {
272*07993756SAndreas Gohr                    $form->addHidden($key, $val);
273*07993756SAndreas Gohr                }
274*07993756SAndreas Gohr
275*07993756SAndreas Gohr                // add input field
276*07993756SAndreas Gohr                $key = $column->getFullQualifiedLabel() . '*~';
277*07993756SAndreas Gohr                $form->addElement(form_makeField('text', 'dataflt[' . $key . ']', $current, ''));
278*07993756SAndreas Gohr                $this->renderer->doc .= $form->getForm();
279*07993756SAndreas Gohr            }
280*07993756SAndreas Gohr            $this->renderer->doc .= '</th>';
281*07993756SAndreas Gohr        }
282*07993756SAndreas Gohr        $this->renderer->doc .= '</tr>';
283*07993756SAndreas Gohr
284*07993756SAndreas Gohr    }
285*07993756SAndreas Gohr
286*07993756SAndreas Gohr    /**
287*07993756SAndreas Gohr     * Display the actual table data
288*07993756SAndreas Gohr     */
289*07993756SAndreas Gohr    protected function renderRows() {
290*07993756SAndreas Gohr        $this->renderer->tabletbody_open();
291*07993756SAndreas Gohr        foreach($this->result as $rownum => $row) {
292*07993756SAndreas Gohr            $this->renderer->tablerow_open();
293*07993756SAndreas Gohr
294*07993756SAndreas Gohr            // row number column
295*07993756SAndreas Gohr            if($this->data['rownumbers']) {
296*07993756SAndreas Gohr                $this->renderer->tablecell_open();
297*07993756SAndreas Gohr                $this->renderer->doc .= $rownum + 1;
298*07993756SAndreas Gohr                $this->renderer->tablecell_close();
299*07993756SAndreas Gohr            }
300*07993756SAndreas Gohr
301*07993756SAndreas Gohr            /** @var Value $value */
302*07993756SAndreas Gohr            foreach($row as $colnum => $value) {
303*07993756SAndreas Gohr                $this->renderer->tablecell_open();
304*07993756SAndreas Gohr                $value->render($this->renderer, $this->mode);
305*07993756SAndreas Gohr                $this->renderer->tablecell_close();
306*07993756SAndreas Gohr
307*07993756SAndreas Gohr                // summarize
308*07993756SAndreas Gohr                if($this->data['summarize'] && is_numeric($value->getValue())) {
309*07993756SAndreas Gohr                    if(!isset($this->sums[$colnum])) {
310*07993756SAndreas Gohr                        $this->sums[$colnum] = 0;
311*07993756SAndreas Gohr                    }
312*07993756SAndreas Gohr                    $this->sums[$colnum] += $value->getValue();
313*07993756SAndreas Gohr                }
314*07993756SAndreas Gohr            }
315*07993756SAndreas Gohr            $this->renderer->tablerow_close();
316*07993756SAndreas Gohr        }
317*07993756SAndreas Gohr        $this->renderer->tabletbody_close();
318*07993756SAndreas Gohr    }
319*07993756SAndreas Gohr
320*07993756SAndreas Gohr    /**
321*07993756SAndreas Gohr     * Renders an information row for when no results were found
322*07993756SAndreas Gohr     */
323*07993756SAndreas Gohr    protected function nullRow() {
324*07993756SAndreas Gohr        $this->renderer->tablerow_open();
325*07993756SAndreas Gohr        $this->renderer->tablecell_open(count($this->data['cols']) + $this->data['rownumbers'], 'center');
326*07993756SAndreas Gohr        $this->renderer->cdata($this->helper->getLang('none'));
327*07993756SAndreas Gohr        $this->renderer->tablecell_close();
328*07993756SAndreas Gohr        $this->renderer->tablerow_close();
329*07993756SAndreas Gohr    }
330*07993756SAndreas Gohr
331*07993756SAndreas Gohr    /**
332*07993756SAndreas Gohr     * Add sums if wanted
333*07993756SAndreas Gohr     */
334*07993756SAndreas Gohr    protected function summarize() {
335*07993756SAndreas Gohr        if($this->data['summarize']) return;
336*07993756SAndreas Gohr
337*07993756SAndreas Gohr        $this->renderer->tablerow_open();
338*07993756SAndreas Gohr        $len = count($this->data['cols']);
339*07993756SAndreas Gohr
340*07993756SAndreas Gohr        if($this->data['rownumbers']) {
341*07993756SAndreas Gohr            $this->renderer->tablecell_open();
342*07993756SAndreas Gohr            $this->renderer->tablecell_close();
343*07993756SAndreas Gohr        }
344*07993756SAndreas Gohr
345*07993756SAndreas Gohr        for($i = 0; $i < $len; $i++) {
346*07993756SAndreas Gohr            $this->renderer->tablecell_open(1, $this->data['align'][$i]);
347*07993756SAndreas Gohr            if(!empty($sums[$i])) {
348*07993756SAndreas Gohr                $this->renderer->cdata('∑ ' . $sums[$i]);
349*07993756SAndreas Gohr            } else {
350*07993756SAndreas Gohr                if($this->mode == 'xhtml') {
351*07993756SAndreas Gohr                    $this->renderer->doc .= '&nbsp;';
352*07993756SAndreas Gohr                }
353*07993756SAndreas Gohr            }
354*07993756SAndreas Gohr            $this->renderer->tablecell_close();
355*07993756SAndreas Gohr        }
356*07993756SAndreas Gohr        $this->renderer->tablerow_close();
357*07993756SAndreas Gohr    }
358*07993756SAndreas Gohr
359*07993756SAndreas Gohr    /**
360*07993756SAndreas Gohr     * Adds pagin controls to the table
361*07993756SAndreas Gohr     */
362*07993756SAndreas Gohr    protected function addLimitControls() {
363*07993756SAndreas Gohr        if(empty($this->data['limit'])) return;
364*07993756SAndreas Gohr        if($this->mode != 'xhtml') ;
365*07993756SAndreas Gohr
366*07993756SAndreas Gohr        $this->renderer->tablerow_open();
367*07993756SAndreas Gohr        $this->renderer->tableheader_open((count($this->data['cols']) + ($this->data['rownumbers'] ? 1 : 0)));
368*07993756SAndreas Gohr        $offset = $this->data['offset'];
369*07993756SAndreas Gohr
370*07993756SAndreas Gohr        // prev link
371*07993756SAndreas Gohr        if($offset) {
372*07993756SAndreas Gohr            $prev = $offset - $this->data['limit'];
373*07993756SAndreas Gohr            if($prev < 0) {
374*07993756SAndreas Gohr                $prev = 0;
375*07993756SAndreas Gohr            }
376*07993756SAndreas Gohr
377*07993756SAndreas Gohr            $dynamic = $this->searchConfig->getDynamicParameters();
378*07993756SAndreas Gohr            $dynamic->setOffset($prev);
379*07993756SAndreas Gohr            $link = wl($this->id, $dynamic->getURLParameters());
380*07993756SAndreas Gohr            $this->renderer->doc .= '<a href="' . $link . '" class="prev">' . $this->helper->getLang('prev') . '</a>';
381*07993756SAndreas Gohr        }
382*07993756SAndreas Gohr
383*07993756SAndreas Gohr        // next link
384*07993756SAndreas Gohr        if($this->resultCount > $offset + $this->data['limit']) {
385*07993756SAndreas Gohr            $next = $offset + $this->data['limit'];
386*07993756SAndreas Gohr            $dynamic = $this->searchConfig->getDynamicParameters();
387*07993756SAndreas Gohr            $dynamic->setOffset($next);
388*07993756SAndreas Gohr            $link = wl($this->id, $dynamic->getURLParameters());
389*07993756SAndreas Gohr            $this->renderer->doc .= '<a href="' . $link . '" class="next">' . $this->helper->getLang('next') . '</a>';
390*07993756SAndreas Gohr        }
391*07993756SAndreas Gohr
392*07993756SAndreas Gohr        $this->renderer->tableheader_close();
393*07993756SAndreas Gohr        $this->renderer->tablerow_close();
394*07993756SAndreas Gohr    }
395*07993756SAndreas Gohr}
396