You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
846 lines
44 KiB
846 lines
44 KiB
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* This file is part of the Carbon package.
|
|
*
|
|
* (c) Brian Nesbitt <brian@nesbot.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Carbon;
|
|
|
|
use Closure;
|
|
use DateTimeImmutable;
|
|
use DateTimeInterface;
|
|
use DateTimeZone;
|
|
use InvalidArgumentException;
|
|
use ReflectionMethod;
|
|
use RuntimeException;
|
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
use Throwable;
|
|
|
|
/**
|
|
* A factory to generate Carbon instances with common settings.
|
|
*
|
|
* <autodoc generated by `composer phpdoc`>
|
|
*
|
|
* @method bool canBeCreatedFromFormat(?string $date, string $format) Checks if the (date)time string is in a given format and valid to create a
|
|
* new instance.
|
|
* @method ?Carbon create($year = 0, $month = 1, $day = 1, $hour = 0, $minute = 0, $second = 0, $timezone = null) Create a new Carbon instance from a specific date and time.
|
|
* If any of $year, $month or $day are set to null their now() values will
|
|
* be used.
|
|
* If $hour is null it will be set to its now() value and the default
|
|
* values for $minute and $second will be their now() values.
|
|
* If $hour is not null then the default values for $minute and $second
|
|
* will be 0.
|
|
* @method Carbon createFromDate($year = null, $month = null, $day = null, $timezone = null) Create a Carbon instance from just a date. The time portion is set to now.
|
|
* @method ?Carbon createFromFormat($format, $time, $timezone = null) Create a Carbon instance from a specific format.
|
|
* @method ?Carbon createFromIsoFormat(string $format, string $time, $timezone = null, ?string $locale = 'en', ?TranslatorInterface $translator = null) Create a Carbon instance from a specific ISO format (same replacements as ->isoFormat()).
|
|
* @method ?Carbon createFromLocaleFormat(string $format, string $locale, string $time, $timezone = null) Create a Carbon instance from a specific format and a string in a given language.
|
|
* @method ?Carbon createFromLocaleIsoFormat(string $format, string $locale, string $time, $timezone = null) Create a Carbon instance from a specific ISO format and a string in a given language.
|
|
* @method Carbon createFromTime($hour = 0, $minute = 0, $second = 0, $timezone = null) Create a Carbon instance from just a time. The date portion is set to today.
|
|
* @method Carbon createFromTimeString(string $time, DateTimeZone|string|int|null $timezone = null) Create a Carbon instance from a time string. The date portion is set to today.
|
|
* @method Carbon createFromTimestamp(string|int|float $timestamp, DateTimeZone|string|int|null $timezone = null) Create a Carbon instance from a timestamp and set the timezone (UTC by default).
|
|
* Timestamp input can be given as int, float or a string containing one or more numbers.
|
|
* @method Carbon createFromTimestampMs(string|int|float $timestamp, DateTimeZone|string|int|null $timezone = null) Create a Carbon instance from a timestamp in milliseconds.
|
|
* Timestamp input can be given as int, float or a string containing one or more numbers.
|
|
* @method Carbon createFromTimestampMsUTC($timestamp) Create a Carbon instance from a timestamp in milliseconds.
|
|
* Timestamp input can be given as int, float or a string containing one or more numbers.
|
|
* @method Carbon createFromTimestampUTC(string|int|float $timestamp) Create a Carbon instance from a timestamp keeping the timezone to UTC.
|
|
* Timestamp input can be given as int, float or a string containing one or more numbers.
|
|
* @method Carbon createMidnightDate($year = null, $month = null, $day = null, $timezone = null) Create a Carbon instance from just a date. The time portion is set to midnight.
|
|
* @method ?Carbon createSafe($year = null, $month = null, $day = null, $hour = null, $minute = null, $second = null, $timezone = null) Create a new safe Carbon instance from a specific date and time.
|
|
* If any of $year, $month or $day are set to null their now() values will
|
|
* be used.
|
|
* If $hour is null it will be set to its now() value and the default
|
|
* values for $minute and $second will be their now() values.
|
|
* If $hour is not null then the default values for $minute and $second
|
|
* will be 0.
|
|
* If one of the set values is not valid, an InvalidDateException
|
|
* will be thrown.
|
|
* @method Carbon createStrict(?int $year = 0, ?int $month = 1, ?int $day = 1, ?int $hour = 0, ?int $minute = 0, ?int $second = 0, $timezone = null) Create a new Carbon instance from a specific date and time using strict validation.
|
|
* @method mixed executeWithLocale(string $locale, callable $func) Set the current locale to the given, execute the passed function, reset the locale to previous one,
|
|
* then return the result of the closure (or null if the closure was void).
|
|
* @method Carbon fromSerialized($value) Create an instance from a serialized string.
|
|
* @method array getAvailableLocales() Returns the list of internally available locales and already loaded custom locales.
|
|
* (It will ignore custom translator dynamic loading.)
|
|
* @method Language[] getAvailableLocalesInfo() Returns list of Language object for each available locale. This object allow you to get the ISO name, native
|
|
* name, region and variant of the locale.
|
|
* @method array getDays() Get the days of the week.
|
|
* @method ?string getFallbackLocale() Get the fallback locale.
|
|
* @method array getFormatsToIsoReplacements() List of replacements from date() format to isoFormat().
|
|
* @method array getIsoUnits() Returns list of locale units for ISO formatting.
|
|
* @method array|false getLastErrors() {@inheritdoc}
|
|
* @method string getLocale() Get the current translator locale.
|
|
* @method int getMidDayAt() get midday/noon hour
|
|
* @method string getTimeFormatByPrecision(string $unitPrecision) Return a format from H:i to H:i:s.u according to given unit precision.
|
|
* @method string|Closure|null getTranslationMessageWith($translator, string $key, ?string $locale = null, ?string $default = null) Returns raw translation message for a given key.
|
|
* @method int getWeekEndsAt(?string $locale = null) Get the last day of week.
|
|
* @method int getWeekStartsAt(?string $locale = null) Get the first day of week.
|
|
* @method bool hasRelativeKeywords(?string $time) Determine if a time string will produce a relative date.
|
|
* @method Carbon instance(DateTimeInterface $date) Create a Carbon instance from a DateTime one.
|
|
* @method bool isImmutable() Returns true if the current class/instance is immutable.
|
|
* @method bool isModifiableUnit($unit) Returns true if a property can be changed via setter.
|
|
* @method bool isMutable() Returns true if the current class/instance is mutable.
|
|
* @method bool localeHasDiffOneDayWords(string $locale) Returns true if the given locale is internally supported and has words for 1-day diff (just now, yesterday, tomorrow).
|
|
* Support is considered enabled if the 3 words are translated in the given locale.
|
|
* @method bool localeHasDiffSyntax(string $locale) Returns true if the given locale is internally supported and has diff syntax support (ago, from now, before, after).
|
|
* Support is considered enabled if the 4 sentences are translated in the given locale.
|
|
* @method bool localeHasDiffTwoDayWords(string $locale) Returns true if the given locale is internally supported and has words for 2-days diff (before yesterday, after tomorrow).
|
|
* Support is considered enabled if the 2 words are translated in the given locale.
|
|
* @method bool localeHasPeriodSyntax($locale) Returns true if the given locale is internally supported and has period syntax support (X times, every X, from X, to X).
|
|
* Support is considered enabled if the 4 sentences are translated in the given locale.
|
|
* @method bool localeHasShortUnits(string $locale) Returns true if the given locale is internally supported and has short-units support.
|
|
* Support is considered enabled if either year, day or hour has a short variant translated.
|
|
* @method ?Carbon make($var, DateTimeZone|string|null $timezone = null) Make a Carbon instance from given variable if possible.
|
|
* Always return a new instance. Parse only strings and only these likely to be dates (skip intervals
|
|
* and recurrences). Throw an exception for invalid format, but otherwise return null.
|
|
* @method void mixin(object|string $mixin) Mix another object into the class.
|
|
* @method Carbon now(DateTimeZone|string|int|null $timezone = null) Get a Carbon instance for the current date and time.
|
|
* @method Carbon parse(DateTimeInterface|WeekDay|Month|string|int|float|null $time, DateTimeZone|string|int|null $timezone = null) Create a carbon instance from a string.
|
|
* This is an alias for the constructor that allows better fluent syntax
|
|
* as it allows you to do Carbon::parse('Monday next week')->fn() rather
|
|
* than (new Carbon('Monday next week'))->fn().
|
|
* @method Carbon parseFromLocale(string $time, ?string $locale = null, DateTimeZone|string|int|null $timezone = null) Create a carbon instance from a localized string (in French, Japanese, Arabic, etc.).
|
|
* @method string pluralUnit(string $unit) Returns standardized plural of a given singular/plural unit name (in English).
|
|
* @method ?Carbon rawCreateFromFormat(string $format, string $time, $timezone = null) Create a Carbon instance from a specific format.
|
|
* @method Carbon rawParse(DateTimeInterface|WeekDay|Month|string|int|float|null $time, DateTimeZone|string|int|null $timezone = null) Create a carbon instance from a string.
|
|
* This is an alias for the constructor that allows better fluent syntax
|
|
* as it allows you to do Carbon::parse('Monday next week')->fn() rather
|
|
* than (new Carbon('Monday next week'))->fn().
|
|
* @method void setFallbackLocale(string $locale) Set the fallback locale.
|
|
* @method void setLocale(string $locale) Set the current translator locale and indicate if the source locale file exists.
|
|
* Pass 'auto' as locale to use the closest language to the current LC_TIME locale.
|
|
* @method void setMidDayAt($hour) @deprecated To avoid conflict between different third-party libraries, static setters should not be used.
|
|
* You should rather consider mid-day is always 12pm, then if you need to test if it's an other
|
|
* hour, test it explicitly:
|
|
* $date->format('G') == 13
|
|
* or to set explicitly to a given hour:
|
|
* $date->setTime(13, 0, 0, 0)
|
|
* Set midday/noon hour
|
|
* @method string singularUnit(string $unit) Returns standardized singular of a given singular/plural unit name (in English).
|
|
* @method void sleep(int|float $seconds)
|
|
* @method Carbon today(DateTimeZone|string|int|null $timezone = null) Create a Carbon instance for today.
|
|
* @method Carbon tomorrow(DateTimeZone|string|int|null $timezone = null) Create a Carbon instance for tomorrow.
|
|
* @method string translateTimeString(string $timeString, ?string $from = null, ?string $to = null, int $mode = CarbonInterface::TRANSLATE_ALL) Translate a time string from a locale to an other.
|
|
* @method string translateWith(TranslatorInterface $translator, string $key, array $parameters = [], $number = null) Translate using translation string or callback available.
|
|
* @method Carbon yesterday(DateTimeZone|string|int|null $timezone = null) Create a Carbon instance for yesterday.
|
|
*
|
|
* </autodoc>
|
|
*/
|
|
class Factory
|
|
{
|
|
protected string $className = Carbon::class;
|
|
|
|
protected array $settings = [];
|
|
|
|
/**
|
|
* A test Carbon instance to be returned when now instances are created.
|
|
*/
|
|
protected Closure|CarbonInterface|null $testNow = null;
|
|
|
|
/**
|
|
* The timezone to restore to when clearing the time mock.
|
|
*/
|
|
protected ?string $testDefaultTimezone = null;
|
|
|
|
/**
|
|
* Is true when test-now is generated by a closure and timezone should be taken on the fly from it.
|
|
*/
|
|
protected bool $useTimezoneFromTestNow = false;
|
|
|
|
/**
|
|
* Default translator.
|
|
*/
|
|
protected TranslatorInterface $translator;
|
|
|
|
/**
|
|
* Days of weekend.
|
|
*/
|
|
protected array $weekendDays = [
|
|
CarbonInterface::SATURDAY,
|
|
CarbonInterface::SUNDAY,
|
|
];
|
|
|
|
/**
|
|
* Format regex patterns.
|
|
*
|
|
* @var array<string, string>
|
|
*/
|
|
protected array $regexFormats = [
|
|
'd' => '(3[01]|[12][0-9]|0[1-9])',
|
|
'D' => '(Sun|Mon|Tue|Wed|Thu|Fri|Sat)',
|
|
'j' => '([123][0-9]|[1-9])',
|
|
'l' => '([a-zA-Z]{2,})',
|
|
'N' => '([1-7])',
|
|
'S' => '(st|nd|rd|th)',
|
|
'w' => '([0-6])',
|
|
'z' => '(36[0-5]|3[0-5][0-9]|[12][0-9]{2}|[1-9]?[0-9])',
|
|
'W' => '(5[012]|[1-4][0-9]|0?[1-9])',
|
|
'F' => '([a-zA-Z]{2,})',
|
|
'm' => '(1[012]|0[1-9])',
|
|
'M' => '([a-zA-Z]{3})',
|
|
'n' => '(1[012]|[1-9])',
|
|
't' => '(2[89]|3[01])',
|
|
'L' => '(0|1)',
|
|
'o' => '([1-9][0-9]{0,4})',
|
|
'Y' => '([1-9]?[0-9]{4})',
|
|
'y' => '([0-9]{2})',
|
|
'a' => '(am|pm)',
|
|
'A' => '(AM|PM)',
|
|
'B' => '([0-9]{3})',
|
|
'g' => '(1[012]|[1-9])',
|
|
'G' => '(2[0-3]|1?[0-9])',
|
|
'h' => '(1[012]|0[1-9])',
|
|
'H' => '(2[0-3]|[01][0-9])',
|
|
'i' => '([0-5][0-9])',
|
|
's' => '([0-5][0-9])',
|
|
'u' => '([0-9]{1,6})',
|
|
'v' => '([0-9]{1,3})',
|
|
'e' => '([a-zA-Z]{1,5})|([a-zA-Z]*\\/[a-zA-Z]*)',
|
|
'I' => '(0|1)',
|
|
'O' => '([+-](1[0123]|0[0-9])[0134][05])',
|
|
'P' => '([+-](1[0123]|0[0-9]):[0134][05])',
|
|
'p' => '(Z|[+-](1[0123]|0[0-9]):[0134][05])',
|
|
'T' => '([a-zA-Z]{1,5})',
|
|
'Z' => '(-?[1-5]?[0-9]{1,4})',
|
|
'U' => '([0-9]*)',
|
|
|
|
// The formats below are combinations of the above formats.
|
|
'c' => '(([1-9]?[0-9]{4})-(1[012]|0[1-9])-(3[01]|[12][0-9]|0[1-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])[+-](1[012]|0[0-9]):([0134][05]))', // Y-m-dTH:i:sP
|
|
'r' => '(([a-zA-Z]{3}), ([123][0-9]|0[1-9]) ([a-zA-Z]{3}) ([1-9]?[0-9]{4}) (2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]) [+-](1[012]|0[0-9])([0134][05]))', // D, d M Y H:i:s O
|
|
];
|
|
|
|
/**
|
|
* Format modifiers (such as available in createFromFormat) regex patterns.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected array $regexFormatModifiers = [
|
|
'*' => '.+',
|
|
' ' => '[ ]',
|
|
'#' => '[;:\\/.,()-]',
|
|
'?' => '([^a]|[a])',
|
|
'!' => '',
|
|
'|' => '',
|
|
'+' => '',
|
|
];
|
|
|
|
public function __construct(array $settings = [], ?string $className = null)
|
|
{
|
|
if ($className) {
|
|
$this->className = $className;
|
|
}
|
|
|
|
$this->settings = $settings;
|
|
}
|
|
|
|
public function getClassName(): string
|
|
{
|
|
return $this->className;
|
|
}
|
|
|
|
public function setClassName(string $className): self
|
|
{
|
|
$this->className = $className;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function className(?string $className = null): self|string
|
|
{
|
|
return $className === null ? $this->getClassName() : $this->setClassName($className);
|
|
}
|
|
|
|
public function getSettings(): array
|
|
{
|
|
return $this->settings;
|
|
}
|
|
|
|
public function setSettings(array $settings): self
|
|
{
|
|
$this->settings = $settings;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function settings(?array $settings = null): self|array
|
|
{
|
|
return $settings === null ? $this->getSettings() : $this->setSettings($settings);
|
|
}
|
|
|
|
public function mergeSettings(array $settings): self
|
|
{
|
|
$this->settings = array_merge($this->settings, $settings);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setHumanDiffOptions(int $humanDiffOptions): void
|
|
{
|
|
$this->mergeSettings([
|
|
'humanDiffOptions' => $humanDiffOptions,
|
|
]);
|
|
}
|
|
|
|
public function enableHumanDiffOption($humanDiffOption): void
|
|
{
|
|
$this->setHumanDiffOptions($this->getHumanDiffOptions() | $humanDiffOption);
|
|
}
|
|
|
|
public function disableHumanDiffOption(int $humanDiffOption): void
|
|
{
|
|
$this->setHumanDiffOptions($this->getHumanDiffOptions() & ~$humanDiffOption);
|
|
}
|
|
|
|
public function getHumanDiffOptions(): int
|
|
{
|
|
return (int) ($this->getSettings()['humanDiffOptions'] ?? 0);
|
|
}
|
|
|
|
/**
|
|
* Register a custom macro.
|
|
*
|
|
* Pass null macro to remove it.
|
|
*
|
|
* @example
|
|
* ```
|
|
* $userSettings = [
|
|
* 'locale' => 'pt',
|
|
* 'timezone' => 'America/Sao_Paulo',
|
|
* ];
|
|
* $factory->macro('userFormat', function () use ($userSettings) {
|
|
* return $this->copy()->locale($userSettings['locale'])->tz($userSettings['timezone'])->calendar();
|
|
* });
|
|
* echo $factory->yesterday()->hours(11)->userFormat();
|
|
* ```
|
|
*/
|
|
public function macro(string $name, ?callable $macro): void
|
|
{
|
|
$macros = $this->getSettings()['macros'] ?? [];
|
|
$macros[$name] = $macro;
|
|
|
|
$this->mergeSettings([
|
|
'macros' => $macros,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Remove all macros and generic macros.
|
|
*/
|
|
public function resetMacros(): void
|
|
{
|
|
$this->mergeSettings([
|
|
'macros' => null,
|
|
'genericMacros' => null,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Register a custom macro.
|
|
*
|
|
* @param callable $macro
|
|
* @param int $priority marco with higher priority is tried first
|
|
*
|
|
* @return void
|
|
*/
|
|
public function genericMacro(callable $macro, int $priority = 0): void
|
|
{
|
|
$genericMacros = $this->getSettings()['genericMacros'] ?? [];
|
|
|
|
if (!isset($genericMacros[$priority])) {
|
|
$genericMacros[$priority] = [];
|
|
krsort($genericMacros, SORT_NUMERIC);
|
|
}
|
|
|
|
$genericMacros[$priority][] = $macro;
|
|
|
|
$this->mergeSettings([
|
|
'genericMacros' => $genericMacros,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Checks if macro is registered globally.
|
|
*/
|
|
public function hasMacro(string $name): bool
|
|
{
|
|
return isset($this->getSettings()['macros'][$name]);
|
|
}
|
|
|
|
/**
|
|
* Get the raw callable macro registered globally for a given name.
|
|
*/
|
|
public function getMacro(string $name): ?callable
|
|
{
|
|
return $this->getSettings()['macros'][$name] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Set the default translator instance to use.
|
|
*/
|
|
public function setTranslator(TranslatorInterface $translator): void
|
|
{
|
|
$this->translator = $translator;
|
|
}
|
|
|
|
/**
|
|
* Initialize the default translator instance if necessary.
|
|
*/
|
|
public function getTranslator(): TranslatorInterface
|
|
{
|
|
return $this->translator ??= Translator::get();
|
|
}
|
|
|
|
/**
|
|
* Reset the format used to the default when type juggling a Carbon instance to a string
|
|
*
|
|
* @return void
|
|
*/
|
|
public function resetToStringFormat(): void
|
|
{
|
|
$this->setToStringFormat(null);
|
|
}
|
|
|
|
/**
|
|
* Set the default format used when type juggling a Carbon instance to a string.
|
|
*/
|
|
public function setToStringFormat(string|Closure|null $format): void
|
|
{
|
|
$this->mergeSettings([
|
|
'toStringFormat' => $format,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* JSON serialize all Carbon instances using the given callback.
|
|
*/
|
|
public function serializeUsing(string|callable|null $format): void
|
|
{
|
|
$this->mergeSettings([
|
|
'toJsonFormat' => $format,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Enable the strict mode (or disable with passing false).
|
|
*/
|
|
public function useStrictMode(bool $strictModeEnabled = true): void
|
|
{
|
|
$this->mergeSettings([
|
|
'strictMode' => $strictModeEnabled,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the strict mode is globally in use, false else.
|
|
* (It can be overridden in specific instances.)
|
|
*/
|
|
public function isStrictModeEnabled(): bool
|
|
{
|
|
return $this->getSettings()['strictMode'] ?? true;
|
|
}
|
|
|
|
/**
|
|
* Indicates if months should be calculated with overflow.
|
|
*/
|
|
public function useMonthsOverflow(bool $monthsOverflow = true): void
|
|
{
|
|
$this->mergeSettings([
|
|
'monthOverflow' => $monthsOverflow,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Reset the month overflow behavior.
|
|
*/
|
|
public function resetMonthsOverflow(): void
|
|
{
|
|
$this->useMonthsOverflow();
|
|
}
|
|
|
|
/**
|
|
* Get the month overflow global behavior (can be overridden in specific instances).
|
|
*/
|
|
public function shouldOverflowMonths(): bool
|
|
{
|
|
return $this->getSettings()['monthOverflow'] ?? true;
|
|
}
|
|
|
|
/**
|
|
* Indicates if years should be calculated with overflow.
|
|
*/
|
|
public function useYearsOverflow(bool $yearsOverflow = true): void
|
|
{
|
|
$this->mergeSettings([
|
|
'yearOverflow' => $yearsOverflow,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Reset the month overflow behavior.
|
|
*/
|
|
public function resetYearsOverflow(): void
|
|
{
|
|
$this->useYearsOverflow();
|
|
}
|
|
|
|
/**
|
|
* Get the month overflow global behavior (can be overridden in specific instances).
|
|
*/
|
|
public function shouldOverflowYears(): bool
|
|
{
|
|
return $this->getSettings()['yearOverflow'] ?? true;
|
|
}
|
|
|
|
/**
|
|
* Get weekend days
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getWeekendDays(): array
|
|
{
|
|
return $this->weekendDays;
|
|
}
|
|
|
|
/**
|
|
* Set weekend days
|
|
*/
|
|
public function setWeekendDays(array $days): void
|
|
{
|
|
$this->weekendDays = $days;
|
|
}
|
|
|
|
/**
|
|
* Checks if the (date)time string is in a given format.
|
|
*
|
|
* @example
|
|
* ```
|
|
* Carbon::hasFormat('11:12:45', 'h:i:s'); // true
|
|
* Carbon::hasFormat('13:12:45', 'h:i:s'); // false
|
|
* ```
|
|
*/
|
|
public function hasFormat(string $date, string $format): bool
|
|
{
|
|
// createFromFormat() is known to handle edge cases silently.
|
|
// E.g. "1975-5-1" (Y-n-j) will still be parsed correctly when "Y-m-d" is supplied as the format.
|
|
// To ensure we're really testing against our desired format, perform an additional regex validation.
|
|
|
|
return $this->matchFormatPattern($date, preg_quote($format, '/'), $this->regexFormats);
|
|
}
|
|
|
|
/**
|
|
* Checks if the (date)time string is in a given format.
|
|
*
|
|
* @example
|
|
* ```
|
|
* Carbon::hasFormatWithModifiers('31/08/2015', 'd#m#Y'); // true
|
|
* Carbon::hasFormatWithModifiers('31/08/2015', 'm#d#Y'); // false
|
|
* ```
|
|
*/
|
|
public function hasFormatWithModifiers(string $date, string $format): bool
|
|
{
|
|
return $this->matchFormatPattern($date, $format, array_merge($this->regexFormats, $this->regexFormatModifiers));
|
|
}
|
|
|
|
/**
|
|
* Set a Carbon instance (real or mock) to be returned when a "now"
|
|
* instance is created. The provided instance will be returned
|
|
* specifically under the following conditions:
|
|
* - A call to the static now() method, ex. Carbon::now()
|
|
* - When a null (or blank string) is passed to the constructor or parse(), ex. new Carbon(null)
|
|
* - When the string "now" is passed to the constructor or parse(), ex. new Carbon('now')
|
|
* - When a string containing the desired time is passed to Carbon::parse().
|
|
*
|
|
* Note the timezone parameter was left out of the examples above and
|
|
* has no affect as the mock value will be returned regardless of its value.
|
|
*
|
|
* Only the moment is mocked with setTestNow(), the timezone will still be the one passed
|
|
* as parameter of date_default_timezone_get() as a fallback (see setTestNowAndTimezone()).
|
|
*
|
|
* To clear the test instance call this method using the default
|
|
* parameter of null.
|
|
*
|
|
* /!\ Use this method for unit tests only.
|
|
*
|
|
* @param DateTimeInterface|Closure|static|string|false|null $testNow real or mock Carbon instance
|
|
*/
|
|
public function setTestNow(mixed $testNow = null): void
|
|
{
|
|
$this->useTimezoneFromTestNow = false;
|
|
$this->testNow = $testNow instanceof self || $testNow instanceof Closure
|
|
? $testNow
|
|
: $this->make($testNow);
|
|
}
|
|
|
|
/**
|
|
* Set a Carbon instance (real or mock) to be returned when a "now"
|
|
* instance is created. The provided instance will be returned
|
|
* specifically under the following conditions:
|
|
* - A call to the static now() method, ex. Carbon::now()
|
|
* - When a null (or blank string) is passed to the constructor or parse(), ex. new Carbon(null)
|
|
* - When the string "now" is passed to the constructor or parse(), ex. new Carbon('now')
|
|
* - When a string containing the desired time is passed to Carbon::parse().
|
|
*
|
|
* It will also align default timezone (e.g. call date_default_timezone_set()) with
|
|
* the second argument or if null, with the timezone of the given date object.
|
|
*
|
|
* To clear the test instance call this method using the default
|
|
* parameter of null.
|
|
*
|
|
* /!\ Use this method for unit tests only.
|
|
*
|
|
* @param DateTimeInterface|Closure|static|string|false|null $testNow real or mock Carbon instance
|
|
*/
|
|
public function setTestNowAndTimezone(mixed $testNow = null, $timezone = null): void
|
|
{
|
|
if ($testNow) {
|
|
$this->testDefaultTimezone ??= date_default_timezone_get();
|
|
}
|
|
|
|
$useDateInstanceTimezone = $testNow instanceof DateTimeInterface;
|
|
|
|
if ($useDateInstanceTimezone) {
|
|
$this->setDefaultTimezone($testNow->getTimezone()->getName(), $testNow);
|
|
}
|
|
|
|
$this->setTestNow($testNow);
|
|
$this->useTimezoneFromTestNow = ($timezone === null && $testNow instanceof Closure);
|
|
|
|
if (!$useDateInstanceTimezone) {
|
|
$now = $this->getMockedTestNow(\func_num_args() === 1 ? null : $timezone);
|
|
$this->setDefaultTimezone($now?->tzName ?? $this->testDefaultTimezone ?? 'UTC', $now);
|
|
}
|
|
|
|
if (!$testNow) {
|
|
$this->testDefaultTimezone = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Temporarily sets a static date to be used within the callback.
|
|
* Using setTestNow to set the date, executing the callback, then
|
|
* clearing the test instance.
|
|
*
|
|
* /!\ Use this method for unit tests only.
|
|
*
|
|
* @template T
|
|
*
|
|
* @param DateTimeInterface|Closure|static|string|false|null $testNow real or mock Carbon instance
|
|
* @param Closure(): T $callback
|
|
*
|
|
* @return T
|
|
*/
|
|
public function withTestNow(mixed $testNow, callable $callback): mixed
|
|
{
|
|
$this->setTestNow($testNow);
|
|
|
|
try {
|
|
$result = $callback();
|
|
} finally {
|
|
$this->setTestNow();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get the Carbon instance (real or mock) to be returned when a "now"
|
|
* instance is created.
|
|
*
|
|
* @return Closure|CarbonInterface|null the current instance used for testing
|
|
*/
|
|
public function getTestNow(): Closure|CarbonInterface|null
|
|
{
|
|
if ($this->testNow === null) {
|
|
$factory = FactoryImmutable::getDefaultInstance();
|
|
|
|
if ($factory !== $this) {
|
|
return $factory->getTestNow();
|
|
}
|
|
}
|
|
|
|
return $this->testNow;
|
|
}
|
|
|
|
public function handleTestNowClosure(
|
|
Closure|CarbonInterface|null $testNow,
|
|
DateTimeZone|string|int|null $timezone = null,
|
|
): ?CarbonInterface {
|
|
if ($testNow instanceof Closure) {
|
|
$callback = Callback::fromClosure($testNow);
|
|
$realNow = new DateTimeImmutable('now');
|
|
$testNow = $testNow($callback->prepareParameter($this->parse(
|
|
$realNow->format('Y-m-d H:i:s.u'),
|
|
$timezone ?? $realNow->getTimezone(),
|
|
)));
|
|
|
|
if ($testNow !== null && !($testNow instanceof DateTimeInterface)) {
|
|
$function = $callback->getReflectionFunction();
|
|
$type = \is_object($testNow) ? $testNow::class : \gettype($testNow);
|
|
|
|
throw new RuntimeException(
|
|
'The test closure defined in '.$function->getFileName().
|
|
' at line '.$function->getStartLine().' returned '.$type.
|
|
'; expected '.CarbonInterface::class.'|null',
|
|
);
|
|
}
|
|
|
|
if (!($testNow instanceof CarbonInterface)) {
|
|
$timezone ??= $this->useTimezoneFromTestNow ? $testNow->getTimezone() : null;
|
|
$testNow = $this->__call('instance', [$testNow, $timezone]);
|
|
}
|
|
}
|
|
|
|
return $testNow;
|
|
}
|
|
|
|
/**
|
|
* Determine if there is a valid test instance set. A valid test instance
|
|
* is anything that is not null.
|
|
*
|
|
* @return bool true if there is a test instance, otherwise false
|
|
*/
|
|
public function hasTestNow(): bool
|
|
{
|
|
return $this->getTestNow() !== null;
|
|
}
|
|
|
|
public function withTimeZone(DateTimeZone|string|int|null $timezone): static
|
|
{
|
|
$factory = clone $this;
|
|
$factory->settings['timezone'] = $timezone;
|
|
|
|
return $factory;
|
|
}
|
|
|
|
public function __call(string $name, array $arguments): mixed
|
|
{
|
|
$method = new ReflectionMethod($this->className, $name);
|
|
$settings = $this->settings;
|
|
|
|
if ($settings && isset($settings['timezone'])) {
|
|
$timezoneParameters = array_filter($method->getParameters(), function ($parameter) {
|
|
return \in_array($parameter->getName(), ['tz', 'timezone'], true);
|
|
});
|
|
$timezoneSetting = $settings['timezone'];
|
|
|
|
if (isset($arguments[0]) && \in_array($name, ['instance', 'make', 'create', 'parse'], true)) {
|
|
if ($arguments[0] instanceof DateTimeInterface) {
|
|
$settings['innerTimezone'] = $settings['timezone'];
|
|
} elseif (\is_string($arguments[0]) && date_parse($arguments[0])['is_localtime']) {
|
|
unset($settings['timezone'], $settings['innerTimezone']);
|
|
}
|
|
}
|
|
|
|
if (\count($timezoneParameters)) {
|
|
$index = key($timezoneParameters);
|
|
|
|
if (!isset($arguments[$index])) {
|
|
array_splice($arguments, key($timezoneParameters), 0, [$timezoneSetting]);
|
|
}
|
|
|
|
unset($settings['timezone']);
|
|
}
|
|
}
|
|
|
|
$clock = FactoryImmutable::getCurrentClock();
|
|
FactoryImmutable::setCurrentClock($this);
|
|
|
|
try {
|
|
$result = $this->className::$name(...$arguments);
|
|
} finally {
|
|
FactoryImmutable::setCurrentClock($clock);
|
|
}
|
|
|
|
if (isset($this->translator)) {
|
|
$settings['translator'] = $this->translator;
|
|
}
|
|
|
|
return $result instanceof CarbonInterface && !empty($settings)
|
|
? $result->settings($settings)
|
|
: $result;
|
|
}
|
|
|
|
/**
|
|
* Get the mocked date passed in setTestNow() and if it's a Closure, execute it.
|
|
*/
|
|
protected function getMockedTestNow(DateTimeZone|string|int|null $timezone): ?CarbonInterface
|
|
{
|
|
$testNow = $this->handleTestNowClosure($this->getTestNow());
|
|
|
|
if ($testNow instanceof CarbonInterface) {
|
|
$testNow = $testNow->avoidMutation();
|
|
|
|
if ($timezone !== null) {
|
|
return $testNow->setTimezone($timezone);
|
|
}
|
|
}
|
|
|
|
return $testNow;
|
|
}
|
|
|
|
/**
|
|
* Checks if the (date)time string is in a given format with
|
|
* given list of pattern replacements.
|
|
*
|
|
* @example
|
|
* ```
|
|
* Carbon::hasFormat('11:12:45', 'h:i:s'); // true
|
|
* Carbon::hasFormat('13:12:45', 'h:i:s'); // false
|
|
* ```
|
|
*
|
|
* @param string $date
|
|
* @param string $format
|
|
* @param array $replacements
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function matchFormatPattern(string $date, string $format, array $replacements): bool
|
|
{
|
|
// Preg quote, but remove escaped backslashes since we'll deal with escaped characters in the format string.
|
|
$regex = str_replace('\\\\', '\\', $format);
|
|
// Replace not-escaped letters
|
|
$regex = preg_replace_callback(
|
|
'/(?<!\\\\)((?:\\\\{2})*)(['.implode('', array_keys($replacements)).'])/',
|
|
static fn ($match) => $match[1].strtr($match[2], $replacements),
|
|
$regex,
|
|
);
|
|
// Replace escaped letters by the letter itself
|
|
$regex = preg_replace('/(?<!\\\\)((?:\\\\{2})*)\\\\(\w)/', '$1$2', $regex);
|
|
// Escape not escaped slashes
|
|
$regex = preg_replace('#(?<!\\\\)((?:\\\\{2})*)/#', '$1\\/', $regex);
|
|
|
|
return (bool) @preg_match('/^'.$regex.'$/', $date);
|
|
}
|
|
|
|
private function setDefaultTimezone(string $timezone, ?DateTimeInterface $date = null): void
|
|
{
|
|
$previous = null;
|
|
$success = false;
|
|
|
|
try {
|
|
$success = date_default_timezone_set($timezone);
|
|
} catch (Throwable $exception) {
|
|
$previous = $exception;
|
|
}
|
|
|
|
if (!$success) {
|
|
$suggestion = @CarbonTimeZone::create($timezone)->toRegionName($date);
|
|
|
|
throw new InvalidArgumentException(
|
|
"Timezone ID '$timezone' is invalid".
|
|
($suggestion && $suggestion !== $timezone ? ", did you mean '$suggestion'?" : '.')."\n".
|
|
"It must be one of the IDs from DateTimeZone::listIdentifiers(),\n".
|
|
'For the record, hours/minutes offset are relevant only for a particular moment, '.
|
|
'but not as a default timezone.',
|
|
0,
|
|
$previous
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|