├── .gitignore ├── tests ├── unit │ ├── bootstrap.php │ ├── TestCase.php │ └── QueryBuilder │ │ └── QueryBuilder │ │ └── QueryBuilderTest.php └── test │ ├── autoload.php │ └── index.php ├── src └── QueryBuilder │ ├── Schema │ ├── Dblib.php │ ├── Schema.php │ ├── Oci.php │ ├── Cubrid.php │ ├── Mssql.php │ ├── Mysql.php │ ├── Pgsql.php │ └── Sqlite.php │ ├── ConnectionAdapters │ ├── Dblib.php │ ├── Sqlite.php │ ├── PdoBridge.php │ ├── Oci.php │ ├── Cubrid.php │ ├── Mssql.php │ ├── BaseAdapter.php │ ├── Pgsql.php │ └── Mysql.php │ ├── Exception │ ├── Exception.php │ └── QueryExecutionFailException.php │ ├── Event │ ├── EventDispatcherInterface.php │ ├── Bridge │ │ └── Symfony │ │ │ ├── SymfonyEvent.php │ │ │ └── SymfonyEventDispatcherBridge.php │ └── EventDispatcher.php │ ├── QueryBuilder │ ├── Transaction.php │ ├── NestedCriteria.php │ ├── Raw.php │ ├── QueryBuilderFactory.php │ ├── JoinBuilder.php │ ├── Query.php │ ├── Compiler.php │ └── QueryBuilder.php │ ├── Container.php │ ├── Parameters.php │ └── Connection.php ├── phpunit.xml ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.phar 3 | composer.lock 4 | *~ 5 | -------------------------------------------------------------------------------- /tests/unit/bootstrap.php: -------------------------------------------------------------------------------- 1 | commitTransaction(); 16 | } 17 | 18 | public function rollback() 19 | { 20 | $this->rollbackTransaction(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryBuilder/Schema/Oci.php: -------------------------------------------------------------------------------- 1 | quoteColumnName($name); 16 | } 17 | 18 | public function quoteColumnName($name) 19 | { 20 | return $name == '*' ? $name : '"'.$name.'"'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryBuilder/ConnectionAdapters/Sqlite.php: -------------------------------------------------------------------------------- 1 | getConfigValue('database'), null, null, $config['options']); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/QueryBuilder/Schema/Cubrid.php: -------------------------------------------------------------------------------- 1 | quoteColumnName($name); 16 | } 17 | 18 | public function quoteColumnName($name) 19 | { 20 | return $name == '*' ? $name : '"'.$name.'"'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryBuilder/Schema/Mssql.php: -------------------------------------------------------------------------------- 1 | quoteColumnName($name); 16 | } 17 | 18 | public function quoteColumnName($name) 19 | { 20 | return $name == '*' ? $name : '['.$name.']'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryBuilder/Schema/Mysql.php: -------------------------------------------------------------------------------- 1 | quoteColumnName($name); 16 | } 17 | 18 | public function quoteColumnName($name) 19 | { 20 | return $name == '*' ? $name : '`'.$name.'`'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryBuilder/Schema/Pgsql.php: -------------------------------------------------------------------------------- 1 | quoteColumnName($name); 16 | } 17 | 18 | public function quoteColumnName($name) 19 | { 20 | return $name == '*' ? $name : '"'.$name.'"'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryBuilder/Schema/Sqlite.php: -------------------------------------------------------------------------------- 1 | quoteColumnName($name); 16 | } 17 | 18 | public function quoteColumnName($name) 19 | { 20 | return $name == '*' ? $name : '`'.$name.'`'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | tests/unit/QueryBuilder/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/QueryBuilder/Container.php: -------------------------------------------------------------------------------- 1 | connections[$name] = $connection; 21 | 22 | return $this; 23 | } 24 | 25 | public function getConnection($name) 26 | { 27 | return $this->connections[$name]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/QueryBuilder/Event/Bridge/Symfony/SymfonyEvent.php: -------------------------------------------------------------------------------- 1 | result = $result; 20 | 21 | return $this; 22 | } 23 | 24 | public function getResult() 25 | { 26 | return $this->result; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/QueryBuilder/ConnectionAdapters/PdoBridge.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 20 | } 21 | 22 | public function getDriverName() 23 | { 24 | return $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME); 25 | } 26 | 27 | protected function doConnect() 28 | { 29 | return $this->pdo; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/QueryBuilder/QueryBuilder/NestedCriteria.php: -------------------------------------------------------------------------------- 1 | querySegments['criteria'][] = [ 16 | 'key' => $this->addTablePrefix($key), 17 | 'operator' => $operator, 18 | 'value' => $value, 19 | 'joiner' => $joiner 20 | ]; 21 | 22 | return $this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/QueryBuilder/QueryBuilder/Raw.php: -------------------------------------------------------------------------------- 1 | value = (string) $value; 20 | $this->bindings = $bindings; 21 | } 22 | 23 | public function getBindings() 24 | { 25 | return $this->bindings; 26 | } 27 | 28 | public function __toString() 29 | { 30 | return (string) $this->value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requtize/query-builder", 3 | "description": "Fast, simple and dependency-free query builder for PHP.", 4 | "homepage": "https://github.com/requtize/query-builder", 5 | "keywords": [ "query builder", "database", "sql" ], 6 | "license": "MIT", 7 | "version": "1.0.1", 8 | "authors": [ 9 | { 10 | "name": "Adam Banaszkiewicz", 11 | "homepage": "http://adambanaszkiewicz.pl", 12 | "role": "Developer" 13 | } 14 | ], 15 | "require": { 16 | "php": "~5.5 || ^7.0 || ~8.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^4.8", 20 | "mockery/mockery": "0.9.4" 21 | }, 22 | "autoload": { 23 | "psr-4": {"Requtize\\QueryBuilder\\": "src/QueryBuilder/"} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryBuilder/ConnectionAdapters/Oci.php: -------------------------------------------------------------------------------- 1 | getConfigValue('database'); 20 | 21 | if($this->getConfigValue('host')) 22 | $url .= ';host='.$this->getConfigValue('host'); 23 | 24 | if($this->getConfigValue('port')) 25 | $url .= '";port='.$this->getConfigValue('port'); 26 | 27 | return new PDO($url, $this->getConfigValue('username'), $this->getConfigValue('password'), $this->getConfigValue('options')); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/QueryBuilder/ConnectionAdapters/Cubrid.php: -------------------------------------------------------------------------------- 1 | getConfigValue('database'); 20 | 21 | if($this->getConfigValue('host')) 22 | $url .= ';host='.$this->getConfigValue('host'); 23 | 24 | if($this->getConfigValue('port')) 25 | $url .= ';port='.$this->getConfigValue('port'); 26 | 27 | return new PDO($url, $this->getConfigValue('username'), $this->getConfigValue('password'), $this->getConfigValue('options')); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/QueryBuilder/ConnectionAdapters/Mssql.php: -------------------------------------------------------------------------------- 1 | getConfigValue('database'); 20 | 21 | if($this->getConfigValue('host')) 22 | $url .= ';host='.$this->getConfigValue('host'); 23 | 24 | if($this->getConfigValue('port')) 25 | $url .= ';port='.$this->getConfigValue('port'); 26 | 27 | return new PDO($url, $this->getConfigValue('username'), $this->getConfigValue('password'), $this->getConfigValue('options')); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/QueryBuilder/ConnectionAdapters/BaseAdapter.php: -------------------------------------------------------------------------------- 1 | config = $config; 20 | 21 | return $this->doConnect($config); 22 | } 23 | 24 | public function getDriverName() 25 | { 26 | return $this->driver; 27 | } 28 | 29 | public function getConfig() 30 | { 31 | return $this->config; 32 | } 33 | 34 | public function getConfigValue($name, $default = null) 35 | { 36 | return isset($this->config[$name]) ? $this->config[$name] : $default; 37 | } 38 | 39 | abstract protected function doConnect(); 40 | } 41 | -------------------------------------------------------------------------------- /src/QueryBuilder/QueryBuilder/QueryBuilderFactory.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 19 | $this->eventDispatcher = $eventDispatcher; 20 | } 21 | 22 | public function __call($name, $args) 23 | { 24 | return call_user_func_array([ $this->create(), $name ], $args); 25 | } 26 | 27 | public function create() 28 | { 29 | return new QueryBuilder($this->connection, $this->eventDispatcher); 30 | } 31 | 32 | public function getEventDispatcher() 33 | { 34 | return $this->eventDispatcher; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Adam Banaszkiewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/QueryBuilder/ConnectionAdapters/Pgsql.php: -------------------------------------------------------------------------------- 1 | getConfigValue('host').';dbname='.$this->getConfigValue('database'); 20 | 21 | if($this->getConfigValue('port')) 22 | $url .= ';port='.$this->getConfigValue('port'); 23 | 24 | $connection = new PDO($url, $this->getConfigValue('username'), $this->getConfigValue('password'), $this->getConfigValue('options')); 25 | 26 | if($this->getConfigValue('charset')) 27 | $connection->prepare("SET NAMES '".$this->getConfigValue('charset')."'")->execute(); 28 | 29 | if($this->getConfigValue('schema')) 30 | $connection->prepare("SET search_path TO '".$this->getConfigValue('schema')."'")->execute(); 31 | 32 | return $connection; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/QueryBuilder/QueryBuilder/JoinBuilder.php: -------------------------------------------------------------------------------- 1 | joinHandler($key, $operator, $value, 'AND'); 16 | } 17 | 18 | public function orOn($key, $operator, $value = null) 19 | { 20 | return $this->joinHandler($key, $operator, $value, 'OR'); 21 | } 22 | 23 | protected function joinHandler($key, $operator = null, $value = null, $joiner = 'AND') 24 | { 25 | if($key && $operator && ! $value) 26 | { 27 | $value = $operator; 28 | $operator = '='; 29 | } 30 | 31 | $this->querySegments['criteria'][] = [ 32 | 'key' => $this->addTablePrefix($key), 33 | 'operator' => $operator, 34 | 'value' => $this->addTablePrefix($value), 35 | 'joiner' => $joiner 36 | ]; 37 | 38 | return $this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/test/index.php: -------------------------------------------------------------------------------- 1 | 'pref_' 18 | ]); 19 | 20 | //$container->addConnection('default', $connection); 21 | 22 | $qb = new QueryBuilderFactory($connection, new EventDispatcher); 23 | 24 | $qb->getEventDispatcher()->registerEventListener('before-select', function (Parameters $parameters) { 25 | //var_dump($parameters->get('query-builder')); 26 | }); 27 | 28 | $result = $qb->from('user') 29 | ->select('*') 30 | ->where('id', '<', 3) 31 | ->where(function($q) { 32 | $q->where('id', 'username') 33 | ->orWhereNotNull('id'); 34 | }) 35 | ->join('user', 'id', 1) 36 | ->limit(5) 37 | ->offset(0); 38 | 39 | var_dump($result->getQuery()->__toString()); 40 | -------------------------------------------------------------------------------- /src/QueryBuilder/ConnectionAdapters/Mysql.php: -------------------------------------------------------------------------------- 1 | getConfigValue('database'); 20 | 21 | if($this->getConfigValue('host')) 22 | $url .= ';host='.$this->getConfigValue('host'); 23 | 24 | if($this->getConfigValue('port')) 25 | $url .= ';port='.$this->getConfigValue('port'); 26 | 27 | if($this->getConfigValue('unix_socket')) 28 | $url .= ';unix_socket='.$this->getConfigValue('unix_socket'); 29 | 30 | $connection = new PDO($url, $this->getConfigValue('username'), $this->getConfigValue('password'), $this->getConfigValue('options')); 31 | 32 | if($this->getConfigValue('charset')) 33 | $connection->prepare('SET NAMES "'.$this->getConfigValue('charset').'"')->execute(); 34 | 35 | return $connection; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/QueryBuilder/Event/EventDispatcher.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 22 | 23 | return $this; 24 | } 25 | 26 | public function registerEventListener($event, \Closure $listener) 27 | { 28 | $this->listeners[$event][] = $listener; 29 | 30 | return $this; 31 | } 32 | 33 | public function removeEventListeners($event) 34 | { 35 | unset($this->listeners[$event]); 36 | 37 | return $this; 38 | } 39 | 40 | public function dispatch($event, array $params = []) 41 | { 42 | if(isset($this->listeners[$event])) 43 | { 44 | $parameters = new Parameters($params); 45 | 46 | foreach($this->listeners[$event] as $listener) 47 | { 48 | $result = call_user_func_array($listener, [ $parameters ]); 49 | 50 | if($result !== null) 51 | { 52 | return $result; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/QueryBuilder/Event/Bridge/Symfony/SymfonyEventDispatcherBridge.php: -------------------------------------------------------------------------------- 1 | symfonyEventDispatcher = $dispatcher; 21 | } 22 | 23 | public function registerEventListener($event, \Closure $listener) 24 | { 25 | $this->symfonyEventDispatcher->addListener($this->createEventName($event), $listener); 26 | } 27 | 28 | public function removeEventListeners($event) 29 | { 30 | 31 | } 32 | 33 | public function dispatch($event, array $params = []) 34 | { 35 | $symfonyEvent = new SymfonyEvent($event, $params); 36 | 37 | $this->symfonyEventDispatcher->dispatch($this->createEventName($event), $symfonyEvent); 38 | 39 | return $symfonyEvent->getResult(); 40 | } 41 | 42 | public function createEventName($event) 43 | { 44 | $event = $this->sanitizeString($event); 45 | 46 | return 'query_builder.'.$event; 47 | } 48 | 49 | public function sanitizeString($string) 50 | { 51 | return preg_replace('/[^a-z0-9\_]/i', '_', $string); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/unit/TestCase.php: -------------------------------------------------------------------------------- 1 | pdo = $this->createPDO(); 19 | 20 | $this->qbf = $this->createQueryBuilder($this->createConnection($this->pdo)); 21 | 22 | $this->qbfPrefixed = $this->createQueryBuilder($this->createConnection($this->pdo, [ 23 | 'prefix' => 'prefix_' 24 | ])); 25 | } 26 | 27 | public function tearDown() 28 | { 29 | //Mockery::close(); 30 | } 31 | 32 | public function createPDO() 33 | { 34 | return new PDO('sqlite::memory:'); 35 | } 36 | 37 | public function createConnection(PDO $pdo, array $options = []) 38 | { 39 | return new Connection(new PdoBridge($pdo), $options); 40 | } 41 | 42 | public function createQueryBuilder(Connection $conn) 43 | { 44 | return new QueryBuilderFactory($conn); 45 | } 46 | 47 | public function assertSamePrefixedAndNot(\Closure $builder, $query) 48 | { 49 | $this->assertSame( 50 | (string) $builder($this->qbf)->getQuery(), 51 | str_replace('__PREFIX__', '', $query) 52 | ); 53 | 54 | $this->assertSame( 55 | (string) $builder($this->qbfPrefixed)->getQuery(), 56 | str_replace('__PREFIX__', 'prefix_', $query) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/QueryBuilder/Parameters.php: -------------------------------------------------------------------------------- 1 | parameters = $parameters; 18 | } 19 | 20 | public function delete($key) 21 | { 22 | unset($this->parameters[$key]); 23 | 24 | return $this; 25 | } 26 | 27 | public function has($key) 28 | { 29 | return isset($this->parameters[$key]); 30 | } 31 | 32 | public function set($key, $value) 33 | { 34 | $this->parameters[$key] = $value; 35 | 36 | return; 37 | } 38 | 39 | public function get($key, $default = null) 40 | { 41 | return isset($this->parameters[$key]) ? $this->parameters[$key] : $default; 42 | } 43 | 44 | public function all() 45 | { 46 | return $this->parameters; 47 | } 48 | 49 | public function replace($parameters) 50 | { 51 | $this->parameters = []; 52 | $this->merge($parameters); 53 | 54 | return $this; 55 | } 56 | 57 | public function merge($parameters) 58 | { 59 | if(is_array($parameters)) 60 | $array = $parameters; 61 | elseif($parameters instanceof Parameters) 62 | $array = $parameters->all(); 63 | else 64 | throw new Exception('Adapter parameters must be an array of Parameters instance.'); 65 | 66 | $this->parameters = array_merge($this->parameters, $array); 67 | 68 | return $this; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/QueryBuilder/QueryBuilder/Query.php: -------------------------------------------------------------------------------- 1 | sql = (string) trim($sql); 24 | $this->bindings = $bindings; 25 | $this->pdo = $pdo; 26 | } 27 | 28 | public function __toString() 29 | { 30 | return $this->getRawSql(); 31 | } 32 | 33 | public function toString() 34 | { 35 | return $this->getRawSql(); 36 | } 37 | 38 | public function getSql() 39 | { 40 | return $this->sql; 41 | } 42 | 43 | public function getBindings() 44 | { 45 | return $this->bindings; 46 | } 47 | 48 | public function getRawSql() 49 | { 50 | return $this->interpolateQuery($this->sql, $this->bindings); 51 | } 52 | 53 | /** 54 | * See: http://stackoverflow.com/a/1376838/656489 55 | */ 56 | protected function interpolateQuery($query, $params) 57 | { 58 | $keys = array(); 59 | $values = $params; 60 | 61 | # build a regular expression for each parameter 62 | foreach($params as $key => $value) 63 | { 64 | if(is_string($key)) 65 | $keys[] = '/:'.$key.'/'; 66 | else 67 | $keys[] = '/[?]/'; 68 | 69 | if(is_string($value)) 70 | $values[$key] = $this->pdo->quote($value); 71 | 72 | if(is_array($value)) 73 | $values[$key] = implode(',', $this->pdo->quote($value)); 74 | 75 | if(is_null($value)) 76 | $values[$key] = 'NULL'; 77 | } 78 | 79 | return preg_replace($keys, $values, $query, 1, $count); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/QueryBuilder/Connection.php: -------------------------------------------------------------------------------- 1 | setAdapter($adapter) 48 | ->setParameters($parameters) 49 | ->connect(); 50 | 51 | $this->createSchemaObject(); 52 | } 53 | 54 | protected function connect() 55 | { 56 | $this->setPdo($this->createAdapterObject()->connect($this->parameters)); 57 | } 58 | 59 | protected function createAdapterObject() 60 | { 61 | if(is_object($this->adapter)) 62 | { 63 | return $this->adapter; 64 | } 65 | elseif(is_string($this->adapter) && in_array($this->adapter, $this->availableDatabases)) 66 | { 67 | $classname = '\\Requtize\\QueryBuilder\\ConnectionAdapters\\'.ucfirst(strtolower($this->adapter)); 68 | 69 | return new $classname; 70 | } 71 | elseif(class_exists($this->adapter)) 72 | { 73 | return new $this->adapter; 74 | } 75 | 76 | throw new Exception('Given adapter must be one of predefined adapters\' types or a valid adapter class.'); 77 | } 78 | 79 | protected function createSchemaObject() 80 | { 81 | $classname = '\\Requtize\\QueryBuilder\\Schema\\'.ucfirst(strtolower($this->adapter->getDriverName())); 82 | 83 | $this->setSchema(new $classname); 84 | } 85 | 86 | /** 87 | * @param \PDO $pdo 88 | * 89 | * @return $this 90 | */ 91 | public function setPdo(\PDO $pdo) 92 | { 93 | $this->pdo = $pdo; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * @return \PDO 100 | */ 101 | public function getPdo() 102 | { 103 | return $this->pdo; 104 | } 105 | 106 | /** 107 | * @param $adapter 108 | * 109 | * @return $this 110 | */ 111 | public function setAdapter($adapter) 112 | { 113 | $this->adapter = $adapter; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * @return string 120 | */ 121 | public function getAdapter() 122 | { 123 | return $this->adapter; 124 | } 125 | 126 | /** 127 | * @param array $parameters 128 | * 129 | * @return $this 130 | */ 131 | public function setParameters($parameters) 132 | { 133 | if(is_array($parameters)) 134 | $this->parameters = new Parameters($parameters); 135 | elseif($parameters instanceof Parameters) 136 | $this->parameters = $parameters; 137 | else 138 | throw new Exception('Adapter parameters must be an array of Parameters instance.'); 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * @return array 145 | */ 146 | public function getParameters() 147 | { 148 | return $this->parameters; 149 | } 150 | 151 | public function setName($name) 152 | { 153 | $this->name = $name; 154 | 155 | return $this; 156 | } 157 | 158 | public function getName() 159 | { 160 | return $this->name; 161 | } 162 | 163 | public function getSchema() 164 | { 165 | return $this->schema; 166 | } 167 | 168 | public function setSchema(Schema $schema) 169 | { 170 | $this->schema = $schema; 171 | 172 | return $this; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /tests/unit/QueryBuilder/QueryBuilder/QueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertSamePrefixedAndNot( 18 | function ($qb) { 19 | return $qb->from('table')->select('*'); 20 | }, 21 | 'SELECT * FROM `__PREFIX__table`' 22 | ); 23 | 24 | /** 25 | * Select as one argument. 26 | */ 27 | $this->assertSamePrefixedAndNot( 28 | function ($qb) { 29 | return $qb->from('table')->select('id'); 30 | }, 31 | 'SELECT `id` FROM `__PREFIX__table`' 32 | ); 33 | 34 | /** 35 | * Selects as many arguments. 36 | */ 37 | $this->assertSamePrefixedAndNot( 38 | function ($qb) { 39 | return $qb->from('table')->select('id', 'table.name'); 40 | }, 41 | 'SELECT `id`, `__PREFIX__table`.`name` FROM `__PREFIX__table`' 42 | ); 43 | 44 | /** 45 | * Selects as array. 46 | */ 47 | $this->assertSamePrefixedAndNot( 48 | function ($qb) { 49 | return $qb->from('table')->select([ 'first_name', 'table.last_name' ]); 50 | }, 51 | 'SELECT `first_name`, `__PREFIX__table`.`last_name` FROM `__PREFIX__table`' 52 | ); 53 | 54 | /** 55 | * RAW select columns not quoted. 56 | */ 57 | $this->assertSamePrefixedAndNot( 58 | function ($qb) { 59 | return $qb->from('table') 60 | ->select($qb->raw('first_name, table.last_name')); 61 | }, 62 | 'SELECT first_name, table.last_name FROM `__PREFIX__table`' 63 | ); 64 | 65 | /** 66 | * RAW select columns quoted. 67 | */ 68 | $this->assertSamePrefixedAndNot( 69 | function ($qb) { 70 | return $qb->from('table') 71 | ->select($qb->raw('`first_name`, table.`last_name`, `table`.`last_name`')); 72 | }, 73 | 'SELECT `first_name`, table.`last_name`, `table`.`last_name` FROM `__PREFIX__table`' 74 | ); 75 | 76 | /** 77 | * Multiple calls and all columns joined. 78 | */ 79 | $this->assertSamePrefixedAndNot( 80 | function ($qb) { 81 | return $qb->from('table') 82 | ->select('id') 83 | ->select([ 'first_name', 'table.last_name' ]) 84 | ->select($qb->raw('raw_column')); 85 | }, 86 | 'SELECT `id`, `first_name`, `__PREFIX__table`.`last_name`, raw_column FROM `__PREFIX__table`' 87 | ); 88 | } 89 | 90 | public function testSelectWithTableSpecified() 91 | { 92 | $builder = function ($qb) { 93 | return $qb 94 | ->from('table') 95 | ->select('table.id'); 96 | }; 97 | 98 | $this->assertSamePrefixedAndNot( 99 | $builder, 100 | 'SELECT `__PREFIX__table`.`id` FROM `__PREFIX__table`' 101 | ); 102 | } 103 | 104 | public function testTable() 105 | { 106 | $builder = function ($qb) { 107 | return $qb 108 | ->select('id') 109 | ->select('table.id') 110 | ->select($qb->raw('prefixed_free.id')) 111 | ->from('table') 112 | ->table('asd', '123') 113 | ->table($qb->raw('prefixed_free')); 114 | }; 115 | 116 | $this->assertSamePrefixedAndNot( 117 | $builder, 118 | 'SELECT `id`, `__PREFIX__table`.`id`, prefixed_free.id FROM `__PREFIX__table`, `__PREFIX__asd`, `__PREFIX__123`, prefixed_free' 119 | ); 120 | } 121 | 122 | public function testWhere() 123 | { 124 | $builder = function ($qb) { 125 | return $qb->table('table') 126 | ->select('*') 127 | ->where('col1', 1) 128 | ->where('col2', '<', 2) 129 | ->orWhere('col3', '!=', 3) 130 | ->whereNot('col4', 4) 131 | ->orWhereNot('col5', 5) 132 | ->where($qb->raw('prefixed_free.col6'), 6) 133 | ->where($qb->raw('prefixed_free.col7 = 7')) 134 | ->where($qb->raw('prefixed_free.col8 = :val', [ ':val' => 8 ])); 135 | }; 136 | 137 | $this->assertSamePrefixedAndNot( 138 | $builder, 139 | 'SELECT * FROM `__PREFIX__table` WHERE `col1` = 1 AND `col2` < 2 OR `col3` != 3 AND NOT `col4` = 4 OR NOT `col5` = 5 AND prefixed_free.col6 = 6 AND prefixed_free.col7 = 7 AND prefixed_free.col8 = :val' 140 | ); 141 | } 142 | 143 | public function testNestedQuery() 144 | { 145 | $builder = function ($qb) { 146 | $subQuery = $qb 147 | ->select('name') 148 | ->from('persons') 149 | ->where('id', 15); 150 | 151 | $query = $qb 152 | ->select('*') 153 | ->select('table.*') 154 | ->from('table') 155 | ->select($qb->subQuery($subQuery, 'alias1')); 156 | 157 | return $qb 158 | ->select('*') 159 | ->from($qb->subQuery($query, 'alias2')); 160 | }; 161 | 162 | $this->assertSamePrefixedAndNot( 163 | $builder, 164 | 'SELECT * FROM (SELECT *, `__PREFIX__table`.*, (SELECT `name` FROM `__PREFIX__persons` WHERE `id` = 15) AS alias1 FROM `__PREFIX__table`) AS alias2' 165 | ); 166 | } 167 | 168 | public function testQueryFetchMode() 169 | { 170 | $qb = $this->qbf->create(); 171 | 172 | /** 173 | * Default fetch mode is an object. 174 | */ 175 | $result = $qb->query('SELECT 1'); 176 | 177 | $this->assertSame(1, count($result)); 178 | $this->assertTrue(isset($result[0])); 179 | $this->assertSame('object', gettype($result[0])); 180 | $this->assertSame('stdClass', get_class($result[0])); 181 | 182 | /** 183 | * Change to ASSOC fetch mode. 184 | */ 185 | $qb->setFetchMode(PDO::FETCH_ASSOC); 186 | 187 | $result = $qb->query('SELECT 1'); 188 | 189 | $this->assertSame(1, count($result)); 190 | $this->assertTrue(isset($result[0])); 191 | $this->assertSame('array', gettype($result[0])); 192 | 193 | /** 194 | * Change to object fetch mode with defined custom class name. 195 | */ 196 | $qb->setFetchMode(PDO::FETCH_CLASS, TableRow::class); 197 | 198 | $result = $qb->query('SELECT 1 as id, \'value\' as value'); 199 | 200 | $this->assertSame(1, count($result)); 201 | $this->assertSame('1', $result[0]->getId()); 202 | $this->assertSame('value', $result[0]->getValue()); 203 | $this->assertSame('object', gettype($result[0])); 204 | $this->assertSame(TableRow::class, get_class($result[0])); 205 | } 206 | 207 | public function testQueryAsObjects() 208 | { 209 | $qb = $this->qbf->create(); 210 | 211 | $result = $qb->select($qb->raw('1 as id, \'value\' as value'))->firstAsObject(TableRow::class); 212 | 213 | $this->assertSame('object', gettype($result)); 214 | $this->assertSame(TableRow::class, get_class($result)); 215 | $this->assertSame('1', $result->getId()); 216 | $this->assertSame('value', $result->getValue()); 217 | 218 | $result = $qb->select($qb->raw('1 as id, \'value\' as value'))->allAsObjects(TableRow::class); 219 | 220 | $this->assertSame('object', gettype($result[0])); 221 | $this->assertSame(TableRow::class, get_class($result[0])); 222 | $this->assertSame('1', $result[0]->getId()); 223 | $this->assertSame('value', $result[0]->getValue()); 224 | } 225 | } 226 | 227 | class TableRow 228 | { 229 | protected $id; 230 | protected $value; 231 | 232 | public function getId() 233 | { 234 | return $this->id; 235 | } 236 | 237 | public function getValue() 238 | { 239 | return $this->value; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QueryBuilder 2 | 3 | Query Builder is a fast, simple, methods-chaining, dependency-free library to create SQL Queries simple and fast to write, extend and manage. Supports databases which are supported by PDO. Can be also used as Database Abstraction Layer. 4 | 5 | ## Installation - via composer.json 6 | ```json 7 | "requtize/query-builder": "dev-master" 8 | ``` 9 | 10 | ### Usage whith connection estabilished before, in some other system's part. 11 | ```php 12 | use Requtize\QueryBuilder\Connection; 13 | use Requtize\QueryBuilder\QueryBuilder\QueryBuilderFactory; 14 | use Requtize\QueryBuilder\ConnectionAdapters\PdoBridge; 15 | 16 | // Somewhere in our application we have created PDO instance 17 | $pdo = new PDO('dns...'); 18 | 19 | // Build Connection object with PdoBridge ad Adapter 20 | $conn = new Connection(new PdoBridge($pdo)); 21 | 22 | // Pass this connection to Factory 23 | $qbf = new QueryBuilderFactory($conn); 24 | 25 | // Now we can use the factory as QueryBuilder - it creates QueryBuilder 26 | // object every time we use some of method from QueryBuilder and returns it. 27 | $result = $qbf->from('table')->where('cost', '>', 120)->all(); 28 | ``` 29 | 30 | # Query Builder Methods 31 | 32 | ### Table selection 33 | ```php 34 | // Set table to operate on. 35 | $qbf->table('table'); 36 | $qbf->table('table', 'next-table'); 37 | $qbf->table('table', 'next-table', 'and-another'); 38 | $qbf->table([ 'table', 'next-table', 'and-another' ]); 39 | // Alias to table() method. 40 | $qbf->from(...); 41 | ``` 42 | 43 | ### Selects 44 | ```php 45 | // Selects 46 | $qbf->select('*'); 47 | $qbf->select('column'); 48 | $qbf->select('column1', 'column2', 'column3'); 49 | $qbf->select([ 'column1', 'column2', 'column3' ]); 50 | // Select DISTINCT 51 | $qbf->selectDistinct('*'); 52 | $qbf->selectDistinct('column'); 53 | $qbf->selectDistinct('column1', 'column2', 'column3'); 54 | $qbf->selectDistinct([ 'column1', 'column2', 'column3' ]); 55 | ``` 56 | ### Wheres 57 | If method not starts with "or*, multiple calls will join it as "AND". 58 | ```php 59 | $qbf->where('name', 'Adam') 60 | ->where('name', '=', 'Adam') 61 | ->orWhere('name', 'Adam') 62 | ->orWhere('name', '=', 'Adam') 63 | ->whereNot('name', 'Adam') 64 | ->whereNot('name', '=', 'Adam') 65 | ->orWhereNot('name', 'Adam') 66 | ->orWhereNot('name', '=', 'Adam') 67 | ->whereIn('name' [ 'Adam', 'Eva' ]) 68 | ->whereNotIn('name' [ 'Adam', 'Eva' ]) 69 | ->orWhereIn('name' [ 'Adam', 'Eva' ]) 70 | ->orWhereNotIn('name' [ 'Adam', 'Eva' ]) 71 | ->whereBetween('age', 10, 20) 72 | ->orWhereBetween('age', 10, 20) 73 | ->whereNull('sex') 74 | ->whereNotNull('sex') 75 | ->orWhereNull('sex') 76 | ->orWhereNotNull('sex'); 77 | ``` 78 | Wheres (all methods above) can also take Closure as first argument. This can make sub-criterias. Sub-criterias will be joined to main query using joined from used method. As argument of the anonymous function is object NestedCriteria that allows You to use all the where() methods above. 79 | ```php 80 | $qbf->where(function ($query) { 81 | $query->where('id', 1) 82 | ->whereNot('status', 2); 83 | }); 84 | ``` 85 | Wheres can also take as first argument RAW query section in two ways. First - all full criteria (column name, operator and value), or only columns/table-column value n first parameter, and value as second. 86 | ```php 87 | $qbf->where($qbf->raw('name'), 'Adam'); 88 | $qbf->where($qbf->raw('name = "Adam"')); 89 | ``` 90 | 91 | ### Joins 92 | ```php 93 | // Simple INNER JOIN 94 | $qbf->join('table', 'name', '=', 'Adam', 'inner') 95 | // INNER JOIN as Closure with advanced ON criteria 96 | ->join('table', function ($join) { 97 | $join->on('name', 'Adam') 98 | ->on('name', '=', 'Adam') 99 | ->orOn('name', 'Adam') 100 | ->orOn('name', '=', 'Adam'); 101 | }) 102 | ->leftJoin('table', 'name', '=', 'Adam') 103 | ->leftJoin('table', function ($join) { 104 | // ... 105 | }) 106 | ->rightJoin('table', 'name', '=', 'Adam') 107 | ->rightJoin('table', function ($join) { 108 | // ... 109 | }) 110 | ->innerJoin('table', 'name', '=', 'Adam') 111 | ->innerJoin('table', function ($join) { 112 | // ... 113 | }); 114 | ``` 115 | 116 | ### Resutls set 117 | ```php 118 | $qbf->all(); // Returns all results. 119 | $qbf->first(); // Returns first result. 120 | $qbf->count($column); 121 | $qbf->max($column); 122 | $qbf->min($column); 123 | $qbf->sum($column); 124 | $qbf->avg($column); 125 | ``` 126 | 127 | ### Inserts 128 | ```php 129 | $qbf->from('table')->insert([ 'name' => 'Adam' ]); 130 | $qbf->insert([ 'name' => 'Adam' ], 'table'); 131 | $qbf->from('table')->insertIgnore([ 'name' => 'Adam' ]); 132 | $qbf->insertIgnore([ 'name' => 'Adam' ], 'table'); 133 | $qbf->from('table')->replace([ 'id' => 12, 'name' => 'Adam' ]); 134 | $qbf->replace([ 'id' => 12, 'name' => 'Adam' ], 'table'); 135 | ``` 136 | 137 | If insert call operate on table with AUTO_INCREMENT column, method will return inserted ID of row. Also You can use another method (after call `insert()` method) to do the same thing: 138 | 139 | ```php 140 | $qbf->getLastId(); 141 | 142 | ``` 143 | 144 | ### Update 145 | ```php 146 | $qbf 147 | ->from('table') 148 | ->where('name', 'John') 149 | ->update([ 'name' => 'Adam' ]); 150 | $qbf 151 | ->where('name', 'John') 152 | ->update([ 'name' => 'Adam' ], 'table'); 153 | $qbf 154 | ->from('table') 155 | ->where('name', 'John') 156 | ->updateOrInsert([ 'name' => 'Adam' ]); 157 | $qbf 158 | ->where('name', 'John') 159 | ->updateOrInsert([ 'name' => 'Adam' ], 'table'); 160 | ``` 161 | 162 | ### Delete 163 | ```php 164 | $qbf 165 | ->where('name', 'Adam') 166 | ->delete('table'); 167 | $qbf 168 | ->from('table') 169 | ->where('name', 'Adam') 170 | ->delete(); 171 | ``` 172 | 173 | ### RAW values 174 | In most of all methods and parameters You can pass the RAW value as argument. To do this You have to only use raw() method and pass the result of this method to any argument You want. 175 | ```php 176 | $qbf->where($qbf->raw('name'), $qbf->raw('Adam')); 177 | $qbf->select($qbf->raw('name')); 178 | $qbf->table($qbf->raw('table')); 179 | // ...and so on... 180 | ``` 181 | 182 | ### Raw query 183 | ```php 184 | // SELECT Query. 185 | $rows = $qbf->query('SELECT * FROM table WHERE name = :name', [ 186 | ':name' => 'Adam' 187 | ]); 188 | // UPDATE, INSERT, DELETE, etc. 189 | $affectedCount = $qbf->exec('UPDATE table SET id = :id WHERE name = :name', [ 190 | ':id' => 15, 191 | ':name' => 'Adam' 192 | ]); 193 | ``` 194 | 195 | ### Sub-Queries and Nested Queries 196 | In some cases You might need to create SubQuery to provide some special functionality. Use the *subQuery()* method to do this thing. Examples of usage: 197 | ```php 198 | $subQuery = $qbf 199 | ->select('name') 200 | ->from('persons') 201 | ->where('id', 15); 202 | 203 | $query = $qbf 204 | ->select('table.*') 205 | ->from('table') 206 | ->select($qbf->subQuery($subQuery, 'alias1')); 207 | 208 | $nestedQuery = $qbf 209 | ->select('*') 210 | ->from($qbf->subQuery($query, 'alias2')); 211 | ``` 212 | Generated query by Query Builder. 213 | ```sql 214 | SELECT * 215 | FROM ( 216 | SELECT `table`.*, 217 | ( 218 | SELECT `name` 219 | FROM `persons` 220 | WHERE `id` = 15 221 | ) AS alias1 222 | FROM `table` 223 | ) AS alias2 224 | ``` 225 | 226 | ## Get compiled Query 227 | If You want to preview a query, before execution, or for debugging intensions, You may want use *getQuery()* method, which returns *Query* object, with compiled query (with placeholders), array of bindings and the *PDO* instance. This object contains all data of current *Query Builder* instance. 228 | ```php 229 | $qbf->getQuery($type = 'select', array $parameters = []); 230 | ``` 231 | 232 | ## #API 233 | ```php 234 | // Returns passed PDO object. 235 | $qbf->getPdo(); 236 | 237 | // Returns all Query Segments created in this instance of Query Builder 238 | $qbf->getQuerySegments(); 239 | 240 | // Or only selected segment 241 | $qbf->getQuerySegment('where'); 242 | 243 | // Sets and gets EventDispatcher 244 | $qbf->getEventDispatcher(); 245 | $qbf->setEventDispatcher(Requtize\QueryBuilder\Event\EventDispatcherInterface $eventDispatcher); 246 | 247 | // Sets FetchMode for PDO. IF PDOs Fetch Mode requires many arguments, just pass this to this method as next arguments. 248 | $qbf->setFetchMode($mode...); 249 | // Sets Fetch mode to Object. 250 | $qbf->asObject($className, $classConstructorArgs = []); 251 | 252 | // Gets and sets DB connection object. 253 | $qbf->setConnection(Requtize\QueryBuilder\Connection $connection); 254 | $qbf->getConnection(); 255 | 256 | // Gets Db schema 257 | $qbf->getSchema(); 258 | 259 | // Create new QueryBuilder instance. 260 | $qbf->newQuery(Requtize\QueryBuilder\Connection $connection = null); 261 | 262 | // Forks query. Copies all Query Segments, settings to new object and returns new object. Allows to create new Query, but with earlier defined criterias. 263 | $qbf->forkQuery(); 264 | ``` 265 | # @todo 266 | 267 | - Where LIKE 268 | 269 | ```PHP 270 | $qb->like('column', 'value'); 271 | // WHERE column LIKE '%value%' 272 | $qb->like('column', 'value', 'left|start'); 273 | // WHERE column LIKE '%value' 274 | $qb->like('column', 'value', 'right|end'); 275 | // WHERE column LIKE 'value%' 276 | ``` 277 | 278 | 279 | 280 | - Scopes - reusable predefined groups of statements. 281 | ```php 282 | $scopes = new ScopesContainer; 283 | $scopes->register('scope-name', function($qb, $arg1, $arg2) { 284 | if($arg1) 285 | $qb->where('add_date', '<', 'NOW()'); 286 | if($arg2) 287 | $qb->where('add_date', '>=', 'NOW()'); 288 | }); 289 | 290 | $qbf->setScopes($scopes); 291 | 292 | // ... 293 | 294 | $qbf->from('table')->scopeName('arg1', 'arg2')->all(); 295 | ``` 296 | 297 | - Chunks of returned rows 298 | Must work only when there's no defined LIMIT statement! 299 | ```php 300 | $qb->where('column', 1)->chunk(30, function (array $chunk) { 301 | foreach($chunk as $row) 302 | { 303 | // Do something with $row... 304 | } 305 | }); 306 | ``` 307 | 308 | - Inserting data as aggregated collection 309 | 310 | ```php 311 | $qb->insert([ 312 | [ 'id' => 1, 'col' => 'val' ], 313 | [ 'id' => 1, 'col' => 'val' ], 314 | [ 'id' => 1, 'col' => 'val' ], 315 | [ 'id' => 1, 'col' => 'val' ] 316 | ], true, 'table'); 317 | ``` 318 | 319 | - Fulltext search 320 | -------------------------------------------------------------------------------- /src/QueryBuilder/QueryBuilder/Compiler.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 27 | $this->eventDispatcher = $eventDispatcher; 28 | 29 | $this->tablePrefix = $connection->getParameters()->get('prefix', null); 30 | } 31 | 32 | public function setTablePrefix($prefix) 33 | { 34 | $this->tablePrefix = $prefix; 35 | 36 | return $this; 37 | } 38 | 39 | public function getTablePrefix() 40 | { 41 | return $this->tablePrefix; 42 | } 43 | 44 | public function compile($type, $querySegments, array $data = []) 45 | { 46 | $allowedTypes = [ 'select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly' ]; 47 | 48 | if(in_array(strtolower($type), $allowedTypes) === false) 49 | throw new Exception($type.' is not a known type.'); 50 | 51 | return $this->{$type}($querySegments, $data); 52 | } 53 | 54 | public function select($querySegments, array $data) 55 | { 56 | if(isset($querySegments['selects']) === false) 57 | $querySegments['selects'][] = '*'; 58 | 59 | list($wheres, $whereBindings) = $this->buildCriteriaOfType($querySegments, 'wheres', 'WHERE'); 60 | 61 | if(isset($querySegments['groupBy']) && $groupBy = $this->arrayToString($querySegments['groupBy'], ', ', 'column')) 62 | $groupBy = 'GROUP BY '.$groupBy; 63 | else 64 | $groupBy = ''; 65 | 66 | if(isset($querySegments['orderBy']) && is_array($querySegments['orderBy'])) 67 | { 68 | foreach($querySegments['orderBy'] as $orderBy) 69 | $orderBy .= $this->quoteTable($orderBy['field']).' '.$orderBy['type'].', '; 70 | 71 | if($orderBy = trim($orderBy, ', ')) 72 | $orderBy = 'ORDER BY '.$orderBy; 73 | } 74 | else 75 | { 76 | $orderBy = ''; 77 | } 78 | 79 | list($havings, $havingBindings) = $this->buildCriteriaOfType($querySegments, 'havings', 'HAVING'); 80 | 81 | $segmentsToBuild = [ 82 | 'SELECT'.(isset($querySegments['distinct']) ? ' DISTINCT' : ''), 83 | $this->arrayToString($querySegments['selects'], ', ', 'column') 84 | ]; 85 | 86 | if(isset($querySegments['tables'])) 87 | { 88 | $tables = $this->arrayToString($querySegments['tables'], ', ', 'table'); 89 | 90 | if($tables) 91 | { 92 | $segmentsToBuild[] = 'FROM'; 93 | $segmentsToBuild[] = $tables; 94 | } 95 | } 96 | 97 | $segmentsToBuild[] = $this->compileJoin($querySegments); 98 | $segmentsToBuild[] = $wheres; 99 | $segmentsToBuild[] = $groupBy; 100 | $segmentsToBuild[] = $havings; 101 | $segmentsToBuild[] = $orderBy; 102 | $segmentsToBuild[] = isset($querySegments['limit']) ? 'LIMIT '.$querySegments['limit'] : ''; 103 | $segmentsToBuild[] = isset($querySegments['offset']) ? 'OFFSET '.$querySegments['offset'] : ''; 104 | 105 | return [ 106 | 'sql' => $this->buildQuerySegment($segmentsToBuild), 107 | 'bindings' => array_merge( 108 | $whereBindings, 109 | $havingBindings 110 | ) 111 | ]; 112 | } 113 | 114 | public function criteriaOnly($querySegments, $binds = true) 115 | { 116 | $sql = ''; 117 | $bindings = []; 118 | 119 | if(isset($querySegments['criteria']) === false) 120 | { 121 | return [ 122 | 'sql' => $sql, 123 | 'bindings' => $bindings 124 | ]; 125 | } 126 | else 127 | { 128 | list($sql, $bindings) = $this->buildCriteria($querySegments['criteria'], $binds); 129 | 130 | return [ 131 | 'sql' => $sql, 132 | 'bindings' => $bindings 133 | ]; 134 | } 135 | } 136 | 137 | private function doInsert($querySegments, array $data, $type) 138 | { 139 | if(! isset($querySegments['tables'])) 140 | throw new Exception('No table given.'); 141 | 142 | $table = end($querySegments['tables']); 143 | 144 | $bindings = []; 145 | $keys = []; 146 | $values = []; 147 | 148 | foreach($data as $key => $value) 149 | { 150 | $keys[] = $key; 151 | 152 | if($value instanceof Raw) 153 | { 154 | $values[] = (string) $value; 155 | } 156 | else 157 | { 158 | $values[] = ':'.$key; 159 | $bindings[':'.$key] = $value; 160 | } } 161 | 162 | $segmentsToBuild = [ 163 | $type.' INTO', 164 | $this->quoteTable($table), 165 | '('.$this->arrayToString($keys, ', ', 'column').')', 166 | 'VALUES', 167 | '('.$this->arrayToString($values, ', ', null).')', 168 | ]; 169 | 170 | if(isset($querySegments['onduplicate'])) 171 | { 172 | if(count($querySegments['onduplicate']) < 1) 173 | throw new Exception('No data given.'); 174 | 175 | list($updateStatement, $updateBindings) = $this->getUpdateStatement($querySegments['onduplicate']); 176 | 177 | $segmentsToBuild[] = 'ON DUPLICATE KEY UPDATE '.$updateStatement; 178 | 179 | $bindings = array_merge($bindings, $updateBindings); 180 | } 181 | 182 | return [ 183 | 'sql' => $this->buildQuerySegment($segmentsToBuild), 184 | 'bindings' => $bindings 185 | ]; 186 | } 187 | 188 | public function insert($querySegments, array $data) 189 | { 190 | return $this->doInsert($querySegments, $data, 'INSERT'); 191 | } 192 | 193 | public function insertIgnore($querySegments, array $data) 194 | { 195 | return $this->doInsert($querySegments, $data, 'INSERT IGNORE'); 196 | } 197 | 198 | public function replace($querySegments, array $data) 199 | { 200 | return $this->doInsert($querySegments, $data, 'REPLACE'); 201 | } 202 | 203 | public function update($querySegments, array $data) 204 | { 205 | if(isset($querySegments['tables']) === false) 206 | throw new Exception('No table given.'); 207 | elseif (count($data) < 1) 208 | throw new Exception('No data given.'); 209 | 210 | $table = end($querySegments['tables']); 211 | 212 | list($updates, $bindings) = $this->getUpdateStatement($data); 213 | 214 | list($wheres, $whereBindings) = $this->buildCriteriaOfType($querySegments, 'wheres', 'WHERE'); 215 | 216 | $limit = isset($querySegments['limit']) ? 'LIMIT '.$querySegments['limit'] : ''; 217 | 218 | $segmentsToBuild = [ 219 | 'UPDATE', 220 | $this->quoteTable($table), 221 | 'SET '.$updates, 222 | $wheres, 223 | $limit 224 | ]; 225 | 226 | return [ 227 | 'sql' => $this->buildQuerySegment($segmentsToBuild), 228 | 'bindings' => array_merge($bindings, $whereBindings) 229 | ]; 230 | } 231 | 232 | public function delete($querySegments) 233 | { 234 | if(isset($querySegments['tables']) === false) 235 | throw new Exception('No table given.'); 236 | 237 | $table = end($querySegments['tables']); 238 | 239 | list($wheres, $whereBindings) = $this->buildCriteriaOfType($querySegments, 'wheres', 'WHERE'); 240 | 241 | $limit = isset($querySegments['limit']) ? 'LIMIT '.$querySegments['limit'] : ''; 242 | 243 | $segmentsToBuild = [ 'DELETE FROM', $this->quoteTable($table), $wheres ]; 244 | 245 | return [ 246 | 'sql' => $this->buildQuerySegment($segmentsToBuild), 247 | 'bindings' => $whereBindings 248 | ]; 249 | } 250 | 251 | public function quoteTable($value) 252 | { 253 | if($value instanceof Raw) 254 | return (string) $value; 255 | elseif($value instanceof \Closure) 256 | return $value; 257 | 258 | if(strpos($value, '.')) 259 | { 260 | $segments = explode('.', $value, 2); 261 | $segments[0] = $this->quoteTableName($segments[0]); 262 | $segments[1] = $this->quoteColumnName($segments[1]); 263 | 264 | return implode('.', $segments); 265 | } 266 | else 267 | { 268 | return $this->quoteTableName($value); 269 | } 270 | } 271 | 272 | public function quoteTableName($value) 273 | { 274 | return $this->connection->getSchema()->quoteTableName($value); 275 | } 276 | 277 | public function quoteColumnName($value) 278 | { 279 | return $this->connection->getSchema()->quoteColumnName($value); 280 | } 281 | 282 | public function quote($value) 283 | { 284 | if($value instanceof Raw) 285 | return (string) $value; 286 | elseif($value instanceof \Closure) 287 | return $value; 288 | 289 | if(strpos($value, '.')) 290 | { 291 | $segments = []; 292 | 293 | foreach(explode('.', $value, 2) as $val) 294 | $segments[] = $this->quoteSingle($val); 295 | 296 | return implode('.', $segments); 297 | } 298 | else 299 | { 300 | return $this->quoteSingle($value); 301 | } 302 | } 303 | 304 | public function quoteSingle($value) 305 | { 306 | return is_int($value) ? $value : $this->connection->getPdo()->quote($value); 307 | } 308 | 309 | public function addTablePrefix($value) 310 | { 311 | if(is_null($this->tablePrefix)) 312 | return $value; 313 | 314 | if(is_string($value) === false) 315 | return $value; 316 | 317 | return $this->tablePrefix.$value; 318 | } 319 | 320 | protected function buildCriteriaOfType($querySegments, $key, $type, $bindValues = true) 321 | { 322 | $criteria = ''; 323 | $bindings = []; 324 | 325 | if(isset($querySegments[$key])) 326 | { 327 | // Get the generic/adapter agnostic criteria string from parent 328 | list($criteria, $bindings) = $this->buildCriteria($querySegments[$key], $bindValues); 329 | 330 | if($criteria) 331 | $criteria = $type.' '.$criteria; 332 | } 333 | 334 | return [ $criteria, $bindings ]; 335 | } 336 | 337 | protected function compileJoin($querySegments) 338 | { 339 | $sql = ''; 340 | 341 | if(isset($querySegments['joins']) === false || is_array($querySegments['joins']) === false) 342 | return $sql; 343 | 344 | foreach($querySegments['joins'] as $joinArr) 345 | { 346 | if(is_array($joinArr['table'])) 347 | $table = $this->quoteTable($joinArr['table'][0]).' AS '.$this->quoteTable($joinArr['table'][1]); 348 | else 349 | $table = $joinArr['table'] instanceof Raw ? (string) $joinArr['table'] : $this->quoteTable($joinArr['table']); 350 | 351 | $joinBuilder = $joinArr['joinBuilder']; 352 | 353 | $sqlArr = [ 354 | $sql, 355 | strtoupper($joinArr['type']), 356 | 'JOIN', 357 | $table, 358 | 'ON', 359 | $joinBuilder->getQuery('criteriaOnly')->getSql() 360 | ]; 361 | 362 | $sql = $this->buildQuerySegment($sqlArr); 363 | } 364 | 365 | return $sql; 366 | } 367 | 368 | protected function getUpdateStatement($data) 369 | { 370 | $bindings = []; 371 | $segment = ''; 372 | 373 | foreach($data as $key => $value) 374 | { 375 | if($value instanceof Raw) 376 | { 377 | $segment .= $this->quoteColumnName($key).' = '.$value.', '; 378 | } 379 | else 380 | { 381 | $segment .= $this->quoteColumnName($key).' = '.$key.' , '; 382 | $bindings[] = $value; 383 | } 384 | } 385 | 386 | return [ trim($segment, ', '), $bindings ]; 387 | } 388 | 389 | protected function arrayToString(array $data, $glue, $quote = 'value') 390 | { 391 | $elements = []; 392 | 393 | foreach($data as $key => $val) 394 | { 395 | if(is_int($val) === false) 396 | { 397 | if($quote === 'table' || $quote === 'column') 398 | $val = $this->quoteTable($val); 399 | else if($quote === 'value') 400 | $val = $this->quote($val); 401 | } 402 | 403 | $elements[] = $val; 404 | } 405 | 406 | return implode($glue, $elements); 407 | } 408 | 409 | protected function buildQuerySegment(array $data) 410 | { 411 | $string = ''; 412 | 413 | foreach($data as $val) 414 | { 415 | $value = trim($val); 416 | 417 | if($value) 418 | { 419 | $string = trim($string).' '.$value; 420 | } 421 | } 422 | 423 | return $string; 424 | } 425 | 426 | protected function buildCriteria($querySegments, $bindValues = true) 427 | { 428 | $criteria = ''; 429 | $bindings = []; 430 | 431 | foreach($querySegments as $segment) 432 | { 433 | $key = is_object($segment['key']) ? $segment['key'] : $this->quoteTable($segment['key']); 434 | $value = $segment['value']; 435 | 436 | if(is_null($value) && $key instanceof \Closure) 437 | { 438 | $nestedCriteria = new NestedCriteria($this->connection, $this->eventDispatcher); 439 | // Call the closure with our new nestedCriteria object 440 | $key($nestedCriteria); 441 | // Get the criteria only query from the nestedCriteria object 442 | $queryObject = $nestedCriteria->getQuery('criteriaOnly'); 443 | // Merge the bindings we get from nestedCriteria object 444 | $bindings = array_merge($bindings, $queryObject->getBindings()); 445 | // Append the sql we get from the nestedCriteria object 446 | $criteria .= $segment['joiner'].' ('.$queryObject->getSql().') '; 447 | } 448 | elseif(is_array($value)) 449 | { 450 | // where_in or between like query 451 | $criteria .= $segment['joiner'].' '.$key.' '.$segment['operator']; 452 | 453 | switch ($segment['operator']) 454 | { 455 | case 'BETWEEN': 456 | $bindings = array_merge($bindings, $segment['value']); 457 | $criteria .= ' ? AND ? '; 458 | 459 | break; 460 | default: 461 | $placeholders = []; 462 | 463 | foreach($segment['value'] as $subValue) 464 | { 465 | $placeholders[] = '?'; 466 | $bindings[] = $subValue; 467 | } 468 | 469 | $criteria .= ' ('.implode(', ', $placeholders).') '; 470 | 471 | break; 472 | } 473 | } 474 | elseif($value instanceof Raw) 475 | { 476 | $criteria .= $segment['joiner'].' '.$key.' '.$segment['operator'].' '.$value.' '; 477 | } 478 | else 479 | { 480 | if(! $bindValues) 481 | { 482 | $value = is_null($value) ? $value : $this->quote($value); 483 | $criteria .= $segment['joiner'].' '.$key.' '.$segment['operator'].' '.$value.' '; 484 | } 485 | elseif($segment['key'] instanceof Raw) 486 | { 487 | if($value === null) 488 | { 489 | $criteria .= $segment['joiner'].' '.$key.' '; 490 | $bindings = array_merge($bindings, $segment['key']->getBindings()); 491 | } 492 | else 493 | { 494 | $criteria .= $segment['joiner'].' '.$key.' '.$segment['operator'].' '.$value.' '; 495 | } 496 | } 497 | else 498 | { 499 | $valuePlaceholder = '?'; 500 | $bindings[] = $value; 501 | $criteria .= $segment['joiner'].' '.$key.' '.$segment['operator'].' '.$valuePlaceholder.' '; 502 | } 503 | } 504 | } 505 | 506 | return [ 507 | // Clear all white spaces, ands and ors from beginning and white spaces from both ends 508 | trim(preg_replace("/^(AND|OR)?/i", '', $criteria)), 509 | $bindings 510 | ]; 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /src/QueryBuilder/QueryBuilder/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | setConnection($connection); 37 | 38 | if($eventDispatcher) 39 | $this->setEventDispatcher($eventDispatcher); 40 | 41 | // Set default compiler. 42 | $this->setCompiler(new Compiler($connection, $eventDispatcher)); 43 | 44 | $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 45 | } 46 | 47 | public function __clone() 48 | { 49 | $this->pdoStatement = null; 50 | } 51 | 52 | public function setCompiler(Compiler $compiler) 53 | { 54 | $this->compiler = $compiler; 55 | 56 | return $this; 57 | } 58 | 59 | public function getCompiler() 60 | { 61 | return $this->compiler; 62 | } 63 | 64 | public function getQuerySegments() 65 | { 66 | return $this->querySegments; 67 | } 68 | 69 | public function getQuerySegment($key) 70 | { 71 | if(isset($this->querySegments[$key])) 72 | return $this->querySegments[$key]; 73 | else 74 | return null; 75 | } 76 | 77 | public function getEventDispatcher() 78 | { 79 | return $this->eventDispatcher; 80 | } 81 | 82 | public function setEventDispatcher(EventDispatcherInterface $eventDispatcher) 83 | { 84 | $this->eventDispatcher = $eventDispatcher; 85 | 86 | return $this; 87 | } 88 | 89 | public function getPdo() 90 | { 91 | return $this->pdo; 92 | } 93 | 94 | public function setFetchMode($mode) 95 | { 96 | $this->fetchMode = func_get_args(); 97 | 98 | return $this; 99 | } 100 | 101 | public function getFetchMode() 102 | { 103 | return $this->fetchMode; 104 | } 105 | 106 | public function setConnection(Connection $connection) 107 | { 108 | $this->connection = $connection; 109 | $this->pdo = $connection->getPdo(); 110 | 111 | return $this; 112 | } 113 | 114 | public function getConnection() 115 | { 116 | return $this->connection; 117 | } 118 | 119 | public function getSchema() 120 | { 121 | return $this->connection->getSchema(); 122 | } 123 | 124 | public function dispatch($event, array $params = []) 125 | { 126 | if(! $this->eventDispatcher) 127 | return; 128 | 129 | $params['query-builder'] = $this; 130 | 131 | return $this->eventDispatcher->dispatch($event, $params); 132 | } 133 | 134 | public function newQuery(Connection $connection = null) 135 | { 136 | if(is_null($connection)) 137 | $connection = $this->connection; 138 | 139 | return new self($connection); 140 | } 141 | 142 | public function forkQuery() 143 | { 144 | return clone $this; 145 | } 146 | 147 | public function query($sql, $bindings = []) 148 | { 149 | $eventResult = $this->dispatch('before-raw-query', [ 150 | 'sql' => $sql, 151 | 'bindings' => $bindings 152 | ]); 153 | 154 | if(is_null($eventResult) === false) 155 | return $eventResult; 156 | 157 | $pdoStatement = $this->prepare( 158 | $sql, 159 | $bindings 160 | ); 161 | 162 | try 163 | { 164 | $pdoStatement->execute(); 165 | $result = call_user_func_array([ $pdoStatement, 'fetchAll' ], $this->fetchMode); 166 | } 167 | catch(PDOException $e) 168 | { 169 | throw new QueryExecutionFailException($e->getMessage().' during query "'.$sql.'"', $e->getCode(), $e); 170 | } 171 | 172 | $pdoStatement->closeCursor(); 173 | $pdoStatement = null; 174 | $this->dispatch('after-raw-query', [ 175 | 'sql' => $sql, 176 | 'bindings' => $bindings, 177 | 'result' => $result 178 | ]); 179 | 180 | return $result; 181 | } 182 | 183 | public function exec($sql, $bindings = []) 184 | { 185 | $eventResult = $this->dispatch('before-exec', [ 186 | 'sql' => $sql, 187 | 'bindings' => $bindings 188 | ]); 189 | 190 | if(is_null($eventResult) === false) 191 | return $eventResult; 192 | 193 | $pdoStatement = $this->prepare( 194 | $sql, 195 | $bindings 196 | ); 197 | 198 | try 199 | { 200 | $result = $pdoStatement->execute(); 201 | } 202 | catch(PDOException $e) 203 | { 204 | throw new QueryExecutionFailException($e->getMessage().' during query "'.$sql.'"', $e->getCode(), $e); 205 | } 206 | 207 | $pdoStatement = null; 208 | $this->dispatch('after-exec', [ 209 | 'sql' => $sql, 210 | 'bindings' => $bindings, 211 | 'result' => $result 212 | ]); 213 | 214 | return $result; 215 | } 216 | 217 | public function raw($value, $bindings = []) 218 | { 219 | return new Raw($value, $bindings); 220 | } 221 | 222 | public function getQuery($type = 'select', array $parameters = []) 223 | { 224 | $result = $this->compiler->compile($type, $this->querySegments, $parameters); 225 | 226 | return new Query($result['sql'], $result['bindings'], $this->pdo); 227 | } 228 | 229 | public function subQuery(QueryBuilder $queryBuilder, $alias = null) 230 | { 231 | $sql = '('.$queryBuilder->getQuery()->getRawSql().')'; 232 | 233 | if($alias) 234 | $sql = $sql.' AS '.$alias; 235 | 236 | return $queryBuilder->raw($sql); 237 | } 238 | 239 | public function all() 240 | { 241 | $query = $this->getQuery('select'); 242 | $sql = $query->getSql(); 243 | $bindings = $query->getBindings(); 244 | 245 | $eventResult = $this->dispatch('before-select', [ 246 | 'query' => $sql, 247 | 'bindings' => $bindings 248 | ]); 249 | 250 | if(is_null($eventResult) === false) 251 | return $eventResult; 252 | 253 | $this->dispatch('before-query', [ 254 | 'type' => 'select', 255 | 'query' => $sql, 256 | 'bindings' => $bindings 257 | ]); 258 | 259 | $pdoStatement = $this->prepareAndExecute($sql, $bindings); 260 | $result = call_user_func_array([ $pdoStatement, 'fetchAll' ], $this->fetchMode); 261 | 262 | $this->dispatch('after-query', [ 263 | 'type' => 'select', 264 | 'query' => $sql, 265 | 'bindings' => $bindings, 266 | 'result' => is_array($result) ? count($result) : $result->rowCount() 267 | ]); 268 | 269 | $this->dispatch('after-select', [ 270 | 'query' => $sql, 271 | 'bindings' => $bindings, 272 | 'result' => $result 273 | ]); 274 | 275 | return $result; 276 | } 277 | 278 | public function allAsObjects($className, $classConstructorArgs = []) 279 | { 280 | return $this->asObjects(function () { 281 | return $this->all(); 282 | }, $className, $classConstructorArgs); 283 | } 284 | 285 | public function first() 286 | { 287 | $this->limit(1); 288 | 289 | $result = $this->all(); 290 | 291 | return ! $result ? null : $result[0]; 292 | } 293 | 294 | public function firstAsObject($className, $classConstructorArgs = []) 295 | { 296 | return $this->asObjects(function () { 297 | return $this->first(); 298 | }, $className, $classConstructorArgs); 299 | } 300 | 301 | private function asObjects(\Closure $callback, $className, $classConstructorArgs) 302 | { 303 | $fetchMode = $this->fetchMode; 304 | $this->setFetchMode(PDO::FETCH_CLASS, $className, $classConstructorArgs); 305 | $result = $callback(); 306 | call_user_func_array([ $this, 'setFetchMode' ], $fetchMode); 307 | return $result; 308 | } 309 | 310 | public function find($value, $field = 'id') 311 | { 312 | $this->where($field, '=', $value); 313 | 314 | return $this->first(); 315 | } 316 | 317 | public function findAll($field, $value) 318 | { 319 | $this->where($field, '=', $value); 320 | 321 | return $this->all(); 322 | } 323 | 324 | public function from($tables) 325 | { 326 | if(is_array($tables) === false) 327 | $tables = func_get_args(); 328 | 329 | $this->addQuerySegment('tables', $this->addTablePrefix($tables, true)); 330 | 331 | return $this; 332 | } 333 | 334 | public function table() 335 | { 336 | return call_user_func_array([ $this, 'from' ], func_get_args()); 337 | } 338 | 339 | public function removeTables() 340 | { 341 | $this->removeQuerySegment('tables'); 342 | 343 | return $this; 344 | } 345 | 346 | public function select($fields) 347 | { 348 | if(is_array($fields) === false) 349 | $fields = func_get_args(); 350 | 351 | $fields = $this->addTablePrefix($fields); 352 | $this->addQuerySegment('selects', $fields); 353 | 354 | return $this; 355 | } 356 | 357 | public function removeSelects() 358 | { 359 | $this->removeQuerySegment('selects'); 360 | 361 | return $this; 362 | } 363 | 364 | public function selectDistinct($fields) 365 | { 366 | $this->select($fields); 367 | $this->addQuerySegment('distinct', true); 368 | 369 | return $this; 370 | } 371 | 372 | public function removeSelectDistinct() 373 | { 374 | $this->removeQuerySegment('distinct'); 375 | 376 | return $this; 377 | } 378 | 379 | public function count($column = '*') 380 | { 381 | $segments = $this->querySegments; 382 | 383 | unset($this->querySegments['orderBy']); 384 | unset($this->querySegments['limit']); 385 | unset($this->querySegments['offset']); 386 | 387 | $count = $this->aggregate('COUNT('.$column.')'); 388 | $this->querySegments = $segments; 389 | 390 | return $count; 391 | } 392 | 393 | public function max($column) 394 | { 395 | return $this->aggregate('MAX('.$column.')'); 396 | } 397 | 398 | public function min($column) 399 | { 400 | return $this->aggregate('MIN('.$column.')'); 401 | } 402 | 403 | public function sum($column) 404 | { 405 | return $this->aggregate('SUM('.$column.')'); 406 | } 407 | 408 | public function avg($column) 409 | { 410 | return $this->aggregate('AVG('.$column.')'); 411 | } 412 | 413 | public function insert($data, $table = null) 414 | { 415 | if($table) 416 | $this->table($table); 417 | 418 | return $this->doInsert($data, 'insert'); 419 | } 420 | 421 | public function insertIgnore($data, $table = null) 422 | { 423 | if($table) 424 | $this->table($table); 425 | 426 | return $this->doInsert($data, 'insertignore'); 427 | } 428 | 429 | public function replace($data, $table = null) 430 | { 431 | if($table) 432 | $this->table($table); 433 | 434 | return $this->doInsert($data, 'replace'); 435 | } 436 | 437 | public function update($data, $table = null) 438 | { 439 | if($table) 440 | $this->table($table); 441 | 442 | $query = $this->getQuery('update', $data); 443 | $sql = $query->getSql(); 444 | $bindings = $query->getBindings(); 445 | 446 | $eventResult = $this->dispatch('before-update', [ 447 | 'query' => $sql, 448 | 'bindings' => $bindings, 449 | 'data' => $data 450 | ]); 451 | 452 | if(is_null($eventResult) === false) 453 | return $eventResult; 454 | 455 | $this->dispatch('before-query', [ 456 | 'type' => 'update', 457 | 'query' => $sql, 458 | 'bindings' => $bindings, 459 | 'data' => $data 460 | ]); 461 | 462 | $result = $this->prepareAndExecute($sql, $bindings); 463 | 464 | $this->dispatch('after-query', [ 465 | 'type' => 'update', 466 | 'query' => $sql, 467 | 'bindings' => $bindings, 468 | 'data' => $data, 469 | 'result' => $result->rowCount() 470 | ]); 471 | 472 | $this->dispatch('after-update', [ 473 | 'query' => $sql, 474 | 'bindings' => $bindings, 475 | 'data' => $data, 476 | 'result' => $result->rowCount() 477 | ]); 478 | 479 | return $result->rowCount(); 480 | } 481 | 482 | public function updateOrInsert($data, $table = null) 483 | { 484 | if($this->first()) 485 | return $this->update($data, $table); 486 | else 487 | return $this->insert($data, $table); 488 | } 489 | 490 | public function onDuplicateKeyUpdate($data) 491 | { 492 | $this->addQuerySegment('onduplicate', $data); 493 | 494 | return $this; 495 | } 496 | 497 | public function delete($table = null) 498 | { 499 | if($table) 500 | $this->table($table); 501 | 502 | $query = $this->getQuery('delete'); 503 | $sql = $query->getSql(); 504 | $bindings = $query->getBindings(); 505 | 506 | $eventResult = $this->dispatch('before-delete', [ 507 | 'query' => $sql, 508 | 'bindings' => $bindings, 509 | ]); 510 | 511 | if(is_null($eventResult) === false) 512 | return $eventResult; 513 | 514 | $this->dispatch('before-query', [ 515 | 'type' => 'delete', 516 | 'query' => $sql, 517 | 'bindings' => $bindings, 518 | ]); 519 | 520 | $result = $this->prepareAndExecute($sql, $bindings); 521 | 522 | $this->dispatch('after-query', [ 523 | 'type' => 'delete', 524 | 'query' => $sql, 525 | 'bindings' => $bindings, 526 | 'result' => $result->rowCount() 527 | ]); 528 | 529 | $this->dispatch('after-delete', [ 530 | 'query' => $sql, 531 | 'bindings' => $bindings, 532 | 'result' => $result->rowCount() 533 | ]); 534 | 535 | return $result->rowCount(); 536 | } 537 | 538 | public function groupBy($field) 539 | { 540 | $this->addQuerySegment('groupBy', $this->addTablePrefix($field)); 541 | 542 | return $this; 543 | } 544 | 545 | public function removeGroupBy() 546 | { 547 | $this->removeQuerySegment('groupBy'); 548 | 549 | return $this; 550 | } 551 | 552 | public function orderBy($fields, $defaultDirection = 'ASC') 553 | { 554 | if(is_array($fields) === false) 555 | $fields = [ $fields ]; 556 | 557 | foreach($fields as $key => $value) 558 | { 559 | $field = $key; 560 | $type = $value; 561 | 562 | if(is_int($key)) 563 | { 564 | $field = $value; 565 | $type = $defaultDirection; 566 | } 567 | 568 | if(! $field instanceof Raw) 569 | { 570 | $field = $this->addTablePrefix($field); 571 | } 572 | 573 | $this->querySegments['orderBy'][] = [ 574 | 'field' => $field, 575 | 'type' => $type 576 | ]; 577 | } 578 | 579 | return $this; 580 | } 581 | 582 | public function removeOrderBys() 583 | { 584 | $this->removeQuerySegment('orderBy'); 585 | 586 | return $this; 587 | } 588 | 589 | public function limit($limit) 590 | { 591 | $this->querySegments['limit'] = $limit; 592 | 593 | return $this; 594 | } 595 | 596 | public function removeLimit() 597 | { 598 | $this->removeQuerySegment('limit'); 599 | 600 | return $this; 601 | } 602 | 603 | public function offset($offset) 604 | { 605 | $this->querySegments['offset'] = $offset; 606 | 607 | return $this; 608 | } 609 | 610 | public function removeOffset() 611 | { 612 | $this->removeQuerySegment('offset'); 613 | 614 | return $this; 615 | } 616 | 617 | public function having($key, $operator, $value, $joiner = 'AND') 618 | { 619 | $this->querySegments['havings'][] = [ 620 | 'key' => $this->addTablePrefix($key), 621 | 'operator' => $operator, 622 | 'value' => $value, 623 | 'joiner' => $joiner 624 | ]; 625 | 626 | return $this; 627 | } 628 | 629 | public function removeHavings() 630 | { 631 | $this->removeQuerySegment('havings'); 632 | 633 | return $this; 634 | } 635 | 636 | public function orHaving($key, $operator, $value) 637 | { 638 | return $this->having($key, $operator, $value, 'OR'); 639 | } 640 | 641 | public function where($key, $operator = null, $value = null) 642 | { 643 | return $this->handleWhere($key, $operator, $value); 644 | } 645 | 646 | public function orWhere($key, $operator = null, $value = null) 647 | { 648 | return $this->handleWhere($key, $operator, $value, 'OR'); 649 | } 650 | 651 | public function whereNot($key, $operator = null, $value = null) 652 | { 653 | return $this->handleWhere($key, $operator, $value, 'AND NOT'); 654 | } 655 | 656 | public function orWhereNot($key, $operator = null, $value = null) 657 | { 658 | return $this->handleWhere($key, $operator, $value, 'OR NOT'); 659 | } 660 | 661 | public function whereIn($key, $values) 662 | { 663 | return $this->handleWhere($key, 'IN', $values, 'AND'); 664 | } 665 | 666 | public function whereNotIn($key, $values) 667 | { 668 | return $this->handleWhere($key, 'NOT IN', $values, 'AND'); 669 | } 670 | 671 | public function orWhereIn($key, $values) 672 | { 673 | return $this->handleWhere($key, 'IN', $values, 'OR'); 674 | } 675 | 676 | public function orWhereNotIn($key, $values) 677 | { 678 | return $this->handleWhere($key, 'NOT IN', $values, 'OR'); 679 | } 680 | 681 | public function whereBetween($key, $valueFrom, $valueTo) 682 | { 683 | return $this->handleWhere($key, 'BETWEEN', [ $valueFrom, $valueTo ], 'AND'); 684 | } 685 | 686 | public function orWhereBetween($key, $valueFrom, $valueTo) 687 | { 688 | return $this->handleWhere($key, 'BETWEEN', [ $valueFrom, $valueTo ], 'OR'); 689 | } 690 | 691 | public function whereNull($key) 692 | { 693 | return $this->handleWhereNull($key); 694 | } 695 | 696 | public function whereNotNull($key) 697 | { 698 | return $this->handleWhereNull($key, 'NOT'); 699 | } 700 | 701 | public function orWhereNull($key) 702 | { 703 | return $this->handleWhereNull($key, '', 'or'); 704 | } 705 | 706 | public function orWhereNotNull($key) 707 | { 708 | return $this->handleWhereNull($key, 'NOT', 'or'); 709 | } 710 | 711 | public function removeWheres() 712 | { 713 | $this->removeQuerySegment('wheres'); 714 | 715 | return $this; 716 | } 717 | 718 | public function join($table, $key, $operator = null, $value = null, $type = 'inner') 719 | { 720 | if(! $key instanceof \Closure) 721 | { 722 | $key = function ($joinBuilder) use ($key, $operator, $value) { 723 | $joinBuilder->on($key, $operator, $value); 724 | }; 725 | } 726 | 727 | $joinBuilder = new JoinBuilder($this->connection, $this->eventDispatcher); 728 | $joinBuilder = & $joinBuilder; 729 | 730 | $key($joinBuilder); 731 | 732 | $this->querySegments['joins'][] = [ 733 | 'type' => $type, 734 | 'table' => $this->addTablePrefix($table, true), 735 | 'joinBuilder' => $joinBuilder 736 | ]; 737 | 738 | return $this; 739 | } 740 | 741 | public function leftJoin($table, $key, $operator = null, $value = null) 742 | { 743 | return $this->join($table, $key, $operator, $value, 'left'); 744 | } 745 | 746 | public function rightJoin($table, $key, $operator = null, $value = null) 747 | { 748 | return $this->join($table, $key, $operator, $value, 'right'); 749 | } 750 | 751 | public function innerJoin($table, $key, $operator = null, $value = null) 752 | { 753 | return $this->join($table, $key, $operator, $value, 'inner'); 754 | } 755 | 756 | public function removeJoins() 757 | { 758 | $this->removeQuerySegment('joins'); 759 | 760 | return $this; 761 | } 762 | 763 | public function beginTransaction() 764 | { 765 | $this->pdo->beginTransaction(); 766 | 767 | return new Transaction($this->connection, $this->eventDispatcher); 768 | } 769 | 770 | public function rollbackTransaction() 771 | { 772 | $this->pdo->rollback(); 773 | 774 | return $this; 775 | } 776 | 777 | public function commitTransaction() 778 | { 779 | $this->pdo->commit(); 780 | 781 | return $this; 782 | } 783 | 784 | public function transaction(\Closure $callback) 785 | { 786 | try 787 | { 788 | $this->pdo->beginTransaction(); 789 | 790 | $transaction = new Transaction($this->connection, $this->eventDispatcher); 791 | 792 | $callback($transaction); 793 | 794 | $this->pdo->commit(); 795 | 796 | return $this; 797 | } 798 | catch(\Exception $e) 799 | { 800 | $this->pdo->rollBack(); 801 | return $this; 802 | } 803 | } 804 | 805 | public function prepareAndExecute($sql, $bindings = []) 806 | { 807 | $pdoStatement = $this->prepare($sql, $bindings); 808 | 809 | try 810 | { 811 | $pdoStatement->execute($bindings); 812 | } 813 | catch(PDOException $e) 814 | { 815 | throw new QueryExecutionFailException($e->getMessage().' during query "'.$sql.'"', $e->getCode(), $e); 816 | } 817 | 818 | return $pdoStatement; 819 | } 820 | 821 | public function prepare($sql, $bindings = []) 822 | { 823 | $pdoStatement = $this->pdo->prepare($sql); 824 | 825 | foreach($bindings as $key => $value) 826 | { 827 | $pdoStatement->bindValue( 828 | is_int($key) ? $key + 1 : $key, 829 | $value, 830 | is_int($value) || is_bool($value) ? PDO::PARAM_INT : PDO::PARAM_STR 831 | ); 832 | } 833 | 834 | return $pdoStatement; 835 | } 836 | 837 | public function addTablePrefix($values, $forceAddToAll = false) 838 | { 839 | $wasSingleValue = false; 840 | 841 | if(is_array($values) === false) 842 | { 843 | $values = [ $values ]; 844 | $wasSingleValue = true; 845 | } 846 | 847 | $result = []; 848 | 849 | foreach($values as $key => $value) 850 | { 851 | if(is_string($value) === false) 852 | { 853 | $result[$key] = $value; 854 | 855 | continue; 856 | } 857 | 858 | $target = & $value; 859 | 860 | if(is_int($key) === false) 861 | $target = & $key; 862 | 863 | if(strpos($target, '.') === false) 864 | { 865 | if($target !== '*' && $forceAddToAll) 866 | { 867 | $target = $this->compiler->addTablePrefix($target); 868 | } 869 | } 870 | else 871 | { 872 | $target = $this->compiler->addTablePrefix($target); 873 | } 874 | 875 | $result[$key] = $value; 876 | } 877 | 878 | return $wasSingleValue ? $result[0] : $result; 879 | } 880 | 881 | public function quote($value) 882 | { 883 | return $this->compiler->quote($value); 884 | } 885 | 886 | public function addQuerySegment($key, $value) 887 | { 888 | if(is_array($value) === false) 889 | $value = [ $value ]; 890 | 891 | if(isset($this->querySegments[$key]) === false) 892 | $this->querySegments[$key] = $value; 893 | else 894 | $this->querySegments[$key] = array_merge($this->querySegments[$key], $value); 895 | } 896 | 897 | public function replaceQuerySegment($key, $value) 898 | { 899 | $this->removeQuerySegment($key); 900 | $this->addQuerySegment($key, $value); 901 | 902 | return $this; 903 | } 904 | 905 | public function removeQuerySegment($key) 906 | { 907 | unset($this->querySegments[$key]); 908 | 909 | return $this; 910 | } 911 | 912 | public function getLastId() 913 | { 914 | return $this->pdo->lastInsertId(); 915 | } 916 | 917 | protected function doInsert($data, $type) 918 | { 919 | $query = $this->getQuery($type, $data); 920 | $sql = $query->getSql(); 921 | $bindings = $query->getBindings(); 922 | 923 | $eventResult = $this->dispatch('before-insert', [ 924 | 'query' => $sql, 925 | 'bindings' => $bindings, 926 | 'data' => $data 927 | ]); 928 | 929 | if(is_null($eventResult) === false) 930 | return $eventResult; 931 | 932 | $this->dispatch('before-query', [ 933 | 'type' => 'insert', 934 | 'query' => $sql, 935 | 'bindings' => $bindings, 936 | 'data' => $data 937 | ]); 938 | 939 | $result = $this->prepareAndExecute($sql, $bindings); 940 | $return = $result->rowCount() === 1 ? $this->getLastId() : null; 941 | 942 | $this->dispatch('after-query', [ 943 | 'type' => 'insert', 944 | 'query' => $sql, 945 | 'bindings' => $bindings, 946 | 'data' => $data, 947 | 'result' => $result->rowCount() 948 | ]); 949 | 950 | $this->dispatch('after-insert', [ 951 | 'query' => $sql, 952 | 'bindings' => $bindings, 953 | 'data' => $data, 954 | 'result' => $result->rowCount() 955 | ]); 956 | 957 | return $return; 958 | } 959 | 960 | protected function aggregate($type) 961 | { 962 | $mainSelects = isset($this->querySegments['selects']) ? $this->querySegments['selects'] : null; 963 | 964 | $this->querySegments['selects'] = [ $this->raw($type.' AS field') ]; 965 | $row = $this->all(); 966 | 967 | if($mainSelects) 968 | $this->querySegments['selects'] = $mainSelects; 969 | else 970 | unset($this->querySegments['selects']); 971 | 972 | if(is_array($row[0])) 973 | return (int) $row[0]['field']; 974 | elseif(is_object($row[0])) 975 | return (int) $row[0]->field; 976 | 977 | return 0; 978 | } 979 | 980 | protected function handleWhere($key, $operator = null, $value = null, $joiner = 'AND') 981 | { 982 | if($key && $operator && ! $value) 983 | { 984 | $value = $operator; 985 | $operator = '='; 986 | } 987 | 988 | $this->querySegments['wheres'][] = [ 989 | 'key' => $this->addTablePrefix($key), 990 | 'operator' => $operator, 991 | 'value' => $value, 992 | 'joiner' => $joiner 993 | ]; 994 | 995 | return $this; 996 | } 997 | 998 | protected function handleWhereNull($key, $prefix = '', $operator = '') 999 | { 1000 | $key = $this->compiler->quoteColumnName($this->addTablePrefix($key)); 1001 | 1002 | return $this->{$operator.'Where'}($this->raw($key.' IS '.$prefix.' NULL')); 1003 | } 1004 | } 1005 | --------------------------------------------------------------------------------