Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e16a05a
Add database tables for queue
srtfisher Jun 4, 2025
8c9b88e
Rename to Database_Schema
srtfisher Jun 4, 2025
a1af764
Starting out a database model
srtfisher Jun 4, 2025
7eabab8
Adding database assertions
srtfisher Jun 5, 2025
a4df6a5
Wrapping up database model crud methods
srtfisher Jun 5, 2025
6353e9e
Adding the basis of a database model
srtfisher Jun 5, 2025
5bfbc7b
Lint fix
srtfisher Jun 5, 2025
838e4ca
Assume the table name from the model
srtfisher Jun 5, 2025
86b1b03
Add a database query builder
srtfisher Jun 5, 2025
d534d92
Remove unused
srtfisher Jun 5, 2025
7a2b5a8
Testing CI
srtfisher Jun 6, 2025
0179e20
CHANGELOG
srtfisher Jun 6, 2025
28d3a89
Merging in database
srtfisher Jun 6, 2025
eb81047
WIP
srtfisher Jun 6, 2025
14b9cf4
Moving things around to simplify
srtfisher Jun 6, 2025
6029dad
Wrapping up database queue itself
srtfisher Jun 6, 2025
e19b2d4
Wrapping up for now
srtfisher Jun 6, 2025
fbd9f50
Merging in 1.x
srtfisher Jun 13, 2025
d2dc36d
Rename
srtfisher Jun 13, 2025
8084c41
WIP
srtfisher Jun 16, 2025
85519ff
Merge remote-tracking branch 'origin/1.x' into feature/mantle-queue-t…
srtfisher Jun 17, 2025
18d2225
Wrapping up the basis
srtfisher Jun 17, 2025
4c62711
Fixing assertions for queues
srtfisher Jun 26, 2025
8466597
WIP
srtfisher Jun 27, 2025
2df0c45
Fixing up queue cleanup
srtfisher Jun 27, 2025
c9c021b
Wrapping up a bunch
srtfisher Jun 27, 2025
e469a51
Contd work
srtfisher Jun 27, 2025
f599e00
Wrapping up the queue
srtfisher Jun 27, 2025
4d988de
Merge remote-tracking branch 'origin/1.x' into feature/mantle-queue-t…
srtfisher Jun 27, 2025
6173eb6
Testing CI
srtfisher Jun 27, 2025
3ba4507
Fix lint
srtfisher Jun 27, 2025
ba4f467
Wiping up
srtfisher Jun 27, 2025
4ea20e7
Lint fixes
srtfisher Jun 30, 2025
a857139
Merge branch '1.x' into feature/mantle-queue-tables
srtfisher Jul 7, 2025
c06cd5b
Merge remote-tracking branch 'origin/1.x' into feature/mantle-queue-t…
srtfisher Jul 10, 2025
de1871b
Updated date column
srtfisher Jul 10, 2025
167f39e
Test and fix retrying
srtfisher Jul 10, 2025
bfc0d70
Fix linting
srtfisher Jul 10, 2025
c18d243
Adjust date styling
srtfisher Jul 10, 2025
d96cf4c
Add note for future self
srtfisher Jul 10, 2025
dbaa8f1
Merge branch '1.x' into feature/mantle-queue-tables
srtfisher Jul 10, 2025
0c66551
Add cleanup for legacy post type
srtfisher Jul 10, 2025
33cafa7
Remove note
srtfisher Jul 21, 2025
6059c34
CHANGELOG
srtfisher Jul 21, 2025
f7123c7
Merge branch '2.x' into feature/mantle-queue-tables
srtfisher Jul 25, 2025
4d49d37
Merging in 2.x
srtfisher Aug 21, 2025
8d95b37
Merge branch '2.x' into feature/mantle-queue-tables
srtfisher Sep 16, 2025
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## v1.8.5

### Changed

- Queue system overhauled to use custom database tables for better performance.

### Added

- Added `Mantle\Support\Reflector::get_attributes_for_class()` method to
Expand Down Expand Up @@ -128,6 +132,13 @@ No changes, just a re-release to fix a bad tag.

## v1.8.0

### Changed

- The `assertInCronQueue()` assertion will now default to not checking arguments
unless `$args` is passed with specific arguments to compare against.

## v1.8.0

### Added

