Test Failed
Push — master ( 87437b...18e920 )
by Chauncey
02:46
created

TemplateableTrait::setTemplateOptions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 13
rs 9.4285
cc 3
eloc 8
nc 3
nop 1
1
<?php
2
3
namespace Charcoal\Cms;
4
5
use RuntimeException;
6
7
// From 'charcoal-core'
8
use Charcoal\Model\Model;
9
use Charcoal\Model\ModelInterface;
10
use Charcoal\Source\StorableTrait;
11
12
// From 'charcoal-property'
13
use Charcoal\Property\PropertyInterface;
14
use Charcoal\Property\SelectablePropertyInterface;
15
use Charcoal\Property\Structure\StructureMetadata;
16
use Charcoal\Property\TemplateOptionsProperty;
17
use Charcoal\Property\TemplateProperty;
18
19
// From 'charcoal-cms'
20
use Charcoal\Cms\TemplateableInterface;
21
22
/**
23
 * Default implementation, as Trait, of the {@see TemplateableInterface}.
24
 *
25
 * Note: Call {@see self::saveTemplateOptions()} in {@see StorableTrait::preSave()}
26
 * and {@see StorableTrait::preUpdate()} when using {@see TemplateOptionsProperty}.
27
 */
28
trait TemplateableTrait
29
{
30
    /**
31
     * The object's template identifier.
32
     *
33
     * @var mixed
34
     */
35
    protected $templateIdent;
36
37
    /**
38
     * The object's template controller identifier.
39
     *
40
     * @var mixed
41
     */
42
    protected $controllerIdent;
43
44
    /**
45
     * The template options values.
46
     *
47
     * @var array
48
     */
49
    protected $templateOptions = [];
50
51
    /**
52
     * The template options structure.
53
     *
54
     * @var StructureMetadata
55
     */
56
    protected $templateOptionsMetadata;
57
58
    /**
59
     * Track the state of the template options structure.
60
     *
61
     * @var boolean
62
     */
63
    protected $areTemplateOptionsFinalized = false;
64
65
66
67
    // Properties
68
    // =========================================================================
69
70
    /**
71
     * Set the renderable object's template identifier.
72
     *
73
     * @param  mixed $template The template ID.
74
     * @return self
75
     */
76
    public function setTemplateIdent($template)
77
    {
78
        $this->areTemplateOptionsFinalized = false;
79
80
        $this->templateIdent = $template;
81
        return $this;
82
    }
83
84
    /**
85
     * Retrieve the renderable object's template identifier.
86
     *
87
     * @return mixed
88
     */
89
    public function templateIdent()
90
    {
91
        return $this->templateIdent;
92
    }
93
94
    /**
95
     * Set the renderable object's template controller identifier.
96
     *
97
     * @param  mixed $ident The template controller identifier.
98
     * @return self
99
     */
100
    public function setControllerIdent($ident)
101
    {
102
        $this->areTemplateOptionsFinalized = false;
103
104
        $this->controllerIdent = $ident;
105
        return $this;
106
    }
107
108
    /**
109
     * Retrieve the renderable object's template controller identifier.
110
     *
111
     * @return mixed
112
     */
113
    public function controllerIdent()
114
    {
115
        return $this->controllerIdent;
116
    }
117
118
    /**
119
     * Customize the template's options.
120
     *
121
     * @param  mixed $options Template options.
122
     * @return self
123
     */
124
    public function setTemplateOptions($options)
125
    {
126
        $this->areTemplateOptionsFinalized = false;
127
128
        if (is_numeric($options)) {
129
            $options = null;
130
        } elseif (is_string($options)) {
131
            $options = json_decode($options, true);
132
        }
133
134
        $this->templateOptions = $options;
0 ignored issues
show
Documentation Bug introduced by
It seems like $options of type * is incompatible with the declared type array of property $templateOptions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
135
        return $this;
136
    }
137
138
    /**
139
     * Retrieve the template's customized options.
140
     *
141
     * @return mixed
142
     */
143
    public function templateOptions()
144
    {
145
        return $this->templateOptions;
146
    }
147
148
149
150
    // Utilities
151
    // =========================================================================
152
153
    /**
154
     * Asserts that the templateable class meets the requirements,
155
     * throws an Exception if not.
156
     *
157
     * Requirements:
158
     * 1. The class implements the {@see TemplateableInterface} and
159
     * 2. The model's "template_options" property uses the {@see TemplateOptionsProperty}.
160
     *
161
     *
162
     * @throws RuntimeException If the model does not implement its requirements.
163
     * @return void
164
     */
165
    final protected function assertValidTemplateStructureDependencies()
166
    {
167
        if (!$this instanceof TemplateableInterface) {
168
            throw new RuntimeException(sprintf(
169
                'Class [%s] must implement [%s]',
170
                get_class($this),
171
                TemplateableInterface::class
172
            ));
173
        }
174
175
        $prop = $this->property('template_options');
176
        if (!$prop instanceof TemplateOptionsProperty) {
177
            throw new RuntimeException(sprintf(
178
                'Property "%s" must use [%s]',
179
                $prop->ident(),
180
                TemplateOptionsProperty::class
181
            ));
182
        }
183
    }
184
185
    /**
186
     * Retrieve the default template propert(y|ies).
187
     *
188
     * @return string[]
189
     */
190
    protected function defaultTemplateProperties()
191
    {
192
        return [
193
            'template_ident'
194
        ];
195
    }
196
197
    /**
198
     * Retrieve the template's structure interface(s).
199
     *
200
     * @see    TemplateProperty::__toString()
201
     * @see    \Charcoal\Admin\Widget\FormGroup\TemplateOptionsFormGroup::finalizeStructure()
202
     * @todo   Migrate `MissingDependencyException` from 'mcaskill/charcoal-support' to 'mcaskill/charcoal-core'.
203
     * @param  PropertyInterface|string ...$properties The properties to lookup.
204
     * @return string[]|null
205
     */
206
    protected function extractTemplateInterfacesFrom(...$properties)
207
    {
208
        $interfaces = [];
209
        foreach ($properties as $property) {
210
            if (!$property instanceof PropertyInterface) {
211
                $property = $this->property($property);
0 ignored issues
show
Bug introduced by
It seems like property() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
212
            }
213
214
            $key = $property->ident();
215
            $val = $this->propertyValue($key);
0 ignored issues
show
Bug introduced by
It seems like propertyValue() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
216
            if ($property instanceof SelectablePropertyInterface) {
217
                if ($property->hasChoice($val)) {
218
                    $choice = $property->choice($val);
219
                    $keys   = [ 'controller', 'template', 'class' ];
220
                    foreach ($keys as $key) {
221
                        if (isset($choice[$key])) {
222
                            $interfaces[] = $choice[$key];
223
                            break;
224
                        }
225
                    }
226
                }
227
            } else {
228
                $interfaces[] = $val;
229
            }
230
        }
231
232
        return $interfaces;
233
    }
234
235
    /**
236
     * Prepare the template options (structure) for use.
237
     *
238
     * @uses   self::assertValidTemplateStructureDependencies() Validates that the model meets requirements.
239
     * @param  (PropertyInterface|string)[]|null $templateIdentProperties The template key properties to parse.
240
     * @return boolean
241
     */
242
    protected function prepareTemplateOptions(array $templateIdentProperties = null)
243
    {
244
        $this->assertValidTemplateStructureDependencies();
245
246
        if ($templateIdentProperties === null) {
247
            $templateIdentProperties = $this->defaultTemplateProperties();
248
        }
249
250
        $templateInterfaces = $this->extractTemplateInterfacesFrom(...$templateIdentProperties);
251
        if (empty($templateInterfaces)) {
252
            return false;
253
        }
254
255
        $structureMetadata   = new StructureMetadata();
256
        $templateStructIdent = sprintf('property/structure/%s/%s', $this->objType(), $this->id());
0 ignored issues
show
Bug introduced by
It seems like objType() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
It seems like id() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
257
        $structureMetadata   = $this->metadataLoader()->load(
0 ignored issues
show
Bug introduced by
It seems like metadataLoader() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
258
            $templateStructIdent,
259
            $structureMetadata,
260
            $templateInterfaces
261
        );
262
263
        $this->templateOptionsMetadata = $structureMetadata;
264
265
        return true;
266
    }
267
268
    /**
269
     * Save the template options structure.
270
     *
271
     * @param  (PropertyInterface|string)[]|null $properties The template properties to parse.
272
     * @return void
273
     */
274
    protected function saveTemplateOptions(array $properties = null)
275
    {
276
        if ($properties === null) {
277
            $properties = $this->defaultTemplateProperties();
278
        }
279
280
        $this->prepareTemplateOptions($properties);
281
282
        $key  = 'template_options';
283
        $prop = $this->property($key);
0 ignored issues
show
Bug introduced by
It seems like property() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
284
        if ($prop->structureModelClass() === Model::class) {
285
            $struct = $this->propertyValue($key);
0 ignored issues
show
Bug introduced by
It seems like propertyValue() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
286
            $struct = $prop->structureVal($struct, $this->templateOptionsMetadata());
287
            foreach ($struct->properties() as $propertyIdent => $property) {
288
                $val = $struct[$propertyIdent];
289
                if ($property->l10n()) {
290
                    $val = $this->translator()->translation($struct[$propertyIdent]);
0 ignored issues
show
Bug introduced by
It seems like translator() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
291
                }
292
293
                $struct[$propertyIdent] = $property->save($val);
294
            }
295
        }
296
    }
297
298
    /**
299
     * Retrieve the object's template options metadata.
300
     *
301
     * @return StructureMetadata|null
302
     */
303
    public function templateOptionsMetadata()
304
    {
305
        if ($this->areTemplateOptionsFinalized === false) {
306
            $this->areTemplateOptionsFinalized = true;
307
            $this->prepareTemplateOptions();
308
        }
309
310
        return $this->templateOptionsMetadata;
311
    }
312
313
    /**
314
     * Retrieve the object's template options as a structured model.
315
     *
316
     * @return ModelInterface|ModelInterface[]|null
317
     */
318
    public function templateOptionsStructure()
319
    {
320
        if ($this->areTemplateOptionsFinalized === false) {
321
            $this->areTemplateOptionsFinalized = true;
322
            $this->prepareTemplateOptions();
323
        }
324
325
        $key  = 'template_options';
326
        $prop = $this->property($key);
0 ignored issues
show
Bug introduced by
It seems like property() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
327
        $val  = $this->propertyValue($key);
0 ignored issues
show
Bug introduced by
It seems like propertyValue() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
328
329
        return $prop->structureVal($val, $this->templateOptionsMetadata());
330
    }
331
}
332