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

DeclarativeProps::getPropValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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