I am preparing a simple class. I want to create a query by following a different path. Class below it is a draft. I look forward to your suggestions.
The Cyclomatic Complexity number 4. PHP Mess Detector, PHPCS fixer tools, I have no problems.
My English is not good, I'm sorry.
<?php
class test
{
protected $q = array();
protected $p = array();
public function get()
{
$new = array();
foreach ($this->p as $value) {
$new = array_merge($new, $value);
}
$this->p = $new;
print_r($this->p);
$query = 'select * from table ';
foreach ($this->q as $sql) {
$query .= implode(' ', $sql) . ' ';
}
echo $query . PHP_EOL;
}
public function camelCase($value)
{
return strtolower(preg_replace('/(.)([A-Z])/', '$1 $2', $value));
}
public function __call($method, $params)
{
$clause = $this->camelCase($method);
$clause = explode(' ', $clause);
if ($key = array_search('in', $clause)) {
$clause[$key] = 'in(?)';
} elseif (isset($clause[2]) && in_array($clause[2], array(
'or',
'and'
))) {
$clause[1] = $clause[1] . ' =?';
$clause[3] = $clause[3] . ' =?';
} elseif (isset($clause[0]) && $clause[0] == 'limit') {
$clause[$key] = 'limit ? offset ?';
} elseif (isset($clause[1]) && $clause[1] != 'by') {
$clause[1] = $clause[1] . ' =?';
}
$this->q[] = $clause;
$this->p[] = $params;
return $this;
}
}
The use of the query creator class is as follows.
<?php
(new test())
->orderByIdDesc()
->limit(15)
->get();
(new test())
->whereIdOrName(6,'foo')
->get();
(new test())
->whereIdIsNotNull(10)
->get();
(new test())
->whereIdIn(9)
->get();
(new test())
->whereIdNotIn(8)
->get();
Output:
Array
(
[0] => 15
)
select * from table order by id desc limit ? offset ?
Array
(
[0] => 6
[1] => foo
)
select * from table where id =? or name =?
Array
(
[0] => 10
)
select * from table where id =? is not null
Array
(
[0] => 9
)
select * from table where id in(?)
Array
(
[0] => 8
)
select * from table where id not in(?)
If it does not make sense, you can write why it doesn't make sense.
In terms of responsibility, your class should never echo in a method.
public function get()
{
$new = array();
foreach ($this->p as $value) {
$new = array_merge($new, $value);
}
$this->p = $new;
print_r($this->p);
$query = 'select * from table ';
foreach($this->q as $sql){
$query .= implode(' ', $sql) . ' ';
}
return $query . PHP_EOL;
}
And
<?= (new test())
->orderByIdDesc()
->limit(15)
->get();
Also, you should have a look at existing libraries rather than inventing and maintaining your own (eloquent for instance).
If you're PHP 5.4+ (which I hope), you could use the short array syntax.
The class could probably be final and with private attributes.
Related
I can get the not-bind query on with this way :
\DB::enableQueryLog();
$items = OrderItem::where('name', '=', 'test')->get();
$log = \DB::getQueryLog();
print_r($log);
Output is :
(
[0] => Array
(
[query] => select * from "order_items" where "order_items"."name" = ? and "order_items"."deleted_at" is null
[bindings] => Array
(
[0] => test
)
[time] => 0.07
)
)
But what I really need is bind query like this :
select * from "order_items" where "order_items"."name" = 'test' and "order_items"."deleted_at" is null
I know I can do this with raw PHP but is there any solution in laravel core?
Actually I've created one function within helpers.php for same. You can also use same function within your helpers.php file
if (! function_exists('ql'))
{
/**
* Get Query Log
*
* #return array of queries
*/
function ql()
{
$log = \DB::getQueryLog();
$pdo = \DB::connection()->getPdo();
foreach ($log as &$l)
{
$bindings = $l['bindings'];
if (!empty($bindings))
{
foreach ($bindings as $key => $binding)
{
// This regex matches placeholders only, not the question marks,
// nested in quotes, while we iterate through the bindings
// and substitute placeholders by suitable values.
$regex = is_numeric($key)
? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"
: "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";
$l['query'] = preg_replace($regex, $pdo->quote($binding), $l['query'], 1);
}
}
}
return $log;
}
}
if (! function_exists('qldd'))
{
/**
* Get Query Log then Dump and Die
*
* #return array of queries
*/
function qldd()
{
dd(ql());
}
}
if (! function_exists('qld'))
{
/**
* Get Query Log then Dump
*
* #return array of queries
*/
function qld()
{
dump(ql());
}
}
Simply place these three functions within your helpers.php file and you can use same as follows:
$items = OrderItem::where('name', '=', 'test')->get();
qldd(); //for dump and die
or you can use
qld(); // for dump only
Here I extended the answer of #blaz
In app\Providers\AppServiceProvider.php
Add this on boot() method
if (env('APP_DEBUG')) {
DB::listen(function($query) {
File::append(
storage_path('/logs/query.log'),
self::queryLog($query->sql, $query->bindings) . "\n\n"
);
});
}
and also added a private method
private function queryLog($sql, $binds)
{
$result = "";
$sql_chunks = explode('?', $sql);
foreach ($sql_chunks as $key => $sql_chunk) {
if (isset($binds[$key])) {
$result .= $sql_chunk . '"' . $binds[$key] . '"';
}
}
$result .= $sql_chunks[count($sql_chunks) -1];
return $result;
}
Yeah, you're right :/
This is a highly requested feature, and i have no idea why its not a part of the framework yet...
This is not the most elegant solution, but you can do something like this:
function getPureSql($sql, $binds) {
$result = "";
$sql_chunks = explode('?', $sql);
foreach ($sql_chunks as $key => $sql_chunk) {
if (isset($binds[$key])) {
$result .= $sql_chunk . '"' . $binds[$key] . '"';
}
}
return $result;
}
$query = OrderItem::where('name', '=', 'test');
$pure_sql_query = getPureSql($query->toSql(), $query->getBindings());
// Or like this:
$data = OrderItem::where('name', '=', 'test')->get();
$log = DB::getQueryLog();
$log = end($log);
$pure_sql_query = getPureSql($log['query'], $log['bindings']);
You can do that with:
OrderItem::where('name', '=', 'test')->toSql();
I have an array that looks like this
$users = array(
array('name'=>'aaa','age'=>2),
array('name'=>'bbb','age'=>9),
array('name'=>'ccc','age'=>7)
);
I would like to create a function that will accept an array like above, creates a clause for a single query-multiple insert, prepares an array of variable that I can bind with PDO.
example output:
$clause = INSERT INTO tablename (`name`,`age`)
VALUES (:name_0,:age_0),(:name_1,:age_1),(:name_2,:age_2);
Then another set of array corresponding to the values above:
$params => Array
(
[name_0] => aaa
[age_0] => 2
[name_1] => bbb
[age_1] => 9
[name_2] => ccc
[age_2] => 7
);
So that the can execute it like so:
$prepared = $connection->prepare($clause);
$prepared->execute($params);
Is it possible to achieve this in a single function?
Yes that very possible, I did exactly the same thing for my custom query builder class:
function INSERT_MULTIPLE_QUERY($ARRS = array()){
$raw_cols = '(`';
// PREPARE THE COLUMNS
foreach($ARRS[0] as $key1 => $value):
$raw_cols .= $key1.'`,`';
endforeach;
$final_cols = rtrim($raw_cols,'`,`') . '`)';
$ctr1=0; $raw_vals='';
// PREPARE THE VALUES
foreach($ARRS as $ARR_VALUE):
$raw_vals .= '(';
foreach($ARR_VALUE as $key => $value): $raw_vals .= ':'.$key.'_'.$ctr1.','; endforeach;
$raw_vals = rtrim($raw_vals,',');
$raw_vals .= '),';
$ctr1++;
endforeach;
$final_vals = rtrim($raw_vals,',');
$ctr2 = 0; $param = array();
// PREPARE THE PARAMETERS
foreach($ARRS as $ARR_PARAM):
foreach($ARR_PARAM as $key_param => $value_param):$param[$key_param.'_'.$ctr2] = $value_param; endforeach;
$ctr2++;
endforeach;
// PREPARE THE CLAUSE
$clause = 'INSERT INTO tablename ' . $final_cols . ' VALUES ' . $final_vals;
// RETURN THE CLAUSE AND THE PARAMETERS
$return['clause'] = $clause;
$return['param'] = $param;
return $return;
}
Now to use this function:
$query = INSERT_MULTIPLE_QUERY($users);
// $users is your example array above
Then:
$prepared = $connection->prepare($query['clause']);
$prepared->execute($query['param']);
You can do it in a OOP style by creating a QueryBuilder and PDOStatementDecorator like below:
class QueryBuilder
{
const BUILD_TYPE_INSERT_MULTIPLE = 'INSERT_MULTIPLE';
protected $table;
protected $values;
protected $buildType;
public function __construct($table)
{
$this->table = $table;
}
public static function onTable($table)
{
return new self($table);
}
public function insertMultiple(Array $values = array())
{
$this->values = $values;
$this->buildType = self::BUILD_TYPE_INSERT_MULTIPLE;
return $this;
}
public function build()
{
switch ($this->buildType) {
case self::BUILD_TYPE_INSERT_MULTIPLE:
return $this->buildInsertMultiple();
}
}
protected function buildInsertMultiple()
{
$fields = array_keys($this->values[0]);
$query = "INSERT INTO {$this->table} (" . implode(',', $fields) . ") VALUES ";
$values = array();
for ($i = 0; $i < count($fields); $i++) {
$values[] = '(' . implode(', ', array_map(function($field) use ($i) {
return ':' . $field . $i;
}, $fields)) . ')';
}
$query .= implode(', ', $values);
return $query;
}
}
class PDOStatementDecorator
{
protected $pdoStatement;
public function __construct(PDOStatement $pdoStatement)
{
$this->pdoStatement = $pdoStatement;
}
public function executeMultiple(Array $bindsGroup = array())
{
$binds = array();
for ($i = 0; $i < count($bindsGroup); $i++) {
foreach ($bindsGroup[$i] as $key => $value) {
$binds[$key . $i] = $value;
}
}
return $this->execute($binds);
}
public function execute(Array $inputParemeters)
{
return $this->pdoStatement->execute($inputParemeters);
}
public function fetch($fetchStyle = null, $cursorOrientation = 'PDO::FETCH_ORI_NEXT', $cursorOffset = 0)
{
return $this->pdoStatement->fetch($fetchStyle, $cursorOrientation, $cursorOffset);
}
/**
* TODO
* Implement all public PDOStatement methods
*/
}
The query builder can be enhanced to be able to build queries for update/delete statements.
Now the usage would be very simple:
$users = array(
array('name' => 'aaa', 'age' => 2),
array('name' => 'bbb', 'age' => 9),
array('name' => 'ccc', 'age' => 7),
);
$query = QueryBuilder::onTable('users')->insertMultiple($users)->build();
$stmt = new PDOStatementDecorator($pdo->prepare($query));
$stmt->executeMultiple($users);
This function require Table Name, your original array, and an optional parameter that is used as default value, only if one field is not present in all array rows:
function buildQuery( $table, $array, $default='NULL' )
{
/* Retrieve complete field names list: */
$fields = array();
foreach( $array as $row ) $fields = array_merge( $fields, array_keys( $row ) );
$fields = array_unique( $fields );
/* Analize each array row, then update parameters and values chunks: */
$values = $params = array();
foreach( $array as $key => $row )
{
$line = array();
foreach( $fields as $field )
{
if( !isset( $row[$field] ) )
{ $line[] = $default; }
else
{
$line[] = ":{$field}_{$key}";
$params["{$field}_{$key}"] = $row[$field];
}
}
$values[] = '('.implode(',',$line).')';
}
/* Compone MySQL query: */
$clause = sprintf
(
"INSERT INTO `%s` (`%s`) VALUES %s;",
$table,
implode( '`,`', $fields ),
implode( ',', $values )
);
/* Return array[ clause, params ]: */
return compact( 'clause', 'params' );
}
Calling it in this way:
$query = buildQuery( 'mytable', $users );
$query will contain this:
Array
(
[clause] => INSERT INTO `mytable` (`name`,`age`) VALUES (:name_0,:age_0),(:name_1,:age_1),(:name_2,:age_2);
[params] => Array
(
[name_0] => aaa
[age_0] => 2
[name_1] => bbb
[age_1] => 9
[name_2] => ccc
[age_2] => 7
)
)
eval.in demo
Is using an object to build up SQL Statements overkill?
Is defining a bare String enough for a SQL statement?
This is my PHP and SQL code:
class SQLStatement {
private $table;
private $sql;
const INNER_JOIN = 'INNER JOIN';
const LEFT_JOIN = 'LEFT JOIN';
const RIGHT_JOIN = 'RIGHT JOIN';
const OUTER_JOIN = 'OUTER JOIN';
public function __construct($table) {
$this->setTable($table);
}
public function setTable($table) {
$this->table = $table;
}
public function buildFromString($string) {
$this->sql = $sql;
}
public function select(array $columns) {
$sql = 'SELECT ';
$columns = implode(',', $columns);
$sql .= $columns;
$sql .= " FROM $this->table";
$this->sql = $sql;
return $this;
}
/**
* Setting up INSERT sql statement
*
* #param array $records The records to insert.The array keys must be the columns, and the array values must be the new values
* #return object Return this object back for method chaining
*/
public function insert(array $records) {
$columns = array_keys($records);
$values = array_values($records);
$values = array_map('quote',$values);
$sql = 'INSERT INTO ';
$sql .= $this->table . '('. implode(',', $columns) . ')';
$sql .= ' VALUES ' . '(' . implode(',', $values) . ')';
$this->sql = $sql;
return $this;
}
/**
* Setting up UPDATE sql statement
*
* #param array $records The records to update. The array keys must be the columns, and the array values must be the new records
* #return object Return this object back for method chaining
*/
public function update(array $records, $where) {
$sql = 'UPDATE ' . $this->table . ' SET ';
$data = array();
foreach ($records as $column => $record) {
$data[] = $column . '=' . quote($record);
}
$sql .= implode(', ', $data);
$this->sql = $sql;
return $this;
}
/**
* Setting up DELETE sql statement
* #return object Return this object back for method chaining
*/
public function delete($where=null) {
$sql = 'DELETE FROM ' . $this->table;
$this->sql = $sql;
if (isset($where)) {
$this->where($where);
}
return $this;
}
/**
* Setting up WHERE clause with equality condition. (Currently only support AND logic)
* #param array $equalityExpression Conditional equality expression. The key is the column, while the value is the conditional values
* #return object Return this object back for method chaining
*
*/
public function where(array $equalityExpression) {
if (!isset($this->sql)) {
throw new BadMethodCallException('You must use SELECT, INSERT, UPDATE, or DELETE first before where clause');
}
$where = ' WHERE ';
$conditions = array();
foreach ($equalityExpression as $column => $value) {
if (is_array($value)) {
$value = array_map('quote', $value);
$conditions[] = "$column IN (" . implode(',', $value) . ')';
}
else {
$value = quote($value);
$conditions[] = "$column = $value";
}
}
$where .= implode(' AND ', $conditions);
$this->sql .= $where;
return $this;
}
/**
* Setting up WHERE clause with expression (Currently only support AND logic)
* #param array $expression Conditional expression. The key is the operator, the value is array with key being the column and the value being the conditional value
* #return object Return this object back for method chaining
*/
public function advancedWhere(array $expression) {
if (!isset($this->sql)) {
throw new BadMethodCallException('You must use SELECT, INSERT, UPDATE, or DELETE first before where clause');
}
if (!is_array(reset($expression))) {
throw new InvalidArgumentException('Invalid format of expression');
}
$where = ' WHERE ';
$conditions = array();
foreach ($expression as $operator => $record) {
foreach ($record as $column => $value) {
$conditions[] = $column . ' ' . $operator . ' ' . quote($value);
}
}
$where .= implode(' AND ', $conditions);
$this->sql .= $where;
return $this;
}
/**
* Setting up join clause (INNER JOIN, LEFT JOIN, RIGHT JOIN, or OUTER JOIN)
* #param array $tables <p>Tables to join as the key and the conditions array as the value (Currently only support ON logic)</p>
* <p>Array example : array('post'=>array('postid'=>'post.id'))</p>
* #param string $mode The mode of join. It can be INNER JOIN, LEFT JOIN, RIGHT JOIN, or OUTER JOIN
* #return object Return this object back for method chaining
*/
public function join(array $joins, $mode = self::INNER_JOIN) {
if (!isset($this->sql) && strpos($this->sql, 'SELECT')) {
throw new BadMethodCallException('You must have SELECT clause before joining another table');
}
if (!is_array(reset($joins))) {
throw new InvalidArgumentException('Invalid format of the join array.');
}
$Conditions = array();
foreach ($joins as $table => $conditions) {
$join = ' ' . $mode . ' ' . $table . ' ON ';
foreach ($conditions as $tbl1 => $tbl2) {
$Conditions[] = $tbl1 . ' = ' . $tbl2;
}
$join .= implode(' AND ', $Conditions);
}
$this->sql .= $join;
return $this;
}
/**
* Setting up GROUP BY clause
* #param array $columns The columns you want to group by
* #param string $sort The type of sort, ascending is the default
* #return object Return this object back for method chaining
*/
public function groupBy(array $columns, $sort = 'ASC') {
if (!isset($this->sql)) {
throw new BadMethodCallException('You must use SELECT, INSERT, UPDATE, or DELETE first before group by clause');
}
$groupBy = ' GROUP BY ' . implode(',', $columns) . ' ' . $sort;
$this->sql .= $groupBy;
return $this;
}
/**
* Setting up HAVING clause with expression
* #param expression $expression Conditional expression. The key is the operator, the value is an array with key being the column and the value being the conditional value
* #return object Return this object back for method chaining
*/
public function having($expression) {
if (!isset($this->sql) && strpos($this->sql, 'GROUP BY') === FALSE) {
throw new BadMethodCallException('You must have SELECT, INSERT, UPDATE, or DELETE and have GROUP BY clause first before where clause');
}
if (!is_array(reset($expression))) {
throw new InvalidArgumentException('Invalid format of expression');
}
$having = ' HAVING ';
$conditions = array();
foreach ($expression as $operator => $record) {
foreach ($record as $column => $value) {
$conditions[] = $column . ' ' . $operator . ' ' . $value;
}
}
$having .= implode(' AND ', $conditions);
$this->sql .= $having;
return $this;
}
/**
* Return the SQL statement if this object is supposed to be string
* #return string The sql statement
*/
public function __toString() {
return $this->sql;
}
}
The disadvantage I found is when I need to use prepared statement since prepared statements contains placeholders.
Should I add a feature to my SQL Statement Object to do prepared statements? When should using prepared statements good practice?
It's not overkill or overengineering to use objects or prepared statements, they are good practice.
The goal is to make something versatile and re-usable, and it looks like you are heading on the right track.
However, many people have done this already, and you may be better off using an existing solution.
Some of the top ones are:
Doctrine
Propel
and I used to personally use:
Idiorm
And if I am not mistaken, all three are built on PDO, and use prepared statements.
If you wanted to make your own solution for this, PDO and prepared statements are a very good idea, if not a must.
I'm trying to build a search form. Currently I fill the data like this:
private function _get_results() {
$search_data = $this->input->post('type');
if ($search_data) {
$search_data = implode(' OR u.type = ', $search_data);
$search_array[] = array(
'key' => 'u.type',
'value' => $search_data,
'operand' => 'eq');
if (count($search_array)) {
$results = $this->accounts_model->get_search_results($search_array);
}
This is my model code.
function get_search_results(
$params = array(),
$single_result = false,
$order_by = array('order_by' => 'u.id', 'direction' => 'DESC')
) {
$qb = $this->doctrine->em->createQueryBuilder();
$qb->select('u');
$qb->from($this->_entity, 'u');
$qb->where($qb->expr()->eq('u.status', 1));
foreach ($params as $param) {
$qb->andWhere(
$qb->expr()->$param['operand']($param['key'], $param['value'])
);
}
$qb->orderBy($order_by['order_by'], $order_by['direction']);
$qb->setFirstResult(0);
$qb->setMaxResults(20);
echo $qb->getQuery()->getDql() . '<br/>';
die;
$result = $qb->getQuery()->getResult();
return $result;
}
the line echo $qb->getQuery()->getDql() . '<br/>'; returns this result:
SELECT u FROM Entities\Account u WHERE u.status = 1 AND (u.type = 1 OR u.type = 2) ORDER BY u.id DESC
is there a way to avoid using implode() to get the same result: ...AND (u.type = 1 OR u.type = 2)
I'm using codeigniter btw.
This is where the IN statement comes in, and the queryBuilder has support for it built in where you can pass in an array of values. Try this:
$qb->andWhere('u.type IN (:types)')->setParameter('types', $search_data);
Feels stupid to answer my own question, but this is my solution.
In the controller:
private function _get_results() {
$counter = 0;
$search_data = $this->input->post('type');
if ($search_data) {
$search_array[$counter] = array(
'operand_main' => 'orX');
foreach($search_data as $key=>$data){
$search_array[$counter]['expr'][] =
array(
'key' => 'u.type',
'value' => $data,
'operand' => '='
);
}
$counter++;
}
if (count($search_array)) {
$results = $this->accounts_model->get_search_results($search_array);
}
}
In the model:
function get_search_results(
$params = array(),
$single_result = false,
$order_by = array('order_by' => 'u.id', 'direction' => 'DESC')
){
$qb = $this->doctrine->em->createQueryBuilder();
$qb->select('u');
$qb->from($this->_entity, 'u');
$qb->where($qb->expr()->eq('u.status', 1));
foreach ($params as $key => $main_param) {
$Expr = $qb->expr()->$main_param['operand_main']();
foreach ($main_param['expr'] as $param) {
$Expr->add($param['key'] . ' ' . $param['operand'] . ' ' . $param['value']);
}
$qb->andWhere($Expr);
}
$qb->orderBy($order_by['order_by'], $order_by['direction']);
$qb->setFirstResult(0);
$qb->setMaxResults(20);
echo $qb->getQuery()->getDql() . '<br/>';
die;
$result = $qb->getQuery()->getResult();
return $result;
}
The line echo $qb->getQuery()->getDql() . '<br/>'; returns:
SELECT u FROM Entities\Account u WHERE u.status = 1 AND (u.type = 1 OR u.type = 2) ORDER BY u.id DESC
Hope this is useful to someone else.
I'm trying to make this function get() that takes tbl_name and condition which will be used in WHERE.
Here $condition is an array like:
$condition = array(
'id' => 'some id',
'anycondition' => 'any value');
public function get($tbl_name, $condition = null) {
if($condition) {
$data = "select * from '$tbl_name' WHERE '$condition'";
}
else {
$data = $tbl_name;
}
return $data;
}
I want the echo be like this
select * from $tbl_name WHERE id='some id' and anycondition='any value'
Try This:
$query ="SELECT * FROM '$tbl_name' WHERE id='".$condition['id']."' AND anycondition='".$condition['anycondition']."'";
$data="select * from '$tbl_name' WHERE
(select array_to_string($condition, 'and', '') as condition);
here (in array_to_string) :
first parameter -->array
2nd -->concatenation text
3rd -->replacement of the null value in your array
This a horrible way a approach a class, which handles the database layer of your application. If I was you I read throughly into mysqli driver http://us2.php.net/manual/en/book.mysqli.php and come up with a better solution.
But here is what I think you were trying to accomplish in your opening post.
class A
{
public function sqlSelect($dbh, $tableName, $params=array()) {
if (!empty($param)) {
foreach ($params as $field => $value) {
// http://www.php.net/manual/en/function.mysql-real-escape-string.php
$value = mysql_real_escape_string($value, $dbh);
$whereSql[] = sprintf("%s = %s", $field, $value);
}
$whereSql = implode(" AND ", $whereSql);
return sprintf("SELECT * FROM %s WHERE %s",
$tableName, $whereSql);
}
return sprintf("SELECT * FROM %s", $tableName);
}
}
$condition = array(
'id' => 1234,
'some_other_column' => 'someOtherValue'
);
echo A.sqlSelect('SomeTableName', $condition);
echo A.sqlSelect('SomeOtherTableName');
Output:
SELECT * FROM SomeTableName WHERE id = 1234 AND some_other_column = 'someOtherValue'
SELECT * FROM SomeOtherTableName