*/ public function ordered_names( FormConfiguration $form ): array { $names = $form->step_names(); if ( function_exists( 'apply_filters' ) ) { /** * Filter the ordered step names for a booking form. * * @param list $names Step names. * @param int $form_id Form id. */ $filtered = apply_filters( 'bookline/steps/order', $names, $form->form_id() ); if ( is_array( $filtered ) ) { $out = []; foreach ( $filtered as $name ) { if ( is_string( $name ) && '' !== $name ) { $out[] = $name; } } if ( [] !== $out ) { $names = $out; } } } return $names; } /** * Whether a step should be skipped (step logic + filter). * * @param Step $step Step. * @param BookingSession $session Session. */ public function should_skip( Step $step, BookingSession $session ): bool { $skip = $step->should_skip( $session ); if ( function_exists( 'apply_filters' ) ) { /** * Filter whether a step is skipped. * * @param bool $skip Skip. * @param string $name Step name. * @param BookingSession $session Session. */ $filtered = apply_filters( 'bookline/steps/should_skip', $skip, $step->name(), $session ); $skip = (bool) $filtered; } return $skip; } /** * First non-skipped step name. * * @param BookingSession $session Session. * * @throws DomainException When no steps remain. */ public function first_step_name( BookingSession $session ): string { foreach ( $this->ordered_names( $session->form() ) as $name ) { $step = $this->registry->get( $name ); if ( ! $this->should_skip( $step, $session ) ) { return $name; } } throw new DomainException( 'steps.empty', 'No steps are available for this booking form.' ); } /** * Next non-skipped step after $current, or null at the end. * * @param BookingSession $session Session. * @param string $current Current step name. */ public function next_step_name( BookingSession $session, string $current ): ?string { $names = $this->ordered_names( $session->form() ); $idx = array_search( $current, $names, true ); if ( false === $idx ) { return null; } for ( $i = $idx + 1, $n = count( $names ); $i < $n; $i++ ) { $step = $this->registry->get( $names[ $i ] ); if ( ! $this->should_skip( $step, $session ) ) { return $names[ $i ]; } } return null; } /** * Previous completed, non-skipped step. * * @param BookingSession $session Session. */ public function previous_step_name( BookingSession $session ): ?string { $completed = $session->completed_steps(); while ( [] !== $completed ) { $previous = array_pop( $completed ); if ( ! is_string( $previous ) ) { continue; } if ( ! $this->registry->has( $previous ) ) { continue; } $step = $this->registry->get( $previous ); if ( ! $this->should_skip( $step, $session ) ) { return $previous; } } return null; } /** * Descriptor for a step with filter. * * @param BookingSession $session Session. * @param string $name Step name. */ public function descriptor( BookingSession $session, string $name ): StepDescriptor { $step = $this->registry->get( $name ); $descriptor = $step->descriptor( $session ); if ( function_exists( 'apply_filters' ) ) { /** * Filter a step descriptor. * * @param StepDescriptor $descriptor Descriptor. * @param string $name Step name. * @param BookingSession $session Session. */ $filtered = apply_filters( 'bookline/steps/descriptor', $descriptor, $name, $session ); if ( $filtered instanceof StepDescriptor ) { $descriptor = $filtered; } } return $descriptor; } /** * Validate input for a step. * * @param BookingSession $session Session. * @param string $name Step name. * @param array $input Input. */ public function validate( BookingSession $session, string $name, array $input ): ValidationResult { $step = $this->registry->get( $name ); $result = $step->validate( $session, $input ); if ( function_exists( 'apply_filters' ) ) { /** * Filter step validation. * * @param ValidationResult $result Result. * @param string $name Step name. * @param array $input Input. */ $filtered = apply_filters( 'bookline/steps/validate', $result, $name, $input ); if ( $filtered instanceof ValidationResult ) { $result = $filtered; } } return $result; } /** * Apply input and optionally advance. * * @param BookingSession $session Session. * @param string $name Step name. * @param array $input Input. * * @throws DomainException On step mismatch. * @throws StepValidationException On validation failure. */ public function submit( BookingSession $session, string $name, array $input ): BookingSession { if ( $name !== $session->current_step() ) { throw new DomainException( 'session.step_mismatch', 'Submitted step does not match the current step.' ); } $result = $this->validate( $session, $name, $input ); if ( ! $result->is_valid() ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- ValidationResult is not HTML. throw new StepValidationException( $result ); } $step = $this->registry->get( $name ); $session = $step->apply( $session, $input ); $session = $session->with_step_completed( $name ); if ( function_exists( 'apply_filters' ) ) { /** * Filter session after a step is applied. * * @param BookingSession $session Session. * @param string $name Step name. */ $filtered = apply_filters( 'bookline/steps/session', $session, $name ); if ( $filtered instanceof BookingSession ) { $session = $filtered; } } $next = $this->next_step_name( $session, $name ); if ( null !== $next ) { $session = $session->with_current_step( $next ); } return $session; } /** * Progress list of descriptors for the form. * * Skipped steps remain in the list (docs/09 ยง5) so the indicator matches * the configured journey. The engine still auto-advances past them. * * @param BookingSession $session Session. * @return list> */ public function progress_list( BookingSession $session ): array { $out = []; $completed = $session->completed_steps(); foreach ( $this->ordered_names( $session->form() ) as $name ) { $step = $this->registry->get( $name ); $skipped = $this->should_skip( $step, $session ); $row = $this->descriptor( $session, $name )->to_array(); $row['skipped'] = $skipped; $row['completed'] = in_array( $name, $completed, true ); $out[] = $row; } return $out; } }