diff --git a/composer.json b/composer.json index 3967b57..bfc1f19 100644 --- a/composer.json +++ b/composer.json @@ -34,14 +34,14 @@ "require-dev": { "consolidation/robo": "^2.0.0", "monolog/monolog": "^2.0.1", - "phploc/phploc": "^6.0", + "phploc/phploc": "^5.0", "phpmd/phpmd": "^2.8", "phpstan/phpstan": "^0.12.2", - "phpunit/phpunit": "^9.1", - "sebastian/phpcpd": "^5", + "phpunit/phpunit": "^8.5", + "sebastian/phpcpd": "^4.1", "simpletest/simpletest": "^1.1", "squizlabs/php_codesniffer": "^3.0.0", - "theseer/phpdox": "*" + "theseer/phpdox": "^0.12.0" }, "autoload": { "psr-4": { diff --git a/src/ConnectionManager.php b/src/ConnectionManager.php index bdef06c..e1a0fa6 100644 --- a/src/ConnectionManager.php +++ b/src/ConnectionManager.php @@ -16,6 +16,7 @@ namespace Query; use DomainException; +use stdClass; /** * Connection manager class to manage connections for the @@ -33,7 +34,7 @@ final class ConnectionManager { * Class instance variable * @var ConnectionManager|null */ - private static $instance; + private static ?ConnectionManager $instance = NULL; /** * Private constructor to prevent multiple instances @@ -119,7 +120,7 @@ final class ConnectionManager { /** * Parse the passed parameters and return a connection * - * @param object|array $params + * @param array|object $params * @throws Exception\BadDBDriverException * @return QueryBuilderInterface */ @@ -161,13 +162,13 @@ final class ConnectionManager { /** * Parses params into a dsn and option array * - * @param object | array $params + * @param array|object $rawParams * @return array * @throws Exception\BadDBDriverException */ - public function parseParams($params): array + public function parseParams($rawParams): array { - $params = (object) $params; + $params = (object) $rawParams; $params->type = strtolower($params->type); $dbType = ($params->type === 'postgresql') ? 'pgsql' : $params->type; $dbType = ucfirst($dbType); @@ -205,10 +206,10 @@ final class ConnectionManager { * * @codeCoverageIgnore * @param string $dbType - * @param array|object $params + * @param stdClass $params * @return string */ - private function createDsn(string $dbType, $params): string + private function createDsn(string $dbType, stdClass $params): string { $pairs = []; diff --git a/src/Drivers/AbstractDriver.php b/src/Drivers/AbstractDriver.php index 281467b..84fe7f4 100644 --- a/src/Drivers/AbstractDriver.php +++ b/src/Drivers/AbstractDriver.php @@ -20,6 +20,7 @@ use function dbFilter; use InvalidArgumentException; use PDO; use PDOStatement; +use function is_string; /** * Base Database class @@ -52,7 +53,7 @@ abstract class AbstractDriver * Reference to sql class * @var SQLInterface */ - protected SQLInterface $sql; + protected SQLInterface $driverSQL; /** * Reference to util class @@ -110,7 +111,7 @@ abstract class AbstractDriver $sqlClass = __NAMESPACE__ . "\\{$driver}\\SQL"; $utilClass = __NAMESPACE__ . "\\{$driver}\\Util"; - $this->sql = new $sqlClass(); + $this->driverSQL = new $sqlClass(); $this->util = new $utilClass($this); } @@ -168,7 +169,7 @@ abstract class AbstractDriver */ public function getSql(): SQLInterface { - return $this->sql; + return $this->driverSQL; } /** @@ -494,7 +495,7 @@ abstract class AbstractDriver 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->driverSQL, $query)) { $query = $this->getSql()->$query(); } @@ -687,7 +688,7 @@ abstract class AbstractDriver // 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 ) diff --git a/src/Drivers/AbstractSQL.php b/src/Drivers/AbstractSQL.php index ead2a31..fdfe1de 100644 --- a/src/Drivers/AbstractSQL.php +++ b/src/Drivers/AbstractSQL.php @@ -25,10 +25,10 @@ abstract class AbstractSQL implements SQLInterface { * * @param string $sql * @param int $limit - * @param int|bool $offset + * @param int $offset * @return string */ - public function limit(string $sql, int $limit, $offset=FALSE): string + public function limit(string $sql, int $limit, ?int $offset=NULL): string { $sql .= "\nLIMIT {$limit}"; diff --git a/src/Drivers/Mysql/Driver.php b/src/Drivers/Mysql/Driver.php index 8e49369..5f0db1a 100644 --- a/src/Drivers/Mysql/Driver.php +++ b/src/Drivers/Mysql/Driver.php @@ -17,6 +17,7 @@ namespace Query\Drivers\Mysql; use PDO; use Query\Drivers\AbstractDriver; +use function defined; /** * MySQL specific class @@ -49,7 +50,7 @@ class Driver extends AbstractDriver { public function __construct(string $dsn, string $username=NULL, string $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/SQL.php b/src/Drivers/Mysql/SQL.php index 1136ac2..edaedd3 100644 --- a/src/Drivers/Mysql/SQL.php +++ b/src/Drivers/Mysql/SQL.php @@ -30,7 +30,7 @@ class SQL extends AbstractSQL { * @param int|boolean $offset * @return string */ - public function limit(string $sql, int $limit, $offset=FALSE): string + public function limit(string $sql, int $limit, ?int $offset=NULL): string { if ( ! is_numeric($offset)) { @@ -166,7 +166,7 @@ SQL; } /** - * SQL to show infromation about columns in a table + * SQL to show information about columns in a table * * @param string $table * @return string @@ -186,7 +186,7 @@ SQL; public function fkList(string $table): string { return <<getDriver()->getDbs(); + $driver = $this->getDriver(); + $dbs = $driver->getDbs(); foreach($dbs as &$d) { @@ -46,11 +47,11 @@ class Util extends AbstractUtil { // @codeCoverageIgnoreEnd // Get the list of tables - $tables = $this->getDriver()->driverQuery("SHOW TABLES FROM `{$d}`", TRUE); + $tables = $driver->driverQuery("SHOW TABLES FROM `{$d}`", TRUE); foreach($tables as $table) { - $array = $this->getDriver()->driverQuery("SHOW CREATE TABLE `{$d}`.`{$table}`", FALSE); + $array = $driver->driverQuery("SHOW CREATE TABLE `{$d}`.`{$table}`", FALSE); $row = current($array); if ( ! isset($row['Create Table'])) @@ -72,9 +73,10 @@ class Util extends AbstractUtil { * @param array $exclude * @return string */ - public function backupData($exclude=[]): string + public function backupData(array $exclude=[]): string { - $tables = $this->getDriver()->getTables(); + $driver = $this->getDriver(); + $tables = $driver->getTables(); // Filter out the tables you don't want if( ! empty($exclude)) @@ -88,7 +90,7 @@ class Util extends AbstractUtil { foreach($tables as $t) { $sql = "SELECT * FROM `{$t}`"; - $res = $this->getDriver()->query($sql); + $res = $driver->query($sql); $rows = $res->fetchAll(PDO::FETCH_ASSOC); // Skip empty tables @@ -110,7 +112,7 @@ class Util extends AbstractUtil { // Workaround for Quercus foreach($row as &$r) { - $r = $this->getDriver()->quote($r); + $r = $driver->quote($r); } unset($r); $row = array_map('trim', $row); diff --git a/src/Drivers/Pgsql/Util.php b/src/Drivers/Pgsql/Util.php index 3c6ead6..3e958a6 100644 --- a/src/Drivers/Pgsql/Util.php +++ b/src/Drivers/Pgsql/Util.php @@ -15,6 +15,7 @@ */ namespace Query\Drivers\Pgsql; +use PDO; use Query\Drivers\AbstractUtil; /** @@ -56,7 +57,7 @@ class Util extends AbstractUtil { { $sql = 'SELECT * FROM "'.trim($t).'"'; $res = $this->getDriver()->query($sql); - $objRes = $res->fetchAll(\PDO::FETCH_ASSOC); + $objRes = $res->fetchAll(PDO::FETCH_ASSOC); // Don't add to the file if the table is empty if (count($objRes) < 1) diff --git a/src/Drivers/SQLInterface.php b/src/Drivers/SQLInterface.php index d1b5d1f..27ee9e8 100644 --- a/src/Drivers/SQLInterface.php +++ b/src/Drivers/SQLInterface.php @@ -25,10 +25,10 @@ interface SQLInterface { * * @param string $sql * @param int $limit - * @param int|bool $offset + * @param int|null $offset * @return string */ - public function limit(string $sql, int $limit, $offset=FALSE): string; + public function limit(string $sql, int $limit, ?int $offset=NULL): string; /** * Modify the query to get the query plan diff --git a/src/Drivers/Sqlite/Driver.php b/src/Drivers/Sqlite/Driver.php index 4230892..c1ced23 100644 --- a/src/Drivers/Sqlite/Driver.php +++ b/src/Drivers/Sqlite/Driver.php @@ -68,7 +68,7 @@ class Driver extends AbstractDriver { */ public function getTables(): array { - $sql = $this->sql->tableList(); + $sql = $this->getSql()->tableList(); $res = $this->query($sql); return dbFilter($res->fetchAll(PDO::FETCH_ASSOC), 'name'); } diff --git a/src/Drivers/Sqlite/Util.php b/src/Drivers/Sqlite/Util.php index e5fb02c..074264a 100644 --- a/src/Drivers/Sqlite/Util.php +++ b/src/Drivers/Sqlite/Util.php @@ -57,7 +57,7 @@ class Util extends AbstractUtil { unset($res); - // If the row is empty, continue; + // If the row is empty, continue if (empty($objRes)) { continue; diff --git a/src/MapType.php b/src/MapType.php new file mode 100644 index 0000000..1d3fabb --- /dev/null +++ b/src/MapType.php @@ -0,0 +1,28 @@ + + * @copyright 2012 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link https://git.timshomepage.net/aviat/Query + * @version 3.0.0 + */ +namespace Query; + +/** + * 'Enum' of query map types + */ +class MapType { + public const GROUP_END = 'group_end'; + public const GROUP_START = 'group_start'; + public const JOIN = 'join'; + public const LIKE = 'like'; + public const WHERE = 'where'; + public const WHERE_IN = 'where_in'; +} \ No newline at end of file diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 51f851f..de083cf 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -16,146 +16,14 @@ namespace Query; use function is_array; -use function regexInArray; +use function is_int; -use BadMethodCallException; -use PDO; use PDOStatement; -use Query\Drivers\DriverInterface; /** * Convenience class for creating sql queries - * - * @method affectedRows(): int - * @method beginTransaction(): bool - * @method commit(): bool - * @method errorCode(): string - * @method errorInfo(): array - * @method exec(string $statement): int - * @method getAttribute(int $attribute) - * @method getColumns(string $table): array | null - * @method getDbs(): array | null - * @method getFks(string $table): array | null - * @method getFunctions(): array | null - * @method getIndexes(string $table): array | null - * @method getLastQuery(): string - * @method getProcedures(): array | null - * @method getSchemas(): array | null - * @method getSequences(): array | null - * @method getSystemTables(): array | null - * @method getTables(): array - * @method getTriggers(): array | null - * @method getTypes(): array | null - * @method getUtil(): \Query\Drivers\AbstractUtil - * @method getVersion(): string - * @method getViews(): array | null - * @method inTransaction(): bool - * @method lastInsertId(string $name = NULL): string - * @method numRows(): int | null - * @method prepare(string $statement, array $driver_options = []): PDOStatement - * @method prepareExecute(string $sql, array $params): PDOStatement - * @method prepareQuery(string $sql, array $data): PDOStatement - * @method query(string $statement): PDOStatement - * @method quote(string $string, int $parameter_type = PDO::PARAM_STR): string - * @method rollback(): bool - * @method setAttribute(int $attribute, $value): bool - * @method setTablePrefix(string $prefix): void - * @method truncate(string $table): PDOStatement */ -class QueryBuilder implements QueryBuilderInterface { - - /** - * Convenience property for connection management - * @var string - */ - public $connName = ''; - - /** - * List of queries executed - * @var array - */ - public $queries = [ - 'total_time' => 0 - ]; - - /** - * Whether to do only an explain on the query - * @var bool - */ - protected $explain = FALSE; - - /** - * Whether to return data from a modification query - * @var bool - */ - protected $returning = FALSE; - - /** - * The current database driver - * @var DriverInterface - */ - protected $driver; - - /** - * Query parser class instance - * @var QueryParser - */ - protected $parser; - - /** - * Query Builder state - * @var State - */ - protected $state; - - // -------------------------------------------------------------------------- - // ! Methods - // -------------------------------------------------------------------------- - - /** - * Constructor - * - * @param DriverInterface $driver - * @param QueryParser $parser - */ - public function __construct(DriverInterface $driver, QueryParser $parser) - { - // Inject driver and parser - $this->driver = $driver; - $this->parser = $parser; - - // Create new State object - $this->state = new State(); - } - - /** - * Destructor - * @codeCoverageIgnore - */ - public function __destruct() - { - $this->driver = NULL; - } - - /** - * Calls a function further down the inheritance chain. - * 'Implements' methods on the driver object - * - * @param string $name - * @param array $params - * @return mixed - * @throws BadMethodCallException - */ - public function __call(string $name, array $params) - { - if (method_exists($this->driver, $name)) - { - return $this->driver->$name(...$params); - } - - throw new BadMethodCallException('Method does not exist'); - } - +class QueryBuilder extends QueryBuilderBase implements QueryBuilderInterface { // -------------------------------------------------------------------------- // ! Select Queries // -------------------------------------------------------------------------- @@ -164,9 +32,9 @@ class QueryBuilder implements QueryBuilderInterface { * Specifies rows to select in a query * * @param string $fields - * @return QueryBuilderInterface + * @return self */ - public function select(string $fields): QueryBuilderInterface + public function select(string $fields): self { // Split fields by comma $fieldsArray = explode(',', $fields); @@ -206,9 +74,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $field * @param string|bool $as - * @return QueryBuilderInterface + * @return self */ - public function selectMax(string $field, $as=FALSE): QueryBuilderInterface + public function selectMax(string $field, $as=FALSE): self { // Create the select string $this->state->appendSelectString(' MAX'.$this->_select($field, $as)); @@ -220,9 +88,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $field * @param string|bool $as - * @return QueryBuilderInterface + * @return self */ - public function selectMin(string $field, $as=FALSE): QueryBuilderInterface + public function selectMin(string $field, $as=FALSE): self { // Create the select string $this->state->appendSelectString(' MIN'.$this->_select($field, $as)); @@ -234,9 +102,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $field * @param string|bool $as - * @return QueryBuilderInterface + * @return self */ - public function selectAvg(string $field, $as=FALSE): QueryBuilderInterface + public function selectAvg(string $field, $as=FALSE): self { // Create the select string $this->state->appendSelectString(' AVG'.$this->_select($field, $as)); @@ -248,9 +116,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $field * @param string|bool $as - * @return QueryBuilderInterface + * @return self */ - public function selectSum(string $field, $as=FALSE): QueryBuilderInterface + public function selectSum(string $field, $as=FALSE): self { // Create the select string $this->state->appendSelectString(' SUM'.$this->_select($field, $as)); @@ -263,7 +131,7 @@ class QueryBuilder implements QueryBuilderInterface { * @param string $fields * @return $this */ - public function returning(string $fields = ''): QueryBuilderInterface + public function returning(string $fields = ''): self { $this->returning = TRUE; @@ -279,9 +147,9 @@ class QueryBuilder implements QueryBuilderInterface { /** * Adds the 'distinct' keyword to a query * - * @return QueryBuilderInterface + * @return self */ - public function distinct(): QueryBuilderInterface + public function distinct(): self { // Prepend the keyword to the select string $this->state->setSelectString(' DISTINCT' . $this->state->getSelectString()); @@ -291,9 +159,9 @@ class QueryBuilder implements QueryBuilderInterface { /** * Tell the database to give you the query plan instead of result set * - * @return QueryBuilderInterface + * @return self */ - public function explain(): QueryBuilderInterface + public function explain(): self { $this->explain = TRUE; return $this; @@ -303,9 +171,9 @@ class QueryBuilder implements QueryBuilderInterface { * Specify the database table to select from * * @param string $tblname - * @return QueryBuilderInterface + * @return self */ - public function from(string $tblname): QueryBuilderInterface + public function from(string $tblname): self { // Split identifiers on spaces $identArray = explode(' ', \mb_trim($tblname)); @@ -331,9 +199,9 @@ class QueryBuilder implements QueryBuilderInterface { * @param string $field * @param mixed $val * @param string $pos - * @return QueryBuilderInterface + * @return self */ - public function like(string $field, $val, string $pos='both'): QueryBuilderInterface + public function like(string $field, $val, string $pos='both'): self { return $this->_like($field, $val, $pos); } @@ -344,9 +212,9 @@ class QueryBuilder implements QueryBuilderInterface { * @param string $field * @param mixed $val * @param string $pos - * @return QueryBuilderInterface + * @return self */ - public function orLike(string $field, $val, string $pos='both'): QueryBuilderInterface + public function orLike(string $field, $val, string $pos='both'): self { return $this->_like($field, $val, $pos, 'LIKE', 'OR'); } @@ -357,9 +225,9 @@ class QueryBuilder implements QueryBuilderInterface { * @param string $field * @param mixed $val * @param string $pos - * @return QueryBuilderInterface + * @return self */ - public function notLike(string $field, $val, string $pos='both'): QueryBuilderInterface + public function notLike(string $field, $val, string $pos='both'): self { return $this->_like($field, $val, $pos, 'NOT LIKE'); } @@ -370,9 +238,9 @@ class QueryBuilder implements QueryBuilderInterface { * @param string $field * @param mixed $val * @param string $pos - * @return QueryBuilderInterface + * @return self */ - public function orNotLike(string $field, $val, string $pos='both'): QueryBuilderInterface + public function orNotLike(string $field, $val, string $pos='both'): self { return $this->_like($field, $val, $pos, 'NOT LIKE', 'OR'); } @@ -386,9 +254,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param mixed $key * @param mixed $val - * @return QueryBuilderInterface + * @return self */ - public function having($key, $val=[]): QueryBuilderInterface + public function having($key, $val=[]): self { return $this->_having($key, $val); } @@ -398,9 +266,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param mixed $key * @param mixed $val - * @return QueryBuilderInterface + * @return self */ - public function orHaving($key, $val=[]): QueryBuilderInterface + public function orHaving($key, $val=[]): self { return $this->_having($key, $val, 'OR'); } @@ -417,9 +285,9 @@ class QueryBuilder implements QueryBuilderInterface { * @param mixed $key * @param mixed $val * @param mixed $escape - * @return QueryBuilderInterface + * @return self */ - public function where($key, $val=[], $escape=NULL): QueryBuilderInterface + public function where($key, $val=[], $escape=NULL): self { return $this->_whereString($key, $val); } @@ -429,9 +297,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $key * @param mixed $val - * @return QueryBuilderInterface + * @return self */ - public function orWhere($key, $val=[]): QueryBuilderInterface + public function orWhere($key, $val=[]): self { return $this->_whereString($key, $val, 'OR'); } @@ -441,9 +309,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param mixed $field * @param mixed $val - * @return QueryBuilderInterface + * @return self */ - public function whereIn($field, $val=[]): QueryBuilderInterface + public function whereIn($field, $val=[]): self { return $this->_whereIn($field, $val); } @@ -453,9 +321,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $field * @param mixed $val - * @return QueryBuilderInterface + * @return self */ - public function orWhereIn($field, $val=[]): QueryBuilderInterface + public function orWhereIn($field, $val=[]): self { return $this->_whereIn($field, $val, 'IN', 'OR'); } @@ -465,9 +333,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $field * @param mixed $val - * @return QueryBuilderInterface + * @return self */ - public function whereNotIn($field, $val=[]): QueryBuilderInterface + public function whereNotIn($field, $val=[]): self { return $this->_whereIn($field, $val, 'NOT IN'); } @@ -477,9 +345,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $field * @param mixed $val - * @return QueryBuilderInterface + * @return self */ - public function orWhereNotIn($field, $val=[]): QueryBuilderInterface + public function orWhereNotIn($field, $val=[]): self { return $this->_whereIn($field, $val, 'NOT IN', 'OR'); } @@ -493,9 +361,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param mixed $key * @param mixed $val - * @return QueryBuilderInterface + * @return self */ - public function set($key, $val = NULL): QueryBuilderInterface + public function set($key, $val = NULL): self { if (is_scalar($key)) { @@ -533,21 +401,21 @@ class QueryBuilder implements QueryBuilderInterface { * @param string $table * @param string $condition * @param string $type - * @return QueryBuilderInterface + * @return self */ - public function join(string $table, string $condition, string $type=''): QueryBuilderInterface + public function join(string $table, string $condition, string $type=''): self { // Prefix and quote table name - $table = explode(' ', mb_trim($table)); - $table[0] = $this->driver->quoteTable($table[0]); - $table = $this->driver->quoteIdent($table); - $table = implode(' ', $table); + $tableArr = explode(' ', mb_trim($table)); + $tableArr[0] = $this->driver->quoteTable($tableArr[0]); + $tableArr = $this->driver->quoteIdent($tableArr); + $table = implode(' ', $tableArr); // Parse out the join condition $parsedCondition = $this->parser->compileJoin($condition); $condition = $table . ' ON ' . $parsedCondition; - $this->state->appendMap("\n" . strtoupper($type) . ' JOIN ', $condition, 'join'); + $this->state->appendMap("\n" . strtoupper($type) . ' JOIN ', $condition, MapType::JOIN); return $this; } @@ -556,16 +424,17 @@ class QueryBuilder implements QueryBuilderInterface { * Group the results by the selected field(s) * * @param mixed $field - * @return QueryBuilderInterface + * @return self */ - public function groupBy($field): QueryBuilderInterface + public function groupBy($field): self { if ( ! is_scalar($field)) { - $newGroupArray = array_map([$this->driver, 'quoteIdent'], $field); - $this->state->setGroupArray( - array_merge($this->state->getGroupArray(), $newGroupArray) + $newGroupArray = array_merge( + $this->state->getGroupArray(), + array_map([$this->driver, 'quoteIdent'], $field) ); + $this->state->setGroupArray($newGroupArray); } else { @@ -582,9 +451,9 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $field * @param string $type - * @return QueryBuilderInterface + * @return self */ - public function orderBy(string $field, string $type=''): QueryBuilderInterface + public function orderBy(string $field, string $type=''): self { // When ordering by random, do an ascending order if the driver // doesn't support random ordering @@ -620,10 +489,10 @@ class QueryBuilder implements QueryBuilderInterface { * Set a limit on the current sql statement * * @param int $limit - * @param int|bool $offset - * @return QueryBuilderInterface + * @param int|null $offset + * @return self */ - public function limit(int $limit, $offset=FALSE): QueryBuilderInterface + public function limit(int $limit, ?int $offset=NULL): self { $this->state->setLimit($limit); $this->state->setOffset($offset); @@ -638,13 +507,13 @@ class QueryBuilder implements QueryBuilderInterface { /** * Adds a paren to the current query for query grouping * - * @return QueryBuilderInterface + * @return self */ - public function groupStart(): QueryBuilderInterface + public function groupStart(): self { $conj = empty($this->state->getQueryMap()) ? ' WHERE ' : ' '; - $this->state->appendMap($conj, '(', 'group_start'); + $this->state->appendMap($conj, '(', MapType::GROUP_START); return $this; } @@ -653,13 +522,13 @@ class QueryBuilder implements QueryBuilderInterface { * Adds a paren to the current query for query grouping, * prefixed with 'NOT' * - * @return QueryBuilderInterface + * @return self */ - public function notGroupStart(): QueryBuilderInterface + public function notGroupStart(): self { $conj = empty($this->state->getQueryMap()) ? ' WHERE ' : ' AND '; - $this->state->appendMap($conj, ' NOT (', 'group_start'); + $this->state->appendMap($conj, ' NOT (', MapType::GROUP_START); return $this; } @@ -668,11 +537,11 @@ class QueryBuilder implements QueryBuilderInterface { * Adds a paren to the current query for query grouping, * prefixed with 'OR' * - * @return QueryBuilderInterface + * @return self */ - public function orGroupStart(): QueryBuilderInterface + public function orGroupStart(): self { - $this->state->appendMap('', ' OR (', 'group_start'); + $this->state->appendMap('', ' OR (', MapType::GROUP_START); return $this; } @@ -681,11 +550,11 @@ class QueryBuilder implements QueryBuilderInterface { * Adds a paren to the current query for query grouping, * prefixed with 'OR NOT' * - * @return QueryBuilderInterface + * @return self */ - public function orNotGroupStart(): QueryBuilderInterface + public function orNotGroupStart(): self { - $this->state->appendMap('', ' OR NOT (', 'group_start'); + $this->state->appendMap('', ' OR NOT (', MapType::GROUP_START); return $this; } @@ -693,11 +562,11 @@ class QueryBuilder implements QueryBuilderInterface { /** * Ends a query group * - * @return QueryBuilderInterface + * @return self */ - public function groupEnd(): QueryBuilderInterface + public function groupEnd(): self { - $this->state->appendMap('', ')', 'group_end'); + $this->state->appendMap('', ')', MapType::GROUP_END); return $this; } @@ -711,11 +580,11 @@ class QueryBuilder implements QueryBuilderInterface { * execute current compiled query * * @param string $table - * @param int|bool $limit - * @param int|bool $offset + * @param int|null $limit + * @param int|null $offset * @return PDOStatement */ - public function get(string $table='', $limit=FALSE, $offset=FALSE): PDOStatement + public function get(string $table='', ?int $limit=NULL, ?int $offset=NULL): PDOStatement { // Set the table if ( ! empty($table)) @@ -724,7 +593,7 @@ class QueryBuilder implements QueryBuilderInterface { } // Set the limit, if it exists - if (\is_int($limit)) + if (is_int($limit)) { $this->limit($limit, $offset); } @@ -737,11 +606,11 @@ class QueryBuilder implements QueryBuilderInterface { * * @param string $table * @param mixed $where - * @param int|bool $limit - * @param int|bool $offset + * @param int|null $limit + * @param int|null $offset * @return PDOStatement */ - public function getWhere(string $table, $where=[], $limit=FALSE, $offset=FALSE): PDOStatement + public function getWhere(string $table, $where=[], ?int $limit=NULL, ?int $offset=NULL): PDOStatement { // Create the where clause $this->where($where); @@ -809,13 +678,11 @@ class QueryBuilder implements QueryBuilderInterface { * @param array $data * @return PDOStatement */ - public function insertBatch(string $table, $data=[]): PDOStatement + public function insertBatch(string $table, $data=[]): ?PDOStatement { // Get the generated values and sql string [$sql, $data] = $this->driver->insertBatch($table, $data); - - return $sql !== NULL ? $this->_run('', $table, $sql, $data) : NULL; @@ -936,472 +803,4 @@ class QueryBuilder implements QueryBuilderInterface { { return $this->_getCompile(QueryType::DELETE, $table, $reset); } - - // -------------------------------------------------------------------------- - // ! Miscellaneous Methods - // -------------------------------------------------------------------------- - - /** - * Clear out the class variables, so the next query can be run - * - * @return void - */ - public function resetQuery(): void - { - $this->state = new State(); - $this->explain = FALSE; - $this->returning = FALSE; - } - - /** - * 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)) - { - // @codeCoverageIgnoreStart - return $field; - // @codeCoverageIgnoreEnd - } - - $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 QueryBuilderInterface - */ - protected function _like(string $field, $val, string $pos, string $like='LIKE', string $conj='AND'): QueryBuilderInterface - { - $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->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; - $this->state->appendMap($conj, $like, 'like'); - - // Add to the values array - $this->state->appendWhereValues($val); - - return $this; - } - - /** - * Simplify building having clauses - * - * @param mixed $key - * @param mixed $values - * @param string $conj - * @return QueryBuilderInterface - */ - protected function _having($key, $values=[], string $conj='AND'): QueryBuilderInterface - { - $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->state->appendHavingMap([ - 'conjunction' => empty($this->state->getHavingMap()) - ? ' HAVING ' - : " {$conj} ", - '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 = []; - $pairs = []; - - if (is_scalar($key)) - { - $pairs[$key] = $val; - } - else - { - $pairs = $key; - } - - foreach($pairs as $k => $v) - { - $where[$k] = $v; - $this->state->appendWhereValues($v); - } - - return $where; - } - - /** - * Simplify generating where string - * - * @param mixed $key - * @param mixed $values - * @param string $defaultConj - * @return QueryBuilderInterface - */ - protected function _whereString($key, $values=[], string $defaultConj='AND'): QueryBuilderInterface - { - // Create key/value placeholders - foreach($this->_where($key, $values) as $f => $val) - { - $queryMap = $this->state->getQueryMap(); - - // 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($queryMap); - - // Determine the correct conjunction - $conjunctionList = array_column($queryMap, 'conjunction'); - if (empty($queryMap) || ( ! regexInArray($conjunctionList, "/^ ?\n?WHERE/i"))) - { - $conj = "\nWHERE "; - } - elseif ($lastItem['type'] === 'group_start') - { - $conj = ''; - } - else - { - $conj = " {$defaultConj} "; - } - - $this->state->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 QueryBuilderInterface - */ - protected function _whereIn($key, $val=[], string $in='IN', string $conj='AND'): QueryBuilderInterface - { - $key = $this->driver->quoteIdent($key); - $params = array_fill(0, count($val), '?'); - $this->state->appendWhereValues($val); - - $conjunction = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; - $str = $key . " {$in} (".implode(',', $params).') '; - - $this->state->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, string $sql=NULL, array $vals=NULL, bool $reset=TRUE): PDOStatement - { - if ($sql === NULL) - { - $sql = $this->_compile($type, $table); - } - - if ($vals === NULL) - { - $vals = array_merge($this->state->getValues(), $this->state->getWhereValues()); - } - - $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; - } - - /** - * Convert the prepared statement into readable sql - * - * @param array $values - * @param string $sql - * @param int $totalTime - * @return void - */ - protected function _appendQuery(array $values, string $sql, int $totalTime): void - { - $evals = is_array($values) ? $values : []; - $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; - } - unset($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 - * - * @codeCoverageIgnore - * @param string $type - * @param string $table - * @return string - */ - protected function _compileType(string $type=QueryType::SELECT, string $table=''): string - { - $setArrayKeys = $this->state->getSetArrayKeys(); - switch($type) - { - case QueryType::INSERT: - $paramCount = count($setArrayKeys); - $params = array_fill(0, $paramCount, '?'); - $sql = "INSERT INTO {$table} (" - . implode(',', $setArrayKeys) - . ")\nVALUES (".implode(',', $params).')'; - break; - - case QueryType::UPDATE: - $setString = $this->state->getSetString(); - $sql = "UPDATE {$table}\nSET {$setString}"; - break; - - case QueryType::DELETE: - $sql = "DELETE FROM {$table}"; - break; - - case QueryType::SELECT: - default: - $fromString = $this->state->getFromString(); - $selectString = $this->state->getSelectString(); - - $sql = "SELECT * \nFROM {$fromString}"; - - // Set the select string - if ( ! empty($selectString)) - { - // Replace the star with the selected fields - $sql = str_replace('*', $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) - { - $func = 'get' . ucFirst($clause); - $param = $this->state->$func(); - if (is_array($param)) - { - foreach($param as $q) - { - $sql .= $q['conjunction'] . $q['string']; - } - } - else - { - $sql .= $param; - } - } - - // Set the limit via the class variables - $limit = $this->state->getLimit(); - if (is_numeric($limit)) - { - $sql = $this->driver->getSql()->limit($sql, $limit, $this->state->getOffset()); - } - - // Set the returning clause, if applicable - $sql = $this->_compileReturning($sql, $type); - - // See if the query plan, rather than the - // query data should be returned - if ($this->explain === TRUE) - { - $sql = $this->driver->getSql()->explain($sql); - } - - return $sql; - } - - /** - * Generate returning clause of query - * - * @param string $sql - * @param string $type - * @return string - */ - protected function _compileReturning(string $sql, string $type): string - { - if ($this->returning === FALSE) - { - return $sql; - } - - $rawSelect = $this->state->getSelectString(); - $selectString = ($rawSelect === '') ? '*' : $rawSelect; - $returningSQL = $this->driver->returning($sql, $selectString); - - if ($returningSQL === $sql) - { - // If the driver doesn't support the returning clause, it returns the original query. - // Fake the same result with a transaction and a select query - if ( ! $this->inTransaction()) - { - $this->beginTransaction(); - } - - // Generate the appropriate select query for the returning clause fallback - switch ($type) - { - case QueryType::INSERT: - // @TODO figure out a good response for insert query - break; - - case QueryType::UPDATE: - // @TODO figure out a good response for update query - break; - - case QueryType::DELETE: - // @TODO Figure out a good response for delete query - break; - } - } - - return $returningSQL; - } } diff --git a/src/QueryBuilderBase.php b/src/QueryBuilderBase.php new file mode 100644 index 0000000..30f9a4e --- /dev/null +++ b/src/QueryBuilderBase.php @@ -0,0 +1,618 @@ + + * @copyright 2012 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link https://git.timshomepage.net/aviat/Query + * @version 3.0.0 + */ +namespace Query; + +use function regexInArray; + +use BadMethodCallException; +use PDO; +use PDOStatement; +use Query\Drivers\DriverInterface; + +/** + * @method affectedRows(): int + * @method beginTransaction(): bool + * @method commit(): bool + * @method errorCode(): string + * @method errorInfo(): array + * @method exec(string $statement): int + * @method getAttribute(int $attribute) + * @method getColumns(string $table): array | null + * @method getDbs(): array | null + * @method getFks(string $table): array | null + * @method getFunctions(): array | null + * @method getIndexes(string $table): array | null + * @method getLastQuery(): string + * @method getProcedures(): array | null + * @method getSchemas(): array | null + * @method getSequences(): array | null + * @method getSystemTables(): array | null + * @method getTables(): array + * @method getTriggers(): array | null + * @method getTypes(): array | null + * @method getUtil(): \Query\Drivers\AbstractUtil + * @method getVersion(): string + * @method getViews(): array | null + * @method inTransaction(): bool + * @method lastInsertId(string $name = NULL): string + * @method numRows(): int | null + * @method prepare(string $statement, array $driver_options = []): PDOStatement + * @method prepareExecute(string $sql, array $params): PDOStatement + * @method prepareQuery(string $sql, array $data): PDOStatement + * @method query(string $statement): PDOStatement + * @method quote(string $string, int $parameter_type = PDO::PARAM_STR): string + * @method rollback(): bool + * @method setAttribute(int $attribute, $value): bool + * @method setTablePrefix(string $prefix): void + * @method truncate(string $table): PDOStatement + */ +class QueryBuilderBase { + + /** + * Convenience property for connection management + * @var string + */ + public string $connName = ''; + + /** + * List of queries executed + * @var array + */ + public array $queries = [ + 'total_time' => 0 + ]; + + /** + * Whether to do only an explain on the query + * @var bool + */ + protected bool $explain = FALSE; + + /** + * Whether to return data from a modification query + * @var bool + */ + protected bool $returning = FALSE; + + /** + * The current database driver + * @var DriverInterface + */ + protected ?DriverInterface $driver; + + /** + * Query parser class instance + * @var QueryParser + */ + protected QueryParser $parser; + + /** + * Query Builder state + * @var State + */ + protected State $state; + + // -------------------------------------------------------------------------- + // ! Methods + // -------------------------------------------------------------------------- + + /** + * Constructor + * + * @param DriverInterface $driver + * @param QueryParser $parser + */ + public function __construct(DriverInterface $driver, QueryParser $parser) + { + // Inject driver and parser + $this->driver = $driver; + $this->parser = $parser; + + // Create new State object + $this->state = new State(); + } + + /** + * Destructor + * @codeCoverageIgnore + */ + public function __destruct() + { + $this->driver = NULL; + } + + /** + * Calls a function further down the inheritance chain. + * 'Implements' methods on the driver object + * + * @param string $name + * @param array $params + * @return mixed + * @throws BadMethodCallException + */ + public function __call(string $name, array $params) + { + if (method_exists($this->driver, $name)) + { + return $this->driver->$name(...$params); + } + + throw new BadMethodCallException('Method does not exist'); + } + + /** + * Clear out the class variables, so the next query can be run + * + * @return void + */ + public function resetQuery(): void + { + $this->state = new State(); + $this->explain = FALSE; + $this->returning = FALSE; + } + + /** + * 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)) + { + // @codeCoverageIgnoreStart + return $field; + // @codeCoverageIgnoreEnd + } + + $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->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; + $this->state->appendMap($conj, $like, MapType::LIKE); + + // Add to the values array + $this->state->appendWhereValues($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->state->appendHavingMap([ + 'conjunction' => empty($this->state->getHavingMap()) + ? ' HAVING ' + : " {$conj} ", + '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 = []; + $pairs = []; + + if (is_scalar($key)) + { + $pairs[$key] = $val; + } else + { + $pairs = $key; + } + + foreach ($pairs as $k => $v) + { + $where[$k] = $v; + $this->state->appendWhereValues($v); + } + + 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) + { + $queryMap = $this->state->getQueryMap(); + + // 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($queryMap); + + // Determine the correct conjunction + $conjunctionList = array_column($queryMap, 'conjunction'); + if (empty($queryMap) || ( ! regexInArray($conjunctionList, "/^ ?\n?WHERE/i"))) + { + $conj = "\nWHERE "; + } elseif ($lastItem['type'] === 'group_start') + { + $conj = ''; + } else + { + $conj = " {$defaultConj} "; + } + + $this->state->appendMap($conj, $item, MapType::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), '?'); + $this->state->appendWhereValues($val); + + $conjunction = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; + $str = $key . " {$in} (" . implode(',', $params) . ') '; + + $this->state->appendMap($conjunction, $str, MapType::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, string $sql = NULL, array $vals = NULL, bool $reset = TRUE): PDOStatement + { + if ($sql === NULL) + { + $sql = $this->_compile($type, $table); + } + + if ($vals === NULL) + { + $vals = array_merge($this->state->getValues(), $this->state->getWhereValues()); + } + + $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; + } + + /** + * Convert the prepared statement into readable sql + * + * @param array $values + * @param string $sql + * @param int $totalTime + * @return void + */ + protected function _appendQuery(array $values, string $sql, int $totalTime): void + { + $evals = is_array($values) ? $values : []; + $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; + } + unset($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 + * + * @codeCoverageIgnore + * @param string $type + * @param string $table + * @return string + */ + protected function _compileType(string $type = QueryType::SELECT, string $table = ''): string + { + $setArrayKeys = $this->state->getSetArrayKeys(); + switch ($type) + { + case QueryType::INSERT: + $paramCount = count($setArrayKeys); + $params = array_fill(0, $paramCount, '?'); + $sql = "INSERT INTO {$table} (" + . implode(',', $setArrayKeys) + . ")\nVALUES (" . implode(',', $params) . ')'; + break; + + case QueryType::UPDATE: + $setString = $this->state->getSetString(); + $sql = "UPDATE {$table}\nSET {$setString}"; + break; + + case QueryType::DELETE: + $sql = "DELETE FROM {$table}"; + break; + + case QueryType::SELECT: + default: + $fromString = $this->state->getFromString(); + $selectString = $this->state->getSelectString(); + + $sql = "SELECT * \nFROM {$fromString}"; + + // Set the select string + if ( ! empty($selectString)) + { + // Replace the star with the selected fields + $sql = str_replace('*', $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) + { + $func = 'get' . ucFirst($clause); + $param = $this->state->$func(); + if (is_array($param)) + { + foreach ($param as $q) + { + $sql .= $q['conjunction'] . $q['string']; + } + } else + { + $sql .= $param; + } + } + + // Set the limit via the class variables + $limit = $this->state->getLimit(); + if (is_numeric($limit)) + { + $sql = $this->driver->getSql()->limit($sql, $limit, $this->state->getOffset()); + } + + // Set the returning clause, if applicable + $sql = $this->_compileReturning($sql, $type); + + // See if the query plan, rather than the + // query data should be returned + if ($this->explain === TRUE) + { + $sql = $this->driver->getSql()->explain($sql); + } + + return $sql; + } + + /** + * Generate returning clause of query + * + * @param string $sql + * @param string $type + * @return string + */ + protected function _compileReturning(string $sql, string $type): string + { + if ($this->returning === FALSE) + { + return $sql; + } + + $rawSelect = $this->state->getSelectString(); + $selectString = ($rawSelect === '') ? '*' : $rawSelect; + $returningSQL = $this->driver->returning($sql, $selectString); + + if ($returningSQL === $sql) + { + // If the driver doesn't support the returning clause, it returns the original query. + // Fake the same result with a transaction and a select query + if ( ! $this->inTransaction()) + { + $this->beginTransaction(); + } + + // Generate the appropriate select query for the returning clause fallback + switch ($type) + { + case QueryType::INSERT: + // @TODO figure out a good response for insert query + break; + + case QueryType::UPDATE: + // @TODO figure out a good response for update query + break; + + case QueryType::INSERT_BATCH: + case QueryType::UPDATE_BATCH: + // @TODO figure out a good response for batch queries + break; + + default: + // On Delete queries, what would we return? + break; + } + } + + return $returningSQL; + } +} \ No newline at end of file diff --git a/src/QueryBuilderInterface.php b/src/QueryBuilderInterface.php index 46b7c8d..b55d033 100644 --- a/src/QueryBuilderInterface.php +++ b/src/QueryBuilderInterface.php @@ -107,14 +107,6 @@ interface QueryBuilderInterface { */ public function selectSum(string $field, $as=FALSE): self; - /** - * Add a 'returning' clause to an insert,update, or delete query - * - * @param string $fields - * @return self - */ - public function returning(string $fields = '*'): self; - /** * Adds the 'distinct' keyword to a query * @@ -308,10 +300,10 @@ interface QueryBuilderInterface { * Set a limit on the current sql statement * * @param int $limit - * @param int|bool $offset + * @param int|null $offset * @return self */ - public function limit(int $limit, $offset=FALSE): self; + public function limit(int $limit, ?int $offset=NULL): self; // -------------------------------------------------------------------------- // ! Query Grouping Methods @@ -364,22 +356,22 @@ interface QueryBuilderInterface { * execute current compiled query * * @param string $table - * @param int|bool $limit - * @param int|bool $offset + * @param int|null $limit + * @param int|null $offset * @return PDOStatement */ - public function get(string $table='', $limit=FALSE, $offset=FALSE): PDOStatement; + public function get(string $table='', ?int $limit=NULL, ?int $offset=NULL): PDOStatement; /** * Convenience method for get() with a where clause * * @param string $table * @param array $where - * @param int|bool $limit - * @param int|bool $offset + * @param int|null $limit + * @param int|null $offset * @return PDOStatement */ - public function getWhere(string $table, $where=[], $limit=FALSE, $offset=FALSE): PDOStatement; + public function getWhere(string $table, $where=[], ?int $limit=NULL, ?int $offset=NULL): PDOStatement; /** * Retrieve the number of rows in the selected table @@ -413,9 +405,9 @@ interface QueryBuilderInterface { * * @param string $table * @param array $data - * @return PDOStatement + * @return PDOStatement | null */ - public function insertBatch(string $table, $data=[]): PDOStatement; + public function insertBatch(string $table, $data=[]): ?PDOStatement; /** * Creates an update clause, and executes it diff --git a/src/QueryType.php b/src/QueryType.php index f79e5e9..706c2dc 100644 --- a/src/QueryType.php +++ b/src/QueryType.php @@ -21,6 +21,8 @@ namespace Query; class QueryType { public const SELECT = 'select'; public const INSERT = 'insert'; + public const INSERT_BATCH = 'insert_batch'; public const UPDATE = 'update'; + public const UPDATE_BATCH = 'update_batch'; public const DELETE = 'delete'; } \ No newline at end of file diff --git a/src/State.php b/src/State.php index f6a539b..8b33f61 100644 --- a/src/State.php +++ b/src/State.php @@ -15,6 +15,8 @@ */ namespace Query; +use function is_array; + /** * Query builder state * @@ -28,14 +30,20 @@ namespace Query; * @method getGroupArray(): array * @method getValues(): array * @method getWhereValues(): array - * @method getLimit(): int + * @method getLimit(): int|null * @method getOffset() * @method getQueryMap(): array * @method getHavingMap(): array * - * @method setSelectString(string): self - * @method setFromString(string): self - * @method setSetString(string): self + * @method setSelectString(string $selectString): self + * @method setFromString(string $fromString): self + * @method setSetString(string $setString): self + * @method setOrderString(string $orderString): self + * @method setGroupString(string $groupString): self + * @method setSetArrayKeys(array $arrayKeys): self + * @method setGroupArray(array $array): self + * @method setLimit(int $limit): self + * @method setOffset(?int $offset): self */ class State { // -------------------------------------------------------------------------- @@ -112,15 +120,15 @@ class State { /** * Value for limit string - * @var integer + * @var int */ - protected int $limit; + protected ?int $limit = NULL; /** * Value for offset in limit string - * @var string|false + * @var int */ - protected $offset = FALSE; + protected ?int $offset = NULL; /** * Query component order mapping @@ -177,26 +185,6 @@ class State { return $this; } - /** - * @param string $orderString - * @return State - */ - public function setOrderString(string $orderString): self - { - $this->orderString = $orderString; - return $this; - } - - /** - * @param string $groupString - * @return State - */ - public function setGroupString(string $groupString): self - { - $this->groupString = $groupString; - return $this; - } - /** * @param array $setArrayKeys * @return State @@ -207,16 +195,6 @@ class State { return $this; } - /** - * @param array $setArrayKeys - * @return State - */ - public function setSetArrayKeys(array $setArrayKeys): self - { - $this->setArrayKeys = $setArrayKeys; - return $this; - } - /** * @param string $key * @param mixed $orderArray @@ -228,16 +206,6 @@ class State { return $this; } - /** - * @param array $groupArray - * @return State - */ - public function setGroupArray(array $groupArray): self - { - $this->groupArray = $groupArray; - return $this; - } - /** * @param string $groupArray * @return State @@ -264,7 +232,7 @@ class State { */ public function appendWhereValues($val): self { - if (\is_array($val)) + if (is_array($val)) { foreach($val as $v) { @@ -278,26 +246,6 @@ class State { return $this; } - /** - * @param int $limit - * @return State - */ - public function setLimit(int $limit): self - { - $this->limit = $limit; - return $this; - } - - /** - * @param string|false $offset - * @return State - */ - public function setOffset($offset): self - { - $this->offset = $offset; - return $this; - } - /** * Add an additional set of mapping pairs to a internal map * diff --git a/src/common.php b/src/common.php index 935326e..53501a8 100644 --- a/src/common.php +++ b/src/common.php @@ -15,10 +15,8 @@ */ namespace { - use Query\{ - ConnectionManager, - QueryBuilderInterface - }; + use Query\ConnectionManager; + use Query\QueryBuilderInterface; /** * Global functions that don't really fit anywhere else @@ -139,6 +137,5 @@ namespace { // Otherwise, return a new connection return $manager->connect($paramsObject); } - } // End of common.php