first commit
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
/**
|
||||
* @package akeebabackup
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('_JEXEC') || die();
|
||||
|
||||
use Exception;
|
||||
use Joomla\Database\DatabaseDriver;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
use Joomla\Database\Mysql\MysqlDriver;
|
||||
use Joomla\Database\Mysqli\MysqliDriver;
|
||||
use Joomla\Database\Pdo\PdoDriver;
|
||||
use Joomla\Database\Pgsql\PgsqlDriver;
|
||||
use Joomla\Database\Sqlazure\SqlazureDriver;
|
||||
use Joomla\Database\Sqlite\SqliteDriver;
|
||||
use Joomla\Database\Sqlsrv\SqlsrvDriver;
|
||||
use ReflectionObject;
|
||||
use RuntimeException;
|
||||
|
||||
class Joomla
|
||||
{
|
||||
/** @var Base The real database connection object */
|
||||
private $dbo;
|
||||
|
||||
/**
|
||||
* Database object constructor
|
||||
*
|
||||
* @param array $options List of options used to configure the connection
|
||||
*/
|
||||
public function __construct($options = [])
|
||||
{
|
||||
// Get the database driver *AND* make sure it's connected.
|
||||
/** @var DatabaseInterface|null $db */
|
||||
$db = \Akeeba\Engine\Platform\Joomla::getDbDriver();
|
||||
|
||||
if (empty($db))
|
||||
{
|
||||
throw new RuntimeException("Joomla does not return a database driver.");
|
||||
}
|
||||
|
||||
$db->connect();
|
||||
|
||||
$options['connection'] = $db->getConnection();
|
||||
|
||||
$driver = $this->getDriverType($db);
|
||||
|
||||
if (empty($driver))
|
||||
{
|
||||
throw new RuntimeException("Unsupported database driver {$db->getName()}");
|
||||
}
|
||||
|
||||
$driver = '\\Akeeba\\Engine\\Driver\\' . ucfirst($driver);
|
||||
$this->dbo = new $driver($options);
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
/**
|
||||
* We should not, in fact, try to close the connection by calling the parent method.
|
||||
*
|
||||
* If you close the connection we ask PHP's mysql / mysqli / pdomysql driver to disconnect the MySQL connection
|
||||
* resource from the database server inside our instance of Akeeba Engine's database driver. However, this
|
||||
* identical resource is also present in Joomla's database driver. Joomla will also try to close the connection
|
||||
* to a now invalid resource, causing a PHP notice to be recorded.
|
||||
*
|
||||
* By setting the connection resource to null in our own driver object we prevent closing the resource,
|
||||
* delegating that responsibility to Joomla. It will gladly do so at the very least automatically, through its
|
||||
* db driver's __destruct.
|
||||
*/
|
||||
$this->dbo->setConnection(null);
|
||||
}
|
||||
|
||||
public function open()
|
||||
{
|
||||
if (method_exists($this->dbo, 'open'))
|
||||
{
|
||||
$this->dbo->open();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (method_exists($this->dbo, 'connect'))
|
||||
{
|
||||
$this->dbo->connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to proxy all calls to the loaded database driver object
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __call($name, array $arguments)
|
||||
{
|
||||
if (is_null($this->dbo))
|
||||
{
|
||||
throw new Exception('Akeeba Engine database driver is not loaded');
|
||||
}
|
||||
|
||||
if (method_exists($this->dbo, $name) || in_array($name, ['q', 'nq', 'qn']))
|
||||
{
|
||||
return $this->dbo->{$name}(...$arguments);
|
||||
}
|
||||
|
||||
throw new Exception('Method ' . $name . ' not found in Akeeba Platform');
|
||||
}
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
if (isset($this->dbo->$name) || property_exists($this->dbo, $name))
|
||||
{
|
||||
return $this->dbo->$name;
|
||||
}
|
||||
|
||||
$this->dbo->$name = null;
|
||||
|
||||
user_error('Database driver does not support property ' . $name);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
if (isset($this->dbo->name) || property_exists($this->dbo, $name))
|
||||
{
|
||||
$this->dbo->$name = $value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dbo->$name = null;
|
||||
user_error('Database driver not support property ' . $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Akeeba Engine database driver type for the Joomla database object.
|
||||
*
|
||||
* Weak typing of the argument is deliberate. The class hierarchy of the database driver classes may change even
|
||||
* within the same major version of Joomla, as happened in the past with Joomla 3. Having weak typing we can amend
|
||||
* this method to straddle the change, i.e. make it compatible with Joomla versions before and after the change. In
|
||||
* simple terms, it's future–proofing.
|
||||
*
|
||||
* @param DatabaseInterface|DatabaseDriver $db
|
||||
*
|
||||
* @return string|null The driver type; null if unsupported
|
||||
*/
|
||||
private function getDriverType($db): ?string
|
||||
{
|
||||
// Make sure we got an object
|
||||
if (!is_object($db))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the Joomla database driver name — assuming the object passed is a DatabaseInterface instance
|
||||
if (method_exists($db, 'getName'))
|
||||
{
|
||||
$jDriverName = $db->getName();
|
||||
}
|
||||
else
|
||||
{
|
||||
// On Joomla 4 this is supposed to raise an E_USER_DEPRECATED notice
|
||||
$jDriverName = $db->name ?? '';
|
||||
}
|
||||
|
||||
// Quick shortcuts to known core Joomla database drivers
|
||||
if (in_array($jDriverName, ['mysql', 'pdomysql']))
|
||||
{
|
||||
return 'pdomysql';
|
||||
}
|
||||
elseif ($jDriverName === 'mysqli')
|
||||
{
|
||||
return 'mysqli';
|
||||
}
|
||||
elseif (
|
||||
(stristr($jDriverName, 'postgre') !== false)
|
||||
|| (stristr($jDriverName, 'pgsql') !== false)
|
||||
|| (stristr($jDriverName, 'oracle') !== false)
|
||||
|| (stristr($jDriverName, 'sqlite') !== false)
|
||||
|| (stristr($jDriverName, 'sqlsrv') !== false)
|
||||
|| (stristr($jDriverName, 'sqlazure') !== false)
|
||||
|| (stristr($jDriverName, 'mssql') !== false)
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* We do not have a driver name known to the core. This is a custom database driver, implemented by a Joomla
|
||||
* extension. This is typically used in two use cases:
|
||||
* - Transparent content translation (JoomFish, Falang, jDiction, ...)
|
||||
* - Support for primary / secondary database servers (primary is read only, secondary is write only)
|
||||
* The custom database drier will be extending one of the core drivers. We will use defensive code to detect
|
||||
* that, making no assumption that the core driver class exists because these classes are an implementation
|
||||
* detail in Joomla which may change over time, even though they are explicitly included in its SemVer promise.
|
||||
* We have been around long enough to know better than believing Joomla won't break SemVer by accident...
|
||||
*/
|
||||
if (
|
||||
(class_exists(MysqlDriver::class) && ($db instanceof MysqlDriver))
|
||||
|| (class_exists(Pdomysql::class) && ($db instanceof Pdomysql))
|
||||
)
|
||||
{
|
||||
return 'pdomysql';
|
||||
}
|
||||
elseif (class_exists(MysqliDriver::class) && ($db instanceof MysqliDriver))
|
||||
{
|
||||
return 'mysqli';
|
||||
}
|
||||
elseif (
|
||||
(class_exists(PgsqlDriver::class) && ($db instanceof PgsqlDriver))
|
||||
|| (class_exists(SqliteDriver::class) && ($db instanceof SqliteDriver))
|
||||
|| (class_exists(SqlsrvDriver::class) && ($db instanceof SqlsrvDriver))
|
||||
|| (class_exists(SqlazureDriver::class) && ($db instanceof SqlazureDriver))
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// We still have no idea. We will need to use reflection. If it's unavailable we give up.
|
||||
if (!class_exists(ReflectionObject::class))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$refDriver = new ReflectionObject($db);
|
||||
|
||||
// Is this a generic PDO driver instance?
|
||||
if ((class_exists(PdoDriver::class) && ($db instanceof PdoDriver)) && $refDriver->hasProperty('options'))
|
||||
{
|
||||
$refOptions = $refDriver->getProperty('options');
|
||||
$refOptions->setAccessible(true);
|
||||
$options = $refOptions->getValue($db);
|
||||
$options = is_array($options) ? $options : [];
|
||||
|
||||
$pdoDriver = $options['driver'] ?? 'odbc';
|
||||
|
||||
switch ($pdoDriver)
|
||||
{
|
||||
// PDO MySQL. We support this!
|
||||
case 'mysql':
|
||||
return 'pdomysql';
|
||||
|
||||
// ODBC: I need to inspect the DSN
|
||||
case 'obdc':
|
||||
$dsn = $options['dsn'] ?? '';
|
||||
|
||||
// No DSN? No joy.
|
||||
if (empty($dsn))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// That's MySQL over ODBC over PDO. OK, rather strained but we can do that.
|
||||
if (stripos($dsn, 'mysql:') === 0)
|
||||
{
|
||||
return 'pdomysql';
|
||||
}
|
||||
|
||||
// Anything else: tough luck.
|
||||
return null;
|
||||
|
||||
// Anything else: tough luck.
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's get the class hierarchy and see if we have anything that looks like MySQL in its name.
|
||||
$classNames = class_parents($db);
|
||||
array_unshift($classNames, get_class($db));
|
||||
|
||||
$isMySQLi = array_reduce($classNames, function (bool $carry, string $className) {
|
||||
return $carry || (stripos($className, 'mysqli') !== false);
|
||||
}, false);
|
||||
|
||||
if ($isMySQLi)
|
||||
{
|
||||
return 'mysqli';
|
||||
}
|
||||
|
||||
$isPdoMySQL = array_reduce($classNames, function (bool $carry, string $className) {
|
||||
return $carry || (stripos($className, 'pdomysql') !== false);
|
||||
}, false);
|
||||
|
||||
if ($isPdoMySQL)
|
||||
{
|
||||
return 'pdomysql';
|
||||
}
|
||||
|
||||
// All possible checks failed. I have no idea what you're doing here, mate.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user