Passed
Pull Request — master (#9)
by Jodie
02:17
created

DeclarativeProps::formatPropValue()   C

Complexity

Conditions 15
Paths 15

Size

Total Lines 43
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 240

Importance

Changes 0
Metric Value
dl 0
loc 43
ccs 0
cts 29
cp 0
rs 5.0504
c 0
b 0
f 0
cc 15
eloc 29
nc 15
nop 2
crap 240

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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, $propKey, $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
        if (!$propDefinition->type()) {
143
            return $value;
144
        }
145
146
        // Built-in types.
147
        // Cast the value according to 'type'
148
        switch ($propDefinition->type()) {
149
            case 'int':
150
            case 'integer':
151
                return (int) $value;
152
            case 'real':
153
            case 'float':
154
            case 'double':
155
                return (float) $value;
156
            case 'string':
157
                return (string) $value;
158
            case 'bool':
159
            case 'boolean':
160
                return (bool) $value;
161
            case 'array':
162
                return (array) $value;
163
            case 'date':
164
                return $this->formatDate($value, $propDefinition);
165
            case 'datetime':
166
                return $this->formatDatetime($value, $propDefinition);
167
            case 'datetime_utc':
168
                return $this->formatDatetimeUtc($value, $propDefinition);
169
            default:
170
                // Fall through
171
                break;
172
173
        }
174
175
        // As a final attempt, try to locate a matching method on the class that
176
        // is prefixed with 'format'.
177
        $method = 'format'.StrHelper::studlyCase($propDefinition->type());
178
        if (method_exists($this, $method)) {
179
            return $this->$method($value, $propDefinition);
180
        }
181
182
        throw new InvalidDefinitionException("Unsupported format type: {$propDefinition->type()}");
183
    }
184
185
    protected function getDefinitionParser(): DefinitionParser
186
    {
187
        if ($this->definitionParser === null) {
188
            $this->definitionParser = new DefinitionParser();
189
            $this->definitionParser->addShortKeys('type', [
190
                'int',
191
                'integer',
192
                'real',
193
                'float',
194
                'double',
195
                'string',
196
                'bool',
197
                'boolean',
198
                'array',
199
                'date',
200
                'datetime',
201
                'datetime_utc',
202
            ]);
203
        }
204
205
        return $this->definitionParser;
206
    }
207
208
    /**
209
     * @param string       $propKey
210
     * @param string|mixed $definition
211
     *
212
     * @throws \Rexlabs\Smokescreen\Exception\ParseDefinitionException
213
     *
214
     * @return PropDefinition
215
     */
216
    protected function parsePropDefinition(string $propKey, $definition): PropDefinition
217
    {
218
        return new PropDefinition($propKey, $this->getDefinitionParser()
219
            ->parse($definition));
220
    }
221
222
    /**
223
     * Given a date object, return a new date object with the specified timezone.
224
     * This method ensures that the original date object is not mutated.
225
     *
226
     * @param DateTimeInterface   $date
227
     * @param DateTimeZone|string $timezone
228
     *
229
     * @return DateTimeInterface|DateTime|DateTimeImmutable
230
     */
231
    protected function convertTimeZone(DateTimeInterface $date, $timezone)
232
    {
233
        $timezone = ($timezone instanceof DateTimeZone) ? $timezone : new DateTimeZone($timezone);
234
235
        if ($date->getTimezone() !== $timezone) {
236
            if ($date instanceof DateTime) {
237
                // DateTime is not immutable, so make a copy
238
                $date = (clone $date);
239
                $date->setTimezone($timezone);
240
            } elseif ($date instanceof DateTimeImmutable) {
241
                $date = $date->setTimezone($timezone);
242
            }
243
        }
244
245
        return $date;
246
    }
247
248
    /**
249
     * Format a date object.
250
     *
251
     * @param DateTimeInterface $date
252
     * @param PropDefinition    $definition
253
     *
254
     * @return string
255
     */
256
    protected function formatDatetime(DateTimeInterface $date, PropDefinition $definition): string
257
    {
258
        $timezone = $definition->get('timezone', $this->getDefaultTimezone());
259
        if ($timezone !== null) {
260
            $date = $this->convertTimeZone($date, $timezone);
261
        }
262
263
        return $date->format($definition->get('format', $this->getDatetimeFormat()));
264
    }
265
266
    /**
267
     * Shortcut method for converting a date to UTC and returning the formatted string.
268
     *
269
     * @param DateTimeInterface $date
270
     * @param PropDefinition    $propDefinition
271
     *
272
     * @return string
273
     *
274
     * @see FormatsFields::formatDateTime()
275
     */
276
    protected function formatDatetimeUtc(DateTimeInterface $date, PropDefinition $propDefinition): string
277
    {
278
        return $this->formatDatetime($date, $propDefinition->set('timezone', 'UTC'));
279
    }
280
281
    /**
282
     * @param DateTimeInterface $date
283
     * @param PropDefinition    $propDefinition
284
     *
285
     * @return string
286
     */
287
    protected function formatDate(DateTimeInterface $date, PropDefinition $propDefinition): string
288
    {
289
        if (!$propDefinition->has('format')) {
290
            $propDefinition->set('format', $this->getDateFormat());
291
        }
292
293
        return $this->formatDatetime($date, $propDefinition);
294
    }
295
}
296