Passed
Pull Request — master (#356)
by Valentin
05:47
created

DatetimeChecker::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
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
     * {@inheritdoc}
24
     */
25
    public const MESSAGES = [
26
        'future'   => '[[Should be a date in the future.]]',
27
        'past'     => '[[Should be a date in the past.]]',
28
        'valid'    => '[[Not a valid date.]]',
29
        'format'   => '[[Value should match the specified date format {1}.]]',
30
        'timezone' => '[[Not a valid timezone.]]',
31
        'before'   => '[[Value {1} should come before value {2}.]]',
32
        'after'    => '[[Value {1} should come after value {2}.]]',
33
    ];
34
    //Possible format mapping
35
    private const MAP_FORMAT = [
36
        'c' => 'Y-m-d\TH:i:sT',
37
    ];
38
39
    /** @var callable|null */
40
    private $now;
41
    /** @var DatetimeChecker\ThresholdChecker  */
42
    private $threshold;
43
44
    public function __construct(callable $now = null)
45
    {
46
        $this->now = $now;
47
        $this->threshold = new DatetimeChecker\ThresholdChecker();
48
    }
49
50
    /**
51
     * Check if date is in the future. Do not compare if the current date is invalid.
52
     *
53
     * @param mixed $value
54
     * @param bool  $orNow
55
     * @param bool  $useMicroSeconds
56
     * @return bool
57
     */
58
    public function future($value, bool $orNow = false, bool $useMicroSeconds = false): bool
59
    {
60
        return $this->threshold->after($this->date($value), $this->now(), $orNow, $useMicroSeconds);
61
    }
62
63
    /**
64
     * Check if date is in the past. Do not compare if the current date is invalid.
65
     *
66
     * @param mixed $value
67
     * @param bool  $orNow
68
     * @param bool  $useMicroSeconds
69
     * @return bool
70
     */
71
    public function past($value, bool $orNow = false, bool $useMicroSeconds = false): bool
72
    {
73
        return $this->threshold->before($this->date($value), $this->now(), $orNow, $useMicroSeconds);
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
        return $this->threshold->before($this->date($value), $this->fromField($field), $orEquals, $useMicroSeconds);
132
    }
133
134
    /**
135
     * Check if date comes after the given one. Do not compare if the given date is missing or invalid.
136
     *
137
     * @param mixed  $value
138
     * @param string $field
139
     * @param bool   $orEquals
140
     * @param bool   $useMicroSeconds
141
     * @return bool
142
     */
143
    public function after($value, string $field, bool $orEquals = false, bool $useMicroSeconds = false): bool
144
    {
145
        return $this->threshold->after($this->date($value), $this->fromField($field), $orEquals, $useMicroSeconds);
146
    }
147
148
    /**
149
     * @param mixed $value
150
     * @return \DateTimeInterface|null
151
     */
152
    private function date($value): ?\DateTimeInterface
153
    {
154
        if ($value instanceof \DateTimeInterface) {
155
            return $value;
156
        }
157
158
        if (!$this->isApplicableValue($value)) {
159
            return null;
160
        }
161
162
        try {
163
            if (!$value) {
164
                $value = '0';
165
            }
166
167
            return new \DateTimeImmutable(is_numeric($value) ? sprintf('@%d', $value) : trim($value));
168
        } catch (\Throwable $e) {
169
            //here's the fail;
170
        }
171
172
        return null;
173
    }
174
175
    /**
176
     * @param mixed $value
177
     * @return bool
178
     */
179
    private function isApplicableValue($value): bool
180
    {
181
        return is_string($value) || is_numeric($value);
182
    }
183
184
    /**
185
     * @return \DateTimeInterface
186
     */
187
    private function now(): ?\DateTimeInterface
188
    {
189
        try {
190
            if (is_callable($this->now)) {
191
                $now = $this->now;
192
                return $now();
193
            }
194
195
            return new \DateTimeImmutable('now');
196
        } catch (\Throwable $e) {
197
            //here's the fail;
198
        }
199
200
        return null;
201
    }
202
203
    /**
204
     * @param string $field
205
     * @return \DateTimeInterface|null
206
     */
207
    private function fromField(string $field): ?\DateTimeInterface
208
    {
209
        $before = $this->getValidator()->getValue($field);
210
        if ($before !== null) {
211
            return $this->date($before);
212
        }
213
214
        return null;
215
    }
216
}
217