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