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

FormatProps::getDefaultTimezone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 2
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
            $prop = StrHelper::snakeCase($key);
77
78
            if ($definition instanceof \Closure) {
79
                // If the definition is a function, execute it on the value
80
                $value = $definition->bindTo($this)($model, $prop);
81
            } else {
82
                // Format the field according to the definition
83
                $settings = $definition !== null ? $this->parseDefinition($definition) : [];
84
                $value = $this->getPropValue($model, $prop, $settings);
85
            }
86
87
            // Set the prop value in the array
88
            ArrayHelper::mutate($data, $prop, $value);
89
        }
90
91
        return $data;
92
    }
93
94
95
    /**
96
     * @param \ArrayAccess|array $model
97
     * @param string             $prop
98
     * @param array              $settings
99
     *
100
     * @return mixed|null
101
     * @throws \InvalidArgumentException
102
     */
103
    protected function getPropValue($model, $prop, $settings)
104
    {
105
        // If a 'map' setting is provided, map to that key on the model instead.
106
        $mapKey = $settings['map'] ?? $prop;
107
108
        // Get the model value via array access.
109
        $value = $model[$mapKey] ?? null;
110
111
        // Format the value the property value.
112
        $value = $this->formatPropValue($value, $settings);
113
114
        return $value;
115
    }
116
117
    /**
118
     * @param $value
119
     * @param $settings
120
     *
121
     * @return array|string
122
     * @throws \InvalidArgumentException
123
     */
124
    protected function formatPropValue($value, $settings)
125
    {
126
        if (empty($settings['type'])) {
127
            return $value;
128
        }
129
130
        // Cast the value according to 'type' (if defined).
131
        switch ($settings['type']) {
132
            case 'int':
133
            case 'integer':
134
                return (int)$value;
135
            case 'real':
136
            case 'float':
137
            case 'double':
138
                return (float)$value;
139
            case 'string':
140
                return (string)$value;
141
            case 'bool':
142
            case 'boolean':
143
                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...
144
            case 'array':
145
                return (array)$value;
146
            case 'date':
147
                return $this->formatDate($value, $settings);
148
            case 'datetime':
149
                return $this->formatDateTime($value, $settings);
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($settings['type']);
159
        if (method_exists($this, $method)) {
160
            return $this->$method($value, $settings);
161
        }
162
163
        throw new \InvalidArgumentException("Unsupported format type: {$settings['type']}");
164
    }
165
166
    /**
167
     * Parses a definition string into an array.
168
     * Supports a value like integer|arg1:val|arg2:val|arg3
169
     *
170
     * @param string $definition
171
     *
172
     * @return array
173
     * @throws \Rexlabs\Smokescreen\Exception\ParseDefinitionException
174
     */
175
    protected function parseDefinition(string $definition): array
176
    {
177
        $settings = [];
178
        $parts = preg_split('/\s*\|\s*/', $definition);
179
        foreach ($parts as $part) {
180
            // Each part may consist of "directive:value" or it may just be "directive".
181
            if (!preg_match('/^([^:]+)(:(.+))?$/', $part, $match)) {
182
                throw new ParseDefinitionException("Unable to parse field definition: $definition");
183
            }
184
            $directive = $match[1];
185
            $value = $match[3] ?? null;
186
187
            // As a short-cut, we will allow the type to be provided without a "type:" prefix.
188
            if (preg_match('/^(int|integer|real|float|double|string|bool|array|date|datetime)$/', $directive)) {
189
                if ($value !== null) {
190
                    // If a value was also provided, we'll store that in a separate entry.
191
                    $settings[StrHelper::snakeCase(strtolower($directive))] = $value;
192
                }
193
194
                $directive = 'type';
195
                $value = $part;
196
            }
197
198
            // Normalise our directive (as snake_case) and store the value.
199
            $settings[StrHelper::snakeCase(strtolower($directive))] = $value;
200
        }
201
202
        return $settings;
203
    }
204
205
    /**
206
     * Given a date object, return a new date object with the specified timezone.
207
     * This method ensures that the original date object is not mutated.
208
     *
209
     * @param DateTimeInterface   $date
210
     * @param DateTimeZone|string $timezone
211
     *
212
     * @return DateTimeInterface|DateTime|DateTimeImmutable
213
     */
214
    protected function convertTimeZone(DateTimeInterface $date, $timezone)
215
    {
216
        $timezone = ($timezone instanceof DateTimeZone) ? $timezone : new DateTimeZone($timezone);
217
218
        if ($date->getTimezone() !== $timezone) {
219
            if ($date instanceof DateTime) {
220
                // DateTime is not immutable, so make a copy
221
                $date = (clone $date);
222
                $date->setTimezone($timezone);
223
            } elseif ($date instanceof DateTimeImmutable) {
224
                $date = $date->setTimezone($timezone);
225
            }
226
        }
227
228
        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...
229
    }
230
231
    /**
232
     * Format a date object
233
     *
234
     * @param DateTimeInterface $date
235
     * @param array             $settings
236
     *
237
     * @return string
238
     */
239
    protected function formatDateTime(DateTimeInterface $date, array $settings = []): string
240
    {
241
        $timezone = $settings['timezone'] ?? $this->getDefaultTimezone();
242
        if ($timezone !== null) {
243
            $date = $this->convertTimeZone($date, $timezone);
244
        }
245
246
        return $date->format($settings['format'] ?? $this->getDateTimeFormat());
247
    }
248
249
    /**
250
     * Shortcut method for converting a date to UTC and returning the formatted string.
251
     *
252
     * @param DateTimeInterface $date
253
     * @param array             $settings
254
     *
255
     * @return string
256
     * @see FormatsFields::formatDateTime()
257
     */
258
    protected function formatDateTimeUtc(DateTimeInterface $date, array $settings = []): string
259
    {
260
        $settings['timezone'] = 'UTC';
261
        return $this->formatDateTime($date, $settings);
262
    }
263
264
    /**
265
     * @param DateTimeInterface $date
266
     * @param array             $settings
267
     *
268
     * @return string
269
     */
270
    protected function formatDate(DateTimeInterface $date, array $settings = []): string
271
    {
272
        return $this->formatDateTime($date, $settings);
273
    }
274
}