xref: /plugin/struct/types/AbstractBaseType.php (revision 1edf6e46bf5ccbff2cab3b71d69ef4cf5f20a379)
1<?php
2namespace dokuwiki\plugin\struct\types;
3
4use dokuwiki\plugin\struct\meta\Column;
5use dokuwiki\plugin\struct\meta\QueryBuilder;
6use dokuwiki\plugin\struct\meta\QueryBuilderWhere;
7use dokuwiki\plugin\struct\meta\StructException;
8use dokuwiki\plugin\struct\meta\TranslationUtilities;
9use dokuwiki\plugin\struct\meta\ValidationException;
10use dokuwiki\plugin\struct\meta\Value;
11
12/**
13 * Class AbstractBaseType
14 *
15 * This class represents a basic type that can be configured to be used in a Schema. It is the main
16 * part of a column definition as defined in meta\Column
17 *
18 * This defines also how the content of the coulmn will be entered and formatted.
19 *
20 * @package dokuwiki\plugin\struct\types
21 * @see Column
22 */
23abstract class AbstractBaseType {
24
25    use TranslationUtilities;
26
27    /**
28     * @var array current config
29     */
30    protected $config = array();
31
32    /**
33     * @var array config keys that should not be cleaned despite not being in $config
34     */
35    protected $keepconfig = array('label', 'hint', 'visibility');
36
37    /**
38     * @var string label for the field
39     */
40    protected $label = '';
41
42    /**
43     * @var bool is this a multivalue field?
44     */
45    protected $ismulti = false;
46
47    /**
48     * @var int the type ID
49     */
50    protected $tid = 0;
51
52    /**
53     * @var null|Column the column context this type is part of
54     */
55    protected $context = null;
56
57    /**
58     * @var \DokuWiki_Plugin
59     */
60    protected $hlp = null;
61
62    /**
63     * AbstractBaseType constructor.
64     * @param array|null $config The configuration, might be null if nothing saved, yet
65     * @param string $label The label for this field (empty for new definitions=
66     * @param bool $ismulti Should this field accept multiple values?
67     * @param int $tid The id of this type if it has been saved, yet
68     */
69    public function __construct($config = null, $label = '', $ismulti = false, $tid = 0) {
70        // general config options
71        $baseconfig = array(
72            'visibility' => array(
73                'inpage' => true,
74                'ineditor' => true,
75            )
76        );
77
78        // use previously saved configuration, ignoring all keys that are not supposed to be here
79        if(!is_null($config)) {
80            $this->mergeConfig($config, $this->config);
81        }
82
83        $this->initTransConfig();
84        $this->config = array_merge($baseconfig, $this->config);
85        $this->label = $label;
86        $this->ismulti = (bool) $ismulti;
87        $this->tid = $tid;
88    }
89
90    /**
91     * Merge the current config with the base config of the type
92     *
93     * Ignores all keys that are not supposed to be there. Recurses into sub keys
94     *
95     * @param array $current Current configuration
96     * @param array $config Base Type configuration
97     */
98    protected function mergeConfig($current, &$config) {
99        foreach($current as $key => $value) {
100            if(isset($config[$key]) || in_array($key, $this->keepconfig)) {
101                if(isset($config[$key]) && is_array($config[$key])) {
102                    $this->mergeConfig($value, $config[$key]);
103                } else {
104                    $config[$key] = $value;
105                }
106            }
107        }
108    }
109
110    /**
111     * Returns data as associative array
112     *
113     * @return array
114     */
115    public function getAsEntry() {
116        return array(
117            'config' => json_encode($this->config),
118            'label' => $this->label,
119            'ismulti' => $this->ismulti,
120            'class' => $this->getClass()
121        );
122    }
123
124    /**
125     * The class name of this type (no namespace)
126     * @return string
127     */
128    public function getClass() {
129        $class = get_class($this);
130        return substr($class, strrpos($class, "\\") + 1);
131    }
132
133    /**
134     * Return the current configuration for this type
135     *
136     * @return array
137     */
138    public function getConfig() {
139        return $this->config;
140    }
141
142    /**
143     * @return boolean
144     */
145    public function isMulti() {
146        return $this->ismulti;
147    }
148
149    /**
150     * @return string
151     */
152    public function getLabel() {
153        return $this->label;
154    }
155
156    /**
157     * Returns the translated label for this type
158     *
159     * Uses the current language as determined by $conf['lang']. Falls back to english
160     * and then to the type label
161     *
162     * @return string
163     */
164    public function getTranslatedLabel() {
165        return $this->getTranslatedKey('label', $this->label);
166    }
167
168    /**
169     * Returns the translated hint for this type
170     *
171     * Uses the current language as determined by $conf['lang']. Falls back to english.
172     * Returns empty string if no hint is configured
173     *
174     * @return string
175     */
176    public function getTranslatedHint() {
177        return $this->getTranslatedKey('hint', '');
178    }
179
180    /**
181     * @return int
182     */
183    public function getTid() {
184        return $this->tid;
185    }
186
187    /**
188     * @throws StructException
189     * @return Column
190     */
191    public function getContext() {
192        if(is_null($this->context))
193            throw new StructException('Empty column context requested. Type was probably initialized outside of Schema.');
194        return $this->context;
195    }
196
197    /**
198     * @param Column $context
199     */
200    public function setContext($context) {
201        $this->context = $context;
202    }
203
204    /**
205     * @return bool
206     */
207    public function isVisibleInEditor() {
208        return $this->config['visibility']['ineditor'];
209    }
210
211    /**
212     * @return bool
213     */
214    public function isVisibleInPage() {
215        return $this->config['visibility']['inpage'];
216    }
217
218    /**
219     * Split a single value into multiple values
220     *
221     * This function is called on saving data when only a single value instead of an array
222     * was submitted.
223     *
224     * Types implementing their own @see multiValueEditor() will probably want to override this
225     *
226     * @param string $value
227     * @return array
228     */
229    public function splitValues($value) {
230        return array_map('trim', explode(',', $value));
231    }
232
233    /**
234     * Return the editor to edit multiple values
235     *
236     * Types can override this to provide a better alternative than multiple entry fields
237     *
238     * @param string $name the form base name where this has to be stored
239     * @param string[] $rawvalues the current values
240     * @param string $htmlID   a unique id to be referenced by the label
241     * @return string html
242     */
243    public function multiValueEditor($name, $rawvalues, $htmlID) {
244        $html = '';
245        foreach($rawvalues as $value) {
246            $html .= '<div class="multiwrap">';
247            $html .= $this->valueEditor($name . '[]', $value, '');
248            $html .= '</div>';
249        }
250        // empty field to add
251        $html .= '<div class="newtemplate">';
252        $html .= '<div class="multiwrap">';
253        $html .= $this->valueEditor($name . '[]', '', $htmlID);
254        $html .= '</div>';
255        $html .= '</div>';
256
257        return $html;
258    }
259
260    /**
261     * Return the editor to edit a single value
262     *
263     * @param string $name     the form name where this has to be stored
264     * @param string $rawvalue the current value
265     * @param string $htmlID   a unique id to be referenced by the label
266     *
267     * @return string html
268     */
269    public function valueEditor($name, $rawvalue, $htmlID) {
270        $class = 'struct_' . strtolower($this->getClass());
271
272        // support the autocomplete configurations out of the box
273        if(isset($this->config['autocomplete']['maxresult']) && $this->config['autocomplete']['maxresult']) {
274            $class .= ' struct_autocomplete';
275        }
276
277        $params = array(
278            'name' => $name,
279            'value' => $rawvalue,
280            'class' => $class,
281            'id' => $htmlID
282        );
283        $attributes = buildAttributes($params, true);
284        return "<input $attributes>";
285    }
286
287    /**
288     * Output the stored data
289     *
290     * @param string|int $value the value stored in the database
291     * @param \Doku_Renderer $R the renderer currently used to render the data
292     * @param string $mode The mode the output is rendered in (eg. XHTML)
293     * @return bool true if $mode could be satisfied
294     */
295    public function renderValue($value, \Doku_Renderer $R, $mode) {
296        $value = $this->displayValue($value);
297        $R->cdata($value);
298        return true;
299    }
300
301    /**
302     * format and return the data
303     *
304     * @param int[]|string[] $values the values stored in the database
305     * @param \Doku_Renderer $R the renderer currently used to render the data
306     * @param string $mode The mode the output is rendered in (eg. XHTML)
307     * @return bool true if $mode could be satisfied
308     */
309    public function renderMultiValue($values, \Doku_Renderer $R, $mode) {
310        $len = count($values);
311        for($i = 0; $i < $len; $i++) {
312            $this->renderValue($values[$i], $R, $mode);
313            if($i < $len - 1) {
314                $R->cdata(', ');
315            }
316        }
317        return true;
318    }
319
320    /**
321     * Render a link in a struct cloud. This should be good for most types, but can be overwritten if necessary.
322     *
323     * @param string|int $value the value stored in the database
324     * @param \Doku_Renderer $R the renderer currently used to render the data
325     * @param string $mode The mode the output is rendered in (eg. XHTML)
326     * @param string $page the target to which should be linked
327     * @param string $filter the filter to apply to the aggregations on $page
328     * @param int $weight the scaled weight of the item. Will already be implemented as css font-size on the outside container
329     */
330    public function renderTagCloudLink($value, \Doku_Renderer $R, $mode, $page, $filter, $weight) {
331        $R->internallink("$page?$filter", $this->displayValue($value));
332    }
333
334    /**
335     * This function is used to modify an aggregation query to add a filter
336     * for the given column matching the given value. A type should add at
337     * least a filter here but could do additional things like joining more
338     * tables needed to handle more complex filters
339     *
340     * Important: $value might be an array. If so, the filter should check against
341     * all provided values ORed together
342     *
343     * @param QueryBuilderWhere $add The where clause where statements can be added
344     * @param string $tablealias The table the currently saved value(s) are stored in
345     * @param string $colname The column name on above table to use in the SQL
346     * @param string $comp The SQL comparator (LIKE, NOT LIKE, =, !=, etc)
347     * @param string|string[] $value this is the user supplied value to compare against. might be multiple
348     * @param string $op the logical operator this filter should use (AND|OR)
349     */
350    public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op) {
351        /** @var QueryBuilderWhere $add Where additionional queries are added to*/
352        if(is_array($value)) {
353            $add = $add->where($op); // sub where group
354            $op = 'OR';
355        }
356        foreach((array) $value as $item) {
357            $pl = $add->getQB()->addValue($item);
358            $add->where($op, "$tablealias.$colname $comp $pl");
359        }
360    }
361
362    /**
363     * Add the proper selection for this type to the current Query
364     *
365     * The default implementation here should be good for nearly all types, it simply
366     * passes the given parameters to the query builder. But type may do more fancy
367     * stuff here, eg. join more tables or select multiple values and combine them to
368     * JSON. If you do, be sure implement a fitting rawValue() method.
369     *
370     * The passed $tablealias.$columnname might be a data_* table (referencing a single
371     * row) or a multi_* table (referencing multiple rows). In the latter case the
372     * multi table has already been joined with the proper conditions.
373     *
374     * You may assume a column alias named 'PID' to be available, should you need the
375     * current page context for a join or sub select.
376     *
377     * @param QueryBuilder $QB
378     * @param string $tablealias The table the currently saved value(s) are stored in
379     * @param string $colname The column name on above table
380     * @param string $alias The added selection *has* to use this column alias
381     */
382    public function select(QueryBuilder $QB, $tablealias, $colname, $alias) {
383        $QB->addSelectColumn($tablealias, $colname, $alias);
384    }
385
386    /**
387     * Sort results by this type
388     *
389     * The default implementation should be good for nearly all types. However some
390     * types may need to do proper SQLite type casting to have the right order.
391     *
392     * Generally if you implemented @see select() you probably want to implement this,
393     * too.
394     *
395     * @param QueryBuilder $QB
396     * @param string $tablealias The table the currently saved value is stored in
397     * @param string $colname The column name on above table (always single column!)
398     * @param string $order either ASC or DESC
399     */
400    public function sort(QueryBuilder $QB, $tablealias, $colname, $order) {
401        $QB->addOrderBy("$tablealias.$colname $order");
402    }
403
404    /**
405     * Get the string by which to sort values of this type
406     *
407     * This implementation is designed to work both as registered function in sqlite
408     * and to provide a string to be used in sorting values of this type in PHP.
409     *
410     * @param string|Value $value The string by which the types would usually be sorted
411     *
412     * @return string
413     */
414    public function getSortString($value) {
415        if (is_string($value)) {
416            return $value;
417        }
418        $display = $value->getDisplayValue();
419        if (is_array($display)) {
420            return blank($display[0]) ? "" : $display[0];
421        }
422        return $display;
423    }
424
425    /**
426     * This allows types to apply a transformation to the value read by select()
427     *
428     * The returned value should always be a single, non-complex string. In general
429     * it is the identifier a type stores in the database.
430     *
431     * This value will be used wherever the raw saved data is needed for comparisons.
432     * The default implementations of renderValue() and valueEditor() will call this
433     * function as well.
434     *
435     * @param string $value The value as returned by select()
436     * @return string The value as saved in the database
437     */
438    public function rawValue($value) {
439        return $value;
440    }
441
442    /**
443     * This is called when a single string is needed to represent this Type's current
444     * value as a single (non-HTML) string. Eg. in a dropdown or in autocompletion.
445     *
446     * @param string $value
447     * @return string
448     */
449    public function displayValue($value) {
450        return $this->rawValue($value);
451    }
452
453    /**
454     * This is the value to be used as argument to a filter for another column.
455     *
456     * In a sense this is the counterpart to the @see filter() function
457     *
458     * @param string $value
459     *
460     * @return string
461     */
462    public function compareValue($value) {
463        return $this->rawValue($value);
464    }
465
466    /**
467     * Validate and optionally clean a single value
468     *
469     * This function needs to throw a validation exception when validation fails.
470     * The exception message will be prefixed by the appropriate field on output
471     *
472     * The function should return the value as it should be saved later on.
473     *
474     * @param string|int $rawvalue
475     * @return int|string the cleaned value
476     * @throws ValidationException
477     */
478    public function validate($rawvalue) {
479        return trim($rawvalue);
480    }
481
482    /**
483     * Overwrite to handle Ajax requests
484     *
485     * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will
486     * be redirected to this function on a fully initialized type. The result is
487     * JSON encoded and returned to the caller. Access additional parameter via $INPUT
488     * as usual
489     *
490     * @throws StructException when something goes wrong
491     * @return mixed
492     */
493    public function handleAjax() {
494        throw new StructException('not implemented');
495    }
496
497    /**
498     * Convenience method to access plugin language strings
499     *
500     * @param string $string
501     * @return string
502     */
503    public function getLang($string) {
504        if(is_null($this->hlp)) $this->hlp = plugin_load('helper', 'struct');
505        return $this->hlp->getLang($string);
506    }
507
508    /**
509     * With what comparator should dynamic filters filter this type?
510     *
511     * This default does a LIKE operation
512     *
513     * @see Search::$COMPARATORS
514     * @return string
515     */
516    public function getDefaultComparator() {
517        return '*~';
518    }
519}
520