From 389b937c6673029e8b41fca4a9ff10edba15374c Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Sat, 15 Mar 2025 05:38:01 -0500 Subject: [PATCH] Initial file import from laminas-db (proposed structure only) Signed-off-by: Joey Smith Signed-off-by: Joey Smith --- .laminas-ci.json | 3 - src/Adapter.php | 434 ++++++++++++++++++ src/ConfigProvider.php | 22 + src/Driver/Mysqli/Connection.php | 297 ++++++++++++ src/Driver/Mysqli/Mysqli.php | 242 ++++++++++ src/Driver/Mysqli/Result.php | 344 ++++++++++++++ src/Driver/Mysqli/Statement.php | 295 ++++++++++++ src/Platform/Mysql.php | 126 +++++ src/Sql/Platform/Ddl/AlterTableDecorator.php | 244 ++++++++++ src/Sql/Platform/Ddl/CreateTableDecorator.php | 178 +++++++ src/Sql/Platform/Mysql.php | 18 + src/Sql/Platform/SelectDecorator.php | 69 +++ 12 files changed, 2269 insertions(+), 3 deletions(-) create mode 100644 src/Adapter.php create mode 100644 src/ConfigProvider.php create mode 100644 src/Driver/Mysqli/Connection.php create mode 100644 src/Driver/Mysqli/Mysqli.php create mode 100644 src/Driver/Mysqli/Result.php create mode 100644 src/Driver/Mysqli/Statement.php create mode 100644 src/Platform/Mysql.php create mode 100644 src/Sql/Platform/Ddl/AlterTableDecorator.php create mode 100644 src/Sql/Platform/Ddl/CreateTableDecorator.php create mode 100644 src/Sql/Platform/Mysql.php create mode 100644 src/Sql/Platform/SelectDecorator.php diff --git a/.laminas-ci.json b/.laminas-ci.json index 13d383a..4e488ab 100644 --- a/.laminas-ci.json +++ b/.laminas-ci.json @@ -1,8 +1,5 @@ { "ignore_php_platform_requirements": { - "8.1": true, - "8.2": true, - "8.3": true, "8.4": true }, "exclude": [ diff --git a/src/Adapter.php b/src/Adapter.php new file mode 100644 index 0000000..eae4eba --- /dev/null +++ b/src/Adapter.php @@ -0,0 +1,434 @@ +createProfiler($parameters); + } + $driver = $this->createDriver($parameters); + } elseif (! $driver instanceof Driver\DriverInterface) { + throw new Exception\InvalidArgumentException( + 'The supplied or instantiated driver object does not implement ' . Driver\DriverInterface::class + ); + } + + $driver->checkEnvironment(); + $this->driver = $driver; + + if ($platform === null) { + $platform = $this->createPlatform($parameters); + } + + $this->platform = $platform; + $this->queryResultSetPrototype = $queryResultPrototype ?: new ResultSet\ResultSet(); + + if ($profiler) { + $this->setProfiler($profiler); + } + } + + /** + * @return $this Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->driver instanceof Profiler\ProfilerAwareInterface) { + $this->driver->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * getDriver() + * + * @throws Exception\RuntimeException + * @return Driver\DriverInterface + */ + public function getDriver() + { + if ($this->driver === null) { + throw new Exception\RuntimeException('Driver has not been set or configured for this adapter.'); + } + return $this->driver; + } + + /** + * @return Platform\PlatformInterface + */ + public function getPlatform() + { + return $this->platform; + } + + /** + * @return ResultSet\ResultSetInterface + */ + public function getQueryResultSetPrototype() + { + return $this->queryResultSetPrototype; + } + + /** @return string */ + public function getCurrentSchema() + { + return $this->driver->getConnection()->getCurrentSchema(); + } + + /** + * query() is a convenience function + * + * @param string $sql + * @param string|array|ParameterContainer $parametersOrQueryMode + * @throws Exception\InvalidArgumentException + * @return Driver\StatementInterface|ResultSet\ResultSet + */ + public function query( + $sql, + $parametersOrQueryMode = self::QUERY_MODE_PREPARE, + ?ResultSet\ResultSetInterface $resultPrototype = null + ) { + if ( + is_string($parametersOrQueryMode) + && in_array($parametersOrQueryMode, [self::QUERY_MODE_PREPARE, self::QUERY_MODE_EXECUTE]) + ) { + $mode = $parametersOrQueryMode; + $parameters = null; + } elseif (is_array($parametersOrQueryMode) || $parametersOrQueryMode instanceof ParameterContainer) { + $mode = self::QUERY_MODE_PREPARE; + $parameters = $parametersOrQueryMode; + } else { + throw new Exception\InvalidArgumentException( + 'Parameter 2 to this method must be a flag, an array, or ParameterContainer' + ); + } + + if ($mode === self::QUERY_MODE_PREPARE) { + $lastPreparedStatement = $this->driver->createStatement($sql); + $lastPreparedStatement->prepare(); + if (is_array($parameters) || $parameters instanceof ParameterContainer) { + if (is_array($parameters)) { + $lastPreparedStatement->setParameterContainer(new ParameterContainer($parameters)); + } else { + $lastPreparedStatement->setParameterContainer($parameters); + } + $result = $lastPreparedStatement->execute(); + } else { + return $lastPreparedStatement; + } + } else { + $result = $this->driver->getConnection()->execute($sql); + } + + if ($result instanceof Driver\ResultInterface && $result->isQueryResult()) { + $resultSet = $resultPrototype ?? $this->queryResultSetPrototype; + $resultSetCopy = clone $resultSet; + + $resultSetCopy->initialize($result); + + return $resultSetCopy; + } + + return $result; + } + + /** + * Create statement + * + * @param string $initialSql + * @param null|ParameterContainer|array $initialParameters + * @return Driver\StatementInterface + */ + public function createStatement($initialSql = null, $initialParameters = null) + { + $statement = $this->driver->createStatement($initialSql); + if ( + $initialParameters === null + || ! $initialParameters instanceof ParameterContainer + && is_array($initialParameters) + ) { + $initialParameters = new ParameterContainer(is_array($initialParameters) ? $initialParameters : []); + } + $statement->setParameterContainer($initialParameters); + return $statement; + } + + public function getHelpers() + { + $functions = []; + $platform = $this->platform; + foreach (func_get_args() as $arg) { + switch ($arg) { + case self::FUNCTION_QUOTE_IDENTIFIER: + $functions[] = function ($value) use ($platform) { + return $platform->quoteIdentifier($value); + }; + break; + case self::FUNCTION_QUOTE_VALUE: + $functions[] = function ($value) use ($platform) { + return $platform->quoteValue($value); + }; + break; + } + } + } + + /** + * @param string $name + * @throws Exception\InvalidArgumentException + * @return Driver\DriverInterface|Platform\PlatformInterface + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'driver': + return $this->driver; + case 'platform': + return $this->platform; + default: + throw new Exception\InvalidArgumentException('Invalid magic property on adapter'); + } + } + + /** + * @param array $parameters + * @return Driver\DriverInterface + * @throws InvalidArgumentException + * @throws Exception\InvalidArgumentException + */ + protected function createDriver($parameters) + { + if (! isset($parameters['driver'])) { + throw new Exception\InvalidArgumentException( + __FUNCTION__ . ' expects a "driver" key to be present inside the parameters' + ); + } + + if ($parameters['driver'] instanceof Driver\DriverInterface) { + return $parameters['driver']; + } + + if (! is_string($parameters['driver'])) { + throw new Exception\InvalidArgumentException( + __FUNCTION__ . ' expects a "driver" to be a string or instance of DriverInterface' + ); + } + + $options = []; + if (isset($parameters['options'])) { + $options = (array) $parameters['options']; + unset($parameters['options']); + } + + $driverName = strtolower($parameters['driver']); + switch ($driverName) { + case 'mysqli': + $driver = new Driver\Mysqli\Mysqli($parameters, null, null, $options); + break; + case 'sqlsrv': + $driver = new Driver\Sqlsrv\Sqlsrv($parameters); + break; + case 'oci8': + $driver = new Driver\Oci8\Oci8($parameters); + break; + case 'pgsql': + $driver = new Driver\Pgsql\Pgsql($parameters); + break; + case 'ibmdb2': + $driver = new Driver\IbmDb2\IbmDb2($parameters); + break; + case 'pdo': + default: + if ($driverName === 'pdo' || strpos($driverName, 'pdo') === 0) { + $driver = new Driver\Pdo\Pdo($parameters); + } + } + + if (! isset($driver) || ! $driver instanceof Driver\DriverInterface) { + throw new Exception\InvalidArgumentException('DriverInterface expected'); + } + + return $driver; + } + + /** + * @param array $parameters + * @return Platform\PlatformInterface + */ + protected function createPlatform(array $parameters) + { + if (isset($parameters['platform'])) { + $platformName = $parameters['platform']; + } elseif ($this->driver instanceof Driver\DriverInterface) { + $platformName = $this->driver->getDatabasePlatformName(Driver\DriverInterface::NAME_FORMAT_CAMELCASE); + } else { + throw new Exception\InvalidArgumentException( + 'A platform could not be determined from the provided configuration' + ); + } + + // currently only supported by the IbmDb2 & Oracle concrete implementations + $options = $parameters['platform_options'] ?? []; + + switch ($platformName) { + case 'Mysql': + // mysqli or pdo_mysql driver + if ($this->driver instanceof Driver\Mysqli\Mysqli || $this->driver instanceof Driver\Pdo\Pdo) { + $driver = $this->driver; + } else { + $driver = null; + } + return new Platform\Mysql($driver); + case 'SqlServer': + // PDO is only supported driver for quoting values in this platform + return new Platform\SqlServer($this->driver instanceof Driver\Pdo\Pdo ? $this->driver : null); + case 'Oracle': + if ($this->driver instanceof Driver\Oci8\Oci8 || $this->driver instanceof Driver\Pdo\Pdo) { + $driver = $this->driver; + } else { + $driver = null; + } + return new Platform\Oracle($options, $driver); + case 'Sqlite': + // PDO is only supported driver for quoting values in this platform + if ($this->driver instanceof Driver\Pdo\Pdo) { + return new Platform\Sqlite($this->driver); + } + return new Platform\Sqlite(null); + case 'Postgresql': + // pgsql or pdo postgres driver + if ($this->driver instanceof Driver\Pgsql\Pgsql || $this->driver instanceof Driver\Pdo\Pdo) { + $driver = $this->driver; + } else { + $driver = null; + } + return new Platform\Postgresql($driver); + case 'IbmDb2': + // ibm_db2 driver escaping does not need an action connection + return new Platform\IbmDb2($options); + default: + return new Platform\Sql92(); + } + } + + /** + * @param array $parameters + * @return Profiler\ProfilerInterface + * @throws Exception\InvalidArgumentException + */ + protected function createProfiler($parameters) + { + if ($parameters['profiler'] instanceof Profiler\ProfilerInterface) { + return $parameters['profiler']; + } + + if (is_bool($parameters['profiler'])) { + return $parameters['profiler'] === true ? new Profiler\Profiler() : null; + } + + throw new Exception\InvalidArgumentException( + '"profiler" parameter must be an instance of ProfilerInterface or a boolean' + ); + } + + /** + * @deprecated + * + * @param array $parameters + * @return Driver\DriverInterface + * @throws InvalidArgumentException + * @throws Exception\InvalidArgumentException + */ + protected function createDriverFromParameters(array $parameters) + { + return $this->createDriver($parameters); + } + + /** + * @deprecated + * + * @return Platform\PlatformInterface + */ + protected function createPlatformFromDriver(Driver\DriverInterface $driver) + { + return $this->createPlatform($driver); + } +} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php new file mode 100644 index 0000000..7121adc --- /dev/null +++ b/src/ConfigProvider.php @@ -0,0 +1,22 @@ + $this->getDependencies(), + ]; + } + + public function getDependencies(): array + { + return [ + 'factories' => [ + Adapter::class => AdapterServiceFactory::class, + ], + ]; + } +} \ No newline at end of file diff --git a/src/Driver/Mysqli/Connection.php b/src/Driver/Mysqli/Connection.php new file mode 100644 index 0000000..85277e0 --- /dev/null +++ b/src/Driver/Mysqli/Connection.php @@ -0,0 +1,297 @@ +setConnectionParameters($connectionInfo); + } elseif ($connectionInfo instanceof \mysqli) { + $this->setResource($connectionInfo); + } elseif (null !== $connectionInfo) { + throw new Exception\InvalidArgumentException( + '$connection must be an array of parameters, a mysqli object or null' + ); + } + } + + /** + * @return $this Provides a fluent interface + */ + public function setDriver(Mysqli $driver) + { + $this->driver = $driver; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getCurrentSchema() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $result = $this->resource->query('SELECT DATABASE()'); + $r = $result->fetch_row(); + + return $r[0]; + } + + /** + * Set resource + * + * @return $this Provides a fluent interface + */ + public function setResource(\mysqli $resource) + { + $this->resource = $resource; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function connect() + { + if ($this->resource instanceof \mysqli) { + return $this; + } + + // localize + $p = $this->connectionParameters; + + // given a list of key names, test for existence in $p + $findParameterValue = function (array $names) use ($p) { + foreach ($names as $name) { + if (isset($p[$name])) { + return $p[$name]; + } + } + + return null; + }; + + $hostname = $findParameterValue(['hostname', 'host']); + $username = $findParameterValue(['username', 'user']); + $password = $findParameterValue(['password', 'passwd', 'pw']); + $database = $findParameterValue(['database', 'dbname', 'db', 'schema']); + $port = isset($p['port']) ? (int) $p['port'] : null; + $socket = $p['socket'] ?? null; + + // phpcs:ignore WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps + $useSSL = $p['use_ssl'] ?? 0; + $clientKey = $p['client_key'] ?? ''; + $clientCert = $p['client_cert'] ?? ''; + $caCert = $p['ca_cert'] ?? ''; + $caPath = $p['ca_path'] ?? ''; + $cipher = $p['cipher'] ?? ''; + + $this->resource = $this->createResource(); + + if (! empty($p['driver_options'])) { + foreach ($p['driver_options'] as $option => $value) { + if (is_string($option)) { + $option = strtoupper($option); + if (! defined($option)) { + continue; + } + $option = constant($option); + } + $this->resource->options($option, $value); + } + } + + $flags = null; + + // phpcs:ignore WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps + if ($useSSL && ! $socket) { + // Even though mysqli docs are not quite clear on this, MYSQLI_CLIENT_SSL + // needs to be set to make sure SSL is used. ssl_set can also cause it to + // be implicitly set, but only when any of the parameters is non-empty. + $flags = MYSQLI_CLIENT_SSL; + $this->resource->ssl_set($clientKey, $clientCert, $caCert, $caPath, $cipher); + //MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT is not valid option, needs to be set as flag + if ( + isset($p['driver_options']) + && isset($p['driver_options'][MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT]) + ) { + $flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; + } + } + + try { + $flags === null + ? $this->resource->real_connect($hostname, $username, $password, $database, $port, $socket) + : $this->resource->real_connect($hostname, $username, $password, $database, $port, $socket, $flags); + } catch (GenericException $e) { + throw new Exception\RuntimeException( + 'Connection error', + $this->resource->connect_errno, + new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno) + ); + } + + if ($this->resource->connect_error) { + throw new Exception\RuntimeException( + 'Connection error', + $this->resource->connect_errno, + new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno) + ); + } + + if (! empty($p['charset'])) { + $this->resource->set_charset($p['charset']); + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isConnected() + { + return $this->resource instanceof \mysqli; + } + + /** + * {@inheritDoc} + */ + public function disconnect() + { + if ($this->resource instanceof \mysqli) { + $this->resource->close(); + } + $this->resource = null; + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $this->resource->autocommit(false); + $this->inTransaction = true; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $this->resource->commit(); + $this->inTransaction = false; + $this->resource->autocommit(true); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function rollback() + { + if (! $this->isConnected()) { + throw new Exception\RuntimeException('Must be connected before you can rollback.'); + } + + if (! $this->inTransaction) { + throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback.'); + } + + $this->resource->rollback(); + $this->resource->autocommit(true); + $this->inTransaction = false; + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\InvalidQueryException + */ + public function execute($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $resultResource = $this->resource->query($sql); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + // if the returnValue is something other than a mysqli_result, bypass wrapping it + if ($resultResource === false) { + throw new Exception\InvalidQueryException($this->resource->error); + } + + return $this->driver->createResult($resultResource === true ? $this->resource : $resultResource); + } + + /** + * {@inheritDoc} + */ + public function getLastGeneratedValue($name = null) + { + return $this->resource->insert_id; + } + + /** + * Create a new mysqli resource + * + * @return \mysqli + */ + protected function createResource() + { + return new \mysqli(); + } +} diff --git a/src/Driver/Mysqli/Mysqli.php b/src/Driver/Mysqli/Mysqli.php new file mode 100644 index 0000000..7cddb5d --- /dev/null +++ b/src/Driver/Mysqli/Mysqli.php @@ -0,0 +1,242 @@ + false, + ]; + + /** + * Constructor + * + * @param array|Connection|\mysqli $connection + * @param array $options + */ + public function __construct( + $connection, + ?Statement $statementPrototype = null, + ?Result $resultPrototype = null, + array $options = [] + ) { + if (! $connection instanceof Connection) { + $connection = new Connection($connection); + } + + $options = array_intersect_key(array_merge($this->options, $options), $this->options); + + $this->registerConnection($connection); + $this->registerStatementPrototype($statementPrototype ?: new Statement($options['buffer_results'])); + $this->registerResultPrototype($resultPrototype ?: new Result()); + } + + /** + * @return $this Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @return $this Provides a fluent interface + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); // needs access to driver to createStatement() + return $this; + } + + /** + * Register statement prototype + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); // needs access to driver to createResult() + } + + /** + * Get statement prototype + * + * @return null|Statement + */ + public function getStatementPrototype() + { + return $this->statementPrototype; + } + + /** + * Register result prototype + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + } + + /** + * @return null|Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + if ($nameFormat === self::NAME_FORMAT_CAMELCASE) { + return 'Mysql'; + } + + return 'MySQL'; + } + + /** + * Check environment + * + * @throws Exception\RuntimeException + * @return void + */ + public function checkEnvironment() + { + if (! extension_loaded('mysqli')) { + throw new Exception\RuntimeException( + 'The Mysqli extension is required for this adapter but the extension is not loaded' + ); + } + } + + /** + * Get connection + * + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Create statement + * + * @param string $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + /** + * @todo Resource tracking + if (is_resource($sqlOrResource) && !in_array($sqlOrResource, $this->resources, true)) { + $this->resources[] = $sqlOrResource; + } + */ + + $statement = clone $this->statementPrototype; + if ($sqlOrResource instanceof mysqli_stmt) { + $statement->setResource($sqlOrResource); + } else { + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } + if (! $this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + } + return $statement; + } + + /** + * Create result + * + * @param resource $resource + * @param null|bool $isBuffered + * @return Result + */ + public function createResult($resource, $isBuffered = null) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue(), $isBuffered); + return $result; + } + + /** + * Get prepare type + * + * @return string + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_POSITIONAL; + } + + /** + * Format parameter name + * + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return '?'; + } + + /** + * Get last generated value + * + * @return mixed + */ + public function getLastGeneratedValue() + { + return $this->getConnection()->getLastGeneratedValue(); + } +} diff --git a/src/Driver/Mysqli/Result.php b/src/Driver/Mysqli/Result.php new file mode 100644 index 0000000..3fcd617 --- /dev/null +++ b/src/Driver/Mysqli/Result.php @@ -0,0 +1,344 @@ + null, 'values' => []]; + + /** @var mixed */ + protected $generatedValue; + + /** + * Initialize + * + * @param mixed $resource + * @param mixed $generatedValue + * @param bool|null $isBuffered + * @return $this Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function initialize($resource, $generatedValue, $isBuffered = null) + { + if ( + ! $resource instanceof mysqli + && ! $resource instanceof mysqli_result + && ! $resource instanceof mysqli_stmt + ) { + throw new Exception\InvalidArgumentException('Invalid resource provided.'); + } + + if ($isBuffered !== null) { + $this->isBuffered = $isBuffered; + } else { + if ( + $resource instanceof mysqli || $resource instanceof mysqli_result + || $resource instanceof mysqli_stmt && $resource->num_rows !== 0 + ) { + $this->isBuffered = true; + } + } + + $this->resource = $resource; + $this->generatedValue = $generatedValue; + return $this; + } + + /** + * Force buffering + * + * @throws Exception\RuntimeException + */ + public function buffer() + { + if ($this->resource instanceof mysqli_stmt && $this->isBuffered !== true) { + if ($this->position > 0) { + throw new Exception\RuntimeException('Cannot buffer a result set that has started iteration.'); + } + $this->resource->store_result(); + $this->isBuffered = true; + } + } + + /** + * Check if is buffered + * + * @return bool|null + */ + public function isBuffered() + { + return $this->isBuffered; + } + + /** + * Return the resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Is query result? + * + * @return bool + */ + public function isQueryResult() + { + return $this->resource->field_count > 0; + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + if ($this->resource instanceof mysqli || $this->resource instanceof mysqli_stmt) { + return $this->resource->affected_rows; + } + + return $this->resource->num_rows; + } + + /** + * Current + * + * @return mixed + */ + #[ReturnTypeWillChange] + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + if ($this->resource instanceof mysqli_stmt) { + $this->loadDataFromMysqliStatement(); + return $this->currentData; + } else { + $this->loadFromMysqliResult(); + return $this->currentData; + } + } + + /** + * Mysqli's binding and returning of statement values + * + * Mysqli requires you to bind variables to the extension in order to + * get data out. These values have to be references: + * + * @see http://php.net/manual/en/mysqli-stmt.bind-result.php + * + * @throws Exception\RuntimeException + * @return bool + */ + protected function loadDataFromMysqliStatement() + { + // build the default reference based bind structure, if it does not already exist + if ($this->statementBindValues['keys'] === null) { + $this->statementBindValues['keys'] = []; + $resultResource = $this->resource->result_metadata(); + foreach ($resultResource->fetch_fields() as $col) { + $this->statementBindValues['keys'][] = $col->name; + } + $this->statementBindValues['values'] = array_fill(0, count($this->statementBindValues['keys']), null); + $refs = []; + foreach ($this->statementBindValues['values'] as $i => &$f) { + $refs[$i] = &$f; + } + call_user_func_array([$this->resource, 'bind_result'], $this->statementBindValues['values']); + } + + if (($r = $this->resource->fetch()) === null) { + if (! $this->isBuffered) { + $this->resource->close(); + } + return false; + } elseif ($r === false) { + throw new Exception\RuntimeException($this->resource->error); + } + + // dereference + for ($i = 0, $count = count($this->statementBindValues['keys']); $i < $count; $i++) { + $this->currentData[$this->statementBindValues['keys'][$i]] = $this->statementBindValues['values'][$i]; + } + $this->currentComplete = true; + $this->nextComplete = true; + $this->position++; + return true; + } + + /** + * Load from mysqli result + * + * @return bool + */ + protected function loadFromMysqliResult() + { + $this->currentData = null; + + if (($data = $this->resource->fetch_assoc()) === null) { + return false; + } + + $this->position++; + $this->currentData = $data; + $this->currentComplete = true; + $this->nextComplete = true; + $this->position++; + return true; + } + + /** + * Next + * + * @return void + */ + #[ReturnTypeWillChange] + public function next() + { + $this->currentComplete = false; + + if ($this->nextComplete === false) { + $this->position++; + } + + $this->nextComplete = false; + } + + /** + * Key + * + * @return mixed + */ + #[ReturnTypeWillChange] + public function key() + { + return $this->position; + } + + /** + * Rewind + * + * @throws Exception\RuntimeException + * @return void + */ + #[ReturnTypeWillChange] + public function rewind() + { + if (0 !== $this->position && false === $this->isBuffered) { + throw new Exception\RuntimeException('Unbuffered results cannot be rewound for multiple iterations'); + } + + $this->resource->data_seek(0); // works for both mysqli_result & mysqli_stmt + $this->currentComplete = false; + $this->position = 0; + } + + /** + * Valid + * + * @return bool + */ + #[ReturnTypeWillChange] + public function valid() + { + if ($this->currentComplete) { + return true; + } + + if ($this->resource instanceof mysqli_stmt) { + return $this->loadDataFromMysqliStatement(); + } + + return $this->loadFromMysqliResult(); + } + + /** + * Count + * + * @throws Exception\RuntimeException + * @return int + */ + #[ReturnTypeWillChange] + public function count() + { + if ($this->isBuffered === false) { + throw new Exception\RuntimeException('Row count is not available in unbuffered result sets.'); + } + return $this->resource->num_rows; + } + + /** + * Get field count + * + * @return int + */ + public function getFieldCount() + { + return $this->resource->field_count; + } + + /** + * Get generated value + * + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } +} diff --git a/src/Driver/Mysqli/Statement.php b/src/Driver/Mysqli/Statement.php new file mode 100644 index 0000000..59ae62a --- /dev/null +++ b/src/Driver/Mysqli/Statement.php @@ -0,0 +1,295 @@ +bufferResults = (bool) $bufferResults; + } + + /** + * Set driver + * + * @return $this Provides a fluent interface + */ + public function setDriver(Mysqli $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @return $this Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Initialize + * + * @return $this Provides a fluent interface + */ + public function initialize(\mysqli $mysqli) + { + $this->mysqli = $mysqli; + return $this; + } + + /** + * Set sql + * + * @param string $sql + * @return $this Provides a fluent interface + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Set Parameter container + * + * @return $this Provides a fluent interface + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Set resource + * + * @return $this Provides a fluent interface + */ + public function setResource(mysqli_stmt $mysqliStatement) + { + $this->resource = $mysqliStatement; + $this->isPrepared = true; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * Get parameter count + * + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * Is prepared + * + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * Prepare + * + * @param string $sql + * @return $this Provides a fluent interface + * @throws Exception\InvalidQueryException + * @throws Exception\RuntimeException + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has already been prepared'); + } + + $sql = $sql ?: $this->sql; + + $this->resource = $this->mysqli->prepare($sql); + if (! $this->resource instanceof mysqli_stmt) { + throw new Exception\InvalidQueryException( + 'Statement couldn\'t be produced with sql: ' . $sql, + $this->mysqli->errno, + new Exception\ErrorException($this->mysqli->error, $this->mysqli->errno) + ); + } + + $this->isPrepared = true; + return $this; + } + + /** + * Execute + * + * @param null|array|ParameterContainer $parameters + * @throws Exception\RuntimeException + * @return mixed + */ + public function execute($parameters = null) + { + if (! $this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (! $this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + $return = $this->resource->execute(); + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($return === false) { + throw new Exception\RuntimeException($this->resource->error); + } + + if ($this->bufferResults === true) { + $this->resource->store_result(); + $this->isPrepared = false; + $buffered = true; + } else { + $buffered = false; + } + + return $this->driver->createResult($this->resource, $buffered); + } + + /** + * Bind parameters from container + * + * @return void + */ + protected function bindParametersFromContainer() + { + $parameters = $this->parameterContainer->getNamedArray(); + $type = ''; + $args = []; + + foreach ($parameters as $name => &$value) { + if ($this->parameterContainer->offsetHasErrata($name)) { + switch ($this->parameterContainer->offsetGetErrata($name)) { + case ParameterContainer::TYPE_DOUBLE: + $type .= 'd'; + break; + case ParameterContainer::TYPE_NULL: + $value = null; // as per @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php#96148 + case ParameterContainer::TYPE_INTEGER: + $type .= 'i'; + break; + case ParameterContainer::TYPE_STRING: + default: + $type .= 's'; + break; + } + } else { + $type .= 's'; + } + $args[] = &$value; + } + + if ($args) { + array_unshift($args, $type); + call_user_func_array([$this->resource, 'bind_param'], $args); + } + } +} diff --git a/src/Platform/Mysql.php b/src/Platform/Mysql.php new file mode 100644 index 0000000..6b0bd54 --- /dev/null +++ b/src/Platform/Mysql.php @@ -0,0 +1,126 @@ +setDriver($driver); + } + } + + /** + * @param \Laminas\Db\Adapter\Driver\Mysqli\Mysqli|\Laminas\Db\Adapter\Driver\Pdo\Pdo|\mysqli|\PDO $driver + * @return $this Provides a fluent interface + * @throws InvalidArgumentException + */ + public function setDriver($driver) + { + // handle Laminas\Db drivers + if ( + $driver instanceof Mysqli\Mysqli + || ($driver instanceof Pdo\Pdo && $driver->getDatabasePlatformName() === 'Mysql') + || $driver instanceof \mysqli + || ($driver instanceof \PDO && $driver->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'mysql') + ) { + $this->driver = $driver; + return $this; + } + + throw new Exception\InvalidArgumentException( + '$driver must be a Mysqli or Mysql PDO Laminas\Db\Adapter\Driver, Mysqli instance or MySQL PDO instance' + ); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'MySQL'; + } + + /** + * {@inheritDoc} + */ + public function quoteIdentifierChain($identifierChain) + { + return '`' . implode('`.`', (array) str_replace('`', '``', $identifierChain)) . '`'; + } + + /** + * {@inheritDoc} + */ + public function quoteValue($value) + { + $quotedViaDriverValue = $this->quoteViaDriver($value); + + return $quotedViaDriverValue ?? parent::quoteValue($value); + } + + /** + * {@inheritDoc} + */ + public function quoteTrustedValue($value) + { + $quotedViaDriverValue = $this->quoteViaDriver($value); + + return $quotedViaDriverValue ?? parent::quoteTrustedValue($value); + } + + /** + * @param string $value + * @return string|null + */ + protected function quoteViaDriver($value) + { + if ($this->driver instanceof DriverInterface) { + $resource = $this->driver->getConnection()->getResource(); + } else { + $resource = $this->driver; + } + + if ($resource instanceof \mysqli) { + return '\'' . $resource->real_escape_string($value) . '\''; + } + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + + return null; + } +} diff --git a/src/Sql/Platform/Ddl/AlterTableDecorator.php b/src/Sql/Platform/Ddl/AlterTableDecorator.php new file mode 100644 index 0000000..b35843f --- /dev/null +++ b/src/Sql/Platform/Ddl/AlterTableDecorator.php @@ -0,0 +1,244 @@ + 0, + 'zerofill' => 1, + 'identity' => 2, + 'serial' => 2, + 'autoincrement' => 2, + 'comment' => 3, + 'columnformat' => 4, + 'format' => 4, + 'storage' => 5, + 'after' => 6, + ]; + + /** + * @param AlterTable $subject + * @return $this Provides a fluent interface + */ + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * @param string $sql + * @return array + */ + protected function getSqlInsertOffsets($sql) + { + $sqlLength = strlen($sql); + $insertStart = []; + + foreach (['NOT NULL', 'NULL', 'DEFAULT', 'UNIQUE', 'PRIMARY', 'REFERENCES'] as $needle) { + $insertPos = strpos($sql, ' ' . $needle); + + if ($insertPos !== false) { + switch ($needle) { + case 'REFERENCES': + $insertStart[2] = ! isset($insertStart[2]) ? $insertPos : $insertStart[2]; + // no break + case 'PRIMARY': + case 'UNIQUE': + $insertStart[1] = ! isset($insertStart[1]) ? $insertPos : $insertStart[1]; + // no break + default: + $insertStart[0] = ! isset($insertStart[0]) ? $insertPos : $insertStart[0]; + } + } + } + + foreach (range(0, 3) as $i) { + $insertStart[$i] = $insertStart[$i] ?? $sqlLength; + } + + return $insertStart; + } + + /** + * @return array + */ + protected function processAddColumns(?PlatformInterface $adapterPlatform = null) + { + $sqls = []; + + foreach ($this->addColumns as $i => $column) { + $sql = $this->processExpression($column, $adapterPlatform); + $insertStart = $this->getSqlInsertOffsets($sql); + $columnOptions = $column->getOptions(); + + uksort($columnOptions, [$this, 'compareColumnOptions']); + + foreach ($columnOptions as $coName => $coValue) { + $insert = ''; + + if (! $coValue) { + continue; + } + + switch ($this->normalizeColumnOption($coName)) { + case 'unsigned': + $insert = ' UNSIGNED'; + $j = 0; + break; + case 'zerofill': + $insert = ' ZEROFILL'; + $j = 0; + break; + case 'identity': + case 'serial': + case 'autoincrement': + $insert = ' AUTO_INCREMENT'; + $j = 1; + break; + case 'comment': + $insert = ' COMMENT ' . $adapterPlatform->quoteValue($coValue); + $j = 2; + break; + case 'columnformat': + case 'format': + $insert = ' COLUMN_FORMAT ' . strtoupper($coValue); + $j = 2; + break; + case 'storage': + $insert = ' STORAGE ' . strtoupper($coValue); + $j = 2; + break; + case 'after': + $insert = ' AFTER ' . $adapterPlatform->quoteIdentifier($coValue); + $j = 2; + } + + if ($insert) { + $j = $j ?? 0; + $sql = substr_replace($sql, $insert, $insertStart[$j], 0); + $insertStartCount = count($insertStart); + for (; $j < $insertStartCount; ++$j) { + $insertStart[$j] += strlen($insert); + } + } + } + $sqls[$i] = $sql; + } + return [$sqls]; + } + + /** + * @return array + */ + protected function processChangeColumns(?PlatformInterface $adapterPlatform = null) + { + $sqls = []; + foreach ($this->changeColumns as $name => $column) { + $sql = $this->processExpression($column, $adapterPlatform); + $insertStart = $this->getSqlInsertOffsets($sql); + $columnOptions = $column->getOptions(); + + uksort($columnOptions, [$this, 'compareColumnOptions']); + + foreach ($columnOptions as $coName => $coValue) { + $insert = ''; + + if (! $coValue) { + continue; + } + + switch ($this->normalizeColumnOption($coName)) { + case 'unsigned': + $insert = ' UNSIGNED'; + $j = 0; + break; + case 'zerofill': + $insert = ' ZEROFILL'; + $j = 0; + break; + case 'identity': + case 'serial': + case 'autoincrement': + $insert = ' AUTO_INCREMENT'; + $j = 1; + break; + case 'comment': + $insert = ' COMMENT ' . $adapterPlatform->quoteValue($coValue); + $j = 2; + break; + case 'columnformat': + case 'format': + $insert = ' COLUMN_FORMAT ' . strtoupper($coValue); + $j = 2; + break; + case 'storage': + $insert = ' STORAGE ' . strtoupper($coValue); + $j = 2; + break; + } + + if ($insert) { + $j = $j ?? 0; + $sql = substr_replace($sql, $insert, $insertStart[$j], 0); + $insertStartCount = count($insertStart); + for (; $j < $insertStartCount; ++$j) { + $insertStart[$j] += strlen($insert); + } + } + } + $sqls[] = [ + $adapterPlatform->quoteIdentifier($name), + $sql, + ]; + } + + return [$sqls]; + } + + /** + * @param string $name + * @return string + */ + private function normalizeColumnOption($name) + { + return strtolower(str_replace(['-', '_', ' '], '', $name)); + } + + /** + * @param string $columnA + * @param string $columnB + * @return int + */ + // phpcs:ignore SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod + private function compareColumnOptions($columnA, $columnB) + { + $columnA = $this->normalizeColumnOption($columnA); + $columnA = $this->columnOptionSortOrder[$columnA] ?? count($this->columnOptionSortOrder); + + $columnB = $this->normalizeColumnOption($columnB); + $columnB = $this->columnOptionSortOrder[$columnB] ?? count($this->columnOptionSortOrder); + + return $columnA - $columnB; + } +} diff --git a/src/Sql/Platform/Ddl/CreateTableDecorator.php b/src/Sql/Platform/Ddl/CreateTableDecorator.php new file mode 100644 index 0000000..177a544 --- /dev/null +++ b/src/Sql/Platform/Ddl/CreateTableDecorator.php @@ -0,0 +1,178 @@ + 0, + 'zerofill' => 1, + 'identity' => 2, + 'serial' => 2, + 'autoincrement' => 2, + 'comment' => 3, + 'columnformat' => 4, + 'format' => 4, + 'storage' => 5, + ]; + + /** + * @param CreateTable $subject + * @return $this Provides a fluent interface + */ + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * @param string $sql + * @return array + */ + protected function getSqlInsertOffsets($sql) + { + $sqlLength = strlen($sql); + $insertStart = []; + + foreach (['NOT NULL', 'NULL', 'DEFAULT', 'UNIQUE', 'PRIMARY', 'REFERENCES'] as $needle) { + $insertPos = strpos($sql, ' ' . $needle); + + if ($insertPos !== false) { + switch ($needle) { + case 'REFERENCES': + $insertStart[2] = ! isset($insertStart[2]) ? $insertPos : $insertStart[2]; + // no break + case 'PRIMARY': + case 'UNIQUE': + $insertStart[1] = ! isset($insertStart[1]) ? $insertPos : $insertStart[1]; + // no break + default: + $insertStart[0] = ! isset($insertStart[0]) ? $insertPos : $insertStart[0]; + } + } + } + + foreach (range(0, 3) as $i) { + $insertStart[$i] = $insertStart[$i] ?? $sqlLength; + } + + return $insertStart; + } + + /** + * {@inheritDoc} + */ + protected function processColumns(?PlatformInterface $platform = null) + { + if (! $this->columns) { + return; + } + + $sqls = []; + + foreach ($this->columns as $i => $column) { + $sql = $this->processExpression($column, $platform); + $insertStart = $this->getSqlInsertOffsets($sql); + $columnOptions = $column->getOptions(); + + uksort($columnOptions, [$this, 'compareColumnOptions']); + + foreach ($columnOptions as $coName => $coValue) { + $insert = ''; + + if (! $coValue) { + continue; + } + + switch ($this->normalizeColumnOption($coName)) { + case 'unsigned': + $insert = ' UNSIGNED'; + $j = 0; + break; + case 'zerofill': + $insert = ' ZEROFILL'; + $j = 0; + break; + case 'identity': + case 'serial': + case 'autoincrement': + $insert = ' AUTO_INCREMENT'; + $j = 1; + break; + case 'comment': + $insert = ' COMMENT ' . $platform->quoteValue($coValue); + $j = 2; + break; + case 'columnformat': + case 'format': + $insert = ' COLUMN_FORMAT ' . strtoupper($coValue); + $j = 2; + break; + case 'storage': + $insert = ' STORAGE ' . strtoupper($coValue); + $j = 2; + break; + } + + if ($insert) { + $j = $j ?? 0; + $sql = substr_replace($sql, $insert, $insertStart[$j], 0); + $insertStartCount = count($insertStart); + for (; $j < $insertStartCount; ++$j) { + $insertStart[$j] += strlen($insert); + } + } + } + + $sqls[$i] = $sql; + } + + return [$sqls]; + } + + /** + * @param string $name + * @return string + */ + private function normalizeColumnOption($name) + { + return strtolower(str_replace(['-', '_', ' '], '', $name)); + } + + /** + * @param string $columnA + * @param string $columnB + * @return int + */ + // phpcs:ignore SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod + private function compareColumnOptions($columnA, $columnB) + { + $columnA = $this->normalizeColumnOption($columnA); + $columnA = $this->columnOptionSortOrder[$columnA] ?? count($this->columnOptionSortOrder); + + $columnB = $this->normalizeColumnOption($columnB); + $columnB = $this->columnOptionSortOrder[$columnB] ?? count($this->columnOptionSortOrder); + + return $columnA - $columnB; + } +} diff --git a/src/Sql/Platform/Mysql.php b/src/Sql/Platform/Mysql.php new file mode 100644 index 0000000..dbba270 --- /dev/null +++ b/src/Sql/Platform/Mysql.php @@ -0,0 +1,18 @@ +setTypeDecorator(Select::class, new SelectDecorator()); + $this->setTypeDecorator(CreateTable::class, new Ddl\CreateTableDecorator()); + $this->setTypeDecorator(AlterTable::class, new Ddl\AlterTableDecorator()); + } +} diff --git a/src/Sql/Platform/SelectDecorator.php b/src/Sql/Platform/SelectDecorator.php new file mode 100644 index 0000000..bb1ee8f --- /dev/null +++ b/src/Sql/Platform/SelectDecorator.php @@ -0,0 +1,69 @@ +subject = $select; + } + + protected function localizeVariables() + { + parent::localizeVariables(); + if ($this->limit === null && $this->offset !== null) { + $this->specifications[self::LIMIT] = 'LIMIT 18446744073709551615'; + } + } + + /** @return null|string[] */ + protected function processLimit( + PlatformInterface $platform, + ?DriverInterface $driver = null, + ?ParameterContainer $parameterContainer = null + ) { + if ($this->limit === null && $this->offset !== null) { + return ['']; + } + if ($this->limit === null) { + return null; + } + if ($parameterContainer) { + $paramPrefix = $this->processInfo['paramPrefix']; + $parameterContainer->offsetSet($paramPrefix . 'limit', $this->limit, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName($paramPrefix . 'limit')]; + } + + return [$this->limit]; + } + + protected function processOffset( + PlatformInterface $platform, + ?DriverInterface $driver = null, + ?ParameterContainer $parameterContainer = null + ) { + if ($this->offset === null) { + return; + } + if ($parameterContainer) { + $paramPrefix = $this->processInfo['paramPrefix']; + $parameterContainer->offsetSet($paramPrefix . 'offset', $this->offset, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName($paramPrefix . 'offset')]; + } + + return [$this->offset]; + } +}