entity_at( $id, false ); } /** * Find by UUID. * * {@inheritDoc} * * @param Uuid $uuid Parameter. * @since 0.1.0 */ public function find_by_uuid( Uuid $uuid ): ?Service { foreach ( $this->occupied_by_slot( false ) as $service ) { if ( $service->uuid()->equals( $uuid ) ) { return $service; } } return null; } /** * Find by slug. * * {@inheritDoc} * * @param string $slug Parameter. * @since 0.1.0 */ public function find_by_slug( string $slug ): ?Service { foreach ( $this->occupied_by_slot( false ) as $service ) { if ( $service->slug() === $slug ) { return $service; } } return null; } /** * Persist the entity into a named service field. * * {@inheritDoc} * * @param Service $service Parameter. * @since 0.1.0 */ public function save( Service $service ): void { $json = $this->encode_payload( $this->to_row( $service ) ); if ( null === $service->id() ) { $slot = ServicePlaces::from_occupied( $this->occupied_by_slot( false ) )->place( $service ); $service->assign_id( $slot ); $this->slots()->write( $slot, $json ); return; } $this->slots()->write( (int) $service->id(), $json ); } /** * Soft-delete the entity. * * {@inheritDoc} * * @param Service $service Parameter. * @param DateTimeImmutable $when Parameter. * @since 0.1.0 */ public function soft_delete( Service $service, DateTimeImmutable $when ): void { $service->soft_delete( $when ); $this->save( $service ); } /** * Active services. * * {@inheritDoc} * * @param int $limit Max rows. * @param int $offset Offset. * @return list * * @since 0.1.0 */ public function find_active( int $limit = 100, int $offset = 0 ): array { $items = []; foreach ( $this->occupied_by_slot( false ) as $service ) { if ( ServiceStatus::Active === $service->status() ) { $items[] = $service; } } usort( $items, static function ( Service $a, Service $b ): int { $cmp = $a->sort_order() <=> $b->sort_order(); return 0 !== $cmp ? $cmp : ( (int) $a->id() <=> (int) $b->id() ); } ); return array_slice( $items, max( 0, $offset ), max( 1, min( 100, $limit ) ) ); } /** * Non-deleted services. * * {@inheritDoc} */ public function count_stored(): int { return count( $this->occupied_by_slot( false ) ); } /** * Occupied named fields keyed by slot. * * @param bool $include_deleted Include soft-deleted occupants. * @return array */ private function occupied_by_slot( bool $include_deleted ): array { $out = []; $store = $this->slots(); $count = $store->field_count(); for ( $slot = 1; $slot <= $count; $slot++ ) { $entity = $this->entity_at( $slot, $include_deleted ); if ( null !== $entity ) { $out[ $slot ] = $entity; } } return $out; } /** * Hydrate one named field. * * @param int $slot Field number. * @param bool $include_deleted Include soft-deleted. */ private function entity_at( int $slot, bool $include_deleted ): ?Service { $json = $this->slots()->payload( $slot ); if ( null === $json ) { return null; } $decoded = json_decode( $json, true ); if ( ! is_array( $decoded ) ) { return null; } $decoded['id'] = $slot; $entity = $this->hydrate( $decoded ); if ( ! $include_deleted && $entity->is_deleted() ) { return null; } return $entity; } /** * Slot store for this catalogue table. */ private function slots(): CatalogueSlotStore { return new CatalogueSlotStore( $this->db, $this->table( 'services' ), 'service', 7 ); } /** * 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 service field.' ); } return $json; } /** * Map a database row to an entity. * * @param array $row Database row. */ private function hydrate( array $row ): Service { $meta = $row['meta'] ?? null; if ( is_array( $meta ) ) { $meta_payload = $meta; } else { $meta_payload = $this->decode_meta( is_string( $meta ) ? $meta : null ); } $deleted = $row['deleted_at'] ?? null; return new Service( (int) $row['id'], new Uuid( (string) $row['uuid'] ), isset( $row['category_id'] ) && null !== $row['category_id'] && '' !== $row['category_id'] ? (int) $row['category_id'] : null, (string) $row['name'], (string) $row['slug'], isset( $row['description'] ) && null !== $row['description'] ? (string) $row['description'] : null, isset( $row['short_description'] ) && null !== $row['short_description'] ? (string) $row['short_description'] : null, isset( $row['image_id'] ) && null !== $row['image_id'] && '' !== $row['image_id'] ? (int) $row['image_id'] : null, isset( $row['colour'] ) && null !== $row['colour'] ? (string) $row['colour'] : null, ServiceStatus::from( (string) $row['status'] ), (int) $row['capacity_min'], (int) $row['capacity_max'], (int) $row['buffer_before_min'], (int) $row['buffer_after_min'], isset( $row['lead_time_min'] ) && null !== $row['lead_time_min'] && '' !== $row['lead_time_min'] ? (int) $row['lead_time_min'] : null, isset( $row['horizon_days'] ) && null !== $row['horizon_days'] && '' !== $row['horizon_days'] ? (int) $row['horizon_days'] : null, isset( $row['slot_step_min'] ) && null !== $row['slot_step_min'] && '' !== $row['slot_step_min'] ? (int) $row['slot_step_min'] : null, DepositType::from( (string) $row['deposit_type'] ), (int) $row['deposit_value'], isset( $row['tax_class'] ) && null !== $row['tax_class'] ? (string) $row['tax_class'] : null, (bool) (int) ( $row['requires_approval'] ?? 0 ), (int) $row['sort_order'], $meta_payload, $this->parse_datetime( (string) $row['created_at'] ), $this->parse_datetime( (string) $row['updated_at'] ), null !== $deleted && '' !== $deleted ? $this->parse_datetime( (string) $deleted ) : null, ); } /** * Map an entity to a database row. * * @param Service $service Entity. * @return array */ private function to_row( Service $service ): array { return [ 'uuid' => $service->uuid()->to_string(), 'category_id' => $service->category_id(), 'name' => $service->name(), 'slug' => $service->slug(), 'description' => $service->description(), 'short_description' => $service->short_description(), 'image_id' => $service->image_id(), 'colour' => $service->colour(), 'status' => $service->status()->value, 'capacity_min' => $service->capacity_min(), 'capacity_max' => $service->capacity_max(), 'buffer_before_min' => $service->buffer_before_min(), 'buffer_after_min' => $service->buffer_after_min(), 'lead_time_min' => $service->lead_time_min(), 'horizon_days' => $service->horizon_days(), 'slot_step_min' => $service->slot_step_min(), 'deposit_type' => $service->deposit_type()->value, 'deposit_value' => $service->deposit_value(), 'tax_class' => $service->tax_class(), 'requires_approval' => $service->requires_approval() ? 1 : 0, 'sort_order' => $service->sort_order(), 'meta' => $service->meta(), 'created_at' => $this->format_datetime( $service->created_at() ), 'updated_at' => $this->format_datetime( $service->updated_at() ), 'deleted_at' => null === $service->deleted_at() ? null : $this->format_datetime( $service->deleted_at() ), ]; } }