diff --git a/composer.json b/composer.json index 355e33c..e649f4c 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "axleus/laminas-db-mysql", + "name": "laminas/laminas-db-mysql-adapter", "description": "MySQL support for laminas-db", "license": "BSD-3-Clause", "keywords": [ @@ -32,7 +32,7 @@ }, "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "axleus/laminas-db": "dev-3.0.x" + "laminas/laminas-db": "3.0.x" }, "require-dev": { "ext-mysqli": "*", diff --git a/src/Adapter.php b/src/Adapter.php index eae4eba..f4c7957 100644 --- a/src/Adapter.php +++ b/src/Adapter.php @@ -1,56 +1,28 @@ 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(); @@ -96,339 +57,4 @@ public function __construct( $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/AdapterServiceFactory.php b/src/AdapterServiceFactory.php new file mode 100644 index 0000000..963b85f --- /dev/null +++ b/src/AdapterServiceFactory.php @@ -0,0 +1,33 @@ +get('config'); + + return new Adapter( + $config['db'], + $container->get(DriverInterface::class), + $container->get(PlatformInterface::class), + $container->has(ResultSetInterface::class) ? $container->get(ResultSetInterface::class) : null, + $container->has(ProfilerInterface::class) ? $container->get(ProfilerInterface::class) : null + ); + } +} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 7121adc..8c3d700 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -2,6 +2,12 @@ namespace Laminas\Db\Mysql; +use Laminas\Db\Adapter\AbstractAdapterServiceFactory; +use Laminas\Db\Adapter\AdapterInterface; +use Laminas\Db\Adapter\Driver\DriverInterface; +use Laminas\Db\Adapter\Platform\PlatformInterface; +use Laminas\ServiceManager\Factory\InvokableFactory; + readonly class ConfigProvider { public function __invoke(): array @@ -14,8 +20,14 @@ public function __invoke(): array public function getDependencies(): array { return [ + 'aliases' => [ + PlatformInterface::class => Platform\Mysql::class, + ], 'factories' => [ - Adapter::class => AdapterServiceFactory::class, + AdapterInterface::class => AdapterServiceFactory::class, + //DriverInterface::class => Driver\Mysqli\DriverFactory::class, + DriverInterface::class => Driver\Pdo\DriverFactory::class, + Platform\Mysql::class => InvokableFactory::class, ], ]; } diff --git a/src/Driver/DatabasePlatformNameTrait.php b/src/Driver/DatabasePlatformNameTrait.php new file mode 100644 index 0000000..5acb485 --- /dev/null +++ b/src/Driver/DatabasePlatformNameTrait.php @@ -0,0 +1,23 @@ +driver = $driver; diff --git a/src/Driver/Mysqli/Mysqli.php b/src/Driver/Mysqli/Driver.php similarity index 90% rename from src/Driver/Mysqli/Mysqli.php rename to src/Driver/Mysqli/Driver.php index 7cddb5d..7d3c8de 100644 --- a/src/Driver/Mysqli/Mysqli.php +++ b/src/Driver/Mysqli/Driver.php @@ -1,10 +1,11 @@ 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 * @@ -174,9 +162,9 @@ public function createStatement($sqlOrResource = null) { /** * @todo Resource tracking - if (is_resource($sqlOrResource) && !in_array($sqlOrResource, $this->resources, true)) { - $this->resources[] = $sqlOrResource; - } + * if (is_resource($sqlOrResource) && !in_array($sqlOrResource, $this->resources, true)) { + * $this->resources[] = $sqlOrResource; + * } */ $statement = clone $this->statementPrototype; diff --git a/src/Driver/Mysqli/DriverFactory.php b/src/Driver/Mysqli/DriverFactory.php new file mode 100644 index 0000000..e3adb9d --- /dev/null +++ b/src/Driver/Mysqli/DriverFactory.php @@ -0,0 +1,24 @@ +get('config')['db']; + $options = []; + if (isset($dbConfig['options'])) { + $options = (array) $dbConfig['options']; + unset($dbConfig['options']); + } + return new Driver($dbConfig, null, null, $options); + } +} diff --git a/src/Driver/Mysqli/Result.php b/src/Driver/Mysqli/Result.php index 3fcd617..19c513c 100644 --- a/src/Driver/Mysqli/Result.php +++ b/src/Driver/Mysqli/Result.php @@ -1,6 +1,6 @@ setConnectionParameters($connectionParameters); + } elseif ($connectionParameters instanceof \PDO) { + $this->setResource($connectionParameters); + } elseif (null !== $connectionParameters) { + throw new Exception\InvalidArgumentException( + '$connection must be an array of parameters, a PDO object or null' + ); + } + } + + /** + * Set driver + * + * @return $this Provides a fluent interface + */ + public function setDriver(DriverInterface $driver) + { + $this->driver = $driver; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function setConnectionParameters(array $connectionParameters) + { + $this->connectionParameters = $connectionParameters; + if (isset($connectionParameters['dsn'])) { + $this->driverName = substr( + $connectionParameters['dsn'], + 0, + strpos($connectionParameters['dsn'], ':') + ); + } elseif (isset($connectionParameters['pdodriver'])) { + $this->driverName = strtolower($connectionParameters['pdodriver']); + } elseif (isset($connectionParameters['driver'])) { + $this->driverName = strtolower(substr( + str_replace(['-', '_', ' '], '', $connectionParameters['driver']), + 3 + )); + } + } + + /** + * Get the dsn string for this connection + * + * @throws RunTimeException + * @return string + */ + public function getDsn() + { + if (! $this->dsn) { + throw new Exception\RuntimeException( + 'The DSN has not been set or constructed from parameters in connect() for this Connection' + ); + } + + return $this->dsn; + } + + /** + * {@inheritDoc} + */ + public function getCurrentSchema() + { + if (! $this->isConnected()) { + $this->connect(); + } + + // switch ($this->driverName) { + // case 'mysql': + // $sql = 'SELECT DATABASE()'; + // break; + // case 'sqlite': + // return 'main'; + // case 'sqlsrv': + // case 'dblib': + // $sql = 'SELECT SCHEMA_NAME()'; + // break; + // case 'pgsql': + // default: + // $sql = 'SELECT CURRENT_SCHEMA'; + // break; + // } + + /** @var PDOStatement $result */ + $result = $this->resource->query('SELECT DATABASE()'); + if ($result instanceof PDOStatement) { + return $result->fetchColumn(); + } + + return false; + } + + /** + * Set resource + * + * @return $this Provides a fluent interface + */ + public function setResource(\PDO $resource) + { + $this->resource = $resource; + $this->driverName = strtolower($this->resource->getAttribute(\PDO::ATTR_DRIVER_NAME)); + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\InvalidConnectionParametersException + * @throws Exception\RuntimeException + */ + public function connect() + { + if ($this->resource) { + return $this; + } + + $dsn = $username = $password = $hostname = $database = null; + $options = []; + foreach ($this->connectionParameters as $key => $value) { + switch (strtolower($key)) { + case 'dsn': + $dsn = $value; + break; + case 'driver': + $value = strtolower((string) $value); + if (strpos($value, 'pdo') === 0) { + $pdoDriver = str_replace(['-', '_', ' '], '', $value); + $pdoDriver = substr($pdoDriver, 3) ?: ''; + } + break; + case 'pdodriver': + $pdoDriver = (string) $value; + break; + case 'user': + case 'username': + $username = (string) $value; + break; + case 'pass': + case 'password': + $password = (string) $value; + break; + case 'host': + case 'hostname': + $hostname = (string) $value; + break; + case 'port': + $port = (int) $value; + break; + case 'database': + case 'dbname': + $database = (string) $value; + break; + case 'charset': + $charset = (string) $value; + break; + case 'unix_socket': + $unixSocket = (string) $value; + break; + case 'version': + $version = (string) $value; + break; + case 'driver_options': + case 'options': + $value = (array) $value; + $options = array_diff_key($options, $value) + $value; + break; + default: + $options[$key] = $value; + break; + } + } + + if (isset($hostname) && isset($unixSocket)) { + throw new Exception\InvalidConnectionParametersException( + 'Ambiguous connection parameters, both hostname and unix_socket parameters were set', + $this->connectionParameters + ); + } + + if (! isset($dsn) && isset($pdoDriver)) { + $dsn = []; + switch ($pdoDriver) { + case 'sqlite': + $dsn[] = $database; + break; + case 'sqlsrv': + if (isset($database)) { + $dsn[] = "database={$database}"; + } + if (isset($hostname)) { + $dsn[] = "server={$hostname}"; + } + break; + default: + if (isset($database)) { + $dsn[] = "dbname={$database}"; + } + if (isset($hostname)) { + $dsn[] = "host={$hostname}"; + } + if (isset($port)) { + $dsn[] = "port={$port}"; + } + if (isset($charset) && $pdoDriver !== 'pgsql') { + $dsn[] = "charset={$charset}"; + } + if (isset($unixSocket)) { + $dsn[] = "unix_socket={$unixSocket}"; + } + if (isset($version)) { + $dsn[] = "version={$version}"; + } + break; + } + $dsn = $pdoDriver . ':' . implode(';', $dsn); + } elseif (! isset($dsn)) { + throw new Exception\InvalidConnectionParametersException( + 'A dsn was not provided or could not be constructed from your parameters', + $this->connectionParameters + ); + } + + $this->dsn = $dsn; + + try { + $this->resource = new \PDO($dsn, $username, $password, $options); + $this->resource->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + if (isset($charset) && $pdoDriver === 'pgsql') { + $this->resource->exec('SET NAMES ' . $this->resource->quote($charset)); + } + $this->driverName = strtolower($this->resource->getAttribute(\PDO::ATTR_DRIVER_NAME)); + } catch (PDOException $e) { + $code = $e->getCode(); + if (! is_int($code)) { + $code = 0; + } + throw new Exception\RuntimeException('Connect Error: ' . $e->getMessage(), $code, $e); + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isConnected() + { + return $this->resource instanceof \PDO; + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + if (! $this->isConnected()) { + $this->connect(); + } + + if (0 === $this->nestedTransactionsCount) { + $this->resource->beginTransaction(); + $this->inTransaction = true; + } + + $this->nestedTransactionsCount++; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if (! $this->isConnected()) { + $this->connect(); + } + + if ($this->inTransaction) { + $this->nestedTransactionsCount -= 1; + } + + /* + * This shouldn't check for being in a transaction since + * after issuing a SET autocommit=0; we have to commit too. + */ + if (0 === $this->nestedTransactionsCount) { + $this->resource->commit(); + $this->inTransaction = false; + } + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + 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->inTransaction = false; + $this->nestedTransactionsCount = 0; + + 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 ($resultResource === false) { + $errorInfo = $this->resource->errorInfo(); + throw new Exception\InvalidQueryException($errorInfo[2]); + } + + return $this->driver->createResult($resultResource, $sql); + } + + /** + * Prepare + * + * @param string $sql + * @return Statement + */ + public function prepare($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + return $this->driver->createStatement($sql); + } + + /** + * {@inheritDoc} + * + * @param string $name + * @return string|null|false + */ + public function getLastGeneratedValue($name = null) + { + if ( + $name === null + && ($this->driverName === 'pgsql' || $this->driverName === 'firebird') + ) { + return; + } + + try { + return $this->resource->lastInsertId($name); + } catch (\Exception $e) { + // do nothing + } + + return false; + } +} diff --git a/src/Driver/Pdo/Driver.php b/src/Driver/Pdo/Driver.php new file mode 100644 index 0000000..2236e0e --- /dev/null +++ b/src/Driver/Pdo/Driver.php @@ -0,0 +1,302 @@ +registerConnection($connection); + $this->registerStatementPrototype($statementPrototype ?: new Statement()); + $this->registerResultPrototype($resultPrototype ?: new Result()); + if (is_array($features)) { + foreach ($features as $name => $feature) { + $this->addFeature($name, $feature); + } + } elseif ($features instanceof AbstractFeature) { + $this->addFeature($features->getName(), $features); + } elseif ($features === self::FEATURES_DEFAULT) { + $this->setupDefaultFeatures(); + } + } + + /** + * @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); + return $this; + } + + /** + * Register statement prototype + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); + } + + /** + * Register result prototype + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + } + + /** + * Add feature + * + * @param string $name + * @param AbstractFeature $feature + * @return $this Provides a fluent interface + */ + public function addFeature($name, $feature) + { + if ($feature instanceof AbstractFeature) { + $name = $feature->getName(); // overwrite the name, just in case + $feature->setDriver($this); + } + $this->features[$name] = $feature; + return $this; + } + + /** + * Setup the default features for Pdo + * + * @return $this Provides a fluent interface + */ + public function setupDefaultFeatures() + { + $driverName = $this->connection->getDriverName(); + if ($driverName === 'sqlite') { + $this->addFeature(null, new Feature\SqliteRowCounter()); + return $this; + } + + if ($driverName === 'oci') { + $this->addFeature(null, new Feature\OracleRowCounter()); + return $this; + } + + return $this; + } + + /** + * Get feature + * + * @param string $name + * @return AbstractFeature|false + */ + public function getFeature($name) + { + if (isset($this->features[$name])) { + return $this->features[$name]; + } + return false; + } + + /** + * Check environment + */ + public function checkEnvironment() + { + if (! extension_loaded('PDO')) { + throw new Exception\RuntimeException( + 'The PDO extension is required for this adapter but the extension is not loaded' + ); + } + } + + /** + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * @param string|PDOStatement $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + if ($sqlOrResource instanceof PDOStatement) { + $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; + } + + /** + * @param resource $resource + * @param mixed $context + * @return Result + */ + public function createResult($resource, $context = null) + { + $result = clone $this->resultPrototype; + $rowCount = null; + + // special feature, sqlite PDO counter + // if ( + // $this->connection->getDriverName() === 'sqlite' + // && ($sqliteRowCounter = $this->getFeature('SqliteRowCounter')) + // && $resource->columnCount() > 0 + // ) { + // $rowCount = $sqliteRowCounter->getRowCountClosure($context); + // } + + // special feature, oracle PDO counter + // if ( + // $this->connection->getDriverName() === 'oci' + // && ($oracleRowCounter = $this->getFeature('OracleRowCounter')) + // && $resource->columnCount() > 0 + // ) { + // $rowCount = $oracleRowCounter->getRowCountClosure($context); + // } + + $result->initialize($resource, $this->connection->getLastGeneratedValue(), $rowCount); + return $result; + } + + /** + * @return Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * @return string + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_NAMED; + } + + /** + * @param string $name + * @param string|null $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + if ($type === null && ! is_numeric($name) || $type === self::PARAMETERIZATION_NAMED) { + $name = ltrim($name, ':'); + // @see https://bugs.php.net/bug.php?id=43130 + if (preg_match('/[^a-zA-Z0-9_]/', $name)) { + throw new Exception\RuntimeException(sprintf( + 'The PDO param %s contains invalid characters.' + . ' Only alphabetic characters, digits, and underscores (_)' + . ' are allowed.', + $name + )); + } + return ':' . $name; + } + + return '?'; + } + + /** + * @param string|null $name + * @return string|null|false + */ + public function getLastGeneratedValue($name = null) + { + return $this->connection->getLastGeneratedValue($name); + } +} diff --git a/src/Driver/Pdo/DriverFactory.php b/src/Driver/Pdo/DriverFactory.php new file mode 100644 index 0000000..d15903b --- /dev/null +++ b/src/Driver/Pdo/DriverFactory.php @@ -0,0 +1,17 @@ +get('config')['db']); + } +} diff --git a/src/Driver/Pdo/Result.php b/src/Driver/Pdo/Result.php new file mode 100644 index 0000000..04b53c4 --- /dev/null +++ b/src/Driver/Pdo/Result.php @@ -0,0 +1,276 @@ +resource = $resource; + $this->generatedValue = $generatedValue; + $this->rowCount = $rowCount; + + return $this; + } + + /** + * @return void + */ + public function buffer() + { + } + + /** + * @return bool|null + */ + public function isBuffered() + { + return false; + } + + /** + * @param int $fetchMode + * @throws Exception\InvalidArgumentException On invalid fetch mode. + */ + public function setFetchMode($fetchMode) + { + if (! in_array($fetchMode, self::VALID_FETCH_MODES, true)) { + throw new Exception\InvalidArgumentException( + 'The fetch mode must be one of the PDO::FETCH_* constants.' + ); + } + + $this->fetchMode = (int) $fetchMode; + } + + /** + * @return int + */ + public function getFetchMode() + { + return $this->fetchMode; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Get the data + * + * @return mixed + */ + #[ReturnTypeWillChange] + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + $this->currentData = $this->resource->fetch($this->fetchMode); + $this->currentComplete = true; + return $this->currentData; + } + + /** + * Next + * + * @return mixed + */ + #[ReturnTypeWillChange] + public function next() + { + $this->currentData = $this->resource->fetch($this->fetchMode); + $this->currentComplete = true; + $this->position++; + return $this->currentData; + } + + /** + * Key + * + * @return mixed + */ + #[ReturnTypeWillChange] + public function key() + { + return $this->position; + } + + /** + * @throws Exception\RuntimeException + * @return void + */ + #[ReturnTypeWillChange] + public function rewind() + { + if ($this->statementMode === self::STATEMENT_MODE_FORWARD && $this->position > 0) { + throw new Exception\RuntimeException( + 'This result is a forward only result set, calling rewind() after moving forward is not supported' + ); + } + if (! $this->currentComplete) { + $this->currentData = $this->resource->fetch($this->fetchMode); + $this->currentComplete = true; + } + $this->position = 0; + } + + /** + * Valid + * + * @return bool + */ + #[ReturnTypeWillChange] + public function valid() + { + return $this->currentData !== false; + } + + /** + * Count + * + * @return int + */ + #[ReturnTypeWillChange] + public function count() + { + if (is_int($this->rowCount)) { + return $this->rowCount; + } + if ($this->rowCount instanceof Closure) { + $this->rowCount = (int) call_user_func($this->rowCount); + } else { + $this->rowCount = (int) $this->resource->rowCount(); + } + return $this->rowCount; + } + + /** + * @return int + */ + public function getFieldCount() + { + return $this->resource->columnCount(); + } + + /** + * Is query result + * + * @return bool + */ + public function isQueryResult() + { + return $this->resource->columnCount() > 0; + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + return $this->resource->rowCount(); + } + + /** + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } +} diff --git a/src/Driver/Pdo/Statement.php b/src/Driver/Pdo/Statement.php new file mode 100644 index 0000000..50968f0 --- /dev/null +++ b/src/Driver/Pdo/Statement.php @@ -0,0 +1,290 @@ +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(\PDO $connectionResource) + { + $this->pdo = $connectionResource; + return $this; + } + + /** + * Set resource + * + * @return $this Provides a fluent interface + */ + public function setResource(PDOStatement $pdoStatement) + { + $this->resource = $pdoStatement; + return $this; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Set sql + * + * @param string $sql + * @return $this Provides a fluent interface + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @return $this Provides a fluent interface + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * @param string $sql + * @throws Exception\RuntimeException + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has been prepared already'); + } + + if ($sql === null) { + $sql = $this->sql; + } + + $this->resource = $this->pdo->prepare($sql); + + if ($this->resource === false) { + $error = $this->pdo->errorInfo(); + throw new Exception\RuntimeException($error[2]); + } + + $this->isPrepared = true; + } + + /** + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * @param null|array|ParameterContainer $parameters + * @throws Exception\InvalidQueryException + * @return Result + */ + 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); + } + + try { + $this->resource->execute(); + } catch (PDOException $e) { + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + $code = $e->getCode(); + if (! is_int($code)) { + $code = 0; + } + + throw new Exception\InvalidQueryException( + 'Statement could not be executed (' . implode(' - ', $this->resource->errorInfo()) . ')', + $code, + $e + ); + } + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + return $this->driver->createResult($this->resource, $this); + } + + /** + * Bind parameters from container + */ + protected function bindParametersFromContainer() + { + if ($this->parametersBound) { + return; + } + + $parameters = $this->parameterContainer->getNamedArray(); + foreach ($parameters as $name => &$value) { + if (is_bool($value)) { + $type = \PDO::PARAM_BOOL; + } elseif (is_int($value)) { + $type = \PDO::PARAM_INT; + } else { + $type = \PDO::PARAM_STR; + } + if ($this->parameterContainer->offsetHasErrata($name)) { + switch ($this->parameterContainer->offsetGetErrata($name)) { + case ParameterContainer::TYPE_INTEGER: + $type = \PDO::PARAM_INT; + break; + case ParameterContainer::TYPE_NULL: + $type = \PDO::PARAM_NULL; + break; + case ParameterContainer::TYPE_LOB: + $type = \PDO::PARAM_LOB; + break; + } + } + + // parameter is named or positional, value is reference + $parameter = is_int($name) ? $name + 1 : $this->driver->formatParameterName($name); + $this->resource->bindParam($parameter, $value, $type); + } + } + + /** + * Perform a deep clone + * + * @return void + */ + public function __clone() + { + $this->isPrepared = false; + $this->parametersBound = false; + $this->resource = null; + if ($this->parameterContainer) { + $this->parameterContainer = clone $this->parameterContainer; + } + } +} diff --git a/src/Module.php b/src/Module.php new file mode 100644 index 0000000..57b4f71 --- /dev/null +++ b/src/Module.php @@ -0,0 +1,15 @@ + (new ConfigProvider())->getDependencyConfig(), + ]; + } +} diff --git a/src/Platform/Mysql.php b/src/Platform/Mysql.php index 6b0bd54..6967a50 100644 --- a/src/Platform/Mysql.php +++ b/src/Platform/Mysql.php @@ -1,12 +1,13 @@ setDriver($driver); @@ -67,6 +65,7 @@ public function setDriver($driver) } /** + * todo: if needed return Backed Enum * {@inheritDoc} */ public function getName() diff --git a/src/Platform/PlatformFactory.php b/src/Platform/PlatformFactory.php new file mode 100644 index 0000000..9606bc6 --- /dev/null +++ b/src/Platform/PlatformFactory.php @@ -0,0 +1,17 @@ +subject = $select; + $this->subject = $subject; } protected function localizeVariables() @@ -34,7 +34,7 @@ protected function processLimit( PlatformInterface $platform, ?DriverInterface $driver = null, ?ParameterContainer $parameterContainer = null - ) { + ): ?array { if ($this->limit === null && $this->offset !== null) { return ['']; } @@ -54,9 +54,9 @@ protected function processOffset( PlatformInterface $platform, ?DriverInterface $driver = null, ?ParameterContainer $parameterContainer = null - ) { + ): ?array { if ($this->offset === null) { - return; + return null; } if ($parameterContainer) { $paramPrefix = $this->processInfo['paramPrefix'];