Skip to content
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
6 changes: 5 additions & 1 deletion lib/Traits/CanModifyWordPressDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
namespace PHPNomad\Integrations\WordPress\Traits;

use PHPNomad\Datastore\Exceptions\DatastoreErrorException;
use PHPNomad\Integrations\WordPress\Traits\LogsDatabaseErrors;

trait CanModifyWordPressDatabase
{
use LogsDatabaseErrors;

/**
* @param string $query
* @param ...$args
Expand All @@ -20,7 +23,8 @@ protected function wpdbQuery(string $query, ...$args): void
$wpdb->query($this->maybePrepare($query, ...$args));

if ($wpdb->last_error) {
throw new DatastoreErrorException('Query responded with error: ' . $wpdb->last_error);
$this->logDatabaseError('Query failed', $wpdb->last_error);
throw new DatastoreErrorException('Query failed.');
}
}

Expand Down
29 changes: 19 additions & 10 deletions lib/Traits/CanQueryWordPressDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
use PHPNomad\Integrations\WordPress\Database\ClauseBuilder;
use PHPNomad\Integrations\WordPress\Database\QueryBuilder as WordPressQueryBuilder;
use PHPNomad\Utils\Helpers\Arr;
use PHPNomad\Integrations\WordPress\Traits\LogsDatabaseErrors;

trait CanQueryWordPressDatabase
{
use LogsDatabaseErrors;

use CanGetDataFormats;

/**
Expand All @@ -28,15 +31,16 @@ protected function wpdbGetResults(QueryBuilder $queryBuilder): array
$query = $queryBuilder->build();
$result = $wpdb->get_results($query, ARRAY_A);
} catch (QueryBuilderException $e) {
throw new DatastoreErrorException('Get results failed. Invalid query: ' . $e->getMessage(), 500, $e);
throw new DatastoreErrorException('Get results failed: invalid query.', 500, $e);
}

if (is_null($result)) {
throw new DatastoreErrorException('Get results failed - ' . $wpdb->last_error);
$this->logDatabaseError('Get results failed', $wpdb->last_error);
throw new DatastoreErrorException('Get results failed.');
}

if (empty($result)) {
throw new RecordNotFoundException('No records found for query: ' . $query);
throw new RecordNotFoundException('No records found for the query.');
}

return $result;
Expand All @@ -61,10 +65,11 @@ protected function wpdbGetRow(QueryBuilder $queryBuilder): array

if (!$result) {
if (!empty($wpdb->last_error)) {
throw new DatastoreErrorException('Get row failed - ' . $wpdb->last_error);
$this->logDatabaseError('Get row failed', $wpdb->last_error);
throw new DatastoreErrorException('Get row failed.');
}

throw new RecordNotFoundException('No record found for query: ' . $query);
throw new RecordNotFoundException('No record found for the query.');
}

return $result;
Expand All @@ -88,7 +93,8 @@ protected function wpdbInsert(Table $table, array $data): array
}

if (false === $inserted) {
throw new DatastoreErrorException('Insert failed - ' . $wpdb->last_error);
$this->logDatabaseError('Insert failed', $wpdb->last_error);
throw new DatastoreErrorException('Insert failed.');
}

$fields = $table->getFieldsForIdentity();
Expand Down Expand Up @@ -127,7 +133,8 @@ protected function wpdbUpdate(Table $table, array $data, array $where): void
$result = $wpdb->update($table->getName(), $data, $where, $this->getFormats($data), $this->getFormats($where));

if (false === $result) {
throw new DatastoreErrorException('Update failed - ' . $wpdb->last_error);
$this->logDatabaseError('Update failed', $wpdb->last_error);
throw new DatastoreErrorException('Update failed.');
}

// When no records were updated, we need to figure out if this is because the record couldn't be found.
Expand Down Expand Up @@ -173,7 +180,8 @@ protected function wpdbDelete(Table $table, array $where): void
global $wpdb;

if (false === $wpdb->delete($table->getName(), $where, $this->getFormats($where))) {
throw new DatastoreErrorException('Delete failed - ' . $wpdb->last_error);
$this->logDatabaseError('Delete failed', $wpdb->last_error);
throw new DatastoreErrorException('Delete failed.');
}
}

Expand All @@ -197,9 +205,10 @@ protected function wpdbGetVar(QueryBuilder $queryBuilder): string

if (is_null($result)) {
if (empty($wpdb->last_error)) {
throw new RecordNotFoundException('No value found for query: ' . $query);
throw new RecordNotFoundException('No value found for the query.');
} else {
throw new DatastoreErrorException('Get var failed - ' . $wpdb->last_error);
$this->logDatabaseError('Get var failed', $wpdb->last_error);
throw new DatastoreErrorException('Get var failed.');
}
}

Expand Down
25 changes: 25 additions & 0 deletions lib/Traits/LogsDatabaseErrors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace PHPNomad\Integrations\WordPress\Traits;

trait LogsDatabaseErrors
{
/**
* Record database failure detail server-side.
*
* Exception messages must stay stable and free of SQL or MySQL error
* text — they can surface in REST error payloads or rendered fatals.
* The diagnostic detail still matters to operators, so it goes to the
* PHP error log (wp-content/debug.log when WP_DEBUG_LOG is enabled,
* the server error log otherwise) at the point of failure.
*/
private function logDatabaseError(string $message, string $detail): void
{
if ($detail === '') {
return;
}

// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- server-side diagnostics only; the thrown exception carries no detail.
error_log('[PHPNomad] ' . $message . ': ' . $detail);
}
}
21 changes: 17 additions & 4 deletions tests/Unit/Traits/CanQueryWordPressDatabaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ protected function tearDown(): void
parent::tearDown();
}

public function testWpdbGetResultsUsesLastErrorWhenQueryReturnsNull(): void
public function testWpdbGetResultsThrowsStableMessageAndLogsLastErrorWhenQueryReturnsNull(): void
{
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
Expand All @@ -53,9 +53,22 @@ public function getResults(QueryBuilder $queryBuilder): array
};

$this->expectException(DatastoreErrorException::class);
$this->expectExceptionMessage('Get results failed - Replica read failed');

$subject->getResults($queryBuilder);
// The thrown message must stay stable and free of MySQL error detail —
// it can surface in REST payloads or rendered fatals. The detail is
// recorded via error_log instead (redirected to a temp file below).
$this->expectExceptionMessage('Get results failed.');

$errorLog = tempnam(sys_get_temp_dir(), 'phpnomad-log');
$previousLog = ini_set('error_log', $errorLog);

try {
$subject->getResults($queryBuilder);
} finally {
ini_set('error_log', $previousLog);
$logged = file_get_contents($errorLog);
unlink($errorLog);
$this->assertStringContainsString('Replica read failed', $logged);
}
}

public function testWpdbUpdateIncludesTableIdentityAndPayloadWhenRecordIsMissing(): void
Expand Down
Loading