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