From 9a1984c6bed4211399e72e41d27b27cf4319039a Mon Sep 17 00:00:00 2001 From: dimitri-bouteille Date: Sun, 10 Mar 2024 11:47:28 +0100 Subject: [PATCH] Test DatabaseV2 --- src/Builders/AbstractWithMetaBuilder.php | 3 +- src/Orm/AbstractModel.php | 2 +- src/Orm/Builder.php | 2 +- src/Orm/DatabaseV2.php | 484 +++++++++++++++++++++++ src/Orm/Resolver.php | 2 +- 5 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 src/Orm/DatabaseV2.php diff --git a/src/Builders/AbstractWithMetaBuilder.php b/src/Builders/AbstractWithMetaBuilder.php index 8601f980..4a858e14 100644 --- a/src/Builders/AbstractWithMetaBuilder.php +++ b/src/Builders/AbstractWithMetaBuilder.php @@ -14,6 +14,7 @@ use Dbout\WpOrm\MetaMappingConfig; use Dbout\WpOrm\Orm\AbstractModel; use Dbout\WpOrm\Orm\Database; +use Dbout\WpOrm\Orm\DatabaseV2; use Illuminate\Database\Eloquent\Model; /** @@ -129,7 +130,7 @@ public function joinToMeta(string $metaKey, string $joinType = 'inner'): self $join->on( sprintf('%s.%s', $metaKey, $this->metaConfig?->columnKey), '=', - Database::getInstance()->raw(sprintf("'%s'", $metaKey)) + DatabaseV2::getInstance()->raw(sprintf("'%s'", $metaKey)) )->on( sprintf('%s.%s', $metaKey, $this->metaConfig?->foreignKey), '=', diff --git a/src/Orm/AbstractModel.php b/src/Orm/AbstractModel.php index 4d934589..2d367e46 100644 --- a/src/Orm/AbstractModel.php +++ b/src/Orm/AbstractModel.php @@ -46,7 +46,7 @@ protected function newBaseQueryBuilder() public function getConnection() { // @phpstan-ignore-next-line - return Database::getInstance(); + return DatabaseV2::getInstance(); } /** diff --git a/src/Orm/Builder.php b/src/Orm/Builder.php index 54b418f4..17aeaef7 100644 --- a/src/Orm/Builder.php +++ b/src/Orm/Builder.php @@ -36,6 +36,6 @@ public function addWhereExistsQuery(EloquentBuilder $query, $boolean = 'and', $n */ public function getConnection(): ConnectionInterface { - return Database::getInstance(); + return DatabaseV2::getInstance(); } } diff --git a/src/Orm/DatabaseV2.php b/src/Orm/DatabaseV2.php new file mode 100644 index 00000000..dec7fa57 --- /dev/null +++ b/src/Orm/DatabaseV2.php @@ -0,0 +1,484 @@ + + */ + +namespace Dbout\WpOrm\Orm; + +use Dbout\WpOrm\Exceptions\WpOrmException; +use Illuminate\Database\Connection; +use Illuminate\Database\LostConnectionException; +use Illuminate\Database\QueryException; +use Illuminate\Database\Schema\Grammars\MySqlGrammar as SchemaGrammar; +use Illuminate\Database\Schema\MySqlBuilder; + +/** + * @see https://developer.wordpress.org/reference/classes/wpdb/ + */ +class DatabaseV2 extends Connection +{ + /** + * @var \wpdb + */ + protected \wpdb $db; + + /** + * Count of active transactions. + * @var int + */ + public int $transactionCount = 0; + + /** + * @var DatabaseV2|null + */ + protected static ?self $instance = null; + + /** + * @return DatabaseV2 + */ + public static function getInstance(): DatabaseV2 + { + if (!self::$instance instanceof \Dbout\WpOrm\Orm\DatabaseV2) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * @throws \Exception + */ + public function __construct() + { + global $wpdb; + if (!$wpdb instanceof \wpdb) { + throw new \Exception('The global variable $wpdb must be instance of \wpdb.'); + } + + $pdo = function () { + throw new \Exception('PDO property can\'t be used.'); + }; + + parent::__construct( + $pdo, + defined('DB_NAME') ? DB_NAME : '', + $wpdb->prefix, + [ + 'name' => 'wp-eloquent-mysql2', + 'charset' => $wpdb->charset, + 'collate' => $wpdb->collate, + 'version' => $wpdb->db_version(), + ] + ); + } + + /** + * @inheritDoc + */ + public function table($table, $as = null): Builder + { + $table = $this->getTablePrefix() . $table; + return $this->query()->from($table, $as); + } + + /** + * @return Builder + */ + public function query(): Builder + { + return new Builder( + $this, + $this->getQueryGrammar(), + $this->getPostProcessor() + ); + } + + /** + * @inheritDoc + */ + public function selectOne($query, $bindings = [], $useReadPdo = true) + { + return $this->run($query, $bindings, function (string $query, array $bindings) { + $query = $this->bindParams($query, $bindings); + return $this->db->get_row($query); + }); + } + + /** + * @inheritDoc + */ + public function select($query, $bindings = [], $useReadPdo = true): array + { + return $this->run($query, $bindings, function (string $query, array $bindings) { + $query = $this->bindParams($query, $bindings); + return $this->db->get_results($query); + }); + } + + /** + * @inheritDoc + */ + public function cursor($query, $bindings = [], $useReadPdo = true): \Generator + { + $results = $this->select($query, $bindings, $useReadPdo); + foreach ($results as $result) { + yield $result; + } + } + + /** + * A hacky way to emulate bind parameters into SQL query. + * + * @param string|null $query + * @param array $bindings + * @return string + */ + private function bindParams(?string $query, array $bindings): string + { + $query = \str_replace('"', '`', (string)$query); + $bindings = $this->prepareBindings($bindings); + if ($bindings === []) { + return $query; + } + + $bindings = \array_map(function ($replace) { + if (\is_string($replace)) { + $replace = "'" . esc_sql($replace) . "'"; + } elseif ($replace === null) { + $replace = "null"; + } + + return $replace; + }, $bindings); + + $query = \str_replace(['%', '?'], ['%%', '%s'], $query); + return \vsprintf($query, $bindings); + } + + /** + * @param $query + * @param $bindings + * @throws \Exception + * @return never + * @deprecated Remove in next version. + */ + public function bind_and_run($query, $bindings = []): never + { + throw new \Exception('This function is no longer usable, it will be removed in a future version.'); + } + + /** + * @inheritDoc + */ + public function insert($query, $bindings = []): bool + { + return $this->statement($query, $bindings); + } + + /** + * @inheritDoc + */ + public function update($query, $bindings = []): int + { + return $this->affectingStatement($query, $bindings); + } + + /** + * @inheritDoc + */ + public function delete($query, $bindings = []): int + { + return $this->affectingStatement($query, $bindings); + } + + /** + * @inheritDoc + */ + public function statement($query, $bindings = []): bool + { + return $this->run($query, $bindings, function (string $query, array $bindings) { + $query = $this->bindParams($query, $bindings); + return $this->db->query($query); + }); + } + + /** + * @inheritDoc + */ + public function affectingStatement($query, $bindings = []): int + { + return $this->run($query, $bindings, function (string $query, array $bindings) { + $newQuery = $this->bindParams($query, $bindings); + $result = $this->db->query($newQuery); + if (!is_numeric($result)) { + return $result; + } + + return (int) $result; + }); + } + + /** + * @inheritDoc + */ + public function unprepared($query): bool + { + return $this->run($query, [], function (string $query) { + return $this->db->query($query); + }); + } + + /** + * @inheritDoc + */ + public function prepareBindings(array $bindings): array + { + $grammar = $this->getQueryGrammar(); + foreach ($bindings as $key => $value) { + if (is_bool($value)) { + $bindings[$key] = (int) $value; + } elseif (is_scalar($value)) { + continue; + } elseif ($value instanceof \DateTimeInterface) { + $bindings[$key] = $value->format($grammar->getDateFormat()); + } + } + + return $bindings; + } + + /** + * @inheritDoc + */ + public function transaction(\Closure $callback, $attempts = 1) + { + $this->beginTransaction(); + try { + $data = $callback(); + $this->commit(); + return $data; + } catch (\Exception $e) { + $this->rollBack(); + throw $e; + } + } + + /** + * @inheritDoc + */ + public function beginTransaction(): void + { + $transaction = $this->unprepared('START TRANSACTION;'); + if ($transaction) { + $this->transactionCount++; + } + } + + /** + * @inheritDoc + */ + public function commit(): void + { + if ($this->transactionCount < 1) { + return; + } + + $transaction = $this->unprepared('COMMIT;'); + if ($transaction) { + $this->transactionCount--; + } + } + + /** + * @inheritDoc + */ + public function rollBack(): void + { + if ($this->transactionCount < 1) { + return; + } + + $transaction = $this->unprepared('ROLLBACK;'); + if ($transaction) { + $this->transactionCount--; + } + } + + /** + * @inheritDoc + */ + public function transactionLevel(): int + { + return $this->transactionCount; + } + + /** + * @inheritDoc + * @throws WpOrmException + * @see https://laravel.com/docs/10.x/eloquent#pruning-models + */ + public function pretend(\Closure $callback): array + { + throw new WpOrmException('pretend feature not supported.'); + } + + /** + * Return the last insert id. + * + * @return int|null + */ + public function lastInsertId(): ?int + { + return $this->db->insert_id; + } + + /** + * Get the current connection. + * + * @return $this + */ + public function getPdo(): static + { + return $this; + } + + /** + * Returns true if the last query has error. + * + * @return bool + * @see \wpdb::print_error() + */ + public function lastRequestHasError(): bool + { + return $this->db->last_error !== null && $this->db->last_error !== ''; + } + + /** + * @inheritDoc + */ + protected function run($query, $bindings, \Closure $callback): mixed + { + $start = microtime(true); + try { + $result = $this->runQueryCallback( + $query, + $bindings, + $callback + ); + } catch (QueryException $exception) { + $result = $this->handleQueryException( + $exception, + $query, + $bindings, + $callback + ); + } + + $this->logQuery($query, $bindings, $this->getElapsedTime($start)); + return $result; + } + + /** + * @inheritDoc + */ + protected function runQueryCallback($query, $bindings, \Closure $callback): mixed + { + try { + // Disable display WP error and save previous state + $suppressionError = $this->db->suppress_errors(); + + $result = $callback($query, $bindings); + + // Restore the state + $this->db->suppress_errors($suppressionError); + + if ($result === false || $this->lastRequestHasError()) { + throw new \Exception($this->db->last_error); + } + + return $result; + } catch (\Exception $exception) { + throw new QueryException( + $this->getName(), + $query, + $this->prepareBindings($bindings), + $exception + ); + } + } + + /** + * Handle a query exception. + * + * @param QueryException $exception + * @param string $query + * @param array $bindings + * @param \Closure $callback + * @return mixed + */ + protected function handleQueryException( + QueryException $e, + $query, + $bindings, + \Closure $callback + ): mixed { + if ($this->transactionCount >= 1) { + throw $e; + } + + if ($this->causedByLostConnection($e->getPrevious())) { + if (!$this->db->db_connect()) { + throw new LostConnectionException('Lost connection.'); + } + + return $this->runQueryCallback($query, $bindings, $callback); + } + + throw $e; + } + + /** + * @inheritDoc + * @see \wpdb::log_query + */ + public function logQuery($query, $bindings, $time = null): void + { + /** + * If you want to log queries, you must enable the constant SAVEQUERIES + * @see https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/#savequeries + */ + } + + /** + * Get the default schema grammar instance. + * + * @return \Illuminate\Database\Schema\Grammars\Grammar + */ + protected function getDefaultSchemaGrammar(): \Illuminate\Database\Schema\Grammars\Grammar + { + // @phpstan-ignore-next-line + ($grammar = new SchemaGrammar())->setConnection($this); + + /** @var \Illuminate\Database\Schema\Grammars\Grammar $grammar */ + $grammar = $this->withTablePrefix($grammar); + return $grammar; + } + + /** + * Get a schema builder instance for the connection. + * + * @return \Illuminate\Database\Schema\Builder + */ + public function getSchemaBuilder(): \Illuminate\Database\Schema\Builder + { + if (!$this->schemaGrammar instanceof \Illuminate\Database\Schema\Grammars\Grammar) { + $this->useDefaultSchemaGrammar(); + } + + // @phpstan-ignore-next-line + return new MySqlBuilder($this); + } +} diff --git a/src/Orm/Resolver.php b/src/Orm/Resolver.php index cf315072..98b4cd4d 100644 --- a/src/Orm/Resolver.php +++ b/src/Orm/Resolver.php @@ -24,7 +24,7 @@ class Resolver implements ConnectionResolverInterface */ public function connection($name = null): ConnectionInterface { - return Database::getInstance(); + return DatabaseV2::getInstance(); } /**