xref: /plugin/struct/types/AbstractBaseType.php (revision 16b7d914c0b69bf27d1ddd8a08e05beddbeedf02)
1<?php
2namespace plugin\struct\types;
3
4use plugin\struct\meta\Column;
5use plugin\struct\meta\StructException;
6use plugin\struct\meta\ValidationException;
7
8/**
9 * Class AbstractBaseType
10 *
11 * This class represents a basic type that can be configured to be used in a Schema. It is the main
12 * part of a column definition as defined in meta\Column
13 *
14 * This defines also how the content of the coulmn will be entered and formatted.
15 *
16 * @package plugin\struct\types
17 * @see Column
18 */
19abstract class AbstractBaseType {
20
21    /**
22     * @var array current config
23     */
24    protected $config = array();
25
26    /**
27     * @var array config keys that should not be cleaned despite not being in $config
28     */
29    protected $keepconfig = array('label', 'hint', 'visibility');
30
31    /**
32     * @var string label for the field
33     */
34    protected $label = '';
35
36    /**
37     * @var bool is this a multivalue field?
38     */
39    protected $ismulti = false;
40
41    /**
42     * @var int the type ID
43     */
44    protected $tid = 0;
45
46    /**
47     * @var null|Column the column context this type is part of
48     */
49    protected $context = null;
50
51    /**
52     * @var \DokuWiki_Plugin
53     */
54    protected $hlp = null;
55
56    /**
57     * AbstractBaseType constructor.
58     * @param array|null $config The configuration, might be null if nothing saved, yet
59     * @param string $label The label for this field (empty for new definitions=
60     * @param bool $ismulti Should this field accept multiple values?
61     * @param int $tid The id of this type if it has been saved, yet
62     */
63    public function __construct($config = null, $label = '', $ismulti = false, $tid = 0) {
64        // general config options
65        $baseconfig = array(
66            'visibility' => array(
67                'inpage' => true,
68                'ineditor' => true,
69            )
70        );
71
72        // use previously saved configuration, ignoring all keys that are not supposed to be here
73        if(!is_null($config)) {
74            foreach($config as $key => $value) {
75                if(isset($this->config[$key]) || in_array($key, $this->keepconfig)) {
76                    $this->config[$key] = $value;
77                }
78            }
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     * Add the translatable keys to the configuration
90     *
91     * This checks if a configuration for the translation plugin exists and if so
92     * adds all configured languages to the config array. This ensures all types
93     * can have translatable labels.
94     */
95    protected function initTransConfig() {
96        global $conf;
97        $lang = $conf['lang'];
98        if(isset($conf['plugin']['translation']['translations'])) {
99            $lang .= ' ' . $conf['plugin']['translation']['translations'];
100        }
101        $langs = explode(' ', $lang);
102        $langs = array_map('trim', $langs);
103        $langs = array_filter($langs);
104        $langs = array_unique($langs);
105
106        if(!isset($this->config['label'])) $this->config['label'] = array();
107        if(!isset($this->config['hint'])) $this->config['hint'] = array();
108        // initialize missing keys
109        foreach($langs as $lang) {
110            if(!isset($this->config['label'][$lang])) $this->config['label'][$lang] = '';
111            if(!isset($this->config['hint'][$lang])) $this->config['hint'][$lang] = '';
112        }
113        // strip unknown languages
114        foreach(array_keys($this->config['label']) as $key) {
115            if(!in_array($key, $langs)) unset($this->config['label'][$key]);
116        }
117        foreach(array_keys($this->config['hint']) as $key) {
118            if(!in_array($key, $langs)) unset($this->config['hint'][$key]);
119        }
120
121    }
122
123    /**
124     * Returns data as associative array
125     *
126     * @return array
127     */
128    public function getAsEntry() {
129        return array(
130            'config' => json_encode($this->config),
131            'label' => $this->label,
132            'ismulti' => $this->ismulti,
133            'class' => $this->getClass()
134        );
135    }
136
137    /**
138     * The class name of this type (no namespace)
139     * @return string
140     */
141    public function getClass() {
142        return substr(get_class($this), 20);
143    }
144
145    /**
146     * Return the current configuration for this type
147     *
148     * @return array
149     */
150    public function getConfig() {
151        return $this->config;
152    }
153
154    /**
155     * @return boolean
156     */
157    public function isMulti() {
158        return $this->ismulti;
159    }
160
161    /**
162     * @return string
163     */
164    public function getLabel() {
165        return $this->label;
166    }
167
168    /**
169     * Returns the translated label for this type
170     *
171     * Uses the current language as determined by $conf['lang']. Falls back to english
172     * and then to the Schema label
173     *
174     * @return string
175     */
176    public function getTranslatedLabel() {
177        global $conf;
178        $lang = $conf['lang'];
179        if(!blank($this->config['label'][$lang])) {
180            return $this->config['label'][$lang];
181        }
182        if(!blank($this->config['label']['en'])) {
183            return $this->config['label']['en'];
184        }
185        return $this->label;
186    }
187
188    /**
189     * Returns the translated hint for this type
190     *
191     * Uses the current language as determined by $conf['lang']. Falls back to english.
192     * Returns empty string if no hint is configured
193     *
194     * @return string
195     */
196    public function getTranslatedHint() {
197        global $conf;
198        $lang = $conf['lang'];
199        if(!blank($this->config['hint'][$lang])) {
200            return $this->config['hint'][$lang];
201        }
202        if(!blank($this->config['hint']['en'])) {
203            return $this->config['hint']['en'];
204        }
205        return '';
206    }
207
208    /**
209     * @return int
210     */
211    public function getTid() {
212        return $this->tid;
213    }
214
215    /**
216     * @throws StructException
217     * @return Column
218     */
219    public function getContext() {
220        if(is_null($this->context))
221            throw new StructException('Empty column context requested. Type was probably initialized outside of Schema.');
222        return $this->context;
223    }
224
225    /**
226     * @param Column $context
227     */
228    public function setContext($context) {
229        $this->context = $context;
230    }
231
232    /**
233     * @return bool
234     */
235    public function isVisibleInEditor() {
236        return $this->config['visibility']['ineditor'];
237    }
238
239    /**
240     * @return bool
241     */
242    public function isVisibleInPage() {
243        return $this->config['visibility']['inpage'];
244    }
245
246    /**
247     * Split a single value into multiple values
248     *
249     * This function is called on saving data when only a single value instead of an array
250     * was submitted.
251     *
252     * Types implementing their own @see multiValueEditor() will probably want to override this
253     *
254     * @param string $value
255     * @return array
256     */
257    public function splitValues($value) {
258        return array_map('trim', explode(',', $value));
259    }
260
261    /**
262     * Return the editor to edit multiple values
263     *
264     * Types can override this to provide a better alternative than multiple entry fields
265     *
266     * @param string $name the form base name where this has to be stored
267     * @param string[] $values the current values
268     * @return string html
269     */
270    public function multiValueEditor($name, $values) {
271        $html = '';
272        foreach($values as $value) {
273            $html .= '<div class="multiwrap">';
274            $html .= $this->valueEditor($name . '[]', $value);
275            $html .= '</div>';
276        }
277        // empty field to add
278        $html .= '<div class="newtemplate">';
279        $html .= '<div class="multiwrap">';
280        $html .= $this->valueEditor($name . '[]', '');
281        $html .= '</div>';
282        $html .= '</div>';
283
284        return $html;
285    }
286
287    /**
288     * Return the editor to edit a single value
289     *
290     * @param string $name the form name where this has to be stored
291     * @param string $value the current value
292     * @return string html
293     */
294    public function valueEditor($name, $value) {
295        $class = 'struct_' . strtolower($this->getClass());
296
297        // support the autocomplete configurations out of the box
298        if(isset($this->config['autocomplete']['maxresult']) && $this->config['autocomplete']['maxresult']) {
299            $class .= ' struct_autocomplete';
300        }
301
302        $name = hsc($name);
303        $value = hsc($value);
304        $html = "<input name=\"$name\" value=\"$value\" class=\"$class\" />";
305        return "$html";
306    }
307
308    /**
309     * Output the stored data
310     *
311     * @param string|int $value the value stored in the database
312     * @param \Doku_Renderer $R the renderer currently used to render the data
313     * @param string $mode The mode the output is rendered in (eg. XHTML)
314     * @return bool true if $mode could be satisfied
315     */
316    public function renderValue($value, \Doku_Renderer $R, $mode) {
317        $R->cdata($value);
318        return true;
319    }
320
321    /**
322     * format and return the data
323     *
324     * @param int[]|string[] $values the values stored in the database
325     * @param \Doku_Renderer $R the renderer currently used to render the data
326     * @param string $mode The mode the output is rendered in (eg. XHTML)
327     * @return bool true if $mode could be satisfied
328     */
329    public function renderMultiValue($values, \Doku_Renderer $R, $mode) {
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     * This function builds a where clause for this column, comparing
342     * the current value stored in $column with $value. Types can use it to do
343     * clever things with the comparison.
344     *
345     * This default implementation is probably good enough for most basic types
346     *
347     * @param string $column The column name to us in the SQL
348     * @param string $comp The comparator @see Search::$COMPARATORS
349     * @param string $value
350     * @return array Tuple with the SQL and parameter array
351     */
352    public function compare($column, $comp, $value) {
353        switch($comp) {
354            case '~':
355                $sql = "$column LIKE ?";
356                $opt = array($value);
357                break;
358            case '!~':
359                $sql = "$column NOT LIKE ?";
360                $opt = array($value);
361                break;
362            default:
363                $sql = "$column $comp ?";
364                $opt = array($value);
365        }
366
367        return array($sql, $opt);
368    }
369
370    /**
371     * Validate and optionally clean a single value
372     *
373     * This function needs to throw a validation exception when validation fails.
374     * The exception message will be prefixed by the appropriate field on output
375     *
376     * The function should return the value as it should be saved later on.
377     *
378     * @param string|int $value
379     * @return int|string the cleaned value
380     * @throws ValidationException
381     */
382    public function validate($value) {
383        return trim($value);
384    }
385
386    /**
387     * Overwrite to handle Ajax requests
388     *
389     * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will
390     * be redirected to this function on a fully initialized type. The result is
391     * JSON encoded and returned to the caller. Access additional parameter via $INPUT
392     * as usual
393     *
394     * @throws StructException when something goes wrong
395     * @return mixed
396     */
397    public function handleAjax() {
398        throw new StructException('not implemented');
399    }
400
401    /**
402     * Convenience method to access plugin language strings
403     *
404     * @param string $string
405     * @return string
406     */
407    public function getLang($string) {
408        if(is_null($this->hlp)) $this->hlp = plugin_load('action', 'struct_autoloader');
409        return $this->hlp->getLang($string);
410    }
411}
412