How could I optimize the following piece of code? - php

$project_query= $db->query("SELECT * FROM projects WHERE id='$task_info->project_id'");
$project_info = $project_query->fetch_object();
$admins_array = $project_info->admins_array;
$admins = unserialize($admins_array);
$is_admin = false;
foreach($admins as $value) {
if($value == $_SESSION['username']) { $is_admin = true; }
}
I have the following code that checks if the currently logged user's username is contained in a serialized array that is stored in a mysql table row. How would I go about optimizing it for maximum performance?

1) if changing your db structure is a possibility, I suggest storing the serialized values in a separate table
project_admins (project_id, user_id) with a unique index on (project_id,user_id)
Then you can quickly determine whether a user is an admin of a project
SELECT 1 from project_admins where project_id = x and user_id = y
2) if you're stuck with storing serialized data in the db I suggest storing the serialized array indexed by username
so you can search it in constant time
i.e.
$is_admin = array_key_exists($_SESSION['username'],$admins);
3) if you can not index the admins array by username, you can minimally optimize your loop by adding a break statement once you find a match
foreach($admins as $value) {
if($value == $_SESSION['username']) {
$is_admin = true;
break; // match found no need to check remaining values
}
}
Actually in this case you're probably better off using in_array
$is_admin = in_array($_SESSION['username'],$admins);

If it's optimization in terms of speed, then I would suggest performing the login in SQL.
It would require you to change your table structure to a more normalised state by switching from serialised strings into separate related entities.

Related

Splitting a string of values like 1030:0,1031:1,1032:2 and storing data in database

I have a bunch of photos on a page and using jQuery UI's Sortable plugin, to allow for them to be reordered.
When my sortable function fires, it writes a new order sequence:
1030:0,1031:1,1032:2,1040:3,1033:4
Each item of the comma delimited string, consists of the photo ID and the order position, separated by a colon. When the user has completely finished their reordering, I'm posting this order sequence to a PHP page via AJAX, to store the changes in the database. Here's where I get into trouble.
I have no problem getting my script to work, but I'm pretty sure it's the incorrect way to achieve what I want, and will suffer hugely in performance and resources - I'm hoping somebody could advise me as to what would be the best approach.
This is my PHP script that deals with the sequence:
if ($sorted_order) {
$exploded_order = explode(',',$sorted_order);
foreach ($exploded_order as $order_part) {
$exploded_part = explode(':',$order_part);
$part_count = 0;
foreach ($exploded_part as $part) {
$part_count++;
if ($part_count == 1) {
$photo_id = $part;
} elseif ($part_count == 2) {
$order = $part;
}
$SQL = "UPDATE article_photos ";
$SQL .= "SET order_pos = :order_pos ";
$SQL .= "WHERE photo_id = :photo_id;";
... rest of PDO stuff ...
}
}
}
My concerns arise from the nested foreach functions and also running so many database updates. If a given sequence contained 150 items, would this script cry for help? If it will, how could I improve it?
** This is for an admin page, so it won't be heavily abused **
you can use one update, with some cleaver code like so:
create the array $data['order'] in the loop then:
$q = "UPDATE article_photos SET order_pos = (CASE photo_id ";
foreach($data['order'] as $sort => $id){
$q .= " WHEN {$id} THEN {$sort}";
}
$q .= " END ) WHERE photo_id IN (".implode(",",$data['order']).")";
a little clearer perhaps
UPDATE article_photos SET order_pos = (CASE photo_id
WHEN id = 1 THEN 999
WHEN id = 2 THEN 1000
WHEN id = 3 THEN 1001
END)
WHERE photo_id IN (1,2,3)
i use this approach for exactly what your doing, updating sort orders
No need for the second foreach: you know it's going to be two parts if your data passes validation (I'm assuming you validated this. If not: you should =) so just do:
if (count($exploded_part) == 2) {
$id = $exploded_part[0];
$seq = $exploded_part[1];
/* rest of code */
} else {
/* error - data does not conform despite validation */
}
As for update hammering: do your DB updates in a transaction. Your db will queue the ops, but not commit them to the main DB until you commit the transaction, at which point it'll happily do the update "for real" at lightning speed.
I suggest making your script even simplier and changing names of the variables, so the code would be way more readable.
$parts = explode(',',$sorted_order);
foreach ($parts as $part) {
list($id, $position) = explode(':',$order_part);
//Now you can work with $id and $position ;
}
More info about list: http://php.net/manual/en/function.list.php
Also, about performance and your data structure:
The way you store your data is not perfect. But that way you will not suffer any performance issues, that way you need to send less data, less overhead overall.
However the drawback of your data structure is that most probably you will be unable to establish relationships between tables and make joins or alter table structure in a correct way.

