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