Skip to content
Draft
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
46 changes: 45 additions & 1 deletion .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,48 @@ jobs:
with:
php_version: "8.3"
version: "9.6"
configuration: phpunit.xml
configuration: phpunit.xml

integration:
runs-on: ubuntu-latest

services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: nomad_coordination_fixture
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -proot"
--health-interval=10s
--health-timeout=5s
--health-retries=12

steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
extensions: mysqli
coverage: none

- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist

- name: Install official WordPress 6.8.3
run: |
curl --fail --silent --show-error --location https://wordpress.org/wordpress-6.8.3.tar.gz \
| tar -xz -C "$RUNNER_TEMP"

- name: PHPUnit integration tests
env:
WORDPRESS_ROOT: ${{ runner.temp }}/wordpress
MYSQL_HOST: 127.0.0.1
MYSQL_PORT: 3306
MYSQL_USER: root
MYSQL_PASSWORD: root
MYSQL_DATABASE: nomad_coordination_fixture
run: vendor/bin/phpunit -c phpunit-integration.xml --colors=never --fail-on-skipped
112 changes: 87 additions & 25 deletions lib/Database/ClauseBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,38 @@ public function __construct(?wpdb $database = null)
$this->database = $database;
}

/**
* Return an operation-local copy that prepares through the supplied wpdb.
*
* The ordinary builder remains global-state compatible. Coordinated
* operations use this seam so a caller's mutable clause is not rebound in
* place while another request is using it.
*/
public function forDatabase(wpdb $database): static
{
$clone = clone $this;
$clone->database = $database;

foreach ($clone->clauses as $index => $clause) {
if (!is_array($clause) || ($clause['type'] ?? null) !== 'group') {
continue;
}

$bound = [];
foreach ($clause['clauses'] as $groupClause) {
if (!$groupClause instanceof self) {
throw new \PHPNomad\Database\Exceptions\UnsupportedCoordinationException(
'Coordinated WordPress queries require official WordPress grouped clause builders.'
);
}
$bound[] = $groupClause->forDatabase($database);
}
$clone->clauses[$index]['clauses'] = $bound;
}

return $clone;
}

/** @inheritDoc */
public function where($field, string $operator, ...$values)
{
Expand Down Expand Up @@ -64,7 +96,7 @@ public function andGroup(string $logic, ClauseBuilderInterface ...$clauses)
$this->clauses[] = 'AND';
}

$this->clauses[] = ['logic' => $logic, 'clauses' => $clauses];
$this->clauses[] = ['type' => 'group', 'logic' => $logic, 'clauses' => $clauses];
return $this;
}

Expand All @@ -77,7 +109,7 @@ public function orGroup(string $logic, ClauseBuilderInterface ...$clauses)
$this->clauses[] = 'OR';
}

$this->clauses[] = ['logic' => $logic, 'clauses' => $clauses];
$this->clauses[] = ['type' => 'group', 'logic' => $logic, 'clauses' => $clauses];
return $this;
}

Expand Down Expand Up @@ -140,34 +172,18 @@ protected function addCondition($field, string $operator, array $values, ?string

$fieldString = $this->getFieldString($field);
$values = $this->normalizeValues($field, $operator, $values);
$placeholder = $this->generatePlaceholder($field, $values, $operator);
$condition = "{$fieldString} {$operator}" . ($placeholder === '' ? '' : " {$placeholder}");

$preparedValues = [];
foreach ($values as $value) {
if (is_array($value)) {
foreach ($value as $tupleValue) {
if ($tupleValue !== null) {
$preparedValues[] = $tupleValue;
}
}
} elseif ($value !== null) {
$preparedValues[] = $value;
}
}

if ($preparedValues !== []) {
$condition = $this->wpdb()->prepare($condition, ...$preparedValues);
if (!is_string($condition) || $condition === '') {
throw new QueryBuilderException('WordPress could not prepare a condition.');
}
}

if ($this->clauses !== [] && $logic !== null) {
$this->clauses[] = $logic;
}

$this->clauses[] = $condition;
$this->clauses[] = [
'type' => 'condition',
'field' => $fieldString,
'placeholderField' => $field,
'operator' => $operator,
'values' => $values,
];
return $this;
}

Expand All @@ -182,6 +198,11 @@ public function build(): string
continue;
}

if (($clause['type'] ?? null) === 'condition') {
$queryParts[] = $this->buildCondition($clause);
continue;
}

