Passed
Push — master ( 893d1e...31005e )
by Kirill
03:35
created

DatetimeChecker   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 33
eloc 64
c 1
b 0
f 0
dl 0
loc 232
rs 9.76

13 Methods

Rating   Name   Duplication   Size   Complexity  
A timezone() 0 7 2
A past() 0 8 3
A now() 0 9 2
A isApplicableValue() 0 3 2
A dropMicroSeconds() 0 6 1
A before() 0 8 3
A after() 0 8 3
A thresholdFromField() 0 8 2
A format() 0 9 2
A date() 0 17 5
A future() 0 8 3
A valid() 0 3 1
A compare() 0 16 4
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license MIT
7
 * @author  Valentin Vintsukevich (vvval)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Validation\Checker;
13
14
use Spiral\Core\Container\SingletonInterface;
15
use Spiral\Validation\AbstractChecker;
16
17
/**
18
 * @inherit-messages
19
 */
20
final class DatetimeChecker extends AbstractChecker implements SingletonInterface
21
{
22
23
    /**
24
     * {@inheritdoc}
25
     */
26
    public const MESSAGES = [
27
        'future'   => '[[Should be a date in the future.]]',
28
        'past'     => '[[Should be a date in the past.]]',
29
        'valid'    => '[[Not a valid date.]]',
30
        'format'   => '[[Value should match the specified date format {1}.]]',
31
        'timezone' => '[[Not a valid timezone.]]',
32
        'before'   => '[[Value {1} should come before value {2}.]]',
33
        'after'    => '[[Value {1} should come after value {2}.]]',
34
    ];
35
    //Possible format mapping
36
    private const MAP_FORMAT = [
37
        'c' => 'Y-m-d\TH:i:sT'
38
    ];
39
40
    /**
41
     * Check if date is in the future. Do not compare if the current date is invalid.
42
     *
43
     * @param mixed $value
44
     * @param bool  $orNow
45
     * @param bool  $useMicroSeconds
46
     * @return bool
47
     */
48
    public function future($value, bool $orNow = false, bool $useMicroSeconds = false): bool
49
    {
50
        $compare = $this->compare($this->date($value), $this->now(), $useMicroSeconds);
51
        if (is_bool($compare)) {
52
            return $compare;
53
        }
54
55
        return $orNow ? $compare >= 0 : $compare > 0;
56
    }
57
58
    /**
59
     * Check if date is in the past. Do not compare if the current date is invalid.
60
     *
61
     * @param mixed $value
62
     * @param bool  $orNow
63
     * @param bool  $useMicroSeconds
64
     * @return bool
65
     */
66
    public function past($value, bool $orNow = false, bool $useMicroSeconds = false): bool
67
    {
68
        $compare = $this->compare($this->date($value), $this->now(), $useMicroSeconds);
69
        if (is_bool($compare)) {
70
            return $compare;
71
        }
72
73
        return $orNow ? $compare <= 0 : $compare < 0;
74
    }
75
76
    /**
77
     * Check if date format matches the provided one.
78
     *
79
     * @param mixed  $value
80
     * @param string $format
81
     * @return bool
82
     */
83
    public function format($value, string $format): bool
84
    {
85
        if (!$this->isApplicableValue($value)) {
86
            return false;
87
        }
88
89
        $date = \DateTimeImmutable::createFromFormat(self::MAP_FORMAT[$format] ?? $format, (string)$value);
90
91
        return $date !== false;
92
    }
93
94
    /**
95
     * Check if date is valid. Empty values are acceptable.
96
     *
97
     * @param mixed $value
98
     * @return bool
99
     */
100
    public function valid($value): bool
101
    {
102
        return $this->date($value) !== null;
103
    }
104
105
    /**
106
     * Value has to be a valid timezone.
107
     *
108
     * @param mixed $value
109
     * @return bool
110
     */
111
    public function timezone($value): bool
112
    {
113
        if (!is_scalar($value)) {
114
            return false;
115
        }
116
117
        return in_array((string)$value, \DateTimeZone::listIdentifiers(), true);
118
    }
119
120
    /**
121
     * Check if date comes before the given one. Do not compare if the given date is missing or invalid.
122
     *
123
     * @param mixed  $value
124
     * @param string $field
125
     * @param bool   $orEquals
126
     * @param bool   $useMicroSeconds
127
     * @return bool
128
     */
129
    public function before($value, string $field, bool $orEquals = false, bool $useMicroSeconds = false): bool
130
    {
131
        $compare = $this->compare($this->date($value), $this->thresholdFromField($field), $useMicroSeconds);
132
        if (is_bool($compare)) {
133
            return $compare;
134
        }
135
136
        return $orEquals ? $compare <= 0 : $compare < 0;
137
    }
138
139
    /**
140
     * Check if date comes after the given one. Do not compare if the given date is missing or invalid.
141
     *
142
     * @param mixed  $value
143
     * @param string $field
144
     * @param bool   $orEquals
145
     * @param bool   $useMicroSeconds
146
     * @return bool
147
     */
148
    public function after($value, string $field, bool $orEquals = false, bool $useMicroSeconds = false): bool
149
    {
150
        $compare = $this->compare($this->date($value), $this->thresholdFromField($field), $useMicroSeconds);
151
        if (is_bool($compare)) {
152
            return $compare;
153
        }
154
155
        return $orEquals ? $compare >= 0 : $compare > 0;
156
    }
157
158
    /**
159
     * @param mixed $value
160
     * @return \DateTimeImmutable|null
161
     */
162
    private function date($value): ?\DateTimeImmutable
163
    {
164
        if (!$this->isApplicableValue($value)) {
165
            return null;
166
        }
167
168
        try {
169
            if (empty($value)) {
170
                $value = '0';
171
            }
172
173
            return new \DateTimeImmutable(is_numeric($value) ? ('@' . (int)$value) : (string)$value);
174
        } catch (\Throwable $e) {
175
            //here's the fail;
176
        }
177
178
        return null;
179
    }
180
181
    /**
182
     * @param mixed $value
183
     * @return bool
184
     */
185
    private function isApplicableValue($value): bool
186
    {
187
        return is_string($value) || is_numeric($value);
188
    }
189
190
    /**
191
     * @return \DateTimeImmutable
192
     */
193
    private function now(): ?\DateTimeImmutable
194
    {
195
        try {
196
            return new \DateTimeImmutable('now');
197
        } catch (\Throwable $e) {
198
            //here's the fail;
199
        }
200
201
        return null;
202
    }
203
204
    /**
205
     * @param string $field
206
     * @return \DateTimeImmutable|null
207
     */
208
    private function thresholdFromField(string $field): ?\DateTimeImmutable
209
    {
210
        $before = $this->getValidator()->getValue($field);
211
        if ($before !== null) {
212
            return $this->date($before);
213
        }
214
215
        return null;
216
    }
217
218
    /**
219
     * @param \DateTimeImmutable|null $date
220
     * @param \DateTimeImmutable|null $threshold
221
     * @param bool                    $useMicroseconds
222
     * @return bool|int
223
     */
224
    private function compare(?\DateTimeImmutable $date, ?\DateTimeImmutable $threshold, bool $useMicroseconds)
225
    {
226
        if ($date === null) {
227
            return false;
228
        }
229
230
        if ($threshold === null) {
231
            return true;
232
        }
233
234
        if (!$useMicroseconds) {
235
            $date = $this->dropMicroSeconds($date);
236
            $threshold = $this->dropMicroSeconds($threshold);
237
        }
238
239
        return $date <=> $threshold;
240
    }
241
242
    /**
243
     * @param \DateTimeImmutable $date
244
     * @return \DateTimeImmutable
245
     */
246
    private function dropMicroSeconds(\DateTimeImmutable $date): \DateTimeImmutable
247
    {
248
        return $date->setTime(
249
            (int)$date->format('H'),
250
            (int)$date->format('i'),
251
            (int)$date->format('s')
252
        );
253
    }
254
}
255