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

FormatProps::propDefinitionToArray()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 0
cts 16
cp 0
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 3
nop 1
crap 42
1
<?php
2
3
namespace Rexlabs\Smokescreen\Transformer\Concerns;
4
5
use DateTime;
6
use DateTimeImmutable;
7
use DateTimeInterface;
8
use DateTimeZone;
9
use Rexlabs\Smokescreen\Exception\ParseDefinitionException;
10
use Rexlabs\Smokescreen\Helpers\ArrayHelper;
11
use Rexlabs\Smokescreen\Helpers\StrHelper;
12
13
trait FormatProps
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
    public function getProps(): array
28
    {
29
        return $this->props;
30
    }
31
32
    protected function getDateFormat(): string
33
    {
34
        return $this->dateFormat;
35
    }
36
37
    protected function getDateTimeFormat(): string
38
    {
39
        return $this->dateTimeFormat;
40
    }
41
42
    protected function getDefaultTimezone(): string
43
    {
44
        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...
45
    }
46
47
    /**
48
     * Helper method for returning formatted properties from a source object
49
     * or array.
50
     *
51
     * @param \ArrayAccess|array $model
52
     * @param array              $props
53
     *
54
     * @return array
55
     * @throws \InvalidArgumentException
56
     */
57
    protected function withProps($model, array $props): array
