is_open( self::timestamp( $ts ) ); } /** * When the schedule next opens: `$ts` itself when open, or null when it * doesn't open within the horizon. * * @param int|null $ts UTC timestamp, or null for now. * @return int|null UTC timestamp. */ public static function next_open( $ts = null ) { return self::engine()->next_open( self::timestamp( $ts ) ); } /** * When the schedule next closes, or null when closed now or open past the horizon. * * @param int|null $ts UTC timestamp, or null for now. * @return int|null UTC timestamp. */ public static function next_close( $ts = null ) { return self::engine()->next_close( self::timestamp( $ts ) ); } /** * Open windows within `[from, to)`, merged and clipped, filtered by * `hourgate_schedule_windows`. * * @param int $from UTC timestamp, inclusive. * @param int $to UTC timestamp, exclusive. * @return Window[] * @throws \InvalidArgumentException When the span is longer than the horizon. */ public static function get_windows( int $from, int $to ) { $windows = self::engine()->windows( $from, $to ); /** * Filters the open windows `StoreSchedule::get_windows()` returns. * * Affects only `get_windows()`, not `is_open()` / `next_open()` / * `next_close()`. A return that isn't a list of `Window` objects is ignored. * * @param Window[] $windows Open windows, merged, clipped to [from, to), sorted. * @param int $from UTC timestamp, inclusive. * @param int $to UTC timestamp, exclusive. */ $filtered = apply_filters( 'hourgate_schedule_windows', $windows, $from, $to ); if ( ! is_array( $filtered ) ) { return $windows; } foreach ( $filtered as $window ) { if ( ! $window instanceof Window ) { return $windows; } } return array_values( $filtered ); } /** * Look-ahead for `next_open()` / `next_close()`, filtered by * `hourgate_schedule_horizon_days`. A return that isn't an integer * from 1 to `Engine::MAX_HORIZON_DAYS` falls back to the default (400). * * @return int */ public static function get_horizon_days() { /** * Filters how many days ahead `next_open()` / `next_close()` look. * * @param int $days Default 400. Must be an integer from 1 to 3660. */ $days = apply_filters( 'hourgate_schedule_horizon_days', Engine::DEFAULT_HORIZON_DAYS ); if ( ! is_int( $days ) || $days < 1 || $days > Engine::MAX_HORIZON_DAYS ) { return Engine::DEFAULT_HORIZON_DAYS; } return $days; } /** * Read an option merged with its defaults: known top-level keys only, each * an array, else the default value for that key. * * @param string $option Option name. * @param array $defaults Defaults. * @return array */ private static function read( $option, array $defaults ) { $stored = get_option( $option, array() ); $stored = is_array( $stored ) ? $stored : array(); $settings = array(); foreach ( $defaults as $key => $default ) { $settings[ $key ] = isset( $stored[ $key ] ) && is_array( $stored[ $key ] ) ? $stored[ $key ] : $default; } return $settings; } /** * The always-open fallback schedule (the defaults). * * @return Schedule * @throws \LogicException If the shipped defaults are invalid (a bug the unit tests guard). */ private static function always_open() { $schedule = ScheduleFactory::from_arrays( ScheduleSettings::get_default_settings(), ClosuresSettings::get_default_settings() ); if ( ! $schedule instanceof Schedule ) { throw new \LogicException( 'HourGate default schedule is invalid.' ); } return $schedule; } /** * Log invalid stored settings once a day per distinct problem. * * Only error paths and codes are logged, never labels or other stored text. * * @param ValidationError[] $errors Validation errors. * @param \Throwable|null $exception Unexpected exception, if that's what happened. * @return void */ private static function log_invalid( array $errors, $exception = null ) { $details = array_map( function ( ValidationError $error ) { return $error->get_path() . ' ' . $error->get_code(); }, $errors ); if ( null !== $exception ) { $details[] = get_class( $exception ) . ': ' . $exception->getMessage(); } $hash = md5( implode( "\n", $details ) ); if ( get_transient( self::LOGGED_TRANSIENT ) === $hash ) { return; } set_transient( self::LOGGED_TRANSIENT, $hash, DAY_IN_SECONDS ); if ( ! function_exists( 'wc_get_logger' ) ) { return; } wc_get_logger()->warning( sprintf( /* translators: %s: list of problems, e.g. "weekly.mon.0.start invalid_time". */ __( 'The saved opening hours are invalid, so HourGate treats the store as open at all times until they are saved again. Problems: %s', 'hourgate' ), implode( '; ', $details ) ), array( 'source' => self::LOG_SOURCE ) ); } /** * A timestamp argument, defaulting to now. * * @param int|null $ts UTC timestamp or null. * @return int */ private static function timestamp( $ts ) { return null === $ts ? time() : (int) $ts; } }