Completed
Pull Request — master (#9)
by Jodie
02:15
created

DeclarativeProps   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 260
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 39
dl 0
loc 260
ccs 0
cts 84
cp 0
rs 8.2857
c 0
b 0
f 0

13 Methods

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