blob: c86faa74c6f8a41bbd58c1437e8f44f6bc8ee83d [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/**
4 * This file is part of the Carbon package.
5 *
6 * (c) Brian Nesbitt <brian@nesbot.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010011
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020012namespace Carbon\Traits;
13
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010014use Carbon\CarbonInterface;
15use Carbon\CarbonTimeZone;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020016use Closure;
17use DateTimeImmutable;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010018use DateTimeInterface;
19use InvalidArgumentException;
20use Throwable;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020021
22trait Test
23{
24 ///////////////////////////////////////////////////////////////////
25 ///////////////////////// TESTING AIDS ////////////////////////////
26 ///////////////////////////////////////////////////////////////////
27
28 /**
29 * A test Carbon instance to be returned when now instances are created.
30 *
31 * @var static
32 */
33 protected static $testNow;
34
35 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010036 * The timezone to resto to when clearing the time mock.
37 *
38 * @var string|null
39 */
40 protected static $testDefaultTimezone;
41
42 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020043 * Set a Carbon instance (real or mock) to be returned when a "now"
44 * instance is created. The provided instance will be returned
45 * specifically under the following conditions:
46 * - A call to the static now() method, ex. Carbon::now()
47 * - When a null (or blank string) is passed to the constructor or parse(), ex. new Carbon(null)
48 * - When the string "now" is passed to the constructor or parse(), ex. new Carbon('now')
49 * - When a string containing the desired time is passed to Carbon::parse().
50 *
51 * Note the timezone parameter was left out of the examples above and
52 * has no affect as the mock value will be returned regardless of its value.
53 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010054 * Only the moment is mocked with setTestNow(), the timezone will still be the one passed
55 * as parameter of date_default_timezone_get() as a fallback (see setTestNowAndTimezone()).
56 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020057 * To clear the test instance call this method using the default
58 * parameter of null.
59 *
60 * /!\ Use this method for unit tests only.
61 *
62 * @param Closure|static|string|false|null $testNow real or mock Carbon instance
63 */
64 public static function setTestNow($testNow = null)
65 {
66 if ($testNow === false) {
67 $testNow = null;
68 }
69
70 static::$testNow = \is_string($testNow) ? static::parse($testNow) : $testNow;
71 }
72
73 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010074 * Set a Carbon instance (real or mock) to be returned when a "now"
75 * instance is created. The provided instance will be returned
76 * specifically under the following conditions:
77 * - A call to the static now() method, ex. Carbon::now()
78 * - When a null (or blank string) is passed to the constructor or parse(), ex. new Carbon(null)
79 * - When the string "now" is passed to the constructor or parse(), ex. new Carbon('now')
80 * - When a string containing the desired time is passed to Carbon::parse().
81 *
82 * It will also align default timezone (e.g. call date_default_timezone_set()) with
83 * the second argument or if null, with the timezone of the given date object.
84 *
85 * To clear the test instance call this method using the default
86 * parameter of null.
87 *
88 * /!\ Use this method for unit tests only.
89 *
90 * @param Closure|static|string|false|null $testNow real or mock Carbon instance
91 */
92 public static function setTestNowAndTimezone($testNow = null, $tz = null)
93 {
94 if ($testNow) {
95 self::$testDefaultTimezone = self::$testDefaultTimezone ?? date_default_timezone_get();
96 }
97
98 $useDateInstanceTimezone = $testNow instanceof DateTimeInterface;
99
100 if ($useDateInstanceTimezone) {
101 self::setDefaultTimezone($testNow->getTimezone()->getName(), $testNow);
102 }
103
104 static::setTestNow($testNow);
105
106 if (!$useDateInstanceTimezone) {
107 $now = static::getMockedTestNow(\func_num_args() === 1 ? null : $tz);
108 $tzName = $now ? $now->tzName : null;
109 self::setDefaultTimezone($tzName ?? self::$testDefaultTimezone ?? 'UTC', $now);
110 }
111
112 if (!$testNow) {
113 self::$testDefaultTimezone = null;
114 }
115 }
116
117 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200118 * Temporarily sets a static date to be used within the callback.
119 * Using setTestNow to set the date, executing the callback, then
120 * clearing the test instance.
121 *
122 * /!\ Use this method for unit tests only.
123 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100124 * @param Closure|static|string|false|null $testNow real or mock Carbon instance
125 * @param Closure|null $callback
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200126 *
127 * @return mixed
128 */
129 public static function withTestNow($testNow = null, $callback = null)
130 {
131 static::setTestNow($testNow);
132 $result = $callback();
133 static::setTestNow();
134
135 return $result;
136 }
137
138 /**
139 * Get the Carbon instance (real or mock) to be returned when a "now"
140 * instance is created.
141 *
142 * @return Closure|static the current instance used for testing
143 */
144 public static function getTestNow()
145 {
146 return static::$testNow;
147 }
148
149 /**
150 * Determine if there is a valid test instance set. A valid test instance
151 * is anything that is not null.
152 *
153 * @return bool true if there is a test instance, otherwise false
154 */
155 public static function hasTestNow()
156 {
157 return static::getTestNow() !== null;
158 }
159
160 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200161 * Get the mocked date passed in setTestNow() and if it's a Closure, execute it.
162 *
163 * @param string|\DateTimeZone $tz
164 *
165 * @return \Carbon\CarbonImmutable|\Carbon\Carbon|null
166 */
167 protected static function getMockedTestNow($tz)
168 {
169 $testNow = static::getTestNow();
170
171 if ($testNow instanceof Closure) {
172 $realNow = new DateTimeImmutable('now');
173 $testNow = $testNow(static::parse(
174 $realNow->format('Y-m-d H:i:s.u'),
175 $tz ?: $realNow->getTimezone()
176 ));
177 }
178 /* @var \Carbon\CarbonImmutable|\Carbon\Carbon|null $testNow */
179
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100180 return $testNow instanceof CarbonInterface
181 ? $testNow->avoidMutation()->tz($tz)
182 : $testNow;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200183 }
184
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100185 protected static function mockConstructorParameters(&$time, $tz)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200186 {
187 /** @var \Carbon\CarbonImmutable|\Carbon\Carbon $testInstance */
188 $testInstance = clone static::getMockedTestNow($tz);
189
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200190 if (static::hasRelativeKeywords($time)) {
191 $testInstance = $testInstance->modify($time);
192 }
193
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100194 $time = $testInstance instanceof self
195 ? $testInstance->rawFormat(static::MOCK_DATETIME_FORMAT)
196 : $testInstance->format(static::MOCK_DATETIME_FORMAT);
197 }
198
199 private static function setDefaultTimezone($timezone, DateTimeInterface $date = null)
200 {
201 $previous = null;
202 $success = false;
203
204 try {
205 $success = date_default_timezone_set($timezone);
206 } catch (Throwable $exception) {
207 $previous = $exception;
208 }
209
210 if (!$success) {
211 $suggestion = @CarbonTimeZone::create($timezone)->toRegionName($date);
212
213 throw new InvalidArgumentException(
214 "Timezone ID '$timezone' is invalid".
215 ($suggestion && $suggestion !== $timezone ? ", did you mean '$suggestion'?" : '.')."\n".
216 "It must be one of the IDs from DateTimeZone::listIdentifiers(),\n".
217 'For the record, hours/minutes offset are relevant only for a particular moment, '.
218 'but not as a default timezone.',
219 0,
220 $previous
221 );
222 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200223 }
224}