How do I take a php variable that has an sql query inside it and add it to an existing sql query using $db->updatePhoneNumbers()?

I am trying to make a database of Users. One user can have an indefinite number of phone numbers. So in the form I’ve created a js function that will give me new input fields and they put the information into a nestled array.
I am doing a double foreach loop to go through my array, and add SQL queries to it based on if the id already exists and just needs to be updated or if it's entirely new and needs to be inserted. I add these SQL queries to a variable $phoneSql . When I echo that variable, it does contain a valid SQL query which works if I try it directly in phpMyAdmin.
This is the foreach loop code:
$phoneSql = 'SELECT id FROM user WHERE id = '.$id.' INTO #id;';
foreach($_POST['phone'] as $key => $value) {
foreach($_POST['user'][$key] as $id => $number) {
if($id == 0 && !$number == ''){
$phoneSql .= 'INSERT INTO phone_number (id, user_id, number) VALUES (NULL, #id, "'.$number.'");';
} else if (!$number == '') {
$phoneSql .= 'UPDATE phone_numbers SET user_id = #id, number = "'.$number.'" WHERE id = '.$id.';';
}
}
}
I have one edit.php page with the form, which posts to update.php where I have the foreach loop from above and following code:
$db->updatePhoneNumber($phoneSql);
It also gets the $id from the user I’m editing at the moment. Then it gets sent to db.php and into this function:
public function updatePhoneNumbers($phoneSql) {
$ phoneSql = $ phoneSql;
$sth = $this->dbh->prepare($phoneSql);
$sth->execute();
if ($sth->execute()) {
return true;
} else {
return false;
}
}
But this is not working. Can I add a variable with sql queries into a function like that or do I have to do it some other way? I’m quite new to this so I’m not sure how to proceed. I’ve tried searching for a solution but haven’t found any. I’m thankful for any advice.
What you should be doing is using an INSERT ... ON DUPLICATE KEY UPDATE ... construct, saving you a lot of that logic.
e.g.
INSERT INTO phone_number (id, user_id, number) VALUES (...)
ON DUPLICATE KEY UPDATE user_id=VALUES(user_id), number=VALUES(number)
With this, no need to select, test, then insert/update. You just insert, and MySQL will transparently convert it into an update if a duplicate key error occurs.

How to remove rows or filter a recordset or collection