- Adds a `PreserveObjectCache` attribute to prevent the object cache from being
Expand Down
3 changes: 1 addition & 2 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ parameters:
- src/mantle/database/factory
- src/mantle/framework/console/class-hook-usage-command.php
- src/mantle/query-monitor
- src/mantle/queue/providers/wordpress/admin/class-queue-jobs-table.php
- src/mantle/queue/providers/wordpress/admin/class-service-provider.php
- src/mantle/queue/admin/class-queue-jobs-table.php
- src/mantle/support/interface-enumerable.php
- src/mantle/testing/class-mock-action.php
- src/mantle/testing/install-wordpress.php
Expand Down
2 changes: 1 addition & 1 deletion src/mantle/contracts/queue/interface-can-queue.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
/**
* Contract to allow a job to be added to a asynchronous queue.
*/
interface Can_Queue { }
interface Can_Queue {}
17 changes: 13 additions & 4 deletions src/mantle/contracts/queue/interface-provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ interface Provider {
/**
* Push a job to the queue.
*
* @param mixed $job Job instance.
* @param mixed $job Job instance.
* @param string $queue Queue name, optional.
*/
public function push( $job ): bool;
public function push( mixed $job, ?string $queue = null ): bool;

/**
* Get the next set of jobs in the queue.
*
* @param string $queue Queue name, optional.
* @param int $count Number of items to return.
* @return Collection<int, \Mantle\Queue\Jobs\Job>
*/
public function pop( ?string $queue = null, int $count = 1 ): Collection;

Expand All @@ -36,10 +38,17 @@ public function pop( ?string $queue = null, int $count = 1 ): Collection;
*/
public function in_queue( mixed $job, ?string $queue = null ): bool;

/**
* Retrieve the total number of jobs in the queue.
*
* @param string|null $queue Queue name, optional.
*/
public function size( ?string $queue = null ): int;

/**
* Retrieve the number of pending jobs in the queue.
*
* @param string $queue Queue name, optional.
* @param string|null $queue Queue name, optional.
*/
public function pending_count( ?string $queue = null ): int;
public function pending_size( ?string $queue = null ): int;
}
13 changes: 13 additions & 0 deletions src/mantle/contracts/queue/interface-queueable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
/**
* Queueable interface file.
*
* @package Mantle
*/

namespace Mantle\Contracts\Queue;

