Passed
Push — master ( 5b4064...cd467c )
by Valentin
17:10 queued 13:04
created

DatetimeChecker::fromField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 8
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|\DateTimeInterface|string|number|null */
40
    private $now;
41
    /** @var DatetimeChecker\ThresholdChecker */
42
    private $threshold;
43
44
    public function __construct($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
     * @return \DateTimeInterface
150
     */
151
    private function now(): ?\DateTimeInterface
152
    {
153
        try {
154
            return $this->date($this->now ?: 'now');
155
        } catch (\Throwable $e) {
156
            //here's the fail;
157
        }
158
159
        return null;
160
    }
161
162
    /**
163
     * @param mixed $value
164
     * @return \DateTimeInterface|null
165
     */
166
    private function date($value): ?\DateTimeInterface
167
    {
168
        if (is_callable($value)) {
169
            $value = $value();
170
        }
171
172
        if ($value instanceof \DateTimeInterface) {
173
            return $value;
174
        }
175
176
        if (!$this->isApplicableValue($value)) {
177
            return null;
178
        }
179
180
        try {
181
            if (!$value) {
182
                $value = '0';
183
            }
184
185
            return new \DateTimeImmutable(is_numeric($value) ? sprintf('@%d', $value) : trim($value));
186
        } catch (\Throwable $e) {
187
            //here's the fail;
188
        }
189
190
        return null;
191
    }
192
193
    /**
194
     * @param mixed $value
195
     * @return bool
196
     */
197
    private function isApplicableValue($value): bool
198
    {
199
        return is_string($value) || is_numeric($value);
200
    }
201
202
    /**
203
     * @param string $field
204
     * @return \DateTimeInterface|null
205
     */
206
    private function fromField(string $field): ?\DateTimeInterface
207
    {
208
        $before = $this->getValidator()->getValue($field);
209
        if ($before !== null) {
210
            return $this->date($before);
211
        }
212
213
        return null;
214
    }
215
}
216