entity_at( $id, false ); } /** * Find by uuid. * * {@inheritDoc} * * @param Uuid $uuid Parameter. */ public function find_by_uuid( Uuid $uuid ): ?Location { foreach ( $this->occupied_by_slot( false ) as $location ) { if ( $location->uuid()->equals( $uuid ) ) { return $location; } } return null; } /** * Persist into a named location field. * * {@inheritDoc} * * @param Location $location Parameter. * @since 0.1.0 */ public function save( Location $location ): void { $json = $this->encode_payload( $this->to_row( $location ) ); if ( null === $location->id() ) { $slot = LocationPlaces::from_occupied( $this->occupied_by_slot( false ) )->place( $location ); $location->assign_id( $slot ); $this->slots()->write( $slot, $json ); return; } $this->slots()->write( (int) $location->id(), $json ); } /** * Soft delete. * * {@inheritDoc} * * @param Location $location Parameter. * @param DateTimeImmutable $when Parameter. * @since 0.1.0 */ public function soft_delete( Location $location, DateTimeImmutable $when ): void { $location->soft_delete( $when ); $this->save( $location ); } /** * Active locations. * * {@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 $location ) { if ( LocationStatus::Active === $location->status() ) { $items[] = $location; } } usort( $items, static function ( Location $a, Location $b ): int { $cmp = $a->name() <=> $b->name(); return 0 !== $cmp ? $cmp : ( (int) $a->id() <=> (int) $b->id() ); } ); return array_slice( $items, max( 0, $offset ), max( 1, min( 100, $limit ) ) ); } /** * Non-deleted locations. * * {@inheritDoc} * * @param int $limit Max rows. * @param int $offset Offset. * @return list */ public function find_stored( int $limit = 100, int $offset = 0 ): array { $items = array_values( $this->occupied_by_slot( false ) ); usort( $items, static function ( Location $a, Location $b ): int { $cmp = $a->name() <=> $b->name(); return 0 !== $cmp ? $cmp : ( (int) $a->id() <=> (int) $b->id() ); } ); return array_slice( $items, max( 0, $offset ), max( 1, min( 200, $limit ) ) ); } /** * Non-deleted locations. * * {@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 ): ?Location { $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( 'locations' ), 'location', 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 location field.' ); } return $json; } /** * Map an entity to a stored payload. * * @param Location $location Entity. * @return array */ private function to_row( Location $location ): array { return [ 'uuid' => $location->uuid()->to_string(), 'name' => $location->name(), 'slug' => $location->slug(), 'address_line1' => $location->address_line1(), 'address_line2' => $location->address_line2(), 'city' => $location->city(), 'region' => $location->region(), 'postcode' => $location->postcode(), 'country' => $location->country(), 'latitude' => $location->latitude(), 'longitude' => $location->longitude(), 'timezone' => $location->timezone()->to_string(), 'phone' => null === $location->phone() ? null : $location->phone()->to_string(), 'email' => null === $location->email() ? null : $location->email()->to_string(), 'status' => $location->status()->value, 'colour' => $location->colour(), 'meta' => $location->meta(), 'created_at' => $this->format_datetime( $location->created_at() ), 'updated_at' => $this->format_datetime( $location->updated_at() ), 'deleted_at' => null === $location->deleted_at() ? null : $this->format_datetime( $location->deleted_at() ), ]; } /** * Map a database row to an entity. * * @param array $row Database row. */ private function hydrate( array $row ): Location { $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; $phone = $row['phone'] ?? null; $email = $row['email'] ?? null; return new Location( (int) $row['id'], new Uuid( (string) $row['uuid'] ), (string) $row['name'], (string) $row['slug'], isset( $row['address_line1'] ) && null !== $row['address_line1'] ? (string) $row['address_line1'] : null, isset( $row['address_line2'] ) && null !== $row['address_line2'] ? (string) $row['address_line2'] : null, isset( $row['city'] ) && null !== $row['city'] ? (string) $row['city'] : null, isset( $row['region'] ) && null !== $row['region'] ? (string) $row['region'] : null, isset( $row['postcode'] ) && null !== $row['postcode'] ? (string) $row['postcode'] : null, isset( $row['country'] ) && null !== $row['country'] ? (string) $row['country'] : null, isset( $row['latitude'] ) && null !== $row['latitude'] ? (string) $row['latitude'] : null, isset( $row['longitude'] ) && null !== $row['longitude'] ? (string) $row['longitude'] : null, new TimeZoneId( (string) $row['timezone'] ), is_string( $phone ) && '' !== $phone ? new PhoneNumber( $phone ) : null, is_string( $email ) && '' !== $email ? new EmailAddress( $email ) : null, LocationStatus::from( (string) $row['status'] ), isset( $row['colour'] ) && null !== $row['colour'] ? (string) $row['colour'] : null, $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, ); } }