/**
* Alias to the Can_Queue interface.
*/
interface Queueable extends Can_Queue {}
30 changes: 24 additions & 6 deletions src/mantle/database/model/class-database-table-model.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ public static function find( mixed $object ): ?static {

$primary_key = static::$primary_key;

$table = static::get_table_name();

if ( ! str_starts_with( $table, $wpdb->prefix ) ) {
$table = $wpdb->prefix . $table;
}

$result = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}" . static::get_table_name() . " WHERE {$primary_key} = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL
"SELECT * FROM {$table} WHERE {$primary_key} = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL
$object,
),
ARRAY_A,
Expand All @@ -77,9 +83,15 @@ public function save( array $attributes = [] ): bool {

assert( $wpdb instanceof \wpdb );

$table = static::get_table_name();

if ( ! str_starts_with( $table, $wpdb->prefix ) ) {
$table = $wpdb->prefix . $table;
}

if ( $this->exists ) {
$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->prefix . static::get_table_name(),
$table,
$this->get_attributes_for_insert(),
[ static::$primary_key => $this->get_attribute( static::$primary_key ) ],
);
Expand All @@ -88,18 +100,18 @@ public function save( array $attributes = [] ): bool {
throw new Model_Exception(
sprintf(
'Failed to update %s table. Please check your database connection and permissions.',
static::get_table_name(),
$table,
),
);
}
} else {
$result = $wpdb->insert( $wpdb->prefix . static::get_table_name(), $this->get_attributes_for_insert() ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$result = $wpdb->insert( $table, $this->get_attributes_for_insert() ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery

if ( ! $result ) {
throw new Model_Exception(
sprintf(
'Failed to insert into %s table. Please check your database connection and permissions.',
static::get_table_name(),
$table,
),
);
}
Expand Down Expand Up @@ -128,8 +140,14 @@ public function delete( bool $force = false ): mixed {
throw new Model_Exception( 'Cannot delete a model that does not exist.' );
}

$table = static::get_table_name();

if ( ! str_starts_with( $table, $wpdb->prefix ) ) {
$table = $wpdb->prefix . $table;
}

$result = $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->prefix . static::get_table_name(),
$table,
[ static::$primary_key => $this->get_attribute( static::$primary_key ) ],
);

Expand Down
21 changes: 17 additions & 4 deletions src/mantle/database/model/concerns/trait-has-attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ trait Has_Attributes {
'timestamp',
];

/**
* Check if the model has an attribute.
*
* @param string $attribute Attribute name.
*/
public function has_attribute( string $attribute ): bool {
return array_key_exists( $attribute, $this->attributes ) || $this->has_get_mutator( $attribute );
}

/**
* Get an attribute from the model.
*
Expand Down Expand Up @@ -535,11 +544,15 @@ protected function as_json( mixed $value ): string {
/**
* Decode the given JSON back into an array or object.
*
* @param string $value Value to convert.
* @param bool $as_object Flag as an object.
* @param string|null $value Value to convert.
* @param bool $as_object Flag as an object.
*/
public function from_json( string $value, bool $as_object = false ): mixed {
return json_decode( $value, ! $as_object, 512, JSON_THROW_ON_ERROR );
public function from_json( string|null $value, bool $as_object = false ): mixed {
if ( null === $value ) {
return $as_object ? new \stdClass() : [];
}

return json_decode( $value, ! $as_object );
}

/**
Expand Down
85 changes: 76 additions & 9 deletions src/mantle/database/query/class-database-query-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
/**
* Database Query Builder
*
* @phpstan-type TBinding array{
* boolean: string,
* column: string,
* operator: string,
* value: mixed
* }
*
* @template TModel of \Mantle\Database\Model\Database_Table_Model
* @extends \Mantle\Database\Query\Builder<TModel>
*/
Expand Down Expand Up @@ -111,14 +118,7 @@ protected function get_query_sql(): string {
$select = $this->select ? implode( ', ', $this->select ) : '*';

$wheres = collect( $this->bindings['where'] )
->map(
fn ( mixed $binding, string $index ) => (string) $wpdb->prepare( // phpcs:ignore WordPress.DB
0 === (int) $index // phpcs:ignore WordPress.DB
? "{$binding['column']} {$binding['operator']} %s" // phpcs:ignore WordPress.DB
: "{$binding['boolean']} {$binding['column']} {$binding['operator']} %s", // phpcs:ignore WordPress.DB
$binding['value']
),
)
->map( $this->compile_where_binding( ... ) )
->filter()
->values()
->to_array();
Expand Down Expand Up @@ -147,15 +147,57 @@ protected function get_query_sql(): string {
$table = $wpdb->prefix . $table;
}

$order = [];

foreach ( $this->order_by as $index => $column ) {
$direction = $this->order[ $index ] ?? 'ASC';

$order[] = sprintf( '%s %s', $column, strtoupper( $direction ) );
}

return sprintf(
'SELECT %s FROM %s %s %s',
'SELECT %s FROM %s %s %s %s',
$select,
$table,
count( $wheres ) > 0 ? 'WHERE ' . implode( ' ', $wheres ) : '',
$order !== [] ? 'ORDER BY ' . implode( ', ', $order ) : '',
$limit
);
}

/**
* Compile a where binding into a SQL string.
*
* @param array $binding Binding to compile.
* @param string $index Index of the binding in the where clause.
* @phpstan-param TBinding $binding
*/
protected function compile_where_binding( array $binding, string $index ): string {
global $wpdb;

assert( $wpdb instanceof \wpdb );

if ( $binding['operator'] === 'IN' && is_array( $binding['value'] ) ) {
$placeholders = implode( ', ', array_fill( 0, count( $binding['value'] ), '%s' ) );

$query = (string) $wpdb->prepare(
"{$binding['column']} {$binding['operator']} ({$placeholders})", // phpcs:ignore WordPress.DB
...$binding['value'],
);

return 0 === (int) $index // phpcs:ignore WordPress.DB
? $query // phpcs:ignore WordPress.DB
: "{$binding['boolean']} {$query}"; // phpcs:ignore WordPress.DB
}

return (string) $wpdb->prepare( // phpcs:ignore WordPress.DB
0 === (int) $index // phpcs:ignore WordPress.DB
? "{$binding['column']} {$binding['operator']} %s" // phpcs:ignore WordPress.DB
: "{$binding['boolean']} {$binding['column']} {$binding['operator']} %s", // phpcs:ignore WordPress.DB
$binding['value']
);
}

/**
* Dump the SQL query being executed.
*/
Expand Down Expand Up @@ -201,6 +243,14 @@ public function where( array|string $attribute, mixed $value = '' ): static {
* @param string $boolean The boolean operator (AND/OR) used to concatenate the clause.
*/
public function where_raw( array|string $column, ?string $operator = null, mixed $value = null, string $boolean = 'AND' ): static {
if ( is_array( $column ) ) {
foreach ( $column as $value ) {
$this->where_raw( ...array_values( $value ) );
}

return $this;
}

$this->bindings['where'][] = [
'boolean' => $boolean,
'column' => $column,
Expand All @@ -210,4 +260,21 @@ public function where_raw( array|string $column, ?string $operator = null, mixed

return $this;
}

/**
* Query an attribute against a list.
*
* @param string $attribute Attribute to query against.
* @param array<mixed> $values List of values.
*/
public function whereIn( string $attribute, array $values, string $boolean = 'AND' ): static {
$this->bindings['where'][] = [
'boolean' => $boolean,
'column' => $attribute,
'operator' => 'IN',
'value' => $values,
];

return $this;
}
}
Loading
Loading