occupied_by_slot() as $form ) { $theme = $form->theme(); $items[] = [ 'id' => (int) $form->id(), 'name' => $form->name(), 'slug' => $form->slug(), 'status' => $form->status()->value, 'step_count' => count( $form->steps() ), 'accent' => $theme['accent'] ?? '', 'updated_at' => $form->updated_at()->format( 'c' ), ]; } usort( $items, static function ( array $a, array $b ): int { $cmp = (string) $a['name'] <=> (string) $b['name']; return 0 !== $cmp ? $cmp : ( (int) $a['id'] <=> (int) $b['id'] ); } ); $total = count( $items ); return [ 'items' => array_slice( $items, $offset, $limit ), 'total' => $total, ]; } /** * Find a form by id (slot 1–2). * * {@inheritDoc} * * @param int $id Form id. */ public function find_by_id( int $id ): ?BookingForm { return $this->entity_at( $id ); } /** * Find a form by slug. * * {@inheritDoc} * * @param string $slug Unique slug. */ public function find_by_slug( string $slug ): ?BookingForm { $slug = trim( $slug ); if ( '' === $slug ) { return null; } foreach ( $this->occupied_by_slot() as $form ) { if ( $form->slug() === $slug ) { return $form; } } return null; } /** * Persist into a named form field. * * {@inheritDoc} * * @param BookingForm $form Form entity. */ public function save( BookingForm $form ): void { $json = $this->encode_payload( $this->to_row( $form ) ); if ( null === $form->id() ) { $slot = BookingFormPlaces::from_occupied( $this->occupied_by_slot() )->place( $form ); $form->assign_id( $slot ); $this->slots()->write( $slot, $json ); return; } $this->slots()->write( (int) $form->id(), $json ); } /** * Clear a named form field. * * {@inheritDoc} * * @param int $id Form id. */ public function delete( int $id ): void { if ( $id <= 0 ) { return; } $this->slots()->write( $id, null ); } /** * Stored forms. * * {@inheritDoc} */ public function count_stored(): int { return count( $this->occupied_by_slot() ); } /** * Occupied named fields keyed by slot. * * @return array */ private function occupied_by_slot(): array { $out = []; $store = $this->slots(); $count = $store->field_count(); for ( $slot = 1; $slot <= $count; $slot++ ) { $entity = $this->entity_at( $slot ); if ( null !== $entity ) { $out[ $slot ] = $entity; } } return $out; } /** * Hydrate one named field. * * @param int $slot Field number. */ private function entity_at( int $slot ): ?BookingForm { $json = $this->slots()->payload( $slot ); if ( null === $json ) { return null; } $decoded = json_decode( $json, true ); if ( ! is_array( $decoded ) ) { return null; } $decoded['id'] = $slot; return $this->hydrate( $decoded ); } /** * Slot store for this catalogue table. */ private function slots(): CatalogueSlotStore { return new CatalogueSlotStore( $this->db, $this->table( 'booking_forms' ), 'form', 2 ); } /** * Encode a payload as JSON. * * @param array $row Payload. * * @throws RuntimeException When encoding fails. */ private function encode_payload( array $row ): string { $json = wp_json_encode( $row ); if ( false === $json ) { throw new RuntimeException( 'Failed to encode a booking-form field.' ); } return $json; } /** * Map an entity to a stored payload. * * @param BookingForm $form Entity. * @return array */ private function to_row( BookingForm $form ): array { return [ 'uuid' => $form->uuid()->to_string(), 'name' => $form->name(), 'slug' => $form->slug(), 'steps' => $form->steps(), 'service_ids' => $form->service_ids(), 'agent_ids' => $form->agent_ids(), 'location_ids' => $form->location_ids(), 'category_ids' => $form->category_ids(), 'settings' => $form->settings(), 'theme' => $form->theme(), 'status' => $form->status()->value, 'created_at' => $this->format_datetime( $form->created_at() ), 'updated_at' => $this->format_datetime( $form->updated_at() ), ]; } /** * Hydrate a stored payload. * * @param array $row Row. */ private function hydrate( array $row ): BookingForm { $created = (string) ( $row['created_at'] ?? '' ); $updated = (string) ( $row['updated_at'] ?? '' ); $steps = $row['steps'] ?? null; $theme = $row['theme'] ?? null; return new BookingForm( (int) ( $row['id'] ?? 0 ), new Uuid( (string) ( $row['uuid'] ?? '' ) ), (string) ( $row['name'] ?? '' ), (string) ( $row['slug'] ?? '' ), is_array( $steps ) ? $this->string_list( $steps ) : $this->decode_string_list( is_string( $steps ) ? $steps : null ), $this->optional_id_list( $row['service_ids'] ?? null ), $this->optional_id_list( $row['agent_ids'] ?? null ), $this->optional_id_list( $row['location_ids'] ?? null ), $this->optional_id_list( $row['category_ids'] ?? null ), $this->decode_settings_value( $row['settings'] ?? null ), is_array( $theme ) ? $this->theme_from_array( $theme ) : $this->decode_theme( is_string( $theme ) ? $theme : null ), BookingFormStatus::from( (string) ( $row['status'] ?? 'inactive' ) ), '' !== $created ? new DateTimeImmutable( $created, new DateTimeZone( 'UTC' ) ) : new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ), '' !== $updated ? new DateTimeImmutable( $updated, new DateTimeZone( 'UTC' ) ) : new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ), ); } /** * String values from a decoded list. * * @param list $decoded Decoded steps. * @return list */ private function string_list( array $decoded ): array { $out = []; foreach ( $decoded as $item ) { if ( is_string( $item ) && '' !== $item ) { $out[] = $item; } } return $out; } /** * Decode a JSON string list. * * @param string|null $json JSON. * @return list */ private function decode_string_list( ?string $json ): array { if ( null === $json || '' === $json ) { return []; } $decoded = json_decode( $json, true ); if ( ! is_array( $decoded ) ) { return []; } return $this->string_list( $decoded ); } /** * Decode settings JSON or array. * * @param mixed $value Stored settings. * @return array */ private function decode_settings_value( mixed $value ): array { if ( is_array( $value ) ) { return $value; } if ( ! is_string( $value ) || '' === $value ) { return []; } $decoded = json_decode( $value, true ); return is_array( $decoded ) ? $decoded : []; } /** * Decode theme JSON with defaults. * * @param string|null $json JSON. * @return array */ private function decode_theme( ?string $json ): array { if ( null === $json || '' === $json ) { return BookingForm::default_theme(); } $decoded = json_decode( $json, true ); if ( ! is_array( $decoded ) ) { return BookingForm::default_theme(); } return $this->theme_from_array( $decoded ); } /** * Merge a theme payload onto the defaults. * * @param array $decoded Theme payload. * @return array */ private function theme_from_array( array $decoded ): array { $theme = BookingForm::default_theme(); foreach ( array_keys( $theme ) as $key ) { if ( isset( $decoded[ $key ] ) && is_string( $decoded[ $key ] ) ) { $theme[ $key ] = $decoded[ $key ]; } } return $theme; } /** * Optional id list from JSON or array. * * @param mixed $value Stored ids. * @return list|null */ private function optional_id_list( mixed $value ): ?array { if ( null === $value || '' === $value ) { return null; } if ( is_array( $value ) ) { $out = []; foreach ( $value as $id ) { $out[] = (int) $id; } return $out; } if ( ! is_string( $value ) ) { return null; } $decoded = json_decode( $value, true ); if ( ! is_array( $decoded ) ) { return null; } $out = []; foreach ( $decoded as $id ) { $out[] = (int) $id; } return $out; } }