Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client for speculatively loading values based on URL #10

Merged
merged 3 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

This library adheres to [Semantic Versioning](https://semver.org/) and [Keep a CHANGELOG](https://keepachangelog.com/en/1.0.0/).

## 0.5.0

### Added

- `Client` interface, extending `Alley\WP\Types\Feature`, for implementing a Big Pit client.
- `Items` class for clients that want to keep their own in-memory cache of fetched items.
- `Big_Speculative_Pit` client for preloading items used the last time a URL was requested.

### Changed

- `Big_Pit` is now in the `Alley\WP\Big_Pit` subnamespace, implements the `Client` interface, and must be instantiated with the `new` keyword.
- The `boot()` method on clients must be called manually.
- The `$wpdb->big_pit` property will be unset if the table is not available.

### Removed

- `Big_Pit::instance()` method.

## 0.4.0

### Added
Expand Down
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,33 @@ You can install the package via Composer:
composer require alleyinteractive/wp-big-pit
```

## Usage
## API

The `Alley\WP\Big_Pit\Client` interface describes the create-read-update-delete operations that can be used with the Big Pit. You can type hint against this interface when using Big Pit as a dependency.

```php
namespace Alley\WP\Big_Pit;

use Alley\WP\Types\Feature;

interface Client extends Feature {
public function get( string $key, string $group ): mixed;

public function set( string $key, mixed $value, string $group ): void;

public function delete( string $key, string $group ): void;

public function flush_group( string $group ): void;
}
```

Each item in the Big Pit has a key and a group, much like the WordPress object cache. Each key is unique within a group.

### Direct Access
`Client` extends the `Alley\WP\Types\Feature` interface from the [Type Extensions](https://github.com/alleyinteractive/wp-type-extensions) library, which includes a `boot()` method for performing side effects.

You must call `boot()` before using the client. If you are compiling features using the `Features` instance from Type Extensions, you can include the Big Pit client, and it will be booted with the rest of your feature classes.

You can perform CRUD operations directly on The Pit:
## Usage

```php
<?php
Expand All @@ -26,14 +46,32 @@ use Alley\WP\Big_Pit;
$external_id = 'abcdef12345';
$api_response = '{"id":"abcdef12345","title":"The Best Movie Ever","rating":5}';

$big_pit = Big_Pit::instance();
$big_pit = new Big_Pit\Big_Pit();
$big_pit->boot();

$big_pit->set( $external_id, $api_response, 'movie_reviews' );
$big_pit->get( $external_id, 'movie_reviews' ); // '{"id":"abcdef12345","title":"The Best Movie Ever","rating":5}'
$big_pit->delete( $external_id, 'movie_reviews' );
$big_pit->flush_group( 'movie_reviews' );
```

### Speculative Client

The `Big_Speculative_Pit` decorator class tracks the items that are fetched during a given request and preloads those items in a single query the next time the same page is requested.

```php
<?php

use Alley\WP\Big_Pit;

$request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();

$big_pit = new Big_Pit\Big_Speculative_Pit(
request: $request,
origin: new Big_Pit\Big_Pit(),
);
```

### PSR-16 Cache Adapter

A PSR-16 adapter is available for caching data in The Pit:
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"php": "^8.1",
"alleyinteractive/composer-wordpress-autoloader": "^1.0",
"alleyinteractive/wp-psr16": "^0.1.0",
"alleyinteractive/wp-type-extensions": "^2.1"
"alleyinteractive/wp-type-extensions": "^2.1",
"symfony/http-foundation": "^6.4"
},
"require-dev": {
"alleyinteractive/alley-coding-standards": "^2.0",
Expand Down Expand Up @@ -68,4 +69,4 @@
],
"tidy": "[ $COMPOSER_DEV_MODE -eq 0 ] || composer normalize"
}
}
}
92 changes: 37 additions & 55 deletions src/class-big-pit.php → src/big-pit/class-big-pit.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,24 @@
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching

namespace Alley\WP;

use Alley\WP\Types\Feature;
namespace Alley\WP\Big_Pit;

/**
* The Big Pit.
*/
final class Big_Pit implements Feature {
final class Big_Pit implements Client {
/**
* Singleton.
* Fetched items.
*
* @var self
* @var Items
*/
private static ?self $instance = null;
private readonly Items $items;

/**
* Whether the database is ready to accept queries.
*
* @var bool
*/
private bool $ready = false;

/**
* Cached values.
*
* @phpstan-var array<string, array<string, mixed>>
*
* @var array[]
* Constructor.
*/
private array $cache = [];

/**
* Instance.
*
* @return self
*/
public static function instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
self::$instance->boot();
}

return self::$instance;
public function __construct() {
$this->items = new Items();
}

/**
Expand All @@ -67,10 +42,8 @@ public function boot(): void {

try {
$this->upsert();
$this->ready = true;
} catch ( \Exception $e ) {
// Do nothing.
unset( $e );
unset( $wpdb->big_pit );
}
}

Expand All @@ -84,24 +57,20 @@ public function boot(): void {
public function get( string $key, string $group ): mixed {
global $wpdb;

if ( ! $this->ready ) {
assert( $wpdb instanceof \wpdb );

if ( ! isset( $wpdb->big_pit ) ) {
return null;
}

if ( isset( $this->cache[ $group ] ) && array_key_exists( $key, $this->cache[ $group ] ) ) {
$value = $this->cache[ $group ][ $key ];

if ( is_object( $value ) ) {
// Don't reuse the same instance across multiple calls.
$value = clone $value;
}

return $value;
if ( $this->items->has( $key, $group ) ) {
return $this->items->get( $key, $group );
}

$value = $wpdb->get_var(
$wpdb->prepare(
"SELECT item_value FROM {$wpdb->big_pit} WHERE item_group = %s AND item_key = %s LIMIT 1",
'SELECT item_value FROM %i WHERE item_group = %s AND item_key = %s LIMIT 1',
$wpdb->big_pit,
$group,
$key
),
Expand All @@ -111,7 +80,7 @@ public function get( string $key, string $group ): mixed {
$value = maybe_unserialize( $value );
}

$this->cache[ $group ][ $key ] = $value;
$this->items->add( $key, $value, $group );

return $value;
}
Expand All @@ -126,15 +95,18 @@ public function get( string $key, string $group ): mixed {
public function set( string $key, mixed $value, string $group ): void {
global $wpdb;

if ( ! $this->ready ) {
assert( $wpdb instanceof \wpdb );

if ( ! isset( $wpdb->big_pit ) ) {
return;
}

$value = maybe_serialize( $value ); // @phpstan-ignore argument.type

$exists = $wpdb->get_var(
$wpdb->prepare(
"SELECT item_id FROM {$wpdb->big_pit} WHERE item_group = %s AND item_key = %s LIMIT 1",
'SELECT item_id FROM %i WHERE item_group = %s AND item_key = %s LIMIT 1',
$wpdb->big_pit,
$group,
$key
),
Expand Down Expand Up @@ -164,7 +136,7 @@ public function set( string $key, mixed $value, string $group ): void {
);
}

unset( $this->cache[ $group ][ $key ] );
$this->items->remove( $key, $group );
}

/**
Expand All @@ -176,7 +148,9 @@ public function set( string $key, mixed $value, string $group ): void {
public function delete( string $key, string $group ): void {
global $wpdb;

if ( ! $this->ready ) {
assert( $wpdb instanceof \wpdb );

if ( ! isset( $wpdb->big_pit ) ) {
return;
}

Expand All @@ -189,7 +163,7 @@ public function delete( string $key, string $group ): void {
[ '%s', '%s' ],
);

unset( $this->cache[ $group ][ $key ] );
$this->items->remove( $key, $group );
}

/**
Expand All @@ -200,7 +174,9 @@ public function delete( string $key, string $group ): void {
public function flush_group( string $group ): void {
global $wpdb;

if ( ! $this->ready ) {
assert( $wpdb instanceof \wpdb );

if ( ! isset( $wpdb->big_pit ) ) {
return;
}

Expand All @@ -212,7 +188,7 @@ public function flush_group( string $group ): void {
[ '%s' ],
);

unset( $this->cache[ $group ] );
$this->items->remove_group( $group );
}

/**
Expand All @@ -223,13 +199,19 @@ public function flush_group( string $group ): void {
private function upsert(): void {
global $wpdb;

assert( $wpdb instanceof \wpdb );

$available_version = '2';
$installed_version = get_option( 'wp_big_pit_database_version', '0' );

if ( $available_version === $installed_version ) {
return;
}

if ( ! isset( $wpdb->big_pit ) ) {
return;
}

if ( ! function_exists( 'dbDelta' ) ) {
require_once ABSPATH . '/wp-admin/includes/upgrade.php';
}
Expand Down
Loading
Loading