entity_at( $id, false ); } /** * Find by uuid. * * {@inheritDoc} * * @param Uuid $uuid Parameter. * @since 0.1.0 */ public function find_by_uuid( Uuid $uuid ): ?Agent { foreach ( $this->occupied_by_slot( false ) as $agent ) { if ( $agent->uuid()->equals( $uuid ) ) { return $agent; } } return null; } /** * Find by WordPress user id. * * {@inheritDoc} * * @param int $wp_user_id Parameter. * @since 0.1.0 */ public function find_by_wp_user_id( int $wp_user_id ): ?Agent { foreach ( $this->occupied_by_slot( false ) as $agent ) { if ( $agent->wp_user_id() === $wp_user_id ) { return $agent; } } return null; } /** * Persist into a named staff field. * * {@inheritDoc} * * @param Agent $agent Parameter. * @since 0.1.0 */ public function save( Agent $agent ): void { $json = $this->encode_payload( $this->to_row( $agent ) ); if ( null === $agent->id() ) { $slot = AgentPlaces::from_occupied( $this->occupied_by_slot( false ) )->place( $agent ); $agent->assign_id( $slot ); $this->slots()->write( $slot, $json ); return; } $this->slots()->write( (int) $agent->id(), $json ); } /** * Soft delete. * * {@inheritDoc} * * @param Agent $agent Parameter. * @param DateTimeImmutable $when Parameter. * @since 0.1.0 */ public function soft_delete( Agent $agent, DateTimeImmutable $when ): void { $agent->soft_delete( $when ); $this->save( $agent ); } /** * Active agents. * * {@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 $agent ) { if ( AgentStatus::Active === $agent->status() ) { $items[] = $agent; } } usort( $items, static function ( Agent $a, Agent $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 agents. * * {@inheritDoc} */ public function count_stored(): int { return count( $this->occupied_by_slot( false ) ); } /** * Link an agent to a service they can deliver. * * {@inheritDoc} * * @param int $agent_id Agent id. * @param int $service_id Service id. * * @since 0.1.0 */ public function assign_service( int $agent_id, int $service_id ): void { if ( $agent_id < 1 || $service_id < 1 ) { return; } $this->db->replace( $this->table( 'agent_services' ), [ 'agent_id' => $agent_id, 'service_id' => $service_id, ], [ '%d', '%d' ] ); } /** * 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 ): ?Agent { $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( 'agents' ), 'agent', 3 ); } /** * 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 staff field.' ); } return $json; } /** * Map an entity to a stored payload. * * @param Agent $agent Entity. * @return array */ private function to_row( Agent $agent ): array { return [ 'uuid' => $agent->uuid()->to_string(), 'wp_user_id' => $agent->wp_user_id(), 'display_name' => $agent->display_name(), 'email' => $agent->email()->to_string(), 'phone' => null === $agent->phone() ? null : $agent->phone()->to_string(), 'bio' => $agent->bio(), 'avatar_id' => $agent->avatar_id(), 'timezone' => $agent->timezone()->to_string(), 'status' => $agent->status()->value, 'colour' => $agent->colour(), 'sort_order' => $agent->sort_order(), 'meta' => $agent->meta(), 'created_at' => $this->format_datetime( $agent->created_at() ), 'updated_at' => $this->format_datetime( $agent->updated_at() ), 'deleted_at' => null === $agent->deleted_at() ? null : $this->format_datetime( $agent->deleted_at() ), ]; } /** * Map a database row to an entity. * * @param array $row Database row. */ private function hydrate( array $row ): Agent { $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; return new Agent( (int) $row['id'], new Uuid( (string) $row['uuid'] ), isset( $row['wp_user_id'] ) && null !== $row['wp_user_id'] && '' !== $row['wp_user_id'] ? (int) $row['wp_user_id'] : null, (string) $row['display_name'], new EmailAddress( (string) $row['email'] ), is_string( $phone ) && '' !== $phone ? new PhoneNumber( $phone ) : null, isset( $row['bio'] ) && null !== $row['bio'] ? (string) $row['bio'] : null, isset( $row['avatar_id'] ) && null !== $row['avatar_id'] && '' !== $row['avatar_id'] ? (int) $row['avatar_id'] : null, new TimeZoneId( (string) $row['timezone'] ), AgentStatus::from( (string) $row['status'] ), isset( $row['colour'] ) && null !== $row['colour'] ? (string) $row['colour'] : null, (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, ); } }