I would like to know if there is a way to remove rows or filter a recordset or collection.
For example, if I have two tables: one for questions and one for the answer choices. The questions belong to different forms. Questions 1-10 belongs to form a, 11-20 belong to form b. Depending on the answers of the previous questions, certain questions may or may not show up, and certain answers later on may or may not show up. Instead of constantly hitting the database, I want to cache the recordset or collection of questions belonging to each form into memory and filter off of the in memory set of questions per session.
This way each user will only hit the database once, at the beginning of their session, instead of every time they click on next.
The Collection object used by the models is extended from lithium\util\Collection which provides a method for filtering an existing collection and returning a new one based on a user provided closure.
$newQuestions = $oldQuestions->find(function($question) {
if (your new criteria) {
return true;
}
return false;
});
Simply determine the criteria you wish to apply and perform the filtering in the closure. Once it runs you should have a new Collection object with only the records that matched.
After you get a Recordset or Collection from your database, you can execute a couple of filters on it. See the lithium\util\Collection for more info.
An example would be
$questions = Questions::all();
$form_questions = $questions->find(function($question) {
if($query->form == 'b') {
return true;
}
return false;
}), true);
To handle keeping these questions persist between page requests, look into lithium\storage\Session.
It's unlikely that simple 'once-per-page' database calls will put much of a strain on your server unless you have truely terrific traffic, but if you do want to do this, the easiest way to do this will be to cache this information in the PHP $_SESSION superglobal when a user logs in. Assuming you've set PHP to use filesystem storage (though even if you use database session storage it will have only a tiny affect on performance), you'll have your questions stored in super fast-to-access files which are already pre-built to be unique to each specific user. As soon as a script is loaded, the session file is automatically read into memory and you can access any of the information from there.
EXAMPLE:
Assuming that your questions table has columns question_number and question_text and your answers table has the columns question_number and answer_text:
<?php
//on login:
//first get the answer array, so we can use it in the logic below:
$query = mysql_query('SELECT * FROM `questions` WHERE [criteria]',[connection identifier]) or die(mysql_error());
if (!mysql_num_rows($query)){
die("No questions!");
}
$answer_array = array();
//create a keyed array that you can access by question number
while($row=mysql_fetch_array($query)){
$answer_array[$row['question_number']] = $row['answer_text'];
}
//now get the questions and put everything into the session variable
$query = mysql_query('SELECT * FROM `questions` WHERE [criteria]',[connection identifier]) or die(mysql_error());
if (!mysql_num_rows($query)){
die("No questions!");
}
//loop through the results and generate session arrays we can work with later
while($row = mysql_fetch_array($query)){
//assign the question to the correct form:
if ($row['question_number']<=10){
$session_key = 'form_a';
} elseif($row['question_number']<=20){
$session_key = 'form_b';
} elseif($row['question_number']<=30){
$session_key = 'form_c';
} else {
$session_key = 'form_d';
}
//if the session variable does exist yet, create it:
if (!isset($_SESSION[$session_key])){
$_SESSION[$session_key] = array();
}
//get the existing answer if it exists, otherwise leave the answer blank:
$my_answer = "";
if(isset($answer_array[$row['question_number']])){
$my_answer = $answer_array[$row['question_number']];
}
//add this question array as a child array element in the session array, keyed by the question number
$_SESSION[$session_key][$row['question_number']] = array(
'question' => $row,
'answer' => $my_answer
);
}
Now, if we're loading Form B, for instance, we can just read it out of the session array $_SESSION['form_b'] and perform any logical switches we want based on the answers to previous questions:
$html = "";
foreach($_SESSION['form_b'] as $question_number => $data){
//perform any logic, for instance, if question 2 from form a is equal to '5', don't show question 3 on form B:
switch($question_number){
case '3': if ($_SESSION['form_a']['2']['answer']=='5'){ continue 2; }; break;
}
//add the question to the form, and populate the answer if they already answered it earlier:
$html .= "<label>".$data['question']."<input type='text' value=\"".$data['answer']."\" name='question_".$question_number."' /></label>";
}
Then, when you submit each form, in addition to updating the mysql answers table, you'll also want to update your _SESSION array. For instance, if you're submitting form B via POST:
$form = 'form_b';
foreach($_POST as $key=>$value){
if (substr($key,0,strlen('question_')!='question_'){
continue;
}
$number = str_replace('question_','',$key); //this will give us the question number
$saved = add_answer($number,$value); //call the function to insert the new answer into the database (this is a dummy function, and please make sure to escape your variables
if ($saved){//assuming it saved:
$_SESSION[$form ][$number]['answer']=$value; //now we've updated the session array as well.
}
}

How to select by condition on JSON column in MySQL?

How to write query for selecting data by where condition on column having JSON array?
i.e.
Suppose I have added user_name, user_role in ci_sessions. and user_data is JSON array.
SELECT *
FROM `ci_sessions` where user_data[user_role]='admin';
*********************/\***************
This where condition is needs to be designed. I require data with having user_role "admin".
Update: To check user_role "admin" is main objective of where condition.
Is there any way to add where condition as user_data[user_role] or user_data->user_role?
Update: This is possible in PostgresSQL DB.
Don't know what you're doing there, but you just call $this->session->userdata('user_role') and CI's automatically chooses how to retrieve that value. If you set (and I think you did) to use a database, the query will be performed automatically.
I don't understand how you actually saved your session variable, if you json_encoded() by yourself or you are referring to the encoding performed by CI.
In the latter, you just:
if($this->session->userdata('user_role') == 'admin')
{
// do stuff
}
Otherwise, $role = json_decode($this->session->userdata('user_role')); and the examine the array but I can't help you much here since you didn't provide clear information
I was keep on waiting for this type of query but I failed to do so. That's why I had made combination of sql and php.
I have got results from all ci_sessions table.
Then I checked in result by decoding field of JSON arraY. aND it's working fine.
SELECT * FROM `ci_sessions`;
foreach ($result as $row) {
$user_data=$row->user_data;
foreach (unserialize($user_data) as $k => $v) {
$final[] = array('key' => $k, 'value' => $v);
if($k=='user_role' && $v='admin') {
//make array of admin information required to show
}
}
}

Checking mySQL db for duplicate uid

I am trying to implement a check in my PHP code, that checks if there is a duplicate uid in the database, and if so, to assign a new uid, and check again, but I am having trouble nailing the logic, here is what I have thus far,
function check($uid){
$sql = mysql_query("SELECT * FROM table WHERE uid='$uid'");
$pre = mysql_num_rows($sql);
if($pre >= 1){
return false;
}else{
return true;
}
}
And then using that function I thought of using a while loop to continue looping through until it evaluates to true
$pre_check = check($uid);
while($pre_check == false){
//having trouble figuring out what should go here
}
So basically, once I have a usable uid, write everything to the database, else keep generating new ones and checking them till it finds one that is not already in use.
It is probably really simple, but for some reason I am having trouble with it.
Thanx in advance!
$uid = 100; // pick some other value you want to start with or have stored based on the last successful insert.
while($pre_check == false){
$pre_check = check(++$uid);
}
Of course ths is exactly what 'auto incrementing' primary keys are useful for. Are you aware of 'auto incrementing' primary keys in mysql?
EDIT
In light of your comment regarding maintaining someone else's code that uses the random function like that (ewwwww)... I would use the method I suggest above and store the last inserted id somewhere you can read it again for the next user. This will allow you to "fill-in-the-blanks" for the uids that are missing. So, if for example you have uids 1, 2, 5, 9, 40, 100... you can start with $uid = 1; Your while loop will return once you get to 3. Now you store the 3 and create the new record. Next time, you start with $uid = 3; and so on. Eventually you will have all numbers filled in.
It is also important to realize that you will need to do the inserts by either locking the tables for WRITES. You don't want to get into a race condition where two different users are given the same uid because they are both searching for an available uid at the same time.
Indeed the best is to use autoincrement ids, but if you don't have the choice, you can do a reccursive function like that:
function find_uid() {
$new_uid = rand(1000000000, 9999999999);
$sql = mysql_query("SELECT COUNT(*) AS 'nb' WHERE uid=".$new_uid.";");
$row = mysql_fetch_assoc();
$pre = $row['nb'];
return ($pre >= 1 ? find_uid() : $new_uid);
}
COUNT(*) should be more performant because the count is made by MySQL and not php.
By the way, if you need a new uid shouldn't the condition be ($pre > 0) instead of ($pre > 1) ?

Categories