This might seem like a stupid and trivial question. I am having problem naming functions in PHP. I have two functions that retrieves all the information of a student given its id or name and email.
Since PHP doesn't have function overloading in the same sense as JAVA, I am having difficulty naming the functions.
Here is what I have done. These are the names that I have given them.
get_students_with_id($id) and get_students_with_name_and_email($name, $email)
But the parameters are gonna increase. I need a better and simple solution to name these functions or methods. BTW, they all belong to the same class. So what am I gonna do? Thanks in advance.
In PHP there doesn't exist the concept of method overriding like in JAVA, for example, but you can send default parameters, for example:
get_students($id, $name = null, $email = null)
This means that you don't need to call the function with the three parameters. You can do it by calling it just with one and it will assume it is the id. For example, if you want to have a function working for your example above, you could do something like:
function get_students($id, $name = null, $email) {
if (!empty($id)) {
// Get students by their ids
} else if (!empty($name) && !empty($email)) {
// Get students by their names and emails
}
}
And you can call the function above:
get_students(1); //Will retrieve studen with id 1
get_students(null, "Name", "email#email.com"); //Will retrieve students with name "Name" and email "email#email.com"
A search method could look something like this:
class Student {
public static $columns = ['id', 'name', 'email', 'password', /* ... */];
// Imagine that this method is called with the following array:
// ['name' => 'Joe', 'password' => 'Pa55w0rD']
public static function search(array $queries) {
// We will be appending WHERE clauses to this SQL query
$sql = 'SELECT * FROM students WHERE ';
// Get the column names
$parameters = array_keys($queries);
// Create a parameterized WHERE clause for each column
foreach ($parameters as & $param) {
if ( ! in_array($param, self::$columns)) {
throw "Invalid column";
}
$param = "{$param} = :{$param}";
}
// Squish parameterized WHERE clauses into one
// and append it to the SQL query
$sql .= implode(' AND ', $parameters);
// The query will now look something like this:
// SELECT * FROM students WHERE name = :name AND password = :password
// Prepare the SQL query
$stmt = DB::instance()->prepare($sql);
// Go over the queries and bind the values to the columns
foreach ($queries as $col => $val) {
$stmt->bindValue(":" . $col, $val);
// Internally the query will look something like this:
// SELECT * FROM students WHERE name = 'Joe' AND password = 'Pa55w0rD'
}
// Execute
$result = $stmt->execute();
// ...
}
}
To use the method you would do something like this:
$student = Student::search([
'name' => 'Joe',
'password' => 'Pa55w0rD',
]);
You would want to handle the data in a safer way (making sure the password is hashed, for instance), but the general idea is there.
Why not use get_students($id=0, $name='', $email='') and so on for your other parameters, then have the function do whatever is necessary based on the passed parameters?
If that gets to be too much, pass an array check for keys. So if array('id' => 1) is passed then if (array_key_exists('id', $input)) {...} would catch it and proceed with actual function work, but if other keys/values are passed then a subsequent appropriate elseif would catch it.
Update: I think a format like this might be able to handle most of your use cases, based on some of the comments I read in the question. Not sure what your DB is, so this was done with MySQL in mind.
function get_students($input) {
$where = array();
$allowed_columns = array('id', 'name', 'email');
foreach ($allowed_columns as $key) {
if (!array_key_exists($key, $input)) continue;
$where[] = "`$key` = '" . mysqli_escape_string($input[$key]) . "'";
}
if ($where) {
$query = 'SELECT ... FROM `...` WHERE ' . join(' AND ', $where);
// etc...
} else {
return false;
}
}
I would use a class instead of multiple functions
class Student
{
public static function byName($name)
{
// ...
}
public static function byId($id)
{
// ...
}
}
$student = Student::byName('joe');
This would allow it to be much cleaner and more extendible, as you can put common logic in protected static methods in the class.
If you want to do multiples you can do some chaining which is a little more complicated.
I've mocked up a quick ideone which you can reverse engineer:
http://ideone.com/duafK4
Related
I'm using the addColumnCondition function as I like how it forms the queries for multiple queries. But I can't find anything in the documentation to change it's comparison operation from the simple = needle to a LIKE %needle%. There is a function that does a LIKE in addSearchCondition() but then it means to get the same query formation result, I'll have to do some for loops and merge conditions which I'd like to avoid if there is a better solution.
Here's the code
foreach($query_set as $query){
foreach($attributes as $attribute=>$v){
$attributes[$attribute] = $query;
}
$criteria->addColumnCondition($attributes, 'OR', 'AND');
}
And I'm getting the condition formed like
(business_name=:ycp0 OR payment_method=:ycp1) AND (business_name=:ycp2 OR payment_method=:ycp3)
So is there a way to configure the function to use LIKE %:ycp0% instead of the simple =:ycp0.
It seems, this feature is not provided by Yii's addColumnCondition method.
therefore i would recommend a way of overriding the method of CDbCriteria class and customize it your own way.
you need to create a new class called "AppCriteria", then place it inside protected/models
The code for the new class should look like,
i.e
class AppCriteria extends CDbCriteria {
public function addColumnCondition($columns, $columnOperator = 'AND', $operator = 'AND', $like = true) {
$params = array();
foreach ($columns as $name=>$value) {
if ($value === null)
$params[] = $name.' IS NULL';
else {
if ($like)
$params[] = $name.' LIKE %'.self::PARAM_PREFIX.self::$paramCount.'%';
else
$params[] = $name.'='.self::PARAM_PREFIX.self::$paramCount;
$this->params[self::PARAM_PREFIX.self::$paramCount++] = $value;
}
}
return $this->addCondition(implode(" $columnOperator ", $params), $operator);
}
}
Note: The 4th param of addColumnCondition, $like = true. you can set it to $like = false and allow the function to work with equal conditions. (A = B)
i.e
(business_name=:ycp0 OR payment_method=:ycp1) AND (business_name=:ycp2 OR payment_method=:ycp3)
if $like = true, it will allow you to have like condition. (A like %B%)
i.e
(business_name LIKE %:ycp0% OR payment_method LIKE %:ycp1%) AND (business_name LIKE %:ycp2% OR payment_method LIKE %:ycp3%)
Now Here's the working code,
$criteria = new AppCriteria();
foreach($query_set as $query){
foreach($attributes as $attribute=>$v){
$attributes[$attribute] = $query;
}
$criteria->addColumnCondition($attributes, 'OR', 'AND');
}
While writing a pdo statement, is it possible to repeat the value of a variable? I mean:
$query = "UPDATE users SET firstname = :name WHERE firstname = :name";
$stmt = $dbh -> prepare($query);
$stmt -> execute(array(":name" => "Jackie"));
Please note that I repeat the ":name" nameholder whereas I provide the value only once. How can I make this work?
The simple answer is: You can't. PDO uses an abstraction for prepared statements which has some limitations. Unfortunately this is one, you have to work-around using something like
$query = "UPDATE users SET firstname = :name1 WHERE firstname = :name2";
$stmt = $dbh -> prepare($query);
$stmt -> execute(array(":name1" => "Jackie", ":name2" => "Jackie"));
In certain cases, such as emulated prepared statements with some versions of the PDO/MySQL driver, repeated named parameters are supported; however, this shouldn't be relied upon, as it's brittle (it can make upgrades require more work, for example).
If you want to support multiple appearances of a named parameter, you can always extend PDO and PDOStatement (by classical inheritance or by composition), or just PDOStatement and set your class as the statement class by setting the PDO::ATTR_STATEMENT_CLASS attribute. The extended PDOStatement (or PDO::prepare) could extract the named parameters, look for repeats and automatically generate replacements. It would also record these duplicates. The bind and execute methods, when passed a named parameter, would test whether the parameter is repeated and bind the value to each replacement parameter.
Note: the following example is untested and likely has bugs (some related to statement parsing are noted in code comments).
class PDO_multiNamed extends PDO {
function prepare($stmt) {
$params = array_count_values($this->_extractNamedParams());
# get just named parameters that are repeated
$repeated = array_filter($params, function ($count) { return $count > 1; });
# start suffixes at 0
$suffixes = array_map(function ($x) {return 0;}, $repeated);
/* Replace repeated named parameters. Doesn't properly parse statement,
* so may replacement portions of the string that it shouldn't. Proper
* implementation left as an exercise for the reader.
*
* $param only contains identifier characters, so no need to escape it
*/
$stmt = preg_replace_callback(
'/(?:' . implode('|', array_keys($repeated)) . ')(?=\W)/',
function ($matches) use (&$suffixes) {
return $matches[0] . '_' . $suffixes[$matches[0]]++;
}, $stmt);
$this->prepare($stmt,
array(
PDO::ATTR_STATEMENT_CLASS => array('PDOStatement_multiNamed', array($repeated)))
);
}
protected function _extractNamedParams() {
/* Not actually sufficient to parse named parameters, but it's a start.
* Proper implementation left as an exercise.
*/
preg_match_all('/:\w+/', $stmt, $params);
return $params[0];
}
}
class PDOStatement_multiNamed extends PDOStatement {
protected $_namedRepeats;
function __construct($repeated) {
# PDOStatement::__construct doesn't like to be called.
//parent::__construct();
$this->_namedRepeats = $repeated;
}
/* 0 may not be an appropriate default for $length, but an examination of
* ext/pdo/pdo_stmt.c suggests it should work. Alternatively, leave off the
* last two arguments and rely on PHP's implicit variadic function feature.
*/
function bindParam($param, &$var, $data_type=PDO::PARAM_STR, $length=0, $driver_options=array()) {
return $this->_bind(__FUNCTION__, $param, func_get_args());
}
function bindValue($param, $var, $data_type=PDO::PARAM_STR) {
return $this->_bind(__FUNCTION__, $param, func_get_args());
}
function execute($input_parameters=NULL) {
if ($input_parameters) {
$params = array();
# could be replaced by array_map_concat, if it existed
foreach ($input_parameters as $name => $val) {
if (isset($this->_namedRepeats[$param])) {
for ($i=0; $i < $this->_namedRepeats[$param], ++$i) {
$params["{$name}_{$i}"] = $val;
}
} else {
$params[$name] = $val;
}
}
return parent::execute($params);
} else {
return parent::execute();
}
}
protected function _bind($method, $param, $args) {
if (isset($this->_namedRepeats[$param])) {
$result = TRUE;
for ($i=0; $i < $this->_namedRepeats[$param], ++$i) {
$args[0] = "{$param}_{$i}";
# should this return early if the call fails?
$result &= call_user_func_array("parent::$method", $args);
}
return $result;
} else {
return call_user_func_array("parent::$method", $args);
}
}
}
In my case this error appeared when I switched from dblib freedts to sqlsrv PDO driver. Dblib driver handled duplicate parameters names with no errors. I have quite complicated dynamic queries with lots of unions and a lot of duplicated params so I used following helper as a workaround:
function prepareMsSqlQueryParams($query, $params): array
{
$paramsCount = [];
$newParams = [];
$pattern = '/(:' . implode('|:', array_keys($params)) . ')/';
$query = preg_replace_callback($pattern, function ($matches) use ($params, &$newParams, &$paramsCount) {
$key = ltrim($matches[0], ':');
if (isset($paramsCount[$key])) {
$paramsCount[$key]++;
$newParams[$key . $paramsCount[$key]] = $params[$key];
return $matches[0] . $paramsCount[$key];
} else {
$newParams[$key] = $params[$key];
$paramsCount[$key] = 0;
return $matches[0];
}
}, $query);
return [$query, $newParams];
}
Then you can use it this way:
$query = "UPDATE users SET firstname = :name WHERE firstname = :name";
$params = [":name" => "Jackie"];
// It will return "UPDATE users SET firstname = :name WHERE firstname = :name1"; with appropriate parameters array
list($query, $params) = prepareMsSqlQueryParams($query, $params);
$stmt = $dbh->prepare($query);
$stmt->execute(params);
if someone could help me with a conceptual question it would be great: Suppose I have a model that deals with a table called Persons. Normally I would have a standard fetch function like this:
public function fetchPersonById($person_id)
{
$result = 0;
if ((int)$person_id > 0) {
$select = $this->select()
->from($this->_name, array('Id' => 'Person_Id',
'Name' => 'Person_Name',
'Age' => 'Person_Age',
'Sex' => 'Person_Sex'));
->where('Person_Id = ?', $person_id);
$result = $this->fetchRow($select);
}
return $result;
}
Now suppose for some reason I need to fetch a person's Sex by it's Name.. and later on it's age by it's name. Would you them add different functions like:
public function fetchPersonSexByName($person_name)
{
// ...
->from($this->_name, array('Sex' => 'Person_Sex');
->where('Person_Name = ?', $person_name); ...
// ...
}
and so on... After a while you could see yourself with thousands of short methods like this.. Are you guys that specific or you wether pull the whole record (fetchall) and than later in the code just keep the column you want to use? On this case wouldn't you be breaking the whole MVC because if I want to get
someone's Sex my model (or whoeve is calling the function) would need to know the columns name in the database?
I also tought about doing something more generic like
public function $this->fetchColumnA_By_ColumnB_ColumnBValue($columnA_name,
$columnB_name, $columnA_name)
{
//...
}
And than have my short methods to be calling this more flexible column. So that I would have something like:
public function fetchPersonSexByName($person_name)
{
//...
$this->fetchColumnA_By_ColumnB_ColumnBValue('sex', 'name', 'martin');
}
Anyway.. How do you guys approach this probably common issue?
I would tend to do a version of your last generic example. The generic method would be protected (or even private) and your more specific (public) methods would call this. To avoid code repetition.
However, I'm not sure how generic I would go. May be just...
protected function _fetchColumnById($id, $column) {...}
protected function _fetchColumnByName($name, $column) {...}
...but this would depend on the requirements.
you could see yourself with thousands of short methods
If you think you'll get to 1000's of requests, then it might be better to read the whole record(s) and cache this somehow?
What you could do is make a magic __call function in your model or in his parent.
If some method doesn't exist it will go thru that magic function. Something like:
class Model_Test {
public function __call($method, $args) {
if(preg_match('/fetch([a-zA-Z]+)by([a-zA-Z]+)/i', $method, $result)) {
$fetch = $result[1];
$column = $result[2];
echo 'SELECT ' . $fetch . ' FROM test WHERE ' . $column . ' = "' . (string)$args[0] . '"';
//build your query here and make sure you make it secure with bind param, etc.
} else {
//call parent __call? Or throw an error?
}
}
}
$model = new Model_test();
$model->fetchSexByName('martin');
Just a quick example, offcourse you need to work it out. Success!
Ok so I have 2 classes. Photo class and PhotoMapper class.
Photo class contains all the values and outputs them.
PhotoMapper sets the values to the Photo class, by a assign class, like this:
$query = "SELECT * FROM users";
$query = $this->_pdo->prepare($query);
$query->bindValue(":id", $photo->id());
$query->execute();
$data = $query->fetch();
$photo->assign($data);
And the assign:
public function assign($data){
if (is_array($data)) {
foreach ($data as $name => $value)
{
$this->{'_' . $name} = $value;
}
}
}
Now where would i check for if $query->rowCount() > 0 ?
Should i inside is_array after the foreach, make a $this->rowCount = .. ?
What would be best to perform this check? I would like to check for the rowCount outside of both classes..
$photo = new Photo($albumID, $photoID, $view, $userID);
$photoMapper->select($photo, $view);
Is how it looks outside the classes. How can i check and output error if select(which is the query above) didnt find any rows?
I would need to have 2 queries? One to check, and one to select them? or?..
Well, if you're expecting data from the fetch operation but there isn't any (you could simply find out by checking if $data has any contents), you should throw an Exception. The following code snippet would typically be placed before the fetch:
// ...
$query->execute();
if ( !( $data = $query->fetch() ) ) {
throw new Exception('photo could not be loaded');
}
$photo->assign($data);
// ...
However, if you want the code to continue regardless, you can reverse the if condition, and put the assign call inside the statement;
$query->execute();
if ( ( $data = $query->fetch() ) ) {
$photo->assign($data);
}
I have the function below in my model for a codeigniter project, and the variable $id is an array and for example, contains (1,2,3). Now that i'm revisiting it, I think that i'm not actually escaping my array $id. I think I would have to change the line
$this->db->escape($id)
to
$id = $this->db->escape($id)
If I do that, then it puts single quotes around every element in the array and treats it as one long string like this: '(1,2,3)'.
Can someone confirm that I am not actually escaping my variable and either suggest a solution or let me know if this is a bug within the codeigniter framework?
function get_ratings($id)
{
$this->db->escape($id); // had to manually escape the variable since it's being used in an "in" in the where clause.
$sql = "select * from toys t
where t.toy_id in ($id)";
$query = $this->db->query($sql, $id);
if($query->num_rows() > 0)
{
return $query->result_array();
}
else
{
return false;
}
}
You may be interested in using the CI Active Record class:
Beyond simplicity, a major benefit to using the Active Record features is that it allows you to create database independent applications, since the query syntax is generated by each database adapter. It also allows for safer queries, since the values are escaped automatically by the system.
Your rewritten query would look like this (assuming $id is an array):
$this->db->where_in('toy_id', $id)->get('toys');
Aside: I will admit I am a bit confused, as it looks like $ids would be a more appropriate variable name, and the way you are using it in the query, I would assume it is a string...
If active record is not your thing, you may also find Query Bindings to be useful:
The secondary benefit of using binds is that the values are automatically escaped, producing safer queries. You don't have to remember to manually escape data; the engine does it automatically for you.
EDIT: Looking back on this later, it looks like this is what you're trying to do. In that case, try replacing:
$sql = "select * from toys t where t.toy_id in ($id)";
With:
$sql = "select * from toys t where t.toy_id in (?)";
And pass $id as the second argument to query(), but as a comma separated string (implode(',', $id) if $id is indeed an array).
Otherwise you may want to use $this->db->escape_str().
$this->db->escape_str() This function escapes the data passed to it, regardless of type.
Here is an excerpt from the source code of the mysql driver to maybe put your mind at ease.
function escape_str($str, $like = FALSE)
{
if (is_array($str))
{
foreach ($str as $key => $val)
{
$str[$key] = $this->escape_str($val, $like);
}
return $str;
}
// continued...
It loops through arrays and escapes their values.
It does indeed seem that $this->db->escape is not going to work for arrays.
$this->db->escape() This function determines the data type so that it can escape only string data.
Here is the source:
function escape($str)
{
if (is_string($str))
{
$str = "'".$this->escape_str($str)."'";
}
elseif (is_bool($str))
{
$str = ($str === FALSE) ? 0 : 1;
}
elseif (is_null($str))
{
$str = 'NULL';
}
return $str;
}
Looks like it ignores arrays.
Anyways, hope you find a solution that works for you. My vote is for Active Record.
What you want to do is escape the individual values in the array. So you can use array_map on the array first.
$id = array_map('some_escape_function', $id);
See: http://php.net/manual/en/function.array-map.php
Then you can do:
$in = join(",",$id);
Your SQL would then be:
WHERE t.toy_id in ($in)
Which gives you:
WHERE t.toy_id in ('1','2','3')
You could try something like this:
$sql = 'select * from toys t where t.toy_id in ('.
join(',',array_map(function($i) {
return $this->db->escape($i);
}, $id)).');';
*Disclaimer: I'm not where I can access my PHP/MySQL server right now, so I haven't validated this. Some modification and/or tweakage may be necessary.
Here's the solution I'm using for this, with CI 2.1.2:
1) Copy /system/database/DB.php to application/database/DB.php, and around line 123, make it look like:
...
if ( ! isset($active_record) OR $active_record == TRUE)
{
require_once(BASEPATH.'database/DB_active_rec.php');
require_once(APPPATH.'database/MY_DB_active_rec' . EXT);
if ( ! class_exists('CI_DB'))
{
eval('class CI_DB extends MY_DB_active_record { }');
}
}
...
2) Create MY_Loader.php in application/core:
class MY_Loader extends CI_Loader
{
function __construct()
{
parent::__construct();
log_message('debug', 'MY Loader Class Initialized');
}
public function database($params = '', $return = FALSE, $active_record = NULL) {
// Grab the super object
$CI = & get_instance();
// Do we even need to load the database class?
if (class_exists('CI_DB') AND $return == FALSE AND $active_record == NULL AND isset($CI->db) AND is_object($CI->db)) {
return FALSE;
}
//require_once(BASEPATH . 'database/DB.php');
require_once(APPPATH . 'database/DB' . EXT);
if ($return === TRUE) {
return DB($params, $active_record);
}
// Initialize the db variable. Needed to prevent
// reference errors with some configurations
$CI->db = '';
// Load the DB class
$CI->db = & DB($params, $active_record);
}
}
3) Create application/database/MY_DB_active_rec.php:
class MY_DB_active_record extends CI_DB_active_record {
public function __construct($params)
{
parent::__construct($params);
log_message('debug', 'MY Active Record Database Driver Class Initialized');
}
private function _array_escape(&$str)
{
$str = "'" . $this->escape_str($str) . "'";
}
function escape($str)
{
if (is_array($str))
{
array_walk($str, array($this, '_array_escape'));
return implode(',', $str);
}
elseif (is_string($str))
{
$this->_array_escape($str);
}
elseif (is_bool($str))
{
$str = ($str === FALSE) ? 0 : 1;
}
elseif (is_null($str))
{
$str = 'NULL';
}
return $str;
}
}
Then you just pass in an array of values:
$in_data = array(1, 2, 3);
$this->db->query('SELECT * FROM table WHERE id IN(?)', array($in_data));
It's not pretty, but it seems to do the trick!
Code Igniter v3 now automaticly escapes array values :
http://www.codeigniter.com/userguide3/database/queries.html
Query Bindings
Bindings enable you to simplify your query syntax by letting the system put the queries together for you. Consider the following example:
$sql = "SELECT * FROM some_table WHERE id = ? AND status = ? AND author = ?";
$this->db->query($sql, array(3, 'live', 'Rick'));
The question marks in the query are automatically replaced with the >values in the array in the second parameter of the query function.
Binding also work with arrays, which will be transformed to IN sets:
$sql = "SELECT * FROM some_table WHERE id IN ? AND status = ? AND author = ?";
$this->db->query($sql, array(array(3, 6), 'live', 'Rick'));
The resulting query will be:
SELECT * FROM some_table WHERE id IN (3,6) AND status = 'live' AND author = 'Rick'
The secondary benefit of using binds is that the values are automatically escaped, producing safer queries. You don’t have to remember to manually escape data; the engine does it automatically for you.
To bind them you can do the following:
$queryParams = [];
// to add the appropriate amount of bindings ?
$idBindings = str_replace(' ', ',', trim(str_repeat("(?) ", count($ids))));
// add each id with int validation
foreach ($ids as $id) {
if(is_int(intVal($id)) == TRUE){
array_push($queryParams, intVal($id));
}
}
// the other option commented out below is to merge without checking -
// note: sometimes values look like numeric values but are actually strings
//queryParams = array_merge($queryParams, $ids);
$sql = "select * from toys t where t.toy_id in ('. $idBindings .')";
$query = $this->db->query($sql, $queryParams );