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