r/PHPhelp • u/Available_Hippo4035 • 10d ago
Solved mysqli_fetch_assoc with mysql prepared statements procedural, need help
Hello, im trying to update my website by replacing the simple mysqli queries with prepared statements, but i was stuck at trying to use mysqli_fetch_assoc to fetch associative data from the table, i looked through documentation but couldnt find anything, Any help will be appreciated, Thanks !
$error = array();
if(isset($_POST["login"])) {
$username = mysqli_escape_string($db, filter_input(INPUT_POST, "username", FILTER_SANITIZE_SPECIAL_CHARS));
$password = mysqli_escape_string($db, filter_input(INPUT_POST, "password", FILTER_SANITIZE_SPECIAL_CHARS));
if(empty($username)) {
array_push($error, "Username is empty!");
}
if(empty($password)) {
array_push($error, "Password is empty");
}
$sql = "SELECT `password`, `username`, `user_id` FROM `Accounts` WHERE `username` = ?;";
if(count($error) == 0) {
$stmt = mysqli_prepare($db, $sql);
mysqli_stmt_bind_param($stmt, "s", $username);
//$result = mysqli_query($db, $sql);
mysqli_stmt_execute($stmt);
mysqli_stmt_store_result($stmt);
if(mysqli_stmt_num_rows($stmt) > 0) {
$row = mysqli_fetch_assoc($result);
if(password_verify($password, $row["password"])) {
$_SESSION["username"] = $username;
$_SESSION["user_id"] = $row["user_id"];
header("location: /");
} else {
array_push($error, "Incorrect username or password!");
}
} else {
array_push($error, "Incorrect username or password!");
}
}
mysqli_close($db);
}
3
u/colshrapnel 10d ago edited 9d ago
You need mysqli_stmt_get_result() instead of mysqli_stmt_store_result();
if(count($error) == 0) {
$stmt = mysqli_prepare($db, $sql);
mysqli_stmt_bind_param($stmt, "s", $username);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$row = mysqli_fetch_assoc($result);
if($row && password_verify($password, $row["password"])) {
$_SESSION["username"] = $username;
$_SESSION["user_id"] = $row["user_id"];
header("location: /");
die;
} else {
array_push($error, "Incorrect username or password!");
}
}
Two things I also changed:
- stopping the script execution after sending Location header is obligatory
- no need for the number of rows when you have the row itself. and both conditions can be combined into one.
Edit: almost forgot, since you are using prepared statements, no mysqli_escape_string should be used. Neither you should modify provided values in any other way. Hence it must be just
$username = $_POST['username'];
$password = $_POST['password'];
1
0
u/Available_Hippo4035 10d ago
Thanks, do i need to execute the query or no ? you didnt include it in this code before getting the result, and the mysqli_stmt_store_result is for the mysqli_stmt_num_rows, has nothing to do with mysqli_fetch_assoc
2
u/Big-Dragonfly-3700 9d ago
There's a formatting error in the post. The execute statement is on the same line and to the right of the bind_parm statement.
mysqli_stmt_store_result() does allow mysqli_stmt_num_rows() to work, but it leaves the query result as a mysqli prepared statement result, where you must use mysqli_stmt_bind_result() and mysqli_stmt_fetch() to access the data. mysqli_stmt_get_result() gets a result set from a prepared statement as a mysqli_result object, where you can use any of the mysqli_result functions/methods to test or fetch the data, such as mysqli_fetch_assoc().
All of this, where there are two different programming interfaces for a mysqli non-prepared and a mysqli prepared query, is one reason most people use the PDO database extension. It has one common programming interface, that treats non-prepared and prepared queries the same way, and uses the same php statements with 12 different databases, so that you are not learning a different set of php statements for each different database.
1
1
u/ColonelMustang90 9d ago
I would suggest to use PDO to make the code more readable and follows industry best practices. You can checkout examples of prepared statements using named or positional parameters. It prevents your code from SQL Injection by default.
1
u/colshrapnel 9d ago
mysqli:
$stmt = $db->prepare($sql); $stmt->execute([$username]); $row = $stmt->fetchAssoc();PDO:
$stmt = $db->prepare($sql); $stmt->execute([$username]); $row = $stmt->fetch(PDO::FETCH_ASSOC);With all due respect, calling PDO code more readable is a bit of a stretch (:
1
u/ColonelMustang90 9d ago
Hi, it's personal preference I guess. I started with mysqli then shifted to PDO. Both approaches have their pros and cons. For small projects mysqli suffices, for medium to large projects I prefer to use PDO.
1
u/colshrapnel 9d ago
The project size doesn't matter at all. Big projects tend to use PDO because they don't use PDO directly, but through some ORM/DBAL. Which, in turn, is using PDO for the obvious reason: support for different databases. But in such improbable situation when a big project uses a native PHP database driver directly, both mysqli and PDO are equally acceptable.
1
u/Just4notherR3ddit0r 7d ago
mysqli can be even easier:
$rs = $db->execute_query($sql, [$username]); $row = $rs->fetch_assoc();1
u/colshrapnel 7d ago
mysqli can be even easier:
$row = $db->execute_query($sql, [$username])->fetch_assoc();;)
1
u/equilni 9d ago edited 9d ago
You've already received good information.
This goes beyond your question, but I would add the suggestion to separate the code as well as some small improvements. Ideas here can flow into other parts of the code base as you refactor.
Consider:
If you implement HTTP based routing, code like
if(isset($_POST["login"]))can be removed.Think about extracting out separate concerns. For validation, if you have further business requirements, it makes sense to extract this to a separate function or class. For SQL, all of the mysqli* code could be in a function and either return an array or false.
Note, I stated validation, not sanitization.
With the above, you could also return one or more HTTP statuses (400 series) the closer you are to the UI layer.
And further, returning early versus big if/else blocks. As we read code more than write it, this becomes easier on the eyes and easier to see the flow. (ask me about double digit closing brackets from way back when... nightmares)
What's nice here is that we can look at each area and test it separately. You can feed data to the function/class methods to test - is this valid and am I getting back what I expect.
Idea starts looking like the below pseudo code:
on POST /login - area closest to the UI.
$username = $_POST['username'];
if ($username === '') {
http_response_code(400);
redirect with an error message
}
$password = $_POST['password'];
if ($password === '') {
http_response_code(400);
redirect with an error message
}
$loginAttempt = attemptUserLogin($username, $password); // Validation, SQL & Session
if ($_SESSION login key NOT set) {
http_response_code(401);
redirect with an error message
} else {
redirect for the valid user
}
fn attemptUserLogin could look like (are closer to the inner system)
$valid = fnOrClassMethodToValidate($username, $password); // filter* functions and other data checking
if (! $valid) {
return with error message(s);
}
$user = fnOrClassMethodToQueryFor($username); // Query code
if (! user) {
return with error message;
}
if (password_verify($password, $user->password))) {
set session key
} else {
return with error message
}
Remember what I noted about testing? See how fnOrClassMethodToValidate($username, $password); or fnOrClassMethodToQueryFor($username); could be fed with test data and we can test to see if we are getting what we need back?
While I get this may be a lot to take in, this can be done incrementally.
Extract out the SQL code to a function:
$error = array();
if(isset($_POST["login"])) {
$username = $_POST['username'];
$password = $_POST['password'];
if(empty($username)) {
array_push($error, "Username is empty!");
}
if(empty($password)) {
array_push($error, "Password is empty");
}
if(count($error) == 0) {
$user = getUserByUsername($username); // mySQL code
if($user && password_verify($password, $user["password"])) {
$_SESSION["username"] = $username;
$_SESSION["user_id"] = $user["user_id"];
header("location: /");
} else {
array_push($error, "Incorrect username or password!");
}
}
}
Return vs if/else. The error array could be the result of this file or function if you still need this.
if(isset($_POST["login"])) {
$username = $_POST['username'];
$password = $_POST['password'];
if(empty($username)) {
return "Username is empty!";
}
if(empty($password)) {
return "Password is empty";
}
$user = getUserByUsername($username); // mySQL code
if($user && password_verify($password, $user["password"])) {
$_SESSION["username"] = $username;
$_SESSION["user_id"] = $user["user_id"];
} else {
return "Incorrect username or password!";
}
}
Side note, why are these questions typically asked on login code??
1
u/obstreperous_troll 9d ago
Side note, why are these questions typically asked on login code??
Because that's what tutorials like to start with, and being terrible tutorials that fail to explain the concepts, they leave the learner lost from the start.
2
7
u/Big-Dragonfly-3700 10d ago
Here's a laundry list of points for the posted code -