data = null; } /** * Loads (and lazily migrates) the stored structure. */ private function load(): array { if ( null === $this->data ) { $raw = get_option( self::OPTION, array() ); $this->data = Migrations::migrate_extras_data( is_array( $raw ) ? $raw : array() ); } return $this->data; } /** * {@inheritDoc} */ public function all(): array { $extras = array(); foreach ( $this->load()['extras'] as $row ) { if ( is_array( $row ) && ! empty( $row['id'] ) ) { $extras[] = Extra::from_array( $row ); } } usort( $extras, static fn( Extra $a, Extra $b ): int => $a->position <=> $b->position ); return $extras; } /** * {@inheritDoc} * * @param string $id Extra id. */ public function find( string $id ): ?Extra { foreach ( $this->all() as $extra ) { if ( $extra->id === $id ) { return $extra; } } return null; } /** * {@inheritDoc} */ public function revision(): int { return (int) $this->load()['revision']; } /** * {@inheritDoc} * * @param Extra $extra Extra. * @param int|null $expected_revision Revision read by the caller. * @throws ConflictException On revision mismatch. */ public function save( Extra $extra, ?int $expected_revision = null ): int { $data = $this->load(); $this->assert_revision( $data, $expected_revision ); $row = $extra->to_array(); $updated = false; foreach ( $data['extras'] as $index => $existing ) { if ( ( $existing['id'] ?? '' ) === $extra->id ) { $data['extras'][ $index ] = $row; $updated = true; break; } } if ( ! $updated ) { $data['extras'][] = $row; } return $this->persist( $data ); } /** * {@inheritDoc} * * @param string $id Extra id. * @param int|null $expected_revision Revision read by the caller. * @throws ConflictException On revision mismatch. */ public function delete( string $id, ?int $expected_revision = null ): int { $data = $this->load(); $this->assert_revision( $data, $expected_revision ); $data['extras'] = array_values( array_filter( $data['extras'], static fn( array $row ): bool => ( $row['id'] ?? '' ) !== $id ) ); return $this->persist( $data ); } /** * {@inheritDoc} * * @param string[] $ids Ordered ids. * @param int|null $expected_revision Revision read by the caller. * @throws ConflictException On revision mismatch. */ public function reorder( array $ids, ?int $expected_revision = null ): int { $data = $this->load(); $this->assert_revision( $data, $expected_revision ); $order = array_flip( array_values( array_map( 'strval', $ids ) ) ); $rows = $data['extras']; usort( $rows, static function ( array $a, array $b ) use ( $order ): int { $pa = $order[ $a['id'] ?? '' ] ?? PHP_INT_MAX; $pb = $order[ $b['id'] ?? '' ] ?? PHP_INT_MAX; if ( $pa === $pb ) { return ( (int) ( $a['position'] ?? 0 ) ) <=> ( (int) ( $b['position'] ?? 0 ) ); } return $pa <=> $pb; } ); foreach ( $rows as $index => $row ) { $rows[ $index ]['position'] = ( $index + 1 ) * 10; } $data['extras'] = $rows; return $this->persist( $data ); } /** * {@inheritDoc} */ public function generate_id(): string { do { $id = 'ext_' . bin2hex( random_bytes( 4 ) ); } while ( null !== $this->find( $id ) ); return $id; } /** * Next free position. */ public function next_position(): int { $max = 0; foreach ( $this->all() as $extra ) { $max = max( $max, $extra->position ); } return $max + 10; } /** * Checks the optimistic lock. * * @param array $data Stored data. * @param int|null $expected Expected revision. * @throws ConflictException On mismatch. */ private function assert_revision( array $data, ?int $expected ): void { if ( null !== $expected && (int) $data['revision'] !== $expected ) { throw new ConflictException( 'Revision mismatch.' ); } } /** * Writes the option and bumps the revision. * * @param array $data Data. */ private function persist( array $data ): int { $data['schema_version'] = Migrations::SCHEMA_VERSION; $data['revision'] = (int) $data['revision'] + 1; $data['extras'] = array_values( $data['extras'] ); update_option( self::OPTION, $data, false ); $this->data = $data; return (int) $data['revision']; } }