From 1dcf32e0daba8d1659ac48b5096aa25c57681dce Mon Sep 17 00:00:00 2001 From: Dave M Date: Mon, 13 Jul 2026 19:41:44 +0000 Subject: [PATCH] - Working on revamping Ulmus bases classes --- src/Adapter/AdapterInterface.php | 3 +- src/Adapter/MsSQL.php | 6 +- src/Adapter/MySQL.php | 6 +- src/Adapter/SQLite.php | 79 +++++++++-------- src/ConnectionAdapter.php | 6 +- src/EntityTrait.php | 5 ++ src/Pdo/Mssql.php | 8 ++ src/Pdo/Mysql.php | 8 ++ src/Pdo/PdoTrait.php | 141 +++++++++++++++++++++++++++++++ src/Pdo/SqlPdoTrait.php | 43 ++++++++++ src/Pdo/Sqlite.php | 8 ++ src/Pdo/SqlitePdoTrait.php | 42 +++++++++ 12 files changed, 303 insertions(+), 52 deletions(-) create mode 100644 src/Pdo/Mssql.php create mode 100644 src/Pdo/Mysql.php create mode 100644 src/Pdo/PdoTrait.php create mode 100644 src/Pdo/SqlPdoTrait.php create mode 100644 src/Pdo/Sqlite.php create mode 100644 src/Pdo/SqlitePdoTrait.php diff --git a/src/Adapter/AdapterInterface.php b/src/Adapter/AdapterInterface.php index 61567a2..fe899ca 100644 --- a/src/Adapter/AdapterInterface.php +++ b/src/Adapter/AdapterInterface.php @@ -2,7 +2,6 @@ namespace Ulmus\Adapter; -use Ulmus\Common\PdoObject; use Ulmus\Migration\FieldDefinition; interface AdapterInterface { @@ -12,7 +11,7 @@ interface AdapterInterface { public const IDENTIFIER_SCHEMA = 4; public const IDENTIFIER_VALUE = 5; - public function connect() : object /* | PdoObject|mixed */; + public function connect() : object; public function buildDataSourceName() : string; public function setup(array $configuration) : void; public function writableValue(/* mixed */ $value); /*: mixed*/ diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php index 1fba020..b04fc13 100644 --- a/src/Adapter/MsSQL.php +++ b/src/Adapter/MsSQL.php @@ -2,7 +2,7 @@ namespace Ulmus\Adapter; -use Ulmus\Common\PdoObject; +use Ulmus\Pdo; use Ulmus\Exception\AdapterConfigurationException; @@ -83,10 +83,10 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { } } - public function connect() : PdoObject + public function connect() : Pdo { try { - $pdo = new PdoObject($this->buildDataSourceName(), $this->username, $this->password); + $pdo = new Pdo\Mssql($this->buildDataSourceName(), $this->username, $this->password); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); $pdo->setAttribute(\PDO::SQLSRV_ATTR_ENCODING, \PDO::SQLSRV_ENCODING_UTF8); diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index 62e7add..6b9c525 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -8,7 +8,7 @@ use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\MigrateInterface; use Ulmus\Migration\SqlMigrationTrait; use Ulmus\QueryBuilder\Sql; -use Ulmus\Common\PdoObject; +use Ulmus\Pdo; use Ulmus\Exception\AdapterConfigurationException; use Ulmus\Repository; @@ -71,10 +71,10 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { } } - public function connect() : PdoObject + public function connect() : \PDO { try { - $pdo = new PdoObject($this->buildDataSourceName(), $this->username, $this->password); + $pdo = new Pdo\Mysql($this->buildDataSourceName(), $this->username, $this->password); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); $pdo->setAttribute(\PDO::ATTR_AUTOCOMMIT, false); diff --git a/src/Adapter/SQLite.php b/src/Adapter/SQLite.php index d55bf4d..fce4b0d 100644 --- a/src/Adapter/SQLite.php +++ b/src/Adapter/SQLite.php @@ -3,7 +3,7 @@ namespace Ulmus\Adapter; use Collator; -use Ulmus\Common\PdoObject; +use Ulmus\Pdo; use Ulmus\ConnectionAdapter; use Ulmus\Entity; use Ulmus\Migration\FieldDefinition; @@ -24,16 +24,15 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface public null|array $pragmaClose = null, ) { } - public function connect() : PdoObject + public function connect() : \Pdo { try { - #$pdo = new PdoObject($this->buildDataSourceName(), null, null); - $pdo = new PdoObject\SqlitePdoObject($this->buildDataSourceName(), null, null); + $pdo = new Pdo\Sqlite($this->buildDataSourceName(), null, null); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); - $pdo->onClose = function(PdoObject $obj) { + $pdo->onClose = function(\PDO $obj) { static::registerPragma($obj, $this->pragmaClose); }; @@ -158,28 +157,28 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface return QueryBuilder\Sql\SqliteQueryBuilder::class; } - public function exportFunctions(PdoObject $pdo) : void + public function exportFunctions(\PDO $pdo) : void { - $pdo->sqliteCreateFunction('if', fn($comparison, $yes, $no) => $comparison ? $yes : $no, 3); - $pdo->sqliteCreateFunction('length', fn($string) => strlen($string), 1); - $pdo->sqliteCreateFunction('lcase', fn($string) => strtolower($string), 1); - $pdo->sqliteCreateFunction('ucase', fn($string) => strtoupper($string), 1); - $pdo->sqliteCreateFunction('md5', fn($string) => md5($string), 1); - $pdo->sqliteCreateFunction('sha1', fn($string) => sha1($string), 1); - $pdo->sqliteCreateFunction('sha256', fn($string) => hash('sha256', $string), 1); - $pdo->sqliteCreateFunction('sha512', fn($string) => hash('sha512', $string), 1); - $pdo->sqliteCreateFunction('whirlpool', fn($string) => hash('whirlpool', $string), 1); - $pdo->sqliteCreateFunction('murmur3a', fn($string) => hash('murmur3a', $string), 1); - $pdo->sqliteCreateFunction('murmur3c', fn($string) => hash('murmur3c', $string), 1); - $pdo->sqliteCreateFunction('murmur3f', fn($string) => hash('murmur3f', $string), 1); - $pdo->sqliteCreateFunction('left', fn($string, $length) => substr($string, 0, $length), 2); - $pdo->sqliteCreateFunction('right', fn($string, $length) => substr($string, -$length), 2); - $pdo->sqliteCreateFunction('strcmp', fn($s1, $s2) => strcmp($s1, $s2), 2); - $pdo->sqliteCreateFunction('lpad', fn($string, $length, $pad) => str_pad($string, $length, $pad, STR_PAD_LEFT), 3); - $pdo->sqliteCreateFunction('rpad', fn($string, $length, $pad) => str_pad($string, $length, $pad, STR_PAD_RIGHT), 3); - $pdo->sqliteCreateFunction('concat', fn(...$args) => implode('', $args), -1); - $pdo->sqliteCreateFunction('concat_ws', fn($separator, ...$args) => implode($separator, $args), -1); - $pdo->sqliteCreateFunction('find_in_set', function($string, $string_list) { + $pdo->createFunction('if', fn($comparison, $yes, $no) => $comparison ? $yes : $no, 3); + $pdo->createFunction('length', fn($string) => strlen($string), 1); + $pdo->createFunction('lcase', fn($string) => strtolower($string), 1); + $pdo->createFunction('ucase', fn($string) => strtoupper($string), 1); + $pdo->createFunction('md5', fn($string) => md5($string), 1); + $pdo->createFunction('sha1', fn($string) => sha1($string), 1); + $pdo->createFunction('sha256', fn($string) => hash('sha256', $string), 1); + $pdo->createFunction('sha512', fn($string) => hash('sha512', $string), 1); + $pdo->createFunction('whirlpool', fn($string) => hash('whirlpool', $string), 1); + $pdo->createFunction('murmur3a', fn($string) => hash('murmur3a', $string), 1); + $pdo->createFunction('murmur3c', fn($string) => hash('murmur3c', $string), 1); + $pdo->createFunction('murmur3f', fn($string) => hash('murmur3f', $string), 1); + $pdo->createFunction('left', fn($string, $length) => substr($string, 0, $length), 2); + $pdo->createFunction('right', fn($string, $length) => substr($string, -$length), 2); + $pdo->createFunction('strcmp', fn($s1, $s2) => strcmp($s1, $s2), 2); + $pdo->createFunction('lpad', fn($string, $length, $pad) => str_pad($string, $length, $pad, STR_PAD_LEFT), 3); + $pdo->createFunction('rpad', fn($string, $length, $pad) => str_pad($string, $length, $pad, STR_PAD_RIGHT), 3); + $pdo->createFunction('concat', fn(...$args) => implode('', $args), -1); + $pdo->createFunction('concat_ws', fn($separator, ...$args) => implode($separator, $args), -1); + $pdo->createFunction('find_in_set', function($string, $string_list) { if ( $string === null || $string_list === null ) { return null; } @@ -190,13 +189,13 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface return (int) in_array($string, explode(',', $string_list)); }, 2); - $pdo->sqliteCreateFunction('day', fn($date) => ( new \DateTime($date) )->format('j'), 1); - $pdo->sqliteCreateFunction('month', fn($date) => ( new \DateTime($date) )->format('n'), 1); - $pdo->sqliteCreateFunction('year', fn($date) => ( new \DateTime($date) )->format('Y'), 1); - $pdo->sqliteCreateFunction('str_to_date', fn($value, $format) => ($f = \DateTime::createFromFormat($format, $value)) ? $f->format('Y-m-d H:i:s') : false); + $pdo->createFunction('day', fn($date) => ( new \DateTime($date) )->format('j'), 1); + $pdo->createFunction('month', fn($date) => ( new \DateTime($date) )->format('n'), 1); + $pdo->createFunction('year', fn($date) => ( new \DateTime($date) )->format('Y'), 1); + $pdo->createFunction('str_to_date', fn($value, $format) => ($f = \DateTime::createFromFormat($format, $value)) ? $f->format('Y-m-d H:i:s') : false); } - public function exportCollations(PdoObject $pdo) : void + public function exportCollations(\PDO $pdo) : void { if ( class_exists('Locale') ) { @@ -205,28 +204,28 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface $collator->setStrength(Collator::TERTIARY); $collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON); - $pdo->sqliteCreateCollation('locale_cmp', fn($e1, $e2) => $collator->compare($e1, $e2)); - $pdo->sqliteCreateCollation('locale_cmp_desc', fn($e1, $e2) => $collator->compare($e2, $e1)); + $pdo->createCollation('locale_cmp', fn($e1, $e2) => $collator->compare($e1, $e2)); + $pdo->createCollation('locale_cmp_desc', fn($e1, $e2) => $collator->compare($e2, $e1)); $collatorCi = new Collator(\Locale::getDefault()); $collatorCi->setStrength(Collator::SECONDARY); $collatorCi->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON); - $pdo->sqliteCreateCollation('locale_cmp_ci', fn($e1, $e2) => $collatorCi->compare($e1, $e2)); - $pdo->sqliteCreateCollation('locale_cmp_ci_desc', fn($e1, $e2) => $collatorCi->compare($e2, $e1)); + $pdo->createCollation('locale_cmp_ci', fn($e1, $e2) => $collatorCi->compare($e1, $e2)); + $pdo->createCollation('locale_cmp_ci_desc', fn($e1, $e2) => $collatorCi->compare($e2, $e1)); } else { $comp = fn(string $e1, string $e2) => \iconv('UTF-8', 'ASCII//TRANSLIT', $e1) <=> \iconv('UTF-8', 'ASCII//TRANSLIT', $e2); - $pdo->sqliteCreateCollation('locale_cmp', fn($e1, $e2) => $comp); - $pdo->sqliteCreateCollation('locale_cmp_desc', fn($e2, $e1) => $comp); + $pdo->createCollation('locale_cmp', fn($e1, $e2) => $comp); + $pdo->createCollation('locale_cmp_desc', fn($e2, $e1) => $comp); $compCi = fn(string $e1, string $e2) => strtoupper(\iconv('UTF-8', 'ASCII//TRANSLIT', $e1)) <=> strtoupper(\iconv('UTF-8', 'ASCII//TRANSLIT', $e2)); - $pdo->sqliteCreateCollation('locale_cmp_ci', fn($e1, $e2) => $compCi); - $pdo->sqliteCreateCollation('locale_cmp_ci_desc', fn($e2, $e1) => $compCi); + $pdo->createCollation('locale_cmp_ci', fn($e1, $e2) => $compCi); + $pdo->createCollation('locale_cmp_ci_desc', fn($e2, $e1) => $compCi); } } - public static function registerPragma(PdoObject $pdo, array $pragmaList) : void + public static function registerPragma(\PDO $pdo, array $pragmaList) : void { $builder = new QueryBuilder\Sql\SqliteQueryBuilder(); diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php index 3e577cf..6f4c292 100644 --- a/src/ConnectionAdapter.php +++ b/src/ConnectionAdapter.php @@ -5,13 +5,11 @@ namespace Ulmus; use Psr\SimpleCache\CacheInterface; use Ulmus\Adapter\AdapterInterface; -use Ulmus\Common\PdoObject; - class ConnectionAdapter { protected AdapterInterface $adapter; - protected PdoObject $pdo; + protected \PDO $pdo; public function __construct( public string $name = "default", @@ -63,7 +61,7 @@ class ConnectionAdapter return $this->adapter; } - public function pdo() : PdoObject + public function pdo() : \PDO { return $this->pdo ?? $this->connect()->pdo; } diff --git a/src/EntityTrait.php b/src/EntityTrait.php index bbd2c5b..483de87 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -237,6 +237,11 @@ trait EntityTrait { return true; } + # Unexisting variable + if (! new \ReflectionClass($this)->hasProperty($name)) { + return false; + } + # Handling undefined 'nullable' return new \ReflectionProperty($this, $name)->isInitialized($this); } diff --git a/src/Pdo/Mssql.php b/src/Pdo/Mssql.php new file mode 100644 index 0000000..15e445d --- /dev/null +++ b/src/Pdo/Mssql.php @@ -0,0 +1,8 @@ +fillDebugSqlParameters($sql, $parameters) ]); + + try { + if (false !== ( $statement = $this->prepare($sql) )) { + $statement->setFetchMode(\PDO::FETCH_ASSOC); + $this->execute($statement, $parameters, false); + } + } + catch (\Throwable $e) { + throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters)); + } + + return $statement; + } + + public function __destruct() + { + if ($this->onClose ?? null) { + call_user_func($this->onClose, $this); + } + } + + public function runQuery(string $sql, array $parameters = []): ? static + { + # static::$dump && call_user_func_array(static::$dump, [ $this->fillDebugSqlParameters($sql, $parameters) ]); + + try { + if (false !== ( $statement = $this->prepare($sql) )) { + return $this->execute($statement, $parameters, true); + } + } + catch(\PDOException $pdo) { + if (!str_starts_with($pdo->getMessage(), 'There is no active transaction')) { + throw $pdo; + } + } + catch (\Throwable $e) { + throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters), (int) $e->getCode(), $e); + } + + return null; + } + + public function runInsertQuery(string $sql, array $parameters = []) : ? static + { + return $this->runQuery($sql, $parameters); + } + + public function runUpdateQuery(string $sql, array $parameters = []) : ? static + { + return $this->runQuery($sql, $parameters); + } + + public function runDeleteQuery(string $sql, array $parameters = []) : ? static + { + return $this->runQuery($sql, $parameters); + } + + public function execute(PDOStatement $statement, array $parameters = [], bool $commit = true) : ? static + { + $this->executionStatus = false; + $this->lastInsertId = null; + + try { + if (! $this->inTransaction()) { + $this->beginTransaction(); + } + + $this->bindVariables($statement, $parameters); + + $this->executionStatus = $statement->execute(); + + if ( $this->executionStatus ) { + $this->lastInsertId = $this->lastInsertId(); + $this->rowCount = $statement->rowCount(); + + if ( $commit ) { + $this->commit(); + } + + return $this; + } + else { + throw new \PDOException($statement->errorCode() . " - " . json_encode($statement->errorInfo())); + } + } + catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + } + + protected function bindVariables(PDOStatement $statement, array &$parameters) : void + { + if ($parameters) { + if (array_is_list($parameters)) { + $parameters = array_combine(range(1, count($parameters)), array_values($parameters)); + } + else { + foreach ($parameters as $key => $value) { + $type = match (strtolower(gettype($value))) { + "boolean", "integer" => Pdo::PARAM_INT, + "null" => Pdo::PARAM_NULL, + default => Pdo::PARAM_STR, + }; + + $statement->bindValue($key, $value, $type); + } + } + } + } + + protected function fillDebugSqlParameters(string $sql, array $parameters) : string + { + $parameters = array_reverse($parameters); + + return str_replace(array_keys($parameters), array_map(fn($e) => is_numeric($e) ? $e : ( + is_bool($e) ? (int) $e : $this->quote($e) + ), array_values($parameters)), $sql); + } +} \ No newline at end of file diff --git a/src/Pdo/SqlPdoTrait.php b/src/Pdo/SqlPdoTrait.php new file mode 100644 index 0000000..e694c0e --- /dev/null +++ b/src/Pdo/SqlPdoTrait.php @@ -0,0 +1,43 @@ +openedTransaction++ ) { + return $this->openTransaction(); + } + + return $this->exec("SAVEPOINT transaction_{$this->openedTransaction}") !== false; + } + + public function commit() : bool + { + if ( 0 === --$this->openedTransaction) { + return parent::commit(); + } + + return $this->exec("RELEASE SAVEPOINT transaction_{$this->openedTransaction}") !== false; + } + + public function rollback() : bool + { + if ($this->openedTransaction > 1) { + return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false; + } + elseif ($this->openedTransaction-- === 1) { + return parent::rollback(); + } + + return false; + } + + protected function openTransaction() : bool + { + return parent::beginTransaction(); + } +} \ No newline at end of file diff --git a/src/Pdo/Sqlite.php b/src/Pdo/Sqlite.php new file mode 100644 index 0000000..6d3d214 --- /dev/null +++ b/src/Pdo/Sqlite.php @@ -0,0 +1,8 @@ +openedTransaction > 0; + } + + public function commit(): bool + { + if ( 0 === --$this->openedTransaction) { + return $this->exec("COMMIT") !== false; + } + + return $this->exec("RELEASE SAVEPOINT transaction_{$this->openedTransaction}") !== false; + } + + public function rollback() : bool + { + if ($this->openedTransaction > 1) { + return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false; + } + elseif ($this->openedTransaction-- === 1) { + # We must do it manually since opening a transaction manually stucks PDO into thinking we've got no transaction opened + return $this->exec('ROLLBACK') !== false; + } + + return false; + } + + protected function openTransaction(): bool + { + return $this->exec("BEGIN IMMEDIATE TRANSACTION") !== false; + } +} \ No newline at end of file