58
    {
59
        if (!(\is_array($model) || $model instanceof \ArrayAccess)) {
0 ignored issues
show
introduced by
$model is always a sub-type of ArrayAccess.
Loading history...
60
            throw new \InvalidArgumentException('Expect array or object implementing \ArrayAccess');
61
        }
62
63
        // Given an array of props (which may include a definition value), we
64
        // cycle through each, resolve the value from the model, and apply any
65
        // conversions to the value as necessary.
66
        $data = [];
67
        foreach ($props as $key => $definition) {
68
            if (\is_int($key)) {
69
                // This isn't a key => value pair, so there is no definition.
70
                $key = $definition;
71
                $definition = null;
72
            }
73
74
            // Convert property to a snake-case version.
75
            // It may be a nested (dot-notation) key.
76
            $propKey = StrHelper::snakeCase($key);
77
            if ($definition instanceof \Closure) {
78
                // If the definition is a function, execute it on the value
79
                $value = $definition->bindTo($this)($model, $propKey);
80
            } else {
81
                // Format the field according to the definition
82
                $value = $this->getPropValue(
83
                    $model,
84
                    $propKey,
85
                    $this->propDefinitionToArray($definition)
86
                );
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 string             $prop
100
     * @param array              $settings
101
     *
102
     * @return mixed|null
103
     * @throws \InvalidArgumentException
104
     */
105
    protected function getPropValue($model, $prop, $settings)
106
    {
107
        // If a 'map' setting is provided, map to that key on the model instead.
108
        $mapKey = $settings['map'] ?? $prop;
109
110
        // Get the model value via array access.
111
        $value = $model[$mapKey] ?? null;
112
113
        // Format the value the property value.
114
        $value = $this->formatPropValue($value, $settings);
115
116
        return $value;
117
    }
118
119
    /**
120
     * @param $value
121
     * @param $settings
122
     *
123
     * @return array|string
124
     * @throws \InvalidArgumentException
125
     */
126
    protected function formatPropValue($value, $settings)
127
    {
128
        if (empty($settings['type'])) {
129
            return $value;
130
        }
131
132
        // Cast the value according to 'type' (if defined).
133
        switch ($settings['type']) {
134
            case 'int':
135
            case 'integer':
136
                return (int)$value;
137
            case 'real':
138
            case 'float':
139
            case 'double':
140
                return (float)$value;
141
            case 'string':
142
                return (string)$value;
143
            case 'bool':
144
            case 'boolean':
145
                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...
146
            case 'array':
147
                return (array)$value;
148
            case 'date':
149
                return $this->formatDate($value, $settings);
150
            case 'datetime':
151
                return $this->formatDateTime($value, $settings);
152
            default:
153
                // Fall through
154
                break;
155
156
        }
157
158
        // As a final attempt, try to locate a matching method on the class that
159
        // is prefixed with 'format'.
160
        $method = 'format' . StrHelper::studlyCase($settings['type']);
161
        if (method_exists($this, $method)) {
162
            return $this->$method($value, $settings);
163
        }
164
165
        throw new \InvalidArgumentException("Unsupported format type: {$settings['type']}");
166
    }
167
168
    /**
169
     * Parses a definition string into an array.
170
     * Supports a value like integer|arg1:val|arg2:val|arg3
171
     *
172
     * @param string $str
173
     *
174
     * @return array
175
     * @throws \Rexlabs\Smokescreen\Exception\ParseDefinitionException
176
     */
177
    protected function propDefinitionToArray($str): array
178
    {
179
        $settings = [];
180
        if (!empty($str)) {
181
            $parts = preg_split('/\s*\|\s*/', $str);
182
            foreach ($parts as $part) {
183
                // Each part may consist of "directive:value" or it may just be "directive".
184
                if (!preg_match('/^([^:]+)(:(.+))?$/', $part, $match)) {
185
                    throw new ParseDefinitionException("Unable to parse field definition: $str");
186
                }
187
                $directive = $match[1];
188
                $value = $match[3] ?? null;
189
190
                // As a short-cut, we will allow the type to be provided without a "type:" prefix.
191
                if (preg_match('/^(int|integer|real|float|double|string|bool|array|date|datetime)$/', $directive)) {
192
                    if ($value !== null) {
193
                        // If a value was also provided, we'll store that in a separate entry.
194
                        $settings[StrHelper::snakeCase(strtolower($directive))] = $value;
195
                    }
196
197
                    $directive = 'type';
198
                    $value = $part;
199
                }
200
201
                // Normalise our directive (as snake_case) and store the value.
202
                $settings[StrHelper::snakeCase(strtolower($directive))] = $value;
203
            }
204
        }
205
206
        return $settings;
207
    }
208
209
    /**
210
     * Given a date object, return a new date object with the specified timezone.
211
     * This method ensures that the original date object is not mutated.
212
     *
213
     * @param DateTimeInterface   $date
214
     * @param DateTimeZone|string $timezone
215
     *
216
     * @return DateTimeInterface|DateTime|DateTimeImmutable
217
     */
218
    protected function convertTimeZone(DateTimeInterface $date, $timezone)
219
    {
220
        $timezone = ($timezone instanceof DateTimeZone) ? $timezone : new DateTimeZone($timezone);
221
222
        if ($date->getTimezone() !== $timezone) {
223
            if ($date instanceof DateTime) {
224
                // DateTime is not immutable, so make a copy
225
                $date = (clone $date);
226
                $date->setTimezone($timezone);
227
            } elseif ($date instanceof DateTimeImmutable) {
228
                $date = $date->setTimezone($timezone);
229
            }
230
        }
231
232
        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...
233
    }
234
235
    /**
236
     * Format a date object
237
     *
238
     * @param DateTimeInterface $date
239
     * @param array             $settings
240
     *
241
     * @return string
242
     */
243
    protected function formatDateTime(DateTimeInterface $date, array $settings = []): string
244
    {
245
        $timezone = $settings['timezone'] ?? $this->getDefaultTimezone();
246
        if ($timezone !== null) {
247
            $date = $this->convertTimeZone($date, $timezone);
248
        }
249
250
        return $date->format($settings['format'] ?? $this->getDateTimeFormat());
251
    }
252
253
    /**
254
     * Shortcut method for converting a date to UTC and returning the formatted string.
255
     *
256
     * @param DateTimeInterface $date
257
     * @param array             $settings
258
     *
259
     * @return string
260
     * @see FormatsFields::formatDateTime()
261
     */
262
    protected function formatDateTimeUtc(DateTimeInterface $date, array $settings = []): string
263
    {
264
        $settings['timezone'] = 'UTC';
265
        return $this->formatDateTime($date, $settings);
266
    }
267
268
    /**
269
     * @param DateTimeInterface $date
270
     * @param array             $settings
271
     *
272
     * @return string
273
     */
274
    protected function formatDate(DateTimeInterface $date, array $settings = []): string
275
    {
276
        return $this->formatDateTime($date, $settings);
277
    }
278
}