Completed
Pull Request — master (#9)
by Jodie
03:44
created

DeclarativeProps::getProps()   A

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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $date could also return false which is incompatible with the documented return type DateTimeImmutable|DateTimeInterface|DateTime. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
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