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