DeclarativeProps::getProps()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 2
1
<?php
2
3
namespace Rexlabs\Smokescreen\Transformer\Props;
4
5
use DateTime;
6
use DateTimeImmutable;
7
use DateTimeInterface;
8
use DateTimeZone;
9
use Rexlabs\Smokescreen\Definition\DefinitionParser;
10
use Rexlabs\Smokescreen\Exception\InvalidDefinitionException;
11
use Rexlabs\Smokescreen\Helpers\ArrayHelper;
12
use Rexlabs\Smokescreen\Helpers\StrHelper;
13
14
trait DeclarativeProps
15
{
16
    /** @var array */
17
    protected $props = [];
18
19
    /** @var string Example: 2018-03-08 */
20
    protected $dateFormat = 'Y-m-d';
21
22
    /** @var string Example: 2018-03-08T19:11:11.234+00:00 */
23
    protected $dateTimeFormat = 'Y-m-d\TH:i:s.vP';
24
25
    /** @var mixed|null Optionally set a default timezone */
26
    protected $defaultTimezone;
27
28
    /** @var DefinitionParser|null */
29
    protected $definitionParser;
30
31
    public function getProps(): array
32
    {
33
        return $this->props;
34
    }
35
36
    /**
37
     * Get the default date formatter string.
38
     *
39
     * @return string
40
     */
41
    protected function getDateFormat(): string
42
    {
43
        return $this->dateFormat;
44
    }
45
46
    /**
47
     * Get the default date-time formatter string.
48
     *
49
     * @return string
50
     */
51
    protected function getDatetimeFormat(): string
52
    {
53
        return $this->dateTimeFormat;
54
    }
55
56
    /**
57
     * Get the default timezone that dates should be coerced into.
58
     * A null value means the datetime object will not be converted.
59
     *
60
     * @return DateTimeZone|string|null
61
     */
62
    protected function getDefaultTimezone()
63
    {
64
        return $this->defaultTimezone;
65
    }
66
67
    /**
68
     * Helper method for returning formatted properties from a source object
69
     * or array.
70
     *
71
     * @param \ArrayAccess|array $model
72
     * @param array              $props
73
     *
74
     * @throws \InvalidArgumentException
75
     *
76
     * @return array
77
     */
78
    protected function withProps($model, array $props): array
79
    {
80
        if (!(\is_array($model) || $model instanceof \ArrayAccess)) {
0 ignored issues
show
introduced by
$model is always a sub-type of ArrayAccess.
Loading history...
81
            throw new \InvalidArgumentException('Expect array or object implementing \ArrayAccess');
82
        }
83
84
        // Given an array of props (which may include a definition value), we
85
        // cycle through each, resolve the value from the model, and apply any
86
        // conversions to the value as necessary.
87
        $data = [];
88
        foreach ($props as $key => $definition) {
89
            if (\is_int($key)) {
90
                // This isn't a key => value pair, so there is no definition.
91
                $key = $definition;
92
                $definition = null;
93
            }
94
95
            // Convert property to a snake-case version.
96
            // It may be a nested (dot-notation) key.
97
            $propKey = StrHelper::snakeCase($key);
98
            if ($definition instanceof \Closure) {
99
                // If the definition is a function, execute it on the value
100
                $value = $definition->bindTo($this)($model, $propKey);
101
            } else {
102
                // Format the field according to the definition
103
                $value = $this->getPropValue($model, $this->parsePropDefinition($propKey, $definition));
104
            }
105
106
            // Set the prop value in the array
107
            ArrayHelper::mutate($data, $key, $value);
108
        }
109
110
        return $data;
111
    }
112
113
    /**
114
     * @param \ArrayAccess|array $model
115
     * @param PropDefinition     $propDefinition
116
     *
117
     * @throws \InvalidArgumentException
118
     *
119
     * @return mixed|null
120
     */
121
    protected function getPropValue($model, PropDefinition $propDefinition)
122
    {
123
        // Get the model value via array access.
124
        $value = $model[$propDefinition->mapKey()] ?? null;
125
126
        // Format the value the property value.
127
        $value = $this->formatPropValue($value, $propDefinition);
128
129
        return $value;
130
    }
131
132
    /**
133
     * @param mixed          $value
134
     * @param PropDefinition $propDefinition
135
     *
136
     * @throws \InvalidArgumentException
137
     *
138
     * @return mixed
139
     */
140
    protected function formatPropValue($value, $propDefinition)
141
    {
142
        // There must be a type defined for this property definition.
143
        if (!$propDefinition->type()) {
144
            return $value;
145
        }
146
147
        // Null values are not formatted.
148
        if ($value === null) {
149
            return $value;
150
        }
151
152
        // Built-in types.
153
        // Cast the value according to 'type'
154
        switch ($propDefinition->type()) {
155
            case 'int':
156
            case 'integer':
157
                return (int) $value;
158
            case 'real':
159
            case 'float':
160
            case 'double':
161
                return (float) $value;
162
            case 'string':
163
                return (string) $value;
164
            case 'bool':
165
            case 'boolean':
166
                return (bool) $value;
167
            case 'array':
168
                return (array) $value;
169
            case 'date':
170
                return $this->formatDate($value, $propDefinition);
171
            case 'datetime':
172
                return $this->formatDatetime($value, $propDefinition);
173
            case 'datetime_utc':
174
                return $this->formatDatetimeUtc($value, $propDefinition);
175
            default:
176
                // Fall through
177
                break;
178
179
        }
180
181
        // As a final attempt, try to locate a matching method on the class that
182
        // is prefixed with 'format'.
183
        $method = 'format'.StrHelper::studlyCase($propDefinition->type());
184
        if (method_exists($this, $method)) {
185
            return $this->$method($value, $propDefinition);
186
        }
187
188
        throw new InvalidDefinitionException("Unsupported format type: {$propDefinition->type()}");
189
    }
190
191
    protected function getDefinitionParser(): DefinitionParser
192
    {
193
        if ($this->definitionParser === null) {
194
            $this->definitionParser = new DefinitionParser();
195
            $this->definitionParser->addShortKeys('type', [
196
                'int',
197
                'integer',
198
                'real',
199
                'float',
200
                'double',
201
                'string',
202
                'bool',
203
                'boolean',
204
                'array',
205
                'date',
206
                'datetime',
207
                'datetime_utc',
208
            ]);
209
        }
210
211
        return $this->definitionParser;
212
    }
213
214
    /**
215
     * @param string       $propKey
216
     * @param string|mixed $definition
217
     *
218
     * @throws \Rexlabs\Smokescreen\Exception\ParseDefinitionException
219
     *
220
     * @return PropDefinition
221
     */
222
    protected function parsePropDefinition(string $propKey, $definition): PropDefinition
223
    {
224
        return new PropDefinition($propKey, $this->getDefinitionParser()
225
            ->parse($definition));
226
    }
227
228
    /**
229
     * Given a date object, return a new date object with the specified timezone.
230
     * This method ensures that the original date object is not mutated.
231
     *
232
     * @param DateTimeInterface   $date
233
     * @param DateTimeZone|string $timezone
234
     *
235
     * @return DateTimeInterface|DateTime|DateTimeImmutable
236
     */
237
    protected function convertTimeZone(DateTimeInterface $date, $timezone)
238
    {
239
        $timezone = ($timezone instanceof DateTimeZone) ? $timezone : new DateTimeZone($timezone);
240
241
        if ($date->getTimezone() !== $timezone) {
242
            if ($date instanceof DateTime) {
243
                // DateTime is not immutable, so make a copy
244
                $date = (clone $date);
245
                $date->setTimezone($timezone);
246
            } elseif ($date instanceof DateTimeImmutable) {
247
                $date = $date->setTimezone($timezone);
248
            }
249
        }
250
251
        return $date;
252
    }
253
254
    /**
255
     * Format a date object.
256
     *
257
     * @param DateTimeInterface $date
258
     * @param PropDefinition    $definition
259
     *
260
     * @return string
261
     */
262
    protected function formatDatetime(DateTimeInterface $date, PropDefinition $definition): string
263
    {
264
        $timezone = $definition->get('timezone', $this->getDefaultTimezone());
265
        if ($timezone !== null) {
266
            $date = $this->convertTimeZone($date, $timezone);
267
        }
268
269
        return $date->format($definition->get('format', $this->getDatetimeFormat()));
270
    }
271
272
    /**
273
     * Shortcut method for converting a date to UTC and returning the formatted string.
274
     *
275
     * @param DateTimeInterface $date
276
     * @param PropDefinition    $propDefinition
277
     *
278
     * @return string
279
     *
280
     * @see FormatsFields::formatDateTime()
281
     */
282
    protected function formatDatetimeUtc(DateTimeInterface $date, PropDefinition $propDefinition): string
283
    {
284
        return $this->formatDatetime($date, $propDefinition->set('timezone', 'UTC'));
285
    }
286
287
    /**
288
     * @param DateTimeInterface $date
289
     * @param PropDefinition    $propDefinition
290
     *
291
     * @return string
292
     */
293
    protected function formatDate(DateTimeInterface $date, PropDefinition $propDefinition): string
294
    {
295
        if (!$propDefinition->has('format')) {
296
            $propDefinition->set('format', $this->getDateFormat());
297
        }
298
299
        return $this->formatDatetime($date, $propDefinition);
300
    }
301
}
302