I have a webapplication with a php backend.
At a certain interval checks need to be run on all users. Running these checks takes some time, more importantly: they should not be executed on non-existing users. The users are received from a database and can change mid-way through running the checks. My current solution is:
<?php
require_once 'databaseUtils.php';
$users = getUsersFromDatabase();
//Sample:
while(true) {
foreach(GetUsers() as &$user) {
//I'd rather not:
if(checkIfUserIsInDatabase($user) {
var_dump($user);
sleep(1); //Checking takes time...
}
}
echo "Going again in 5s!";
sleep(5); //Interval
}
function GetUsers() {
return $users;
}
//Called from outside
function removeUser($user) {
global $users;
removeUserFromDatabase($user);
if(($key = array_search($user, $users)) !== false) {
unset($users[$key]);
}
}
?>
But I'm still looping needlessly through the user that isn't realy in the GetUsers() anymore. Is there a way to loop through all values of an array in which elements can get deleted from outside?
If you want to check each user if he is still in the database, you will need to query his information from database, so it will take more efforts than checking each item in array (if it is not more resource spending).
But I will suggest to get users in smaller portions instead of all users. And check portion by portion.
Firstly, you might need to consider to rework your entire business logic.
Do not use endless loops (while(true)) and global variables (global $users).
To loop through all users you can use generator concept and retrieve existent users one by one. So you will get only the next user from the database during each iteration:
function getUsersFromDatabase($db)
{
$id = 0;
while (($user = $db->query("SELECT * FROM `users` WHERE `id` > $id LIMIT 1"))) {
yield $user;
$id = $user->id;
}
}
foreach (getUsersFromDatabase($db) as $user) {
checkUser($user);
}
I do not know your framework or DB adapter details, so the code snippet is generic and should be considered as pseudo code.
Related
I'm a beginner to OOP. The following is the jist of my code, which I am trying to find a proper design pattern for:
class data {
public $location = array();
public $restaurant = array();
}
$data = new data;
$query = mysqli_query($mysqli, "SELECT * FROM restaurants");
//actually a big long query, simplified for illustrative purposes here
$i = 0;
while ($row = mysqli_fetch_array($query)) {
$data->location[] = $i.')'.$row['location']."<br>";
$data->restaurant[] = $row['restaurant']."<br>";
$i++;
}
I'd like to access the data class from another PHP page. (To print out information in HTML, hence the tags). I prefer not to run the query twice. I understand that classes are created and destroyed in a single PHP page load. I'd appreciated design pattern guidance for managing HTTP application state and minimizing computer resources in such a situation.
If you store the data object in a $_SESSION variable, you will have access to it from other pages and upon refresh. As mentioned in other post and comments, you want to separate out the HTML from data processing.
class data {
public $location = array();
public $restaurant = array();
}
// start your session
session_start();
$data = new data;
$query = mysqli_query($mysqli, "SELECT * FROM restaurants");
//actually a big long query, simplified for illustrative purposes here
$i = 0;
while ($row = mysqli_fetch_array($query)) {
$data->location[] = $i.')'.$row['location'];
$data->restaurant[] = $row['restaurant'];
$i++;
}
// HTML (separate from data processing)
foreach ($data->location as $location) {
echo $location . '<br />';
}
// save your session
$_SESSION['data'] = $data;
When you wish to reference the data object from another page
// start your session
session_start();
// get data object
$data = $_SESSION['data'];
// do something with data
foreach($data->location as $location) {
echo $location . '<br />';
}
SELECT data in database is rather inexpensive, in general speaking. You didn't need to worry about running the query twice. MySQL will do the caching part.
From your codes, you mixed up database data with HTML. I suggest to separate it.
// fetch data part
while ($row = mysqli_fetch_array($query)) {
$data->location[] = $row['location'];
$data->restaurant[] = $row['restaurant'];
}
// print HTML part
$i = 0;
foreach($data->location as $loc) {
echo $i . ')' . $loc . '<br />';
}
First you say this:
I'm a beginner to OOP.
Then you say this:
I prefer not to run the query twice. I understand that classes are
created and destroyed in a single PHP page load.
You are overthinking this. PHP is a scripting language based on a user request to that script. Meaning, it will always reload—and rerun—the code on each load of the PHP page. So there is no way around that.
And when I say you are overthinking this, PHP is basically a part of a L.A.M.P. stack (Linux, Apache, MySQL & PHP) so the burden of query speed rests on the MySQL server which will cache the request anyway.
Meaning while you are thinking of PHP efficiency, the inherent architecture of PHP insists that queries be run on each load. And with that in mind the burden of managing the queries falls on MySQL and on the efficiency of the server & the design of the data structures in the database.
So if you are worried about you code eating up resources, think about improving MySQL efficiency in some way. But each layer of a L.A.M.P. stack has its purpose. And the PHP layer’s purpose is to just reload & rerun scripts in each request.
You are probably are looking for the Repository pattern.
The general idea is to have a class that can retrieve data objects for you.
Example:
$db = new Db(); // your db instance; you can use PDO for this.
$repo = new RestaurantRepository($db); // create a repo instance
$restaurants = $repo->getRestaurants(); // retrieve and array of restaurants instances
Implentation:
class RestaurantRepository {
public function __construct($db) {
$this->db = $db;
}
public function getRestaurants() {
// do query and return an array of instances
}
}
Code is untested and may have typos but it's a starter.
Saving the query results to a $_SESSION variable in the form of an array results in not having to re-run the query on another page. Additionally, it manages state correctly as I can unset($_SESSION['name']) if the query is re-run with different parameters.
I can also save the output of class data to a session variable. It seems to me that this design pattern makes more sense than running a new query for page refreshes.
So let me explain my problem, lets assume that I run query like so:
$myquery = sql_query("SELECT name FROM table WHERE name='example' LIMIT 0,1");
Now.. I want to store the retrieved name into a variable so I would do something like this:
while ($myrow = sql_fetch_assoc($myquery)) {
transfer_row($myrow);
print"Name: $row_name";
}
$stored_name = $row_name;
NOTE: transfer_row() is just a function I wrote that takes $myrow['name'] and stores it in $row_name, for easier reference
Now, all is fine at this stage, here is where it gets interesting. Note that at this stage I still have a name assigned to $row_name. Further down the page I run another query to retrieve some other information from the table, and one of the things I need to retrieve is a list of names again, so I would simply run this query:
$myquery = sql_query("SELECT name, year FROM table WHERE DESC LIMIT 0,10");
while ($myrow = sql_fetch_assoc($myquery)) {
transfer_row($myrow);
$year = $row_year;
$link = "/$year";
print "<li style=\"margin-bottom: 6px;\">$row_name\n";
}
Now, I want to write an if statement that executes something if the $row_name from this query matches the $row_name from the old query, this is why we stored the first $row_name inside the variable.
if ($row_name == $stored_name){
// execute code
}
However as most of you know, this WONT work, the reason is, it simply takes $stored_name again and puts the new $row_name into $stored_name, so therefore the value of the first $row_name is lost, now it is crucial for my application that I access the first $row_name and compare it AFTER the second query has been run, what can I do here people? if nothing can be done what is an alternative to achieving something like this.
Thanks.
EDIT, MY transfer_row() function:
function transfer_row($myrow) {
global $GLOBALS;
if(is_array($myrow)) {
foreach ($myrow as $key=>$value) {
$key=str_replace(":","",$key);
$GLOBALS["row_$key"] = $value;
}
}
}
Without you posting the code for the function transfer_row, we won't be able to give you an answer that exactly matches what you request, but I can give you an answer that will solve the problem at hand.
When matching to check if the names are the same, you can modify the if statement to the following.
if ($row_name == $myrow['name']){
// execute code
}
What I suggest you do though, but since I don't have the code to the transfer_row function, is to pass a second variable to that function. The second variable will be a prefix for the variable name, so you can have unique values stored and saved.
Refrain from using the transfor_row function in the second call so your comparison becomes:
if ($myrow['name'] == $row_name)
If you need to use this function, you could do an assignment before the second database call:
$stored_name = $row_name;
...
transfer_row($myrow);
In your first query you are selecting the name field WHERE name='example' , Why are you quering then? You already have what you want.
Your are querying like:
Hey? roll no 21 what is your roll no?
So perform the second query only and use the if condition as :
if ($row_name == 'example'){
// execute code
}
Does it make sense?
Update
//How about using prefix while storing the values in `$GLOBAL` ??
transfer_row($myrow, 'old_'); //for the first query
transfer_row($myrow, 'new_'); //for the second query
function transfer_row($myrow, $prefix) {
global $GLOBALS;
if(is_array($myrow)) {
foreach ($myrow as $key=>$value) {
$key=str_replace(":","",$key);
$GLOBALS["$prefix"."row_$key"] = $value;
}
}
}
//Now compare as
if ($new_row_name == $old_row_name){
// execute code
}
//You'll not need `$stored_name = $row_name;` any more
I think I might be getting the concept wrong or not thinking about something correctly. I'm looking for a way to connect to db, and then run a selenium test (in phantomjs) for every row of a table. The test is to check for broken images on a bespoke CMS, and could be applied to any CMS.
I basically want to run an acceptance test for every page (of a specific type) by loading their IDs from the db and then running a separate test for each ID.
This is what I have so far:
$I = new WebGuy($scenario);
$results = $I->getArrayFromDB('talkthrough', '`key`', array());
foreach ($results as $r) {
$I->wantTo('Check helpfile '.$r['key'].'for broken images');
$I->amOnPage('/talkThrough.php?id='.$r['key']);
$I->seeAllImages();
}
This works to some extent in that it executes until the first failure (because it is running as 1 test with many assertions).
How do I make this run as individual tests?
I ended up looping through and storing the key that failed in a comma delimited string and setting a bool to say failures found.
$I = new WebGuy($scenario);
$results = $I->getArrayFromDB('talkthrough', '`key`', array());
$failures = "Broken help files are: ";
$failures_found = false;
foreach ($results as $key => $r) {
$I->wantTo('Check helpfile '.$r['key'].'for broken images');
$I->amOnPage('/talkThrough.php?id='.$r['key']);
$allImagesFine = $I->checkAllImages();
if($allImagesFine != '1')
{
$fail = $r['key'].",";
$failures.= $fail;
$failures_found = true;
}
}
$I->seeBrokenImages($failures_found,$failures);
With following as my webhelper
<?php
namespace Codeception\Module;
// here you can define custom functions for WebGuy
class WebHelper extends \Codeception\Module
{
function checkAllImages()
{
$result = $this->getModule('Selenium2')->session->evaluateScript("return (function(){ return Array.prototype.slice.call(document.images).every(function (img) {return img.complete && img.naturalWidth > 0;}); })()");
return $result;
}
function getArrayFromDB($table, $column, $criteria = array())
{
$dbh = $this->getModule('Db');
$query = $dbh->driver->select($column, $table, $criteria);
$dbh->debugSection('Query', $query, json_encode($criteria));
$sth = $dbh->driver->getDbh()->prepare($query);
if (!$sth) \PHPUnit_Framework_Assert::fail("Query '$query' can't be executed.");
$sth->execute(array_values($criteria));
return $sth->fetchAll();
}
function seeBrokenImages($bool,$failArray)
{
$this->assertFalse($bool,$failArray);
}
}
Thanks for the submitted answers
That's not going to work. Please avoid loops and conditionals in your tests.
You should place the key manually. And not get them from database. As it introduces additional complexity.
It might not be the best design choice, but If you really want to follow this approach you could use the specify tool from codeception, in order to allow your test continue running even if one assertion fails:
https://github.com/Codeception/Specify
I've built a contest system for a website, how it works is a user logs in, submits a ballot based on a real life event (sale of a particular object), and then at the end of the month, a random ballot is chosen and the owner of that ballot is the winner.
I've been asked to create a script which will email all users in the database the current amount of ballots they have in the system.
My current login/registration system is a heavily edited version of HTML-Form-Guies Simple PHP Registration System.
I know the pseudo code for what I want to do.
Step by step, the method needed goes like this.
Call on EmailUsersTotalEntries, populates an array with all the users in the database, pick the first entry in the array, user 1, find the sum of the all the rows in the itemsold column with the userid 1. then send user one an email with the results of the select sum(itemsold) from ballots where userid = 1; to user 1. Then the loop goes to user 2 and does the same thing, until it has sent an email to every user in the database.
Here are a few of the methods that I have either written or that are from the login system that will be used to accomplish this. My only problem is I dont know how to make a loop so that it will start from user 1 and then keep going all the way to user 2, and I dont know how to query the database for the user_id of whatever user the database/loop is currently on.
Methods are as follows:
This is the main method, it will call sub methods to collect the users and then send the actual email. I'm not sure if TotalEntries should be an array or not
function EmailTotalEntries()
{
if(empty($_POST['email']))
{
$this->HandleError("Email is empty!");
return false;
}
$user_rec = array();
if(false === $this->GetUsers($user_rec))
{
return false;
}
**/* $TotalEntries = array(); */**
if(false === $this->GetTotalEntriesForEmail($user_rec, $TotalEntries)
{
return false;
}
//At this point, I have an array, user_rec, populated with all the data from my users table, and an array $TotalEntries that will have nothing since its trying to pull from user_rec, which usually is one user but right now is all of the users.
//This is where I know I should have already started the loop, so chosen the first element in user_rec, and applied the GetTotalEntriesForEmail method, then the SendUserEmail method, then gone to the top of the loop and gone to the second user_rec element and repeat.
if(false === $this->SendUsersEmail($user_rec, $TotalEntries))
{
return false;
}
return true;
}
This is the method that collects the users
function GetUsers(&$user_rec)
{
if(!$this->DBLogin())
{
$this->HandleError("Database login failed!");
return false;
}
$result = mysql_query("Select * from $this->tablename",$this->connection);
$user_rec = mysql_fetch_assoc($result);
return true;
}
Here is the method I wrote to get the TotalEntries for a user that is logged in (checking his control panel to see how many entries he has)
function GetTotalEntries()
{
if(!$this->CheckLogin())
{
$this->HandleError("Not logged in!");
return false;
}
$user_rec = array();
if(!$this->GetUserFromEmail($this->UserEmail(),$user_rec))
{
return false;
}
$qry = "SELECT SUM(itemsold) AS TotalEntries FROM entries WHERE user_id = '".$user_rec['id_user']."'";
$result = mysql_query($qry,$this->connection);
while($row = mysql_fetch_array($result))
{
echo $row['TotalEntries'];
}
}
And here is how I believe it needs to be adapted to work in the email.
function GetTotalEntriesForEmail($user_rec, &$TotalEntries)
{
if(!$this->DBLogin())
{
$this->HandleError("Database login failed!");
return false;
}
$qry = "SELECT SUM(itemsold) FROM entries WHERE user_id = '".$user_rec['id_user']."'"; //$user_rec['id_user'] should the be id of the user the loop is currently on.
$result = mysql_query($qry,$this->connection);
$TotalEntries = mysql_fetch_assoc($result);
return true;
}
Heres the actual email
function SendUsers($user_rec, $TotalBallots)
{
$email = $user_rec['email']; //should be the for the user the loop is currently on.
$mailer = new PHPMailer();
$mailer->CharSet = 'utf-8';
$mailer->AddAddress($email,$user_rec['name']); //user the loop is currently on.
$mailer->Subject = "Total Ballots to Date";
$mailer->From = $this->GetFromAddress();
$mailer->Body ="Hello ".$user_rec['name']."\r\n\r\n". //Same thing
"To date you have: "/* .$TotalBallots. */" ballots.\r\n" //Same thing
if(!$mailer->Send())
{
return false;
}
return true;
}
I'm not very good at PHP, and this whole thing is a learning experience for me, so help is greatly appreciated.
If I havent been clear, maybe giving an example in another language would be clearer, so heres what I want to do, but in java
for(x = 0; x <= user_rec.length; x++)
{
int ballots = getTotalEntriesForUser(x);
sendEmailToUser(ballots)
}
If I havent been clear enough, please let me know and I will try to clarify as best as possible.
How can I combine the above code with a loop that will send all users an email, one by one, each email unique to the user it is sent to?
Are your functions part of a class? You wouldn't necessarily need them to do this. Here's my recommendation, which you can turn into functions, or a class, if you want. Also, you may want to consider looking into, and using MySQLi, and taking advantage of the classes it uses. Again, all just my recommendations.
Without knowing your table structure, I'm just taking a guess at this.
$sql = mysql_query("SELECT u.*,
u.user_id AS user,
COALESCE(SUM(e.itemssold), 0) AS total_items
FROM users u
LEFT JOIN entries e ON e.user_id = u.user_id
GROUP BY u.user_id");
while($row = mysql_fetch_array($sql))
{
$user = $row['user'];
$email = $row['user_email'];
$items = $row['total_items'];
yourEmailFunction($email, $items);
}
This pulls information from your users table, and your entries table based on matching User ID's. It sets the User ID from the user table as user so you don't have to try and distinguish between the two later. To learn about the COALESCE function, read here. The while() function will loop through every user it pulls from that SQL statement.
This hasn't been tested in any way, but that's basically what you need. Just pass the User's email, and the total Items, and write your email function to send that info to that email address.
However, if you know your functions work properly, and want to use a for loop, such as the one you provided in Java, here's how you'd write it in PHP.
for($x = 0; $x <= count($user_rec); $x++)
{
$ballots = getTotalEntriesForUser($x);
sendEmailToUser($ballots);
}
I'm just getting started on writing functions instead of writing everything inline. Is this how a reusable function is typically written?
function test_user($user) {
$conn = get_db_conn();
$res = mysql_query("SELECT * FROM users WHERE uid = $user");
$row = mysql_fetch_assoc($res);
if (count($row) == 1) {
return true;
}
else {
return false;
}
}
When someone logs in, I have their UID. I want to see if that's in the DB already. It's basic logic will be used in a
"If exists, display preferences, if !exists, display signup box" sort of flow. Obviously it's dependent on how it's used in the rest of the code, but will this work as advertised and have I fallen for any pitfalls? Thanks!
Try this:
$conn = get_db_conn(); # should reuse a connection if it exists
# Have MySQL count the rows, instead of fetching a list (also prevent injection)
$res = mysql_query(sprintf("SELECT COUNT(*) FROM users WHERE uid=%d", $user));
# if the query fails
if (!$res) return false;
# explode the result
list($count) = mysql_fetch_row($res);
return ($count === '1');
Thoughts:
You'll want better handling of a failed query, since return false means the user doesn't already exist.
Use the database to count, it'll be faster.
I'm assuming uid is an integer in the sprintf statement. This is now safe for user input.
If you have an if statement that looks like if (something) { true } else { false } you should collapse it to just return something.
HTH
That is reuseable, yes. You may want to consider moving the SQL out of the PHP code itself.
Although you weren't asking for optimization necessarily, you might want to consider querying for the user's display preferences (which I assume are stored in the DB) and if it comes back empty, display the signup box. You'll save a trip to the database and depending on your traffic, that could be huge. If you decide to keep this implementation, I would suggest only selecting one column from the database in your SELECT. As long as you don't care about the data, there's no reason to fetch every single column.
First off, you need to call
$user = mysql_real_escape_string($user);
because there's an sql injection bug in your code, see the manual. Second, you can simplify your logic by changing your query to:
SELECT COUNT(1) FROM user WHERE uid = $user;
which just lets you evaluate a single return value from $row. Last thing, once you have the basics of php down, consider looking at a php framework. They can cause you trouble and won't make you write good code, but they likely will save you a lot of work.
Indent!
Overall it looks not bad...check the comments..
function test_user($user)
{
$conn = get_db_conn(); //this should be done only once. Maybe somewhere else...?
$res = mysql_query("SELECT uid FROM users WHERE uid = $user");
$row = mysql_fetch_assoc($res);
//I can't remember...can you return count($row) and have that forced to boolean ala C? It would reduce lines of code and make it easier to read.
if (count($row) == 1) {
return true;
}
else {
return false;
}
}
Also,
if (condition) {
return true;
}
else {
return false;
}
can be rewritten as:
return condition;
which saves quite a bit of typing and reading :)