$groupParts = [];
foreach ($clause['clauses'] as $groupClause) {
$builtClause = $groupClause->build();
Expand All @@ -204,6 +225,16 @@ public function build(): string
/** @inheritDoc */
public function reset()
{
foreach ($this->clauses as $clause) {
if (!is_array($clause) || ($clause['type'] ?? null) !== 'group') {
continue;
}
foreach ($clause['clauses'] as $groupClause) {
if ($groupClause instanceof ClauseBuilderInterface) {
$groupClause->reset();
}
}
}
$this->clauses = [];
$this->preparedValues = [];
return $this;
Expand Down Expand Up @@ -301,6 +332,7 @@ private function normalizeValues($field, string $operator, array $values): array
private function appendGroup(string $logic, array $clauses): void
{
$this->clauses[] = [
'type' => 'group',
'logic' => $this->validateGroup($logic, $clauses),
'clauses' => $clauses,
];
Expand Down Expand Up @@ -332,4 +364,34 @@ private function wpdb()

return $wpdb;
}

/** @param array{field:string, placeholderField:string|string[], operator:string, values:array<int,mixed>} $clause */
private function buildCondition(array $clause): string
{
$values = $clause['values'];
$preparedValues = [];
foreach ($values as $value) {
if (is_array($value)) {
foreach ($value as $tupleValue) {
if ($tupleValue !== null) {
$preparedValues[] = $tupleValue;
}
}
} elseif ($value !== null) {
$preparedValues[] = $value;
}
}

$placeholder = $this->generatePlaceholder($clause['placeholderField'], $values, $clause['operator']);
$condition = $clause['field'] . ' ' . $clause['operator']
. ($placeholder === '' ? '' : ' ' . $placeholder);
if ($preparedValues !== []) {
$condition = $this->wpdb()->prepare($condition, ...$preparedValues);
if (!is_string($condition) || $condition === '') {
throw new QueryBuilderException('WordPress could not prepare a condition.');
}
}

return $condition;
}
}
35 changes: 32 additions & 3 deletions lib/Database/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
namespace PHPNomad\Integrations\WordPress\Database;

use PHPNomad\Database\Exceptions\QueryBuilderException;
use PHPNomad\Database\Interfaces\ClauseBuilder;
use PHPNomad\Database\Interfaces\ClauseBuilder as ClauseBuilderInterface;
use PHPNomad\Database\Interfaces\HasQueryTables;
use PHPNomad\Database\Interfaces\QueryBuilder as QueryBuilderInterface;
use PHPNomad\Database\Interfaces\Table;
use PHPNomad\Database\Traits\WithPrependedFields;
use PHPNomad\Integrations\WordPress\Traits\CanGetDataFormats;
use PHPNomad\Integrations\WordPress\Database\ClauseBuilder as WordPressClauseBuilder;
use PHPNomad\Utils\Helpers\Arr;
use wpdb;

Expand Down Expand Up @@ -41,7 +42,7 @@ class QueryBuilder implements QueryBuilderInterface, HasQueryTables

protected array $orderBy = [];

protected ?ClauseBuilder $clauseBuilder = null;
protected ?ClauseBuilderInterface $clauseBuilder = null;
protected array $groupBy = [];

private ?Table $rootTable = null;
Expand All @@ -56,6 +57,31 @@ public function __construct(?wpdb $database = null)
$this->database = $database;
}

/**
* Return an operation-local copy bound to one wpdb resource.
*
* Query builders are mutable and may be shared by the regular database
* provider. Coordinated calls clone instead of changing that provider's
* builder or the process-global wpdb object.
*/
public function forDatabase(wpdb $database): static
{
$clone = clone $this;
$clone->database = $database;

if ($clone->clauseBuilder !== null) {
$clauseBuilder = $clone->clauseBuilder;
if (!$clauseBuilder instanceof WordPressClauseBuilder) {
throw new \PHPNomad\Database\Exceptions\UnsupportedCoordinationException(
'Coordinated WordPress queries require the official WordPress clause builder.'
);
}
$clone->clauseBuilder = $clauseBuilder->forDatabase($database);
}

return $clone;
}

/** @inheritDoc */
public function select(string $field, string ...$fields)
{
Expand Down Expand Up @@ -86,7 +112,7 @@ public function from(Table $table)
}

/** @inheritDoc */
public function where(?ClauseBuilder $clauseBuilder)
public function where(?ClauseBuilderInterface $clauseBuilder)
{
$this->clauseBuilder = $clauseBuilder->useTable($this->table);

Expand Down Expand Up @@ -307,6 +333,9 @@ public function getReferencedTables(): array
/** @inheritDoc */
public function reset()
{
if ($this->clauseBuilder !== null) {
$this->clauseBuilder->reset();
}
$this->select = [];
$this->clauseBuilder = null;
$this->from = [];
Expand Down
Loading
Loading