src/BusinessTime/MixinBase.php
<?php
namespace BusinessTime;
use BusinessTime\Exceptions\InvalidArgumentException;
use Carbon\CarbonInterface;
use Closure;
use Cmixin\BusinessDay;
use Spatie\OpeningHours\OpeningHours;
use SplObjectStorage;
class MixinBase extends BusinessDay
{
const HOLIDAYS_ARE_CLOSED = 0x01;
const USE_DAYLIGHT_SAVING_TIME = 0x02;
const CLOSED_TIME = 0x04;
const RELATIVE_DIFF = 0x08;
const MAX_ITERATION = 8192;
const HOUR_UNIT = 'hours';
const MINUTE_UNIT = 'minutes';
const IS_OPEN_METHOD = 'isOpen';
const IS_CLOSED_METHOD = 'isClosed';
const IS_OPEN_HOLIDAYS_METHOD = 'isOpenExcludingHolidays';
const IS_CLOSED_HOLIDAYS_METHOD = 'isClosedIncludingHolidays';
const NEXT_OPEN_METHOD = 'nextOpen';
const NEXT_CLOSE_METHOD = 'nextClose';
const PREVIOUS_OPEN_METHOD = 'previousOpen';
const PREVIOUS_CLOSE_METHOD = 'previousClose';
const NEXT_OPEN_HOLIDAYS_METHOD = 'nextOpenExcludingHolidays';
const NEXT_CLOSE_HOLIDAYS_METHOD = 'nextCloseIncludingHolidays';
const PREVIOUS_OPEN_HOLIDAYS_METHOD = 'previousOpenExcludingHolidays';
const PREVIOUS_CLOSE_HOLIDAYS_METHOD = 'previousCloseIncludingHolidays';
const CURRENT_OPEN_RANGE_START_METHOD = 'currentOpenRangeStart';
const CURRENT_OPEN_RANGE_END_METHOD = 'currentOpenRangeEnd';
const HOLIDAYS_OPTION_KEY = 'holidays';
const HOLIDAYS_ARE_CLOSED_OPTION_KEY = 'holidaysAreClosed';
const REGION_OPTION_KEY = 'region';
const ADDITIONAL_HOLIDAYS_OPTION_KEY = 'with';
const LOCAL_MODE = 'local';
const GLOBAL_MODE = 'global';
protected static $days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
/**
* @var OpeningHours|null
*/
public $openingHours;
/**
* @var SplObjectStorage<object, OpeningHours>
*/
protected $localOpeningHours;
public function __construct()
{
$this->localOpeningHours = new SplObjectStorage();
}
/**
* Returns day English name in lower case.
*
* @return \Closure<string>
*/
public function normalizeDay()
{
/**
* Returns day English name in lower case.
*
* @SuppressWarnings(PHPMD.StaticAccess)
*
* @param string|int $day can be a day number, 0 is Sunday, 1 is Monday, etc. or the day name as
* string with any case.
*
* @return string
*/
return static function ($day) {
return Normalizer::normalizeDay($day, static::$days);
};
}
/**
* Returns an OpeningHours instance (the one given if already an instance of OpeningHours, or else create
* a new one from array definition given).
*
* @return \Closure<\Spatie\OpeningHours\OpeningHours>
*/
public function convertOpeningHours()
{
/**
* Returns an OpeningHours instance (the one given if already an instance of OpeningHours, or else create
* a new one from array definition given).
*
* @SuppressWarnings(PHPMD.StaticAccess)
*
* @param array|\Spatie\OpeningHours\OpeningHours $defaultOpeningHours opening hours instance or array
* definition
*
* @throws InvalidArgumentException if $defaultOpeningHours has an invalid type
*
* @return \Spatie\OpeningHours\OpeningHours
*/
return static function ($defaultOpeningHours, $data = null) {
if ($defaultOpeningHours instanceof OpeningHours) {
return $defaultOpeningHours;
}
if (is_array($defaultOpeningHours)) {
$hours = ['data' => $data];
foreach ($defaultOpeningHours as $key => $value) {
$day = is_a(static::class, CarbonInterface::class, true) && static::hasMacro('normalizeDay')
? static::normalizeDay($key)
: Normalizer::normalizeDay($key);
$hours[$day] = $value;
}
return OpeningHours::create($hours);
}
throw new InvalidArgumentException('Opening hours parameter should be a '.
OpeningHours::class.
' instance or an array.');
};
}
/**
* Returns the mixin for the given class or an array where the key is the class and value the mixin
* if an array (of classes) was passed in.
*
* @param array|\Spatie\OpeningHours\OpeningHours $defaultOpeningHours opening hours instance or array
* definition
*
* @throws InvalidArgumentException if $defaultOpeningHours has an invalid type
*
* @return array|static
*/
public static function enable($carbonClass = null, $defaultOpeningHours = null)
{
if ($carbonClass === null) {
return static function () {
return true;
};
}
$arguments = array_slice(func_get_args(), 1);
$isArray = is_array($carbonClass);
$carbonClasses = (array) $carbonClass;
$mixins = [];
[$region, $holidays, $defaultOpeningHours] = self::getOpeningHoursOptions(
$defaultOpeningHours,
$arguments,
function ($date) use ($carbonClasses) {
foreach ($carbonClasses as $carbonClass) {
if (is_a($date, $carbonClass)) {
return $date->isHoliday();
}
}
return $carbonClasses[0]::instance($date)->isHoliday();
}
);
foreach ($carbonClasses as $carbonClass) {
/* @var static $mixin */
$mixin = parent::enable($carbonClass);
$carbonClass::mixin($mixin);
self::setRegionAndHolidays($carbonClass, $region, $holidays);
if ($defaultOpeningHours) {
$mixin->openingHours = $carbonClass::convertOpeningHours($defaultOpeningHours);
}
if ($isArray) {
$mixins[$carbonClass] = $mixin;
}
}
return $isArray ? $mixins : $mixin;
}
/**
* Set the opening hours for the class/instance.
*
* @param string|null $mode
* @param string|null $carbonClass
* @param \Spatie\OpeningHours\OpeningHours|null $openingHours
* @param \DateTimeInterface|null $context
*
* @return \Closure<$this|null>|null
*/
public function setOpeningHours($mode = null, $carbonClass = null, $openingHours = null, $context = null)
{
switch ($mode) {
case static::GLOBAL_MODE:
$this->openingHours = $openingHours;
self::setHolidaysFromOpeningHours($carbonClass, $openingHours);
return null;
case static::LOCAL_MODE:
$this->localOpeningHours[$context] = $openingHours;
self::setHolidaysFromOpeningHours($carbonClass, $openingHours);
return null;
default:
$mixin = $this;
/**
* Set the opening hours for the class/instance.
*
* @param \Spatie\OpeningHours\OpeningHours|array $openingHours
*
* @return $this|null
*/
return static function ($openingHours) use ($mixin, &$staticStorage) {
$parser = new DefinitionParser($mixin, $openingHours, function ($date) {
return static::instance($date)->isHoliday();
});
$openingHours = $parser->getEmbeddedOpeningHours(static::class);
$date = end(static::$macroContextStack);
if ($date) {
$mixin->setOpeningHours($mixin::LOCAL_MODE, static::class, $openingHours, $date);
return $date;
}
$mixin->setOpeningHours($mixin::GLOBAL_MODE, static::class, $openingHours);
return null;
};
}
}
/**
* Reset the opening hours for the class/instance.
*
* @return \Closure<$this|null>|null
*/
public function resetOpeningHours($mode = null, $context = null)
{
switch ($mode) {
case static::GLOBAL_MODE:
$this->openingHours = null;
return null;
case static::LOCAL_MODE:
unset($this->localOpeningHours[$context]);
return null;
default:
$mixin = $this;
/**
* Reset the opening hours for the class/instance.
*
* @return $this|null
*/
return static function () use ($mixin) {
$date = end(static::$macroContextStack);
if ($date) {
$mixin->resetOpeningHours($mixin::LOCAL_MODE, $date);
return $date;
}
$mixin->resetOpeningHours($mixin::GLOBAL_MODE);
return null;
};
}
}
/**
* Get the opening hours of the class/instance.
*
* @return \Closure<\Spatie\OpeningHours\OpeningHours>|\Spatie\OpeningHours\OpeningHours
*/
public function getOpeningHours($mode = null, $context = null)
{
switch ($mode) {
case static::GLOBAL_MODE:
return $this->openingHours;
case static::LOCAL_MODE:
return $this->localOpeningHours[$context] ?? null;
default:
$mixin = $this;
/**
* Get the opening hours of the class/instance.
*
* @throws InvalidArgumentException if Opening hours have not been set
*
* @return \Spatie\OpeningHours\OpeningHours
*/
return static function ($mode = null) use ($mixin): ?OpeningHours {
if ((
(!$mode || $mode === $mixin::LOCAL_MODE) &&
($date = end(static::$macroContextStack)) &&
($hours = $mixin->getOpeningHours($mixin::LOCAL_MODE, $date))
) ||
((!$mode || $mode === $mixin::GLOBAL_MODE) && (
$hours = $mixin->getOpeningHours($mixin::GLOBAL_MODE)
))
) {
return $hours;
}
throw new InvalidArgumentException('Opening hours have not been set.');
};
}
}
/**
* @internal
*
* Call a method on the OpeningHours of the current instance.
*
* @return \Closure<mixed>
*/
public function safeCallOnOpeningHours()
{
/**
* Call a method on the OpeningHours of the current instance.
*
* @return mixed
*/
return static function ($method, ...$arguments) {
return static::this()->getOpeningHours()->$method(...$arguments);
};
}
/**
* @internal
*
* Get a closure to be executed on OpeningHours on the current instance (or now if called globally) that should
* return a date, then convert it into a Carbon/sub-class instance.
*
* @param string $callee
*
* @return \Closure<\Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface|bool>
*/
public function getCalleeAsMethod($callee = null)
{
/**
* Get a closure to be executed on OpeningHours on the current instance (or now if called globally) that should
* return a date, then convert it into a Carbon/sub-class instance.
*
* @param string $method
*
* @return \Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface|bool
*/
return static function ($method = null) use ($callee) {
$method = is_string($method) ? $method : $callee;
$date = end(static::$macroContextStack);
if ($date) {
/* @var \Carbon\Carbon|static $date */
return $date->setDateTimeFrom($date->safeCallOnOpeningHours($method, clone $date));
}
return static::this()->$method();
};
}
/**
* Loop on the current instance (or now if called statically) with a given method until it's not a holiday.
*
* @param string $method
* @param string $fallbackMethod
*
* @return \Closure<\Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface>
*/
public function getMethodLoopOnHoliday($method = null, $fallbackMethod = null)
{
/**
* Loop on the current instance (or now if called statically) with a given method until it's not a holiday.
*
* @param string $method
* @param string $fallbackMethod
*
* @return \Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface
*/
return static function () use ($method, $fallbackMethod) {
$date = end(static::$macroContextStack);
if ($date) {
do {
$date = $date->$method();
} while ($date->isHoliday());
return $date;
}
return static::this()->$fallbackMethod();
};
}
/**
* Get a method that return current date-time if $testMethod applied on it return true,
* else return the result of $method called on it.
*
* @param string $testMethod method for the condition.
* @param string $method method to apply if condition is false.
*
* @return \Closure<\Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface>
*/
public function getTernaryMethod($testMethod = null, $method = null)
{
/**
* Get a method that return current date-time if $testMethod applied on it return true,
* else return the result of $method called on it.
*
* @param string $testMethod method for the condition.
* @param string $method method to apply if condition is false.
*
* @return \Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface
*/
return static function () use ($testMethod, $method) {
$date = static::this();
return $date->$testMethod() ? $date : $date->$method();
};
}
private static function getOpeningHoursOptions(
$defaultOpeningHours = null,
array $arguments = [],
Closure $isHoliday = null
) {
return (new DefinitionParser(static::class, $defaultOpeningHours, $isHoliday))
->getDefinition($arguments);
}
private static function setRegionAndHolidays($carbonClass, $region, $holidays)
{
/* @var \Carbon\Carbon $carbonClass */
if ($region) {
$carbonClass::setHolidaysRegion($region);
if ($holidays) {
$carbonClass::addHolidays($region, $holidays);
}
}
}
private static function setHolidaysFromOpeningHours($carbonClass, OpeningHours $openingHours)
{
$data = ($openingHours->getData() ?: [])[static::HOLIDAYS_OPTION_KEY] ?? [];
self::setRegionAndHolidays(
$carbonClass,
$data[static::REGION_OPTION_KEY] ?? null,
$data[static::ADDITIONAL_HOLIDAYS_OPTION_KEY] ?? null
);
}
}