diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 2346d54d..00000000 --- a/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -[MIT LICENSE] - -Copyright (c) 2017 Angel Lai, https://medoo.in - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -Software), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, andor sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..aaf1c4b3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Angel Lai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index cb0e0858..f31e1d06 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,28 @@ -![](https://cloud.githubusercontent.com/assets/1467904/19835326/ca62bc36-9ebd-11e6-8b37-7240d76319cd.png) +

+ +

-## [Medoo](https://medoo.in) - -[![Total Downloads](https://poser.pugx.org/catfan/medoo/downloads)](https://packagist.org/packages/catfan/medoo) -[![Latest Stable Version](https://poser.pugx.org/catfan/medoo/v/stable)](https://packagist.org/packages/catfan/medoo) -[![License](https://poser.pugx.org/catfan/medoo/license)](https://packagist.org/packages/catfan/medoo) +

+ Total Downloads + Latest Stable Version + License +

> The Lightest PHP database framework to accelerate development -## Main Features +## Features -* **Lightweight** - 32KB around with only one file. +* **Lightweight** - Less than 100 KB, portable with only one file -* **Easy** - Extremely easy to learn and use, friendly construction. +* **Easy** - Extremely easy to learn and use, friendly construction -* **Powerful** - Supports various common and complex SQL queries, data mapping, and prevent SQL injection. +* **Powerful** - Supports various common and complex SQL queries, data mapping, and prevent SQL injection -* **Compatible** - Supports all SQL databases, including MySQL, MSSQL, SQLite, MariaDB, Sybase, Oracle, PostgreSQL and more. +* **Compatible** - Supports all SQL databases, including MySQL, MSSQL, SQLite, MariaDB, PostgreSQL, Sybase, Oracle and more -* **Friendly** - Works well with every PHP frameworks, like Laravel, Codeigniter, Yii, Slim, and framework which supports singleton extension. +* **Friendly** - Works well with every PHP frameworks, like Laravel, Codeigniter, Yii, Slim, and framework which supports singleton extension or composer -* **Free** - Under MIT license, you can use it anywhere if you want. +* **Free** - Under MIT license, you can use it anywhere whatever you want ## Requirement diff --git a/composer.json b/composer.json index 81b73cee..39c1a9de 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,10 @@ "keywords": ["database", "lightweight", "PHP framework", "SQL", "MySQL", "MSSQL", "SQLite", "PostgreSQL", "MariaDB", "Oracle"], "homepage": "https://medoo.in", "license": "MIT", + "support": { + "issues": "https://github.com/catfan/Medoo/issues", + "source": "https://github.com/catfan/Medoo" + }, "authors": [ {"name": "Angel Lai", "email": "angel@catfan.me"} ], @@ -14,7 +18,7 @@ }, "suggest": { "ext-pdo_mysql": "For MySQL or MariaDB database", - "ext-pdo_sqlsrv": "For MSSQL database on Windows platform", + "ext-pdo_sqlsrv": "For MSSQL database", "ext-pdo_dblib": "For MSSQL or Sybase database on Linux/UNIX platform", "ext-pdo_oci": "For Oracle database", "ext-pdo_oci8": "For Oracle version 8 database", diff --git a/src/Medoo.php b/src/Medoo.php index 097d998d..762bfca7 100644 --- a/src/Medoo.php +++ b/src/Medoo.php @@ -2,7 +2,7 @@ /*! * Medoo database framework * https://medoo.in - * Version 1.4.5 + * Version 1.5 * * Copyright 2017, Angel Lai * Released under the MIT license @@ -14,9 +14,16 @@ use Exception; use PDOException; +class Raw { + public $map; + public $value; +} + class Medoo { - protected $database_type; + public $pdo; + + protected $type; protected $prefix; @@ -34,200 +41,192 @@ class Medoo public function __construct($options = null) { - try { - if (is_array($options)) - { - if (isset($options[ 'database_type' ])) - { - $this->database_type = strtolower($options[ 'database_type' ]); - } - } - else - { - return false; - } + if (!is_array($options)) + { + return false; + } - if (isset($options[ 'prefix' ])) - { - $this->prefix = $options[ 'prefix' ]; - } + if (isset($options[ 'database_type' ])) + { + $this->type = strtolower($options[ 'database_type' ]); + } - if (isset($options[ 'option' ])) - { - $this->option = $options[ 'option' ]; - } + if (isset($options[ 'prefix' ])) + { + $this->prefix = $options[ 'prefix' ]; + } - if (isset($options[ 'logging' ]) && is_bool($options[ 'logging' ])) - { - $this->logging = $options[ 'logging' ]; - } + if (isset($options[ 'option' ])) + { + $this->option = $options[ 'option' ]; + } - if (isset($options[ 'command' ]) && is_array($options[ 'command' ])) - { - $commands = $options[ 'command' ]; - } - else + if (isset($options[ 'logging' ]) && is_bool($options[ 'logging' ])) + { + $this->logging = $options[ 'logging' ]; + } + + if (isset($options[ 'command' ]) && is_array($options[ 'command' ])) + { + $commands = $options[ 'command' ]; + } + else + { + $commands = []; + } + + if (isset($options[ 'dsn' ])) + { + if (is_array($options[ 'dsn' ]) && isset($options[ 'dsn' ][ 'driver' ])) { - $commands = []; + $attr = $options[ 'dsn' ]; } - if (isset($options[ 'dsn' ])) + return false; + } + else + { + if ( + isset($options[ 'port' ]) && + is_int($options[ 'port' ] * 1) + ) { - if (isset($options[ 'dsn' ][ 'driver' ])) - { - $attr = $options[ 'dsn' ]; - } - else - { - return false; - } + $port = $options[ 'port' ]; } - else - { - if ( - isset($options[ 'port' ]) && - is_int($options[ 'port' ] * 1) - ) - { - $port = $options[ 'port' ]; - } - $is_port = isset($port); + $is_port = isset($port); - switch ($this->database_type) - { - case 'mariadb': - case 'mysql': - $attr = [ - 'driver' => 'mysql', - 'dbname' => $options[ 'database_name' ] - ]; + switch ($this->type) + { + case 'mariadb': + case 'mysql': + $attr = [ + 'driver' => 'mysql', + 'dbname' => $options[ 'database_name' ] + ]; - if (isset($options[ 'socket' ])) + if (isset($options[ 'socket' ])) + { + $attr[ 'unix_socket' ] = $options[ 'socket' ]; + } + else + { + $attr[ 'host' ] = $options[ 'server' ]; + + if ($is_port) { - $attr[ 'unix_socket' ] = $options[ 'socket' ]; + $attr[ 'port' ] = $port; } - else - { - $attr[ 'host' ] = $options[ 'server' ]; + } - if ($is_port) - { - $attr[ 'port' ] = $port; - } - } + // Make MySQL using standard quoted identifier + $commands[] = 'SET SQL_MODE=ANSI_QUOTES'; + break; - // Make MySQL using standard quoted identifier - $commands[] = 'SET SQL_MODE=ANSI_QUOTES'; - break; + case 'pgsql': + $attr = [ + 'driver' => 'pgsql', + 'host' => $options[ 'server' ], + 'dbname' => $options[ 'database_name' ] + ]; - case 'pgsql': - $attr = [ - 'driver' => 'pgsql', - 'host' => $options[ 'server' ], - 'dbname' => $options[ 'database_name' ] - ]; + if ($is_port) + { + $attr[ 'port' ] = $port; + } - if ($is_port) - { - $attr[ 'port' ] = $port; - } + break; - break; + case 'sybase': + $attr = [ + 'driver' => 'dblib', + 'host' => $options[ 'server' ], + 'dbname' => $options[ 'database_name' ] + ]; + + if ($is_port) + { + $attr[ 'port' ] = $port; + } - case 'sybase': + break; + + case 'oracle': + $attr = [ + 'driver' => 'oci', + 'dbname' => $options[ 'server' ] ? + '//' . $options[ 'server' ] . ($is_port ? ':' . $port : ':1521') . '/' . $options[ 'database_name' ] : + $options[ 'database_name' ] + ]; + + if (isset($options[ 'charset' ])) + { + $attr[ 'charset' ] = $options[ 'charset' ]; + } + + break; + + case 'mssql': + if (isset($options[ 'driver' ]) && $options[ 'driver' ] === 'dblib') + { $attr = [ 'driver' => 'dblib', - 'host' => $options[ 'server' ], + 'host' => $options[ 'server' ] . ($is_port ? ':' . $port : ''), 'dbname' => $options[ 'database_name' ] ]; - - if ($is_port) - { - $attr[ 'port' ] = $port; - } - - break; - - case 'oracle': + } + else + { $attr = [ - 'driver' => 'oci', - 'dbname' => $options[ 'server' ] ? - '//' . $options[ 'server' ] . ($is_port ? ':' . $port : ':1521') . '/' . $options[ 'database_name' ] : - $options[ 'database_name' ] + 'driver' => 'sqlsrv', + 'Server' => $options[ 'server' ] . ($is_port ? ',' . $port : ''), + 'Database' => $options[ 'database_name' ] ]; + } - if (isset($options[ 'charset' ])) - { - $attr[ 'charset' ] = $options[ 'charset' ]; - } - - break; - - case 'mssql': - if (strstr(PHP_OS, 'WIN')) - { - $attr = [ - 'driver' => 'sqlsrv', - 'Server' => $options[ 'server' ] . ($is_port ? ',' . $port : ''), - 'Database' => $options[ 'database_name' ] - ]; - } - else - { - $attr = [ - 'driver' => 'dblib', - 'host' => $options[ 'server' ] . ($is_port ? ':' . $port : ''), - 'dbname' => $options[ 'database_name' ] - ]; - } - - // Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting - $commands[] = 'SET QUOTED_IDENTIFIER ON'; + // Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting + $commands[] = 'SET QUOTED_IDENTIFIER ON'; - // Make ANSI_NULLS is ON for NULL value - $commands[] = 'SET ANSI_NULLS ON'; - break; + // Make ANSI_NULLS is ON for NULL value + $commands[] = 'SET ANSI_NULLS ON'; + break; - case 'sqlite': - $this->pdo = new PDO('sqlite:' . $options[ 'database_file' ], null, null, $this->option); + case 'sqlite': + $attr = [ + 'driver' => 'sqlite', + $options[ 'database_file' ] + ]; - return; - } + break; } + } - $driver = $attr[ 'driver' ]; + $driver = $attr[ 'driver' ]; - unset($attr[ 'driver' ]); + unset($attr[ 'driver' ]); - $stack = []; + $stack = []; - foreach ($attr as $key => $value) - { - if (is_int($key)) - { - $stack[] = $value; - } - else - { - $stack[] = $key . '=' . $value; - } - } + foreach ($attr as $key => $value) + { + $stack[] = is_int($key) ? $value : $key . '=' . $value; + } - $dsn = $driver . ':' . implode($stack, ';'); + $dsn = $driver . ':' . implode($stack, ';'); - if ( - in_array($this->database_type, ['mariadb', 'mysql', 'pgsql', 'sybase', 'mssql']) && - isset($options[ 'charset' ]) - ) - { - $commands[] = "SET NAMES '" . $options[ 'charset' ] . "'"; - } + if ( + in_array($this->type, ['mariadb', 'mysql', 'pgsql', 'sybase', 'mssql']) && + isset($options[ 'charset' ]) + ) + { + $commands[] = "SET NAMES '" . $options[ 'charset' ] . "'"; + } + try { $this->pdo = new PDO( $dsn, - $options[ 'username' ], - $options[ 'password' ], + isset($options[ 'username' ]) ? $options[ 'username' ] : null, + isset($options[ 'password' ]) ? $options[ 'password' ] : null, $this->option ); @@ -237,41 +236,27 @@ public function __construct($options = null) } } catch (PDOException $e) { - throw new Exception($e->getMessage()); + throw new PDOException($e->getMessage()); } } - public function query($query, $map = []) + public function __call($name, $arguments) { - if (!empty($map)) - { - foreach ($map as $key => $value) - { - switch (gettype($value)) - { - case 'NULL': - $map[ $key ] = [null, PDO::PARAM_NULL]; - break; + $aggregation = ['avg', 'count', 'max', 'min', 'sum']; - case 'resource': - $map[ $key ] = [$value, PDO::PARAM_LOB]; - break; + if (in_array($name, $aggregation)) + { + array_unshift($arguments, $name); - case 'boolean': - $map[ $key ] = [($value ? '1' : '0'), PDO::PARAM_BOOL]; - break; + return call_user_func_array([$this, 'aggregate'], $arguments); + } + } - case 'integer': - case 'double': - $map[ $key ] = [$value, PDO::PARAM_INT]; - break; + public function query($query, $map = []) + { + $raw = $this->raw($query, $map); - case 'string': - $map[ $key ] = [$value, PDO::PARAM_STR]; - break; - } - } - } + $query = $this->buildRaw($raw, $map); return $this->exec($query, $map); } @@ -311,28 +296,94 @@ public function exec($query, $map = []) return $statement; } - else - { - return false; - } + + return false; } protected function generate($query, $map) { + $identifier = [ + 'mysql' => '`$1`', + 'mariadb' => '`$1`', + 'mssql' => '[$1]' + ]; + + $query = preg_replace( + '/"([a-zA-Z0-9_]+)"/i', + isset($identifier[ $this->type ]) ? $identifier[ $this->type ] : '"$1"', + $query + ); + foreach ($map as $key => $value) { if ($value[ 1 ] === PDO::PARAM_STR) { - $query = str_replace($key, $this->quote($value[ 0 ]), $query); + $replace = $this->quote($value[ 0 ]); } elseif ($value[ 1 ] === PDO::PARAM_NULL) { - $query = str_replace($key, 'NULL', $query); + $replace = 'NULL'; + } + elseif ($value[ 1 ] === PDO::PARAM_LOB) + { + $replace = '{LOB_DATA}'; } else { - $query = str_replace($key, $value[ 0 ], $query); + $replace = $value[ 0 ]; } + + $query = str_replace($key, $replace, $query); + } + + return $query; + } + + public static function raw($string, $map = []) + { + $raw = new Raw(); + + $raw->map = $map; + $raw->value = $string; + + return $raw; + } + + protected function isRaw($object) + { + return $object instanceof Raw; + } + + protected function buildRaw($raw, &$map) + { + if (!$this->isRaw($raw)) + { + return false; + } + + $query = preg_replace_callback( + '/((FROM|TABLE|INTO|UPDATE)\s*)?\<([a-zA-Z0-9_\.]+)\>/i', + function ($matches) + { + if (!empty($matches[ 2 ])) + { + return $matches[ 2 ] . ' ' . $this->tableQuote($matches[ 3 ]); + } + + return $this->columnQuote($matches[ 3 ]); + }, + $raw->value); + + $raw_map = $raw->map; + + if (!empty($raw_map)) + { + foreach ($raw_map as $key => $value) + { + $raw_map[ $key ] = $this->typeMap($value, gettype($value)); + } + + $map = $raw_map; } return $query; @@ -353,19 +404,41 @@ protected function mapKey() return ':MeDoO_' . $this->guid++ . '_mEdOo'; } - protected function columnQuote($string) + protected function typeMap($value, $type) { - preg_match('/(^#)?([a-zA-Z0-9_]*)\.([a-zA-Z0-9_]*)(\s*\[JSON\]$)?/', $string, $column_match); + $map = [ + 'NULL' => PDO::PARAM_NULL, + 'integer' => PDO::PARAM_INT, + 'double' => PDO::PARAM_STR, + 'boolean' => PDO::PARAM_BOOL, + 'string' => PDO::PARAM_STR, + 'object' => PDO::PARAM_STR, + 'resource' => PDO::PARAM_LOB + ]; - if (isset($column_match[ 2 ], $column_match[ 3 ])) + if ($type === 'boolean') { - return '"' . $this->prefix . $column_match[ 2 ] . '"."' . $column_match[ 3 ] . '"'; + $value = ($value ? '1' : '0'); + } + elseif ($type === 'NULL') + { + $value = null; + } + + return [$value, $map[ $type ]]; + } + + protected function columnQuote($string) + { + if (strpos($string, '.') !== false) + { + return '"' . $this->prefix . str_replace('.', '"."', $string) . '"'; } return '"' . $string . '"'; } - protected function columnPush(&$columns) + protected function columnPush(&$columns, &$map) { if ($columns === '*') { @@ -383,17 +456,28 @@ protected function columnPush(&$columns) { if (is_array($value)) { - $stack[] = $this->columnPush($value); + $stack[] = $this->columnPush($value, $map); } - else + elseif (!is_int($key) && $raw = $this->buildRaw($value, $map)) + { + preg_match('/(?[a-zA-Z0-9_\.]+)(\s*\[(?(String|Bool|Int|Number))\])?/i', $key, $match); + + $stack[] = $raw . ' AS ' . $this->columnQuote( $match[ 'column' ] ); + } + elseif (is_int($key) && is_string($value)) { - preg_match('/(?[a-zA-Z0-9_\.]+)(?:\s*\((?[a-zA-Z0-9_]+)\)|\s*\[(?(String|Bool|Int|Number|Object|JSON))\])?/i', $value, $match); + preg_match('/(?[a-zA-Z0-9_\.]+)(?:\s*\((?[a-zA-Z0-9_]+)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/i', $value, $match); if (!empty($match[ 'alias' ])) { $stack[] = $this->columnQuote( $match[ 'column' ] ) . ' AS ' . $this->columnQuote( $match[ 'alias' ] ); $columns[ $key ] = $match[ 'alias' ]; + + if (!empty($match[ 'type' ])) + { + $columns[ $key ] .= ' [' . $match[ 'type' ] . ']'; + } } else { @@ -429,198 +513,209 @@ protected function innerConjunct($data, $map, $conjunctor, $outer_conjunctor) return implode($outer_conjunctor . ' ', $stack); } - protected function fnQuote($column, $string) - { - return (strpos($column, '#') === 0 && preg_match('/^[A-Z0-9\_]*\([^)]*\)$/', $string)) ? - - $string : - - $this->quote($string); - } - protected function dataImplode($data, &$map, $conjunctor) { - $wheres = []; + $stack = []; foreach ($data as $key => $value) { - $map_key = $this->mapKey(); - $type = gettype($value); + if ($type === 'array') + { + $relationship = strpos($key, 'AND', 0) !== false ? 'AND' : + (strpos($key, 'OR', 0) !== false ? 'OR' : false); + + if ($relationship) + { + $stack[] = !empty(array_diff_key($value, array_keys(array_keys($value)))) ? + '(' . $this->dataImplode($value, $map, ' ' . $relationship) . ')' : + '(' . $this->innerConjunct($value, $map, ' ' . $relationship, $conjunctor) . ')'; + + continue; + } + } + + $map_key = $this->mapKey(); + if ( - preg_match("/^(AND|OR)(\s+#.*)?$/i", $key, $relation_match) && - $type === 'array' + is_int($key) && + preg_match('/([a-zA-Z0-9_\.]+)\[(?\>\=?|\<\=?|\!|\=)\]([a-zA-Z0-9_\.]+)/i', $value, $match) ) { - $wheres[] = 0 !== count(array_diff_key($value, array_keys(array_keys($value)))) ? - '(' . $this->dataImplode($value, $map, ' ' . $relation_match[ 1 ]) . ')' : - '(' . $this->innerConjunct($value, $map, ' ' . $relation_match[ 1 ], $conjunctor) . ')'; + $stack[] = $this->columnQuote($match[ 1 ]) . ' ' . $match[ 'operator' ] . ' ' . $this->columnQuote($match[ 3 ]); } else { - if ( - is_int($key) && - preg_match('/([a-zA-Z0-9_\.]+)\[(?\>|\>\=|\<|\<\=|\!|\=)\]([a-zA-Z0-9_\.]+)/i', $value, $match) - ) - { - $wheres[] = $this->columnQuote($match[ 1 ]) . ' ' . $match[ 'operator' ] . ' ' . $this->columnQuote($match[ 3 ]); - } - else + preg_match('/([a-zA-Z0-9_\.]+)(\[(?\>\=?|\<\=?|\!|\<\>|\>\<|\!?~|REGEXP)\])?/i', $key, $match); + $column = $this->columnQuote($match[ 1 ]); + + if (isset($match[ 'operator' ])) { - preg_match('/(#?)([a-zA-Z0-9_\.]+)(\[(?\>|\>\=|\<|\<\=|\!|\<\>|\>\<|\!?~)\])?/i', $key, $match); - $column = $this->columnQuote($match[ 2 ]); + $operator = $match[ 'operator' ]; - if (!empty($match[ 1 ])) + if (in_array($operator, ['>', '>=', '<', '<='])) { - $wheres[] = $column . - (isset($match[ 'operator' ]) ? ' ' . $match[ 'operator' ] . ' ' : ' = ') . - $this->fnQuote($key, $value); + $condition = $column . ' ' . $operator . ' '; + + if (is_numeric($value)) + { + $condition .= $map_key; + $map[ $map_key ] = [$value, PDO::PARAM_INT]; + } + elseif ($raw = $this->buildRaw($value, $map)) + { + $condition .= $raw; + } + else + { + $condition .= $map_key; + $map[ $map_key ] = [$value, PDO::PARAM_STR]; + } - continue; + $stack[] = $condition; } - - if (isset($match[ 'operator' ])) + elseif ($operator === '!') { - $operator = $match[ 'operator' ]; - - if ($operator === '!') + switch ($type) { - switch ($type) - { - case 'NULL': - $wheres[] = $column . ' IS NOT NULL'; - break; - - case 'array': - $wheres[] = $column . ' NOT IN (' . $this->arrayQuote($value) . ')'; - break; - - case 'integer': - case 'double': - $wheres[] = $column . ' != ' . $map_key; - $map[ $map_key ] = [$value, PDO::PARAM_INT]; - break; - - case 'boolean': - $wheres[] = $column . ' != ' . $map_key; - $map[ $map_key ] = [($value ? '1' : '0'), PDO::PARAM_BOOL]; - break; - - case 'string': - $wheres[] = $column . ' != ' . $map_key; - $map[ $map_key ] = [$value, PDO::PARAM_STR]; - break; - } - } + case 'NULL': + $stack[] = $column . ' IS NOT NULL'; + break; - if ($operator === '<>' || $operator === '><') - { - if ($type === 'array') - { - if ($operator === '><') + case 'array': + $placeholders = []; + + foreach ($value as $index => $item) { - $column .= ' NOT'; + $placeholders[] = $map_key . $index; + $map[ $map_key . $index ] = [ + $item, + $this->typeMap($item, gettype($item)) + ]; } - $wheres[] = '(' . $column . ' BETWEEN ' . $map_key . 'a AND ' . $map_key . 'b)'; + $stack[] = $column . ' NOT IN (' . implode(', ', $placeholders) . ')'; + break; - $data_type = (is_numeric($value[ 0 ]) && is_numeric($value[ 1 ])) ? PDO::PARAM_INT : PDO::PARAM_STR; + case 'object': + if ($raw = $this->buildRaw($value, $map)) + { + $stack[] = $column . ' != ' . $raw; + } + break; - $map[ $map_key . 'a' ] = [$value[ 0 ], $data_type]; - $map[ $map_key . 'b' ] = [$value[ 1 ], $data_type]; - } + case 'integer': + case 'double': + case 'boolean': + case 'string': + $stack[] = $column . ' != ' . $map_key; + $map[ $map_key ] = $this->typeMap($value, $type); + break; } - - if ($operator === '~' || $operator === '!~') + } + elseif ($operator === '~' || $operator === '!~') + { + if ($type !== 'array') { - if ($type !== 'array') - { - $value = [ $value ]; - } + $value = [ $value ]; + } - $connector = ' OR '; - $stack = array_values($value); + $connector = ' OR '; + $data = array_values($value); - if (is_array($stack[ 0 ])) + if (is_array($data[ 0 ])) + { + if (isset($value[ 'AND' ]) || isset($value[ 'OR' ])) { - if (isset($value[ 'AND' ]) || isset($value[ 'OR' ])) - { - $connector = ' ' . array_keys($value)[ 0 ] . ' '; - $value = $stack[ 0 ]; - } + $connector = ' ' . array_keys($value)[ 0 ] . ' '; + $value = $data[ 0 ]; } + } - $like_clauses = []; - - foreach ($value as $index => $item) - { - $item = strval($item); + $like_clauses = []; - if (!preg_match('/(\[.+\]|_|%.+|.+%)/', $item)) - { - $item = '%' . $item . '%'; - } + foreach ($value as $index => $item) + { + $item = strval($item); - $like_clauses[] = $column . ($operator === '!~' ? ' NOT' : '') . ' LIKE ' . $map_key . 'L' . $index; - $map[ $map_key . 'L' . $index ] = [$item, PDO::PARAM_STR]; + if (!preg_match('/(\[.+\]|_|%.+|.+%)/', $item)) + { + $item = '%' . $item . '%'; } - $wheres[] = '(' . implode($connector, $like_clauses) . ')'; + $like_clauses[] = $column . ($operator === '!~' ? ' NOT' : '') . ' LIKE ' . $map_key . 'L' . $index; + $map[ $map_key . 'L' . $index ] = [$item, PDO::PARAM_STR]; } - if (in_array($operator, ['>', '>=', '<', '<='])) + $stack[] = '(' . implode($connector, $like_clauses) . ')'; + } + elseif ($operator === '<>' || $operator === '><') + { + if ($type === 'array') { - $condition = $column . ' ' . $operator . ' '; - - if (is_numeric($value)) + if ($operator === '><') { - $condition .= $map_key; - $map[ $map_key ] = [$value, PDO::PARAM_INT]; - } - else - { - $condition .= $map_key; - $map[ $map_key ] = [$value, PDO::PARAM_STR]; + $column .= ' NOT'; } - $wheres[] = $condition; + $stack[] = '(' . $column . ' BETWEEN ' . $map_key . 'a AND ' . $map_key . 'b)'; + + $data_type = (is_numeric($value[ 0 ]) && is_numeric($value[ 1 ])) ? PDO::PARAM_INT : PDO::PARAM_STR; + + $map[ $map_key . 'a' ] = [$value[ 0 ], $data_type]; + $map[ $map_key . 'b' ] = [$value[ 1 ], $data_type]; } } - else + elseif ($operator === 'REGEXP') { - switch ($type) - { - case 'NULL': - $wheres[] = $column . ' IS NULL'; - break; + $stack[] = $column . ' REGEXP ' . $map_key; + $map[ $map_key ] = [$value, PDO::PARAM_STR]; + } + } + else + { + switch ($type) + { + case 'NULL': + $stack[] = $column . ' IS NULL'; + break; - case 'array': - $wheres[] = $column . ' IN (' . $this->arrayQuote($value) . ')'; - break; + case 'array': + $placeholders = []; - case 'integer': - case 'double': - $wheres[] = $column . ' = ' . $map_key; - $map[ $map_key ] = [$value, PDO::PARAM_INT]; - break; + foreach ($value as $index => $item) + { + $placeholders[] = $map_key . $index; + $map[ $map_key . $index ] = [ + $item, + $this->typeMap($item, gettype($item)) + ]; + } - case 'boolean': - $wheres[] = $column . ' = ' . $map_key; - $map[ $map_key ] = [($value ? '1' : '0'), PDO::PARAM_BOOL]; - break; + $stack[] = $column . ' IN (' . implode(', ', $placeholders) . ')'; + break; - case 'string': - $wheres[] = $column . ' = ' . $map_key; - $map[ $map_key ] = [$value, PDO::PARAM_STR]; - break; - } + case 'object': + if ($raw = $this->buildRaw($value, $map)) + { + $stack[] = $column . ' = ' . $raw; + } + break; + + case 'integer': + case 'double': + case 'boolean': + case 'string': + $stack[] = $column . ' = ' . $map_key; + $map[ $map_key ] = $this->typeMap($value, $type); + break; } } } } - return implode($conjunctor . ' ', $wheres); + return implode($conjunctor . ' ', $stack); } protected function whereClause($where, &$map) @@ -630,33 +725,14 @@ protected function whereClause($where, &$map) if (is_array($where)) { $where_keys = array_keys($where); - $where_AND = preg_grep("/^AND\s*#?$/i", $where_keys); - $where_OR = preg_grep("/^OR\s*#?$/i", $where_keys); - $single_condition = array_diff_key($where, array_flip( - ['AND', 'OR', 'GROUP', 'ORDER', 'HAVING', 'LIMIT', 'LIKE', 'MATCH'] + $conditions = array_diff_key($where, array_flip( + ['GROUP', 'ORDER', 'HAVING', 'LIMIT', 'LIKE', 'MATCH'] )); - if (!empty($single_condition)) - { - $condition = $this->dataImplode($single_condition, $map, ' AND'); - - if ($condition !== '') - { - $where_clause = ' WHERE ' . $condition; - } - } - - if (!empty($where_AND)) + if (!empty($conditions)) { - $value = array_values($where_AND); - $where_clause = ' WHERE ' . $this->dataImplode($where[ $value[ 0 ] ], $map, ' AND'); - } - - if (!empty($where_OR)) - { - $value = array_values($where_OR); - $where_clause = ' WHERE ' . $this->dataImplode($where[ $value[ 0 ] ], $map, ' OR'); + $where_clause = ' WHERE ' . $this->dataImplode($conditions, $map, ' AND'); } if (isset($where[ 'MATCH' ])) @@ -702,14 +778,25 @@ protected function whereClause($where, &$map) $where_clause .= ' GROUP BY ' . implode($stack, ','); } + elseif ($raw = $this->buildRaw($GROUP, $map)) + { + $where_clause .= ' GROUP BY ' . $raw; + } else { - $where_clause .= ' GROUP BY ' . $this->columnQuote($where[ 'GROUP' ]); + $where_clause .= ' GROUP BY ' . $this->columnQuote($GROUP); } if (isset($where[ 'HAVING' ])) { - $where_clause .= ' HAVING ' . $this->dataImplode($where[ 'HAVING' ], $map, ' AND'); + if ($raw = $this->buildRaw($where[ 'HAVING' ], $map)) + { + $where_clause .= ' HAVING ' . $raw; + } + else + { + $where_clause .= ' HAVING ' . $this->dataImplode($where[ 'HAVING' ], $map, ' AND'); + } } } @@ -739,6 +826,10 @@ protected function whereClause($where, &$map) $where_clause .= ' ORDER BY ' . implode($stack, ','); } + elseif ($raw = $this->buildRaw($ORDER, $map)) + { + $where_clause .= ' ORDER BY ' . $raw; + } else { $where_clause .= ' ORDER BY ' . $this->columnQuote($ORDER); @@ -746,16 +837,16 @@ protected function whereClause($where, &$map) if ( isset($where[ 'LIMIT' ]) && - in_array($this->database_type, ['oracle', 'mssql']) + in_array($this->type, ['oracle', 'mssql']) ) { $LIMIT = $where[ 'LIMIT' ]; if (is_numeric($LIMIT)) { - $where_clause .= ' FETCH FIRST ' . $LIMIT . ' ROWS ONLY'; + $LIMIT = [0, $LIMIT]; } - + if ( is_array($LIMIT) && is_numeric($LIMIT[ 0 ]) && @@ -767,7 +858,7 @@ protected function whereClause($where, &$map) } } - if (isset($where[ 'LIMIT' ]) && !in_array($this->database_type, ['oracle', 'mssql'])) + if (isset($where[ 'LIMIT' ]) && !in_array($this->type, ['oracle', 'mssql'])) { $LIMIT = $where[ 'LIMIT' ]; @@ -775,8 +866,7 @@ protected function whereClause($where, &$map) { $where_clause .= ' LIMIT ' . $LIMIT; } - - if ( + elseif ( is_array($LIMIT) && is_numeric($LIMIT[ 0 ]) && is_numeric($LIMIT[ 1 ]) @@ -786,12 +876,9 @@ protected function whereClause($where, &$map) } } } - else + elseif ($raw = $this->buildRaw($where, $map)) { - if ($where !== null) - { - $where_clause .= ' ' . $where; - } + $where_clause .= ' ' . $raw; } return $where_clause; @@ -832,7 +919,7 @@ protected function selectContext($table, &$map, $join, &$columns = null, $where foreach($join as $sub_table => $relation) { - preg_match('/(\[(?\<|\>|\>\<|\<\>)\])?(?[a-zA-Z0-9_]+)\s?(\((?[a-zA-Z0-9_]+)\))?/', $sub_table, $match); + preg_match('/(\[(?\<\>?|\>\[a-zA-Z0-9_]+)\s?(\((?[a-zA-Z0-9_]+)\))?/', $sub_table, $match); if ($match[ 'join' ] !== '' && $match[ 'table' ] !== '') { @@ -887,26 +974,18 @@ protected function selectContext($table, &$map, $join, &$columns = null, $where { if (is_null($columns)) { - if (is_null($where)) + if ( + !is_null($where) || + (is_array($join) && isset($column_fn)) + ) { - if ( - is_array($join) && - isset($column_fn) - ) - { - $where = $join; - $columns = null; - } - else - { - $where = null; - $columns = $join; - } + $where = $join; + $columns = null; } else { - $where = $join; - $columns = null; + $where = null; + $columns = $join; } } else @@ -935,12 +1014,12 @@ protected function selectContext($table, &$map, $join, &$columns = null, $where $where = $join; } - $column = $column_fn . '(' . $this->columnPush($columns) . ')'; + $column = $column_fn . '(' . $this->columnPush($columns, $map) . ')'; } } else { - $column = $this->columnPush($columns); + $column = $this->columnPush($columns, $map); } return 'SELECT ' . $column . ' FROM ' . $table_query . $this->whereClause($where, $map); @@ -957,11 +1036,11 @@ protected function columnMap($columns, &$stack) { if (is_int($key)) { - preg_match('/(?[a-zA-Z0-9_\.]*)(?:\s*\((?[a-zA-Z0-9_]+)\)|\s*\[(?(String|Bool|Int|Number|Object|JSON))\])?/i', $value, $key_match); + preg_match('/([a-zA-Z0-9_]+\.)?(?[a-zA-Z0-9_]+)(?:\s*\((?[a-zA-Z0-9_]+)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/i', $value, $key_match); $column_key = !empty($key_match[ 'alias' ]) ? $key_match[ 'alias' ] : - preg_replace('/^[\w]*\./i', '', $key_match[ 'column' ]); + $key_match[ 'column' ]; if (isset($key_match[ 'type' ])) { @@ -972,7 +1051,22 @@ protected function columnMap($columns, &$stack) $stack[ $value ] = [$column_key, 'String']; } } - else + elseif ($this->isRaw($value)) + { + preg_match('/([a-zA-Z0-9_]+\.)?(?[a-zA-Z0-9_]+)(\s*\[(?(String|Bool|Int|Number))\])?/i', $key, $key_match); + + $column_key = $key_match[ 'column' ]; + + if (isset($key_match[ 'type' ])) + { + $stack[ $key ] = [$column_key, $key_match[ 'type' ]]; + } + else + { + $stack[ $key ] = [$column_key, 'String']; + } + } + elseif (!is_int($key) && is_array($value)) { $this->columnMap($value, $stack); } @@ -985,16 +1079,27 @@ protected function dataMap($data, $columns, $column_map, &$stack) { foreach ($columns as $key => $value) { - if (is_int($key)) + $isRaw = $this->isRaw($value); + + if (is_int($key) || $isRaw) { - $map = $column_map[ $value ]; + $map = $column_map[ $isRaw ? $key : $value ]; + $column_key = $map[ 0 ]; if (isset($map[ 1 ])) { + if ($isRaw && in_array($map[ 1 ], ['Object', 'JSON'])) + { + continue; + } + switch ($map[ 1 ]) { case 'Number': + $stack[ $column_key ] = (double) $data[ $column_key ]; + break; + case 'Int': $stack[ $column_key ] = (int) $data[ $column_key ]; break; @@ -1042,7 +1147,7 @@ public function select($table, $join, $columns = null, $where = null) $column = $where === null ? $join : $columns; - $is_single_column = (is_string($column) && $column !== '*'); + $is_single = (is_string($column) && $column !== '*'); $query = $this->exec($this->selectContext($table, $map, $join, $columns, $where), $map); @@ -1058,7 +1163,7 @@ public function select($table, $join, $columns = null, $where = null) return $query->fetchAll(PDO::FETCH_ASSOC); } - if ($is_single_column) + if ($is_single) { return $query->fetchAll(PDO::FETCH_COLUMN); } @@ -1105,9 +1210,9 @@ public function insert($table, $datas) foreach ($columns as $key) { - if (strpos($key, '#') === 0) + if ($raw = $this->buildRaw($data[ $key ], $map)) { - $values[] = $this->fnQuote($key, $data[ $key ]); + $values[] = $raw; continue; } @@ -1123,12 +1228,10 @@ public function insert($table, $datas) { $value = $data[ $key ]; - switch (gettype($value)) - { - case 'NULL': - $map[ $map_key ] = [null, PDO::PARAM_NULL]; - break; + $type = gettype($value); + switch ($type) + { case 'array': $map[ $map_key ] = [ strpos($key, '[JSON]') === strlen($key) - 6 ? @@ -1139,24 +1242,15 @@ public function insert($table, $datas) break; case 'object': - $map[ $map_key ] = [serialize($value), PDO::PARAM_STR]; - break; + $value = serialize($value); + case 'NULL': case 'resource': - $map[ $map_key ] = [$value, PDO::PARAM_LOB]; - break; - case 'boolean': - $map[ $map_key ] = [($value ? '1' : '0'), PDO::PARAM_BOOL]; - break; - case 'integer': case 'double': - $map[ $map_key ] = [$value, PDO::PARAM_INT]; - break; - case 'string': - $map[ $map_key ] = [$value, PDO::PARAM_STR]; + $map[ $map_key ] = $this->typeMap($value, $type); break; } } @@ -1167,7 +1261,7 @@ public function insert($table, $datas) foreach ($columns as $key) { - $fields[] = $this->columnQuote(preg_replace("/(^#|\s*\[JSON\]$)/i", '', $key)); + $fields[] = $this->columnQuote(preg_replace("/(\s*\[JSON\]$)/i", '', $key)); } return $this->exec('INSERT INTO ' . $this->tableQuote($table) . ' (' . implode(', ', $fields) . ') VALUES ' . implode(', ', $stack), $map); @@ -1180,11 +1274,11 @@ public function update($table, $data, $where = null) foreach ($data as $key => $value) { - $column = $this->columnQuote(preg_replace("/(^#|\s*\[(JSON|\+|\-|\*|\/)\]$)/i", '', $key)); + $column = $this->columnQuote(preg_replace("/(\s*\[(JSON|\+|\-|\*|\/)\]$)/i", '', $key)); - if (strpos($key, '#') === 0) + if ($raw = $this->buildRaw($value, $map)) { - $fields[] = $column . ' = ' . $value; + $fields[] = $column . ' = ' . $raw; continue; } @@ -1203,12 +1297,10 @@ public function update($table, $data, $where = null) { $fields[] = $column . ' = ' . $map_key; - switch (gettype($value)) - { - case 'NULL': - $map[ $map_key ] = [null, PDO::PARAM_NULL]; - break; + $type = gettype($value); + switch ($type) + { case 'array': $map[ $map_key ] = [ strpos($key, '[JSON]') === strlen($key) - 6 ? @@ -1219,24 +1311,15 @@ public function update($table, $data, $where = null) break; case 'object': - $map[ $map_key ] = [serialize($value), PDO::PARAM_STR]; - break; + $value = serialize($value); + case 'NULL': case 'resource': - $map[ $map_key ] = [$value, PDO::PARAM_LOB]; - break; - case 'boolean': - $map[ $map_key ] = [($value ? '1' : '0'), PDO::PARAM_BOOL]; - break; - case 'integer': case 'double': - $map[ $map_key ] = [$value, PDO::PARAM_INT]; - break; - case 'string': - $map[ $map_key ] = [$value, PDO::PARAM_STR]; + $map[ $map_key ] = $this->typeMap($value, $type); break; } } @@ -1254,41 +1337,36 @@ public function delete($table, $where) public function replace($table, $columns, $where = null) { + if (!is_array($columns) || empty($columns)) + { + return false; + } + $map = []; + $stack = []; - if (is_array($columns)) + foreach ($columns as $column => $replacements) { - $replace_query = []; - - foreach ($columns as $column => $replacements) + if (is_array($replacements)) { - if (is_array($replacements[ 0 ])) - { - foreach ($replacements as $replacement) - { - $map_key = $this->mapKey(); - - $replace_query[] = $this->columnQuote($column) . ' = REPLACE(' . $this->columnQuote($column) . ', ' . $map_key . 'a, ' . $map_key . 'b)'; - - $map[ $map_key . 'a' ] = [$replacement[ 0 ], PDO::PARAM_STR]; - $map[ $map_key . 'b' ] = [$replacement[ 1 ], PDO::PARAM_STR]; - } - } - else + foreach ($replacements as $old => $new) { $map_key = $this->mapKey(); - $replace_query[] = $this->columnQuote($column) . ' = REPLACE(' . $this->columnQuote($column) . ', ' . $map_key . 'a, ' . $map_key . 'b)'; + $stack[] = $this->columnQuote($column) . ' = REPLACE(' . $this->columnQuote($column) . ', ' . $map_key . 'a, ' . $map_key . 'b)'; - $map[ $map_key . 'a' ] = [$replacements[ 0 ], PDO::PARAM_STR]; - $map[ $map_key . 'b' ] = [$replacements[ 1 ], PDO::PARAM_STR]; + $map[ $map_key . 'a' ] = [$old, PDO::PARAM_STR]; + $map[ $map_key . 'b' ] = [$new, PDO::PARAM_STR]; } } + } - $replace_query = implode(', ', $replace_query); + if (!empty($stack)) + { + return $this->exec('UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $stack) . $this->whereClause($where, $map), $map); } - return $this->exec('UPDATE ' . $this->tableQuote($table) . ' SET ' . $replace_query . $this->whereClause($where, $map), $map); + return false; } public function get($table, $join = null, $columns = null, $where = null) @@ -1297,9 +1375,18 @@ public function get($table, $join = null, $columns = null, $where = null) $stack = []; $column_map = []; - $column = $where === null ? $join : $columns; + if ($where === null) + { + $column = $join; + unset($columns[ 'LIMIT' ]); + } + else + { + $column = $columns; + unset($where[ 'LIMIT' ]); + } - $is_single_column = (is_string($column) && $column !== '*'); + $is_single = (is_string($column) && $column !== '*'); $query = $this->exec($this->selectContext($table, $map, $join, $columns, $where) . ' LIMIT 1', $map); @@ -1318,22 +1405,18 @@ public function get($table, $join = null, $columns = null, $where = null) $this->dataMap($data[ 0 ], $columns, $column_map, $stack); - if ($is_single_column) + if ($is_single) { return $stack[ $column_map[ $column ][ 0 ] ]; } return $stack; } - else - { - return false; - } - } - else - { + return false; } + + return false; } public function has($table, $join, $where = null) @@ -1345,75 +1428,28 @@ public function has($table, $join, $where = null) if ($query) { - return $query->fetchColumn() === '1'; - } - else - { - return false; - } - } - - public function count($table, $join = null, $column = null, $where = null) - { - $map = []; - - $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, 'COUNT'), $map); - - return $query ? 0 + $query->fetchColumn() : false; - } - - public function max($table, $join, $column = null, $where = null) - { - $map = []; - - $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, 'MAX'), $map); - - if ($query) - { - $max = $query->fetchColumn(); + $result = $query->fetchColumn(); - return is_numeric($max) ? $max + 0 : $max; - } - else - { - return false; + return $result === '1' || $result === true; } + + return false; } - public function min($table, $join, $column = null, $where = null) + private function aggregate($type, $table, $join = null, $column = null, $where = null) { $map = []; - $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, 'MIN'), $map); + $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, strtoupper($type)), $map); if ($query) { - $min = $query->fetchColumn(); + $number = $query->fetchColumn(); - return is_numeric($min) ? $min + 0 : $min; + return is_numeric($number) ? $number + 0 : $number; } - else - { - return false; - } - } - - public function avg($table, $join, $column = null, $where = null) - { - $map = []; - $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, 'AVG'), $map); - - return $query ? 0 + $query->fetchColumn() : false; - } - - public function sum($table, $join, $column = null, $where = null) - { - $map = []; - - $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, 'SUM'), $map); - - return $query ? 0 + $query->fetchColumn() : false; + return false; } public function action($actions) @@ -1422,26 +1458,33 @@ public function action($actions) { $this->pdo->beginTransaction(); - $result = $actions($this); + try { + $result = $actions($this); - if ($result === false) - { - $this->pdo->rollBack(); + if ($result === false) + { + $this->pdo->rollBack(); + } + else + { + $this->pdo->commit(); + } } - else - { - $this->pdo->commit(); + catch (Exception $e) { + $this->pdo->rollBack(); + + throw $e; } + + return $result; } - else - { - return false; - } + + return false; } public function id() { - $type = $this->database_type; + $type = $this->type; if ($type === 'oracle') {