diff --git a/README.md b/README.md index 6e45031..b5df5fa 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ A query builder/database abstraction layer, using prepared statements for securi ## Databases Supported -* Firebird (via interbase extension) * MySQL * PostgreSQL * SQLite @@ -41,7 +40,7 @@ $params = array( 'database' => 'test_db', // Only required for - // SQLite or Firebird + // SQLite 'file' => '/path/to/db/file', // Optional paramaters @@ -85,7 +84,7 @@ Underscored methods are also aliased to camel case methods. #### You can also run queries manually. To run a prepared statement, call -`$db->prepare_execute($sql, $params)`. +`$db->prepareExecute($sql, $params)`. To run a plain query, `$db->query($sql)` @@ -98,8 +97,8 @@ An example of a moderately complex query: $query = $db->select('id, key as k, val') ->from('table t') ->where('k >', 3) - ->or_where('id !=' 5) - ->order_by('val', 'DESC') + ->orWhere('id !=' 5) + ->orderBy('val', 'DESC') ->limit(3, 1) ->get(); ``` diff --git a/build/phpunit.xml b/build/phpunit.xml index e252fcb..218b0cc 100644 --- a/build/phpunit.xml +++ b/build/phpunit.xml @@ -24,14 +24,6 @@ ./../tests/Drivers/SQLite/ - diff --git a/phpstan.neon b/phpstan.neon index b340330..eb20b2c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,12 +1,6 @@ parameters: autoload_files: - %rootDir%/../../../tests/bootstrap.php - - %rootDir%/../../../tests/databases/mysql/MySQLTest.php - - %rootDir%/../../../tests/databases/mysql/MySQLQBTest.php - - %rootDir%/../../../tests/databases/pgsql/PgSQLTest.php - - %rootDir%/../../../tests/databases/pgsql/PgSQLQBTest.php - - %rootDir%/../../../tests/databases/sqlite/SQLiteTest.php - - %rootDir%/../../../tests/databases/sqlite/SQLiteQBTest.php ignoreErrors: - '#Access to an undefined property Aviat\\\Ion\\\Friend::\$[a-zA-Z0-9_]+#' - '#Call to an undefined method Aviat\\\Ion\\\Friend::[a-zA-Z0-9_]+\(\)#' diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index 1bc947b..0000000 --- a/phpunit.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - src/ - autoload.php - - - - - tests/core/core_test.php - tests/core/query_parser_test.php - tests/core/connection_manager_test.php - - - tests/databases/mysql/MySQLTest.php - tests/databases/mysql/MySQLQBTest.php - - - tests/databases/pgsql/PgSQLTest.php - tests/databases/pgsql/PgSQLQBTest.php - - - tests/databases/sqlite/SQLiteTest.php - tests/databases/sqlite/SQLiteQBTest.php - - - tests/databases/firebird/FirebirdTest.php - tests/databases/firebird/FirebirdQBTest.php - - - \ No newline at end of file diff --git a/src/Drivers/AbstractDriver.php b/src/Drivers/AbstractDriver.php index 4c706a7..fda2f59 100644 --- a/src/Drivers/AbstractDriver.php +++ b/src/Drivers/AbstractDriver.php @@ -98,10 +98,10 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return void */ - protected function _loadSubClasses() + protected function _loadSubClasses(): void { // Load the sql and util class for the driver - $thisClass = get_class($this); + $thisClass = \get_class($this); $nsArray = explode("\\", $thisClass); array_pop($nsArray); $driver = array_pop($nsArray); @@ -124,11 +124,11 @@ abstract class AbstractDriver extends PDO implements DriverInterface { { if ( isset($this->$name) - && is_object($this->$name) + && \is_object($this->$name) && method_exists($this->$name, '__invoke') ) { - return call_user_func_array([$this->$name, '__invoke'], $args); + return \call_user_func_array([$this->$name, '__invoke'], $args); } } @@ -152,7 +152,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @param string $queryString * @return void */ - public function setLastQuery(string $queryString) + public function setLastQuery(string $queryString): void { $this->lastQuery = $queryString; } @@ -205,9 +205,9 @@ abstract class AbstractDriver extends PDO implements DriverInterface { // Prepare the sql, save the statement for easy access later $this->statement = $this->prepare($sql); - if( ! (is_array($data) || is_object($data))) + if( ! (\is_array($data) || \is_object($data))) { - throw new InvalidArgumentException("Data argument must be an object or associative array"); + throw new InvalidArgumentException('Data argument must be an object or associative array'); } // Bind the parameters @@ -232,7 +232,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @param array $params * @return PDOStatement */ - public function prepareExecute($sql, $params) + public function prepareExecute($sql, $params): PDOStatement { $this->statement = $this->prepareQuery($sql, $params); $this->statement->execute(); @@ -245,7 +245,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return int */ - public function affectedRows() + public function affectedRows(): int { // Return number of rows affected return $this->statement->rowCount(); @@ -256,7 +256,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @param string $table * @return string */ - public function prefixTable($table) + public function prefixTable($table): string { // Add the prefix to the table name // before quoting it @@ -286,7 +286,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @param string $table * @return string */ - public function quoteTable($table) + public function quoteTable($table): string { $table = $this->prefixTable($table); @@ -298,11 +298,11 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * Surrounds the string with the databases identifier escape characters * * @param mixed $identifier - * @return string + * @return string|array */ public function quoteIdent($identifier) { - if (is_array($identifier)) + if (\is_array($identifier)) { return array_map([$this, __METHOD__], $identifier); } @@ -335,7 +335,6 @@ abstract class AbstractDriver extends PDO implements DriverInterface { } return $raw; - } /** @@ -343,7 +342,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getSchemas() + public function getSchemas(): ?array { return NULL; } @@ -353,7 +352,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getTables() + public function getTables(): ?array { $tables = $this->driverQuery('tableList'); natsort($tables); @@ -365,7 +364,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getDbs() + public function getDbs(): array { return $this->driverQuery('dbList'); } @@ -375,7 +374,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getViews() + public function getViews(): ?array { $views = $this->driverQuery('viewList'); sort($views); @@ -387,7 +386,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getSequences() + public function getSequences(): ?array { return $this->driverQuery('sequenceList'); } @@ -397,7 +396,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getFunctions() + public function getFunctions(): ?array { return $this->driverQuery('functionList', FALSE); } @@ -407,7 +406,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getProcedures() + public function getProcedures(): ?array { return $this->driverQuery('procedureList', FALSE); } @@ -417,7 +416,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getTriggers() + public function getTriggers(): ?array { return $this->driverQuery('triggerList', FALSE); } @@ -428,7 +427,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getSystemTables() + public function getSystemTables(): ?array { return $this->driverQuery('systemTableList'); } @@ -439,7 +438,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @param string $table * @return array */ - public function getColumns($table) + public function getColumns($table): ?array { return $this->driverQuery($this->getSql()->columnList($this->prefixTable($table)), FALSE); } @@ -450,7 +449,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @param string $table * @return array */ - public function getFks($table) + public function getFks($table): ?array { return $this->driverQuery($this->getSql()->fkList($table), FALSE); } @@ -461,7 +460,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @param string $table * @return array */ - public function getIndexes($table) + public function getIndexes($table): ?array { return $this->driverQuery($this->getSql()->indexList($this->prefixTable($table)), FALSE); } @@ -471,7 +470,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @return array */ - public function getTypes() + public function getTypes(): ?array { return $this->driverQuery('typeList', FALSE); } @@ -481,19 +480,19 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * * @param string|array|null $query * @param bool $filteredIndex - * @return array + * @return array|null */ - public function driverQuery($query, $filteredIndex=TRUE) + public function driverQuery($query, $filteredIndex=TRUE): ?array { // Call the appropriate method, if it exists - if (is_string($query) && method_exists($this->sql, $query)) + if (\is_string($query) && method_exists($this->sql, $query)) { $query = $this->getSql()->$query(); } // Return if the values are returned instead of a query, // or if the query doesn't apply to the driver - if ( ! is_string($query)) + if ( ! \is_string($query)) { return $query; } @@ -501,10 +500,10 @@ abstract class AbstractDriver extends PDO implements DriverInterface { // Run the query! $res = $this->query($query); - $flag = ($filteredIndex) ? PDO::FETCH_NUM : PDO::FETCH_ASSOC; + $flag = $filteredIndex ? PDO::FETCH_NUM : PDO::FETCH_ASSOC; $all = $res->fetchAll($flag); - return ($filteredIndex) ? \db_filter($all, 0) : $all; + return $filteredIndex ? \db_filter($all, 0) : $all; } /** @@ -513,7 +512,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @see http://us3.php.net/manual/en/pdostatement.rowcount.php#87110 * @return int|null */ - public function numRows() + public function numRows(): ?int { $regex = '/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/i'; $output = []; @@ -537,7 +536,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { public function insertBatch($table, $data=[]) { $data = (array) $data; - $firstRow = (array) current($data); + $firstRow = current($data); if (is_scalar($firstRow)) { return NULL; @@ -558,7 +557,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { // Create the placeholder groups $params = array_fill(0, count($fields), '?'); - $paramString = "(" . implode(',', $params) . ")"; + $paramString = '(' . implode(',', $params) . ')'; $paramList = array_fill(0, count($data), $paramString); // Append the placeholder groups to the query @@ -594,7 +593,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { // and is not already quoted before quoting // that value, otherwise, return the original value return ( - is_string($str) + \is_string($str) && strpos($str, $this->escapeCharOpen) !== 0 && strrpos($str, $this->escapeCharClose) !== 0 ) @@ -609,7 +608,7 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @param string $str * @return string */ - protected function _prefix($str) + protected function _prefix($str): string { // Don't prefix an already prefixed table if (strpos($str, $this->tablePrefix) !== FALSE) @@ -626,9 +625,9 @@ abstract class AbstractDriver extends PDO implements DriverInterface { * @param string $table * @return PDOStatement */ - public function truncate($table) + public function truncate($table): PDOStatement { - $sql = ($this->hasTruncate) + $sql = $this->hasTruncate ? 'TRUNCATE TABLE ' : 'DELETE FROM '; diff --git a/src/Drivers/AbstractUtil.php b/src/Drivers/AbstractUtil.php index 8b781a1..0bcfa4d 100644 --- a/src/Drivers/AbstractUtil.php +++ b/src/Drivers/AbstractUtil.php @@ -26,16 +26,16 @@ abstract class AbstractUtil { * Reference to the current connection object * @var DriverInterface */ - private $conn; + private $connection; /** * Save a reference to the connection object for later use * - * @param DriverInterface $conn + * @param DriverInterface $connection */ - public function __construct(DriverInterface $conn) + public function __construct(DriverInterface $connection) { - $this->conn = $conn; + $this->connection = $connection; } /** @@ -45,7 +45,7 @@ abstract class AbstractUtil { */ public function getDriver() { - return $this->conn; + return $this->connection; } /** @@ -59,7 +59,7 @@ abstract class AbstractUtil { */ public function createTable($name, $fields, array $constraints=[], $ifNotExists=TRUE) { - $existsStr = ($ifNotExists) ? ' IF NOT EXISTS ' : ' '; + $existsStr = $ifNotExists ? ' IF NOT EXISTS ' : ' '; // Reorganize into an array indexed with column information // Eg $columnArray[$colname] = array( @@ -77,8 +77,8 @@ abstract class AbstractUtil { foreach($columnArray as $n => $props) { $str = $this->getDriver()->quoteIdent($n); - $str .= (isset($props['type'])) ? " {$props['type']}" : ""; - $str .= (isset($props['constraint'])) ? " {$props['constraint']}" : ""; + $str .= isset($props['type']) ? " {$props['type']}" : ""; + $str .= isset($props['constraint']) ? " {$props['constraint']}" : ""; $columns[] = $str; } @@ -97,7 +97,7 @@ abstract class AbstractUtil { * @param string $name * @return string */ - public function deleteTable($name) + public function deleteTable($name): string { return 'DROP TABLE IF EXISTS '.$this->getDriver()->quoteTable($name); } diff --git a/src/Drivers/DriverInterface.php b/src/Drivers/DriverInterface.php index 3eef138..d608254 100644 --- a/src/Drivers/DriverInterface.php +++ b/src/Drivers/DriverInterface.php @@ -152,8 +152,6 @@ interface DriverInterface extends PDOInterface { */ public function prepareExecute($sql, $params); - - /** * Method to simplify retrieving db results for meta-data queries * diff --git a/src/Drivers/Mysql/Driver.php b/src/Drivers/Mysql/Driver.php index 8465e51..2dca6b1 100644 --- a/src/Drivers/Mysql/Driver.php +++ b/src/Drivers/Mysql/Driver.php @@ -49,7 +49,7 @@ class Driver extends AbstractDriver implements DriverInterface { public function __construct($dsn, $username=NULL, $password=NULL, array $options=[]) { // Set the charset to UTF-8 - if (defined('\\PDO::MYSQL_ATTR_INIT_COMMAND')) + if (\defined('\\PDO::MYSQL_ATTR_INIT_COMMAND')) { $options = array_merge($options, [ PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF-8 COLLATE 'UTF-8'", diff --git a/src/Drivers/Mysql/Util.php b/src/Drivers/Mysql/Util.php index 3b51988..0eec061 100644 --- a/src/Drivers/Mysql/Util.php +++ b/src/Drivers/Mysql/Util.php @@ -27,7 +27,7 @@ class Util extends AbstractUtil { * * @return string */ - public function backupStructure() + public function backupStructure(): string { $string = []; @@ -37,7 +37,7 @@ class Util extends AbstractUtil { foreach($dbs as &$d) { // Skip built-in dbs - if ($d == 'mysql') + if ($d === 'mysql') { continue; } @@ -69,7 +69,7 @@ class Util extends AbstractUtil { * @param array $exclude * @return string */ - public function backupData($exclude=[]) + public function backupData($exclude=[]): string { $tables = $this->getDriver()->getTables(); diff --git a/src/Drivers/Pgsql/Driver.php b/src/Drivers/Pgsql/Driver.php index 6ead87c..7180f42 100644 --- a/src/Drivers/Pgsql/Driver.php +++ b/src/Drivers/Pgsql/Driver.php @@ -48,7 +48,7 @@ class Driver extends AbstractDriver implements DriverInterface { * * @return array */ - public function getSchemas() + public function getSchemas(): ?array { $sql = << 'CASCADE', @@ -80,10 +80,8 @@ SQL; { foreach(['update', 'delete'] AS $type) { - if ( ! isset($valueMap[$key[$type]])) - { - continue; - } + if ( ! isset($valueMap[$key[$type]])) continue; + $key[$type] = $valueMap[$key[$type]]; } diff --git a/src/Drivers/Pgsql/Util.php b/src/Drivers/Pgsql/Util.php index 791c205..b1890ac 100644 --- a/src/Drivers/Pgsql/Util.php +++ b/src/Drivers/Pgsql/Util.php @@ -17,7 +17,7 @@ namespace Query\Drivers\Pgsql; use Query\Drivers\AbstractUtil; /** - * Posgres-specific backup, import and creation methods + * Postgres-specific backup, import and creation methods */ class Util extends AbstractUtil { @@ -26,7 +26,7 @@ class Util extends AbstractUtil { * * @return string */ - public function backupStructure() + public function backupStructure(): string { // @TODO Implement Backup function return ''; @@ -38,7 +38,7 @@ class Util extends AbstractUtil { * @param array $exclude * @return string */ - public function backupData($exclude=[]) + public function backupData(array $exclude=[]): string { $tables = $this->getDriver()->getTables(); diff --git a/src/Drivers/Sqlite/Driver.php b/src/Drivers/Sqlite/Driver.php index 186aae6..35b1084 100644 --- a/src/Drivers/Sqlite/Driver.php +++ b/src/Drivers/Sqlite/Driver.php @@ -61,7 +61,7 @@ class Driver extends AbstractDriver implements DriverInterface { * * @return mixed */ - public function getTables() + public function getTables(): array { $sql = $this->sql->tableList(); $res = $this->query($sql); @@ -74,7 +74,7 @@ class Driver extends AbstractDriver implements DriverInterface { * @param string $table * @return array */ - public function getFks($table) + public function getFks($table): array { $returnRows = []; diff --git a/src/Drivers/Sqlite/Util.php b/src/Drivers/Sqlite/Util.php index f590131..21d2a2e 100644 --- a/src/Drivers/Sqlite/Util.php +++ b/src/Drivers/Sqlite/Util.php @@ -33,7 +33,7 @@ class Util extends AbstractUtil { * @param array $excluded * @return string */ - public function backupData($excluded=[]) + public function backupData(array $excluded=[]): string { // Get a list of all the objects $sql = 'SELECT DISTINCT "name" @@ -103,7 +103,7 @@ class Util extends AbstractUtil { * * @return string */ - public function backupStructure() + public function backupStructure(): string { // Fairly easy for SQLite...just query the master table $sql = 'SELECT "sql" FROM "sqlite_master"'; diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 3fd8792..b7f30e8 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -16,12 +16,172 @@ namespace Query; use BadMethodCallException; use PDOStatement; -use Query\Drivers\DriverInterface; +use Query\Drivers\{ + AbstractUtil, + DriverInterface, + SQLInterface +}; /** * Convenience class for creating sql queries + * @method query(mixed $sql): PDOStatement; */ -class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface { +class QueryBuilder implements QueryBuilderInterface { + + // -------------------------------------------------------------------------- + // ! Constants + // -------------------------------------------------------------------------- + + const KEY = 0; + const VALUE = 1; + const BOTH = 2; + + // -------------------------------------------------------------------------- + // ! SQL Clause Strings + // -------------------------------------------------------------------------- + + /** + * Compiled 'select' clause + * @var string + */ + protected $selectString = ''; + + /** + * Compiled 'from' clause + * @var string + */ + protected $fromString = ''; + + /** + * Compiled arguments for insert / update + * @var string + */ + protected $setString; + + /** + * Order by clause + * @var string + */ + protected $orderString; + + /** + * Group by clause + * @var string + */ + protected $groupString; + + // -------------------------------------------------------------------------- + // ! SQL Clause Arrays + // -------------------------------------------------------------------------- + + /** + * Keys for insert/update statement + * @var array + */ + protected $setArrayKeys = []; + + /** + * Key/val pairs for order by clause + * @var array + */ + protected $orderArray = []; + + /** + * Key/val pairs for group by clause + * @var array + */ + protected $groupArray = []; + + // -------------------------------------------------------------------------- + // ! Other Class vars + // -------------------------------------------------------------------------- + + /** + * Values to apply to prepared statements + * @var array + */ + protected $values = []; + + /** + * Values to apply to where clauses in prepared statements + * @var array + */ + protected $whereValues = []; + + /** + * Value for limit string + * @var string + */ + protected $limit; + + /** + * Value for offset in limit string + * @var integer + */ + protected $offset; + + /** + * Query component order mapping + * for complex select queries + * + * Format: + * array( + * 'type' => 'where', + * 'conjunction' => ' AND ', + * 'string' => 'k=?' + * ) + * + * @var array + */ + protected $queryMap = []; + + /** + * Map for having clause + * @var array + */ + protected $havingMap; + + /** + * Convenience property for connection management + * @var string + */ + public $connName = ''; + + /** + * List of queries executed + * @var array + */ + public $queries; + + /** + * Whether to do only an explain on the query + * @var boolean + */ + protected $explain; + + /** + * The current database driver + * @var DriverInterface + */ + public $driver; + + /** + * Query parser class instance + * @var QueryParser + */ + protected $parser; + + /** + * Alias to driver util class + * @var AbstractUtil + */ + protected $util; + + /** + * Alias to driver sql class + * @var SQLInterface + */ + protected $sql; /** * String class values to be reset @@ -61,20 +221,20 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface /** * Constructor * - * @param DriverInterface $db + * @param DriverInterface $driver * @param QueryParser $parser */ - public function __construct(DriverInterface $db, QueryParser $parser) + public function __construct(DriverInterface $driver, QueryParser $parser) { // Inject driver and parser - $this->db = $db; + $this->driver = $driver; $this->parser = $parser; $this->queries['total_time'] = 0; // Alias driver sql and util classes - $this->sql = $this->db->getSql(); - $this->util = $this->db->getUtil(); + $this->sql = $this->driver->getSql(); + $this->util = $this->driver->getUtil(); } /** @@ -83,7 +243,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface */ public function __destruct() { - $this->db = NULL; + $this->driver = NULL; } /** @@ -99,7 +259,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface // Alias snake_case method calls $camelName = \to_camel_case($name); - foreach([$this, $this->db] as $object) + foreach([$this, $this->driver] as $object) { foreach([$name, $camelName] as $methodName) { @@ -114,6 +274,15 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface throw new BadMethodCallException('Method does not exist'); } + // -------------------------------------------------------------------------- + // ! Driver setters + // -------------------------------------------------------------------------- + + public function setDriver(DriverInterface $driver) + { + + } + // -------------------------------------------------------------------------- // ! Select Queries // -------------------------------------------------------------------------- @@ -141,14 +310,14 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface } // Quote the identifiers - $safeArray = $this->db->quoteIdent($fieldsArray); + $safeArray = $this->driver->quoteIdent($fieldsArray); unset($fieldsArray); // Join the strings back together for($i = 0, $c = count($safeArray); $i < $c; $i++) { - if (is_array($safeArray[$i])) + if (\is_array($safeArray[$i])) { $safeArray[$i] = implode(' AS ', $safeArray[$i]); } @@ -253,8 +422,8 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface $identArray = array_map('\\mb_trim', $identArray); // Quote the identifiers - $identArray[0] = $this->db->quoteTable($identArray[0]); - $identArray = $this->db->quoteIdent($identArray); + $identArray[0] = $this->driver->quoteTable($identArray[0]); + $identArray = $this->driver->quoteIdent($identArray); // Paste it back together $this->fromString = implode(' ', $identArray); @@ -443,7 +612,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface // Use the keys of the array to make the insert/update string // Escape the field names - $this->setArrayKeys = array_map([$this->db, '_quote'], $this->setArrayKeys); + $this->setArrayKeys = array_map([$this->driver, '_quote'], $this->setArrayKeys); // Generate the "set" string $this->setString = implode('=?,', $this->setArrayKeys); @@ -464,8 +633,8 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface { // Prefix and quote table name $table = explode(' ', mb_trim($table)); - $table[0] = $this->db->quoteTable($table[0]); - $table = $this->db->quoteIdent($table); + $table[0] = $this->driver->quoteTable($table[0]); + $table = $this->driver->quoteIdent($table); $table = implode(' ', $table); // Parse out the join condition @@ -487,12 +656,12 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface { if ( ! is_scalar($field)) { - $newGroupArray = array_map([$this->db, 'quoteIdent'], $field); + $newGroupArray = array_map([$this->driver, 'quoteIdent'], $field); $this->groupArray = array_merge($this->groupArray, $newGroupArray); } else { - $this->groupArray[] = $this->db->quoteIdent($field); + $this->groupArray[] = $this->driver->quoteIdent($field); } $this->groupString = ' GROUP BY ' . implode(',', $this->groupArray); @@ -507,7 +676,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface * @param string $type * @return QueryBuilderInterface */ - public function orderBy($field, $type=""): QueryBuilderInterface + public function orderBy($field, $type=''): QueryBuilderInterface { // When ordering by random, do an ascending order if the driver // doesn't support random ordering @@ -518,7 +687,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface } // Set fields for later manipulation - $field = $this->db->quoteIdent($field); + $field = $this->driver->quoteIdent($field); $this->orderArray[$field] = $type; $orderClauses = []; @@ -563,7 +732,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface */ public function groupStart(): QueryBuilderInterface { - $conj = (empty($this->queryMap)) ? ' WHERE ' : ' '; + $conj = empty($this->queryMap) ? ' WHERE ' : ' '; $this->_appendMap($conj, '(', 'group_start'); @@ -578,7 +747,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface */ public function notGroupStart(): QueryBuilderInterface { - $conj = (empty($this->queryMap)) ? ' WHERE ' : ' AND '; + $conj = empty($this->queryMap) ? ' WHERE ' : ' AND '; $this->_appendMap($conj, ' NOT (', 'group_start'); @@ -679,8 +848,8 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface */ public function countAll($table): int { - $sql = 'SELECT * FROM '.$this->db->quoteTable($table); - $res = $this->db->query($sql); + $sql = 'SELECT * FROM '.$this->driver->quoteTable($table); + $res = $this->driver->query($sql); return (int) count($res->fetchAll()); } @@ -720,7 +889,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface $this->set($data); } - return $this->_run("insert", $table); + return $this->_run('insert', $table); } /** @@ -733,7 +902,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface public function insertBatch($table, $data=[]): PDOStatement { // Get the generated values and sql string - list($sql, $data) = $this->db->insertBatch($table, $data); + list($sql, $data) = $this->driver->insertBatch($table, $data); return ( ! is_null($sql)) ? $this->_run('', $table, $sql, $data) @@ -754,7 +923,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface $this->set($data); } - return $this->_run("update", $table); + return $this->_run('update', $table); } /** @@ -769,7 +938,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface public function updateBatch($table, $data, $where) { // Get the generated values and sql string - list($sql, $data) = $this->db->updateBatch($table, $data, $where); + list($sql, $data) = $this->driver->updateBatch($table, $data, $where); return ( ! is_null($sql)) ? $this->_run('', $table, $sql, $data) @@ -790,7 +959,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface $this->set($data); } - return $this->_run("replace", $table); + return $this->_run('replace', $table); } /** @@ -808,7 +977,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface $this->where($where); } - return $this->_run("delete", $table); + return $this->_run('delete', $table); } // -------------------------------------------------------------------------- @@ -878,7 +1047,7 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface * * @return void */ - public function resetQuery() + public function resetQuery(): void { // Reset strings and booleans foreach($this->stringVars as $var) @@ -892,5 +1061,434 @@ class QueryBuilder extends AbstractQueryBuilder implements QueryBuilderInterface $this->$var = []; } } + + /** + * Set values in the class, with either an array or key value pair + * + * @param array $var + * @param mixed $key + * @param mixed $val + * @param int $valType + * @return array + */ + protected function _mixedSet(array &$var, $key, $val=NULL, int $valType=self::BOTH): array + { + $arg = (is_scalar($key) && is_scalar($val)) + ? [$key => $val] + : $key; + + foreach($arg as $k => $v) + { + if (\in_array($valType, [self::KEY, self::VALUE], TRUE)) + { + $var[] = ($valType === self::KEY) + ? $k + : $v; + } + else + { + $var[$k] = $v; + } + } + + return $var; + } + + /** + * Method to simplify select_ methods + * + * @param string $field + * @param string|bool $as + * @return string + */ + protected function _select(string $field, $as = FALSE): string + { + // Escape the identifiers + $field = $this->driver->quoteIdent($field); + + if ( ! \is_string($as)) + { + return $field; + } + + $as = $this->driver->quoteIdent($as); + return "({$field}) AS {$as} "; + } + + /** + * Helper function for returning sql strings + * + * @param string $type + * @param string $table + * @param bool $reset + * @return string + */ + protected function _getCompile(string $type, string $table, bool $reset): string + { + $sql = $this->_compile($type, $table); + + // Reset the query builder for the next query + if ($reset) + { + $this->resetQuery(); + } + + return $sql; + } + + /** + * Simplify 'like' methods + * + * @param string $field + * @param mixed $val + * @param string $pos + * @param string $like + * @param string $conj + * @return self + */ + protected function _like(string $field, $val, string $pos, string $like='LIKE', string $conj='AND'): self + { + $field = $this->driver->quoteIdent($field); + + // Add the like string into the order map + $like = $field. " {$like} ?"; + + if ($pos === 'before') + { + $val = "%{$val}"; + } + elseif ($pos === 'after') + { + $val = "{$val}%"; + } + else + { + $val = "%{$val}%"; + } + + $conj = empty($this->queryMap) ? ' WHERE ' : " {$conj} "; + $this->_appendMap($conj, $like, 'like'); + + // Add to the values array + $this->whereValues[] = $val; + + return $this; + } + + /** + * Simplify building having clauses + * + * @param mixed $key + * @param mixed $values + * @param string $conj + * @return self + */ + protected function _having($key, $values=[], string $conj='AND'): self + { + $where = $this->_where($key, $values); + + // Create key/value placeholders + foreach($where as $f => $val) + { + // Split each key by spaces, in case there + // is an operator such as >, <, !=, etc. + $fArray = explode(' ', trim($f)); + + $item = $this->driver->quoteIdent($fArray[0]); + + // Simple key value, or an operator + $item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?"; + + // Put in the having map + $this->havingMap[] = [ + 'conjunction' => ( ! empty($this->havingMap)) ? " {$conj} " : ' HAVING ', + 'string' => $item + ]; + } + + return $this; + } + + /** + * Do all the redundant stuff for where/having type methods + * + * @param mixed $key + * @param mixed $val + * @return array + */ + protected function _where($key, $val=[]): array + { + $where = []; + $this->_mixedSet($where, $key, $val); + $this->_mixedSet($this->whereValues, $key, $val, self::VALUE); + return $where; + } + + /** + * Simplify generating where string + * + * @param mixed $key + * @param mixed $values + * @param string $defaultConj + * @return self + */ + protected function _whereString($key, $values=[], string $defaultConj='AND'): self + { + // Create key/value placeholders + foreach($this->_where($key, $values) as $f => $val) + { + // Split each key by spaces, in case there + // is an operator such as >, <, !=, etc. + $fArray = explode(' ', trim($f)); + + $item = $this->driver->quoteIdent($fArray[0]); + + // Simple key value, or an operator + $item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?"; + $lastItem = end($this->queryMap); + + // Determine the correct conjunction + $conjunctionList = array_column($this->queryMap, 'conjunction'); + if (empty($this->queryMap) || ( ! regex_in_array($conjunctionList, "/^ ?\n?WHERE/i"))) + { + $conj = "\nWHERE "; + } + elseif ($lastItem['type'] === 'group_start') + { + $conj = ''; + } + else + { + $conj = " {$defaultConj} "; + } + + $this->_appendMap($conj, $item, 'where'); + } + + return $this; + } + + /** + * Simplify where_in methods + * + * @param mixed $key + * @param mixed $val + * @param string $in - The (not) in fragment + * @param string $conj - The where in conjunction + * @return self + */ + protected function _whereIn($key, $val=[], string $in='IN', string $conj='AND'): self + { + $key = $this->driver->quoteIdent($key); + $params = array_fill(0, count($val), '?'); + + foreach($val as $v) + { + $this->whereValues[] = $v; + } + + $conjunction = ( ! empty($this->queryMap)) ? " {$conj} " : ' WHERE '; + $str = $key . " {$in} (".implode(',', $params).') '; + + $this->_appendMap($conjunction, $str, 'where_in'); + + return $this; + } + + /** + * Executes the compiled query + * + * @param string $type + * @param string $table + * @param string $sql + * @param array|null $vals + * @param boolean $reset + * @return PDOStatement + */ + protected function _run(string $type, string $table, $sql=NULL, $vals=NULL, bool $reset=TRUE): PDOStatement + { + if ($sql === NULL) + { + $sql = $this->_compile($type, $table); + } + + if ($vals === NULL) + { + $vals = array_merge($this->values, (array) $this->whereValues); + } + + $startTime = microtime(TRUE); + + $res = empty($vals) + ? $this->driver->query($sql) + : $this->driver->prepareExecute($sql, $vals); + + $endTime = microtime(TRUE); + $totalTime = number_format($endTime - $startTime, 5); + + // Add this query to the list of executed queries + $this->_appendQuery($vals, $sql, (int) $totalTime); + + // Reset class state for next query + if ($reset) + { + $this->resetQuery(); + } + + return $res; + } + + /** + * Add an additional set of mapping pairs to a internal map + * + * @param string $conjunction + * @param string $string + * @param string $type + * @return void + */ + protected function _appendMap(string $conjunction = '', string $string = '', string $type = '') + { + $this->queryMap[] = [ + 'type' => $type, + 'conjunction' => $conjunction, + 'string' => $string + ]; + } + + /** + * Convert the prepared statement into readable sql + * + * @param array $vals + * @param string $sql + * @param int $totalTime + * @return void + */ + protected function _appendQuery($vals, string $sql, int $totalTime) + { + $evals = \is_array($vals) ? $vals : []; + $esql = str_replace('?', "%s", $sql); + + // Quote string values + foreach($evals as &$v) + { + $v = ( ! is_numeric($v)) + ? htmlentities($this->driver->quote($v), ENT_NOQUOTES, 'utf-8') + : $v; + } + + // Add the query onto the array of values to pass + // as arguments to sprintf + array_unshift($evals, $esql); + + // Add the interpreted query to the list of executed queries + $this->queries[] = [ + 'time' => $totalTime, + 'sql' => sprintf(...$evals) + ]; + + $this->queries['total_time'] += $totalTime; + + // Set the last query to get rowcounts properly + $this->driver->setLastQuery($sql); + } + + /** + * Sub-method for generating sql strings + * + * @param string $type + * @param string $table + * @return string + */ + protected function _compileType(string $type='', string $table=''): string + { + switch($type) + { + case 'insert': + $paramCount = count($this->setArrayKeys); + $params = array_fill(0, $paramCount, '?'); + $sql = "INSERT INTO {$table} (" + . implode(',', $this->setArrayKeys) + . ")\nVALUES (".implode(',', $params).')'; + break; + + case 'update': + $sql = "UPDATE {$table}\nSET {$this->setString}"; + break; + + case 'replace': + // @TODO implement + $sql = ''; + break; + + case 'delete': + $sql = "DELETE FROM {$table}"; + break; + + // Get queries + default: + $sql = "SELECT * \nFROM {$this->fromString}"; + + // Set the select string + if ( ! empty($this->selectString)) + { + // Replace the star with the selected fields + $sql = str_replace('*', $this->selectString, $sql); + } + break; + } + + return $sql; + } + + /** + * String together the sql statements for sending to the db + * + * @param string $type + * @param string $table + * @return string + */ + protected function _compile(string $type='', string $table=''): string + { + // Get the base clause for the query + $sql = $this->_compileType($type, $this->driver->quoteTable($table)); + + $clauses = [ + 'queryMap', + 'groupString', + 'orderString', + 'havingMap', + ]; + + // Set each type of subclause + foreach($clauses as $clause) + { + $param = $this->$clause; + if (\is_array($param)) + { + foreach($param as $q) + { + $sql .= $q['conjunction'] . $q['string']; + } + } + else + { + $sql .= $param; + } + } + + // Set the limit via the class variables + if (is_numeric($this->limit)) + { + $sql = $this->sql->limit($sql, $this->limit, $this->offset); + } + + // See if the query plan, rather than the + // query data should be returned + if ($this->explain === TRUE) + { + $sql = $this->sql->explain($sql); + } + + return $sql; + } } -// End of query_builder.php \ No newline at end of file diff --git a/src/QueryBuilderInterface.php b/src/QueryBuilderInterface.php index 676d941..657c120 100644 --- a/src/QueryBuilderInterface.php +++ b/src/QueryBuilderInterface.php @@ -527,7 +527,7 @@ interface QueryBuilderInterface { * * @return void */ - public function resetQuery(); + public function resetQuery(): void; } // End of QueryBuilderInterface.php diff --git a/tests/BaseQueryBuilderTest.php b/tests/BaseQueryBuilderTest.php index 75071ef..c950737 100644 --- a/tests/BaseQueryBuilderTest.php +++ b/tests/BaseQueryBuilderTest.php @@ -62,6 +62,8 @@ abstract class BaseQueryBuilderTest extends TestCase { $query = self::$db->get('test'); $this->assertIsA($query, 'PDOStatement'); + $lastQuery = self::$db->getLastQuery(); + $this->assertTrue(\is_string($lastQuery)); } public function testPrefixGet() @@ -519,6 +521,7 @@ abstract class BaseQueryBuilderTest extends TestCase { ->insert('test'); $this->assertIsA($query, 'PDOStatement'); + $this->assertTrue(self::$db->affectedRows() > 0); } public function testInsertArray() @@ -569,6 +572,12 @@ abstract class BaseQueryBuilderTest extends TestCase { $this->assertIsA($query, 'PDOStatement'); } + public function testUpdateBatch() + { + $query = self::$db->updateBatch('test', [], ''); + $this->assertNull($query); + } + public function testSetArrayUpdate() { $array = array( diff --git a/tests/Drivers/MySQL/MySQLDriverTest.php b/tests/Drivers/MySQL/MySQLDriverTest.php index aad4df5..fee331b 100644 --- a/tests/Drivers/MySQL/MySQLDriverTest.php +++ b/tests/Drivers/MySQL/MySQLDriverTest.php @@ -51,7 +51,7 @@ class MySQLDriverTest extends BaseDriverTest { public function testExists() { - $this->assertTrue(in_array('mysql', PDO::getAvailableDrivers())); + $this->assertTrue(\in_array('mysql', PDO::getAvailableDrivers(), TRUE)); } // -------------------------------------------------------------------------- @@ -97,7 +97,7 @@ class MySQLDriverTest extends BaseDriverTest { //Check $dbs = self::$db->getTables(); - $this->assertTrue(in_array('create_test', $dbs)); + $this->assertTrue(\in_array('create_test', $dbs, TRUE)); } diff --git a/tests/Drivers/PgSQL/PgSQLQueryBuilderTest.php b/tests/Drivers/PgSQL/PgSQLQueryBuilderTest.php index bfb0889..42fe2fa 100644 --- a/tests/Drivers/PgSQL/PgSQLQueryBuilderTest.php +++ b/tests/Drivers/PgSQL/PgSQLQueryBuilderTest.php @@ -43,11 +43,11 @@ class PgSQLQueryBuilderTest extends BaseQueryBuilderTest { else if ($params !== FALSE) { $params = $params->pgsql; - $params->type = "pgsql"; + $params->type = 'pgsql'; //$params->port = 5432; //$params->prefix = 'create_'; $params->options = array(); - $params->options[\PDO::ATTR_PERSISTENT] = TRUE; + $params->options[PDO::ATTR_PERSISTENT] = TRUE; } self::$db = Query($params); @@ -56,7 +56,7 @@ class PgSQLQueryBuilderTest extends BaseQueryBuilderTest { public function setUp() { // If the database isn't installed, skip the tests - if ( ! in_array('pgsql', PDO::getAvailableDrivers())) + if ( ! \in_array('pgsql', PDO::getAvailableDrivers(), TRUE)) { $this->markTestSkipped("Postgres extension for PDO not loaded"); } @@ -66,7 +66,7 @@ class PgSQLQueryBuilderTest extends BaseQueryBuilderTest { public function testExists() { - $this->assertTrue(in_array('pgsql', PDO::getAvailableDrivers())); + $this->assertTrue(\in_array('pgsql', PDO::getAvailableDrivers(), TRUE)); } // -------------------------------------------------------------------------- @@ -84,7 +84,7 @@ class PgSQLQueryBuilderTest extends BaseQueryBuilderTest { // The exact results are version dependent // The important thing is that there is an array // of results returned - $this->assertTrue(is_array($res)); + $this->assertTrue(\is_array($res)); $this->assertTrue(count($res) > 1); $this->assertTrue(array_key_exists('QUERY PLAN', $res[0])); @@ -117,6 +117,6 @@ class PgSQLQueryBuilderTest extends BaseQueryBuilderTest { public function testBackupStructure() { - $this->assertEquals('', self::$db->util->backupStructure()); + $this->assertEquals('', self::$db->getUtil()->backupStructure()); } } \ No newline at end of file diff --git a/tests/Drivers/SQLite/SQLiteDriverTest.php b/tests/Drivers/SQLite/SQLiteDriverTest.php index 41f5102..a00c6c3 100644 --- a/tests/Drivers/SQLite/SQLiteDriverTest.php +++ b/tests/Drivers/SQLite/SQLiteDriverTest.php @@ -183,7 +183,7 @@ SQL; $db = new $class(QTEST_DIR.QDS.'db_files'.QDS.'test_sqlite.db'); $this->assertIsA($db, $class); - $this->assertIsA(self::$db->db, $class); + $this->assertIsA(self::$db->driver, $class); unset($db); } @@ -286,13 +286,13 @@ SQL; public function testNullMethods() { - $sql = self::$db->sql->functionList(); + $sql = self::$db->getSQL()->functionList(); $this->assertEqual(NULL, $sql); - $sql = self::$db->sql->procedureList(); + $sql = self::$db->getSQL()->procedureList(); $this->assertEqual(NULL, $sql); - $sql = self::$db->sql->sequenceList(); + $sql = self::$db->getSQL()->sequenceList(); $this->assertEqual(NULL, $sql); } diff --git a/tests/QueryParserTest.php b/tests/QueryParserTest.php index 4914cd7..d12af98 100644 --- a/tests/QueryParserTest.php +++ b/tests/QueryParserTest.php @@ -21,6 +21,10 @@ use Query\Drivers\Sqlite\Driver; * Tests for the Query Parser */ class QueryParserTest extends TestCase { + /** + * @var QueryParser + */ + protected $parser; public function setUp() {