PHP Error:Fatal error: Call to a member function fetch_assoc() on a non-object in

Hi,

I have the following function that gets posts in a category.

function category_posts($category_id) {
	$posts = array();
	$query = "SELECT * FROM posts WHERE post_category = $category_id ORDER BY post_id DESC LIMIT 10";
	$result = DB::$db->query($query);
	while ($row = $result->fetch_assoc()) $posts[] = $row;
	return $posts;
}

When I check my error log, I see the following error:

PHP Error:Fatal error: Call to a member function fetch_assoc() on a non-object in

which points to the 5th line:

while ($row = $r->fetch_assoc()) $posts[] = $row;

My issue is that I checked all the pages on my site one by one but I couldn’t replicate this error, so I don’t know which post or category causes this issue. I understand that the error is caused by $result not returning as object but since I can’t replicate the issue I don’t know how to fix it.

Any tips are welcome.

Thanks.

If it only happened the once and you can’t replicate the error, my guess would be that the database server failed. Rare, but it can happen.

For this query it doesn’t look like much damage could happen from the fail other than an annoyed visitor. But perhaps you should put in a try catch to log the error and handle the fail gracefully.

In your DB class, before connecting to mysqli, add this line:

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

Note that you are using mysqli wrong way. Instead of adding a variable to the query directly, you should use placeholders in place of variables, which have to be sent separately. Something like this

function category_posts($category_id) {
    $query = "SELECT * FROM posts WHERE post_category = ? ORDER BY post_id DESC LIMIT 10";
    $stmt = DB::$db->prepare($query);
    $stmt->bind_param("i", $category_id);
    $stmt->execute();
    $res = $stmt->get_result();
    return $res->fetch_all(MYSQLI_ASSOC);
}
1 Like

I have objections regarding this statement.

First, you cannot catch a Fatal Error, only exceptions can be caught.
Second, it is my strong conviction that even exceptions should never be caught for the purpose of a graceful handling.

It’s an error handler’s job. It’s just insane to put a try catch for the every query call only to handle it gracefully, while it can be done only once, in a small function defined as an error handler. I’ve got an example for an error handler that does all the job without the need of adding any extra lines in your application.

The error is logged in the error log on the server multiple times (4-5), but that’s few compared to the total visitors, so I am suspecting it is triggered on certain posts-categories, which I couldn’t identify even though I checked all posts manually. Maybe it happens on categories with only 1 post, but they didn’t replicate the error either.

I don’t think I have an idea how to do “try catch to log the error and handle the fail gracefully”.

Does that supposed to log the MySQL error in the error log file, or display it on the page? I can’t replicate the error, so if the above line will display it on the page it won’t help me much.

Asking to learn:

  • Can you please explain why I should use placeholders instead of variables?
  • Can you tell why I need prepare(), bind_param(), execute() and get_result(), as compared to I can do all in one step? I mean I know what they do, I want to learn why I need them if there is something simpler that does the same job.

Thanks both.

Thank you for the questions.

Does that supposed to log the MySQL error in the error log file, or display it on the page?

Neither. This command only makes mysqli to throw an exception if SQL fails. While the rest is just a routine PHP error handling: uncaught exception is a fatal error, which will be treated like any other error. In your case it will be logged, making you aware of the error cause.

Can you please explain why I should use placeholders instead of variables?

Well, I’ve got a rather lengthy, but quite convincing explanation, The Hitchhiker’s Guide to SQL Injection prevention. I promise it worth to read if you want to learn.

Can you tell why I need prepare(), bind_param(), execute() and get_result(),

Only to handle placeholders you added to the query.

Interestingly, that your case most likely is a justification for the prepared statements as well. From the nature of the error I suppose that it is caused by empty $caregory_id which makes a query incorrect. Given the main purpose of a prepared statement to make SQL correct, it will solve your issue as well.

1 Like

Thanks for the tips and the link. I will surely read it. I will try the prepared statements and see if it will solve the issue but for that I need to wait a day or two to see the updated error log.

One last question: Is there a way to log the URL that’s causing the error? So that I will know if a post/category is problematic that I need to fix or it is some non-accepted manual user input that I need to filter to prevent the error.

Of course, in your error handler you can update error message with whatever additional information you want. Just add $_SERVER['REQUEST_URI'] to it.

1 Like

I’m on a shared server and I believe I don’t have access to the error handler.

I don’t quite understand which error handler you mean, but I meant one I described above, which you define yourself and to which, therefore, you obviously have full access.

set_error_handler("myErrorHandler");
function myErrorHandler($errno, $errstr, $errfile, $errline)
{
    error_log("$errstr in $errfile:$errline; Request: ".$_SERVER['REQUEST_URI']);
    header('HTTP/1.1 500 Internal Server Error', TRUE, 500);
    readfile("500.html");
    exit;
}
1 Like

Sorry, I never had my own error handler, didn’t think I needed till now. I am just checking the errors in the error_log file that the server is generating automatically.

So, I will put the code you provided into my script and is it supposed to log the errors in a 500.html file? Did I get it right?

Thanks a lot for your continuous help so far.

No.

It will log errors right where they were logged before, but adding a request uri to the error message.

While 500.html is just a general purpose error page that have to be shown to a site user.

1 Like

Thanks, got it. I will now try to identify and fix the issue with the light of your advice of prepared statements and error handling. Hopefully I will figure it out soon. I’ll report back when I do.

Hi colshrapnel,

After your comments, I have been doing some deep research in the last couple of days and learned more about security and prepared statements. I know I still have a long way to go but every small step counts.

I have modified my PHP files and functions and how I handle parameters and SQL queries and I wanted to ask your opinion (if you don’t mind) if I am doing it the right way or need any touches before I continue and apply the concept to other files. My current setup works as intended, just want to know if I am doing it correctly and if I missed anything.

I have the following in my post.php file which displays a post in a certain category. This file receives category and post parameters (via .htaccess rewrite) like:

post.php?c=sample-category&p=sample-post

<?php
if (!(isset($_GET['c']) && isset($_GET['p']))) {
	header('HTTP/1.1 404 Not Found');
	header('Location: /404.php');
	exit;
}
$p = get_post($_GET['c'], $_GET['p']);
?>

The idea is if category and post are not provided, display 404 error, otherwise continue with getting the post.

I have the following as the get_post function:

function get_post($category, $post) {
	$stmt = DB::$db->prepare("SELECT * FROM posts LEFT JOIN categories ON category_id = post_category WHERE category_name = ? AND post_name = ?");
	$stmt->bind_param('ss', $category, $post);
	$stmt->execute();
	$result = $stmt->get_result();
	$post = $result->fetch_assoc();
	if (empty($post)) {
		header('HTTP/1.1 404 Not Found');
		header('Location: /404.php');
		exit;
	}
	return $post;
}

I saw somewhere the use of filter_var before the prepare. Like

$category = filter_var($category, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_HIGH);
$post = filter_var($post, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_HIGH);
$stmt = DB::$db->prepare(...)
...

Is such use of filter_var needed? Or, is prepare enough for a safe query.

Anything you would suggest that I modify in my code or approach?

Thank you very much again for all your contribution.

Yes - you should always validate that the field contains meaningful data before trying to use it. In this case you are probably not going far enough - is every possible value that can get through that filter really a valid category - if not then you are not validating sufficiently. When you validate only meaningful valid values should get through.

1 Like

Hi felgall, and thanks for your comment. I’m not sure how to filter for a valid category, that’s why I put the following which will redirect to the 404 page if the requested entry (correct category + post combination) does not exist in the database.

if (empty($post)) {
	header('HTTP/1.1 404 Not Found');
	header('Location: /404.php');
	exit;
}

Open to advice on how I can improve what I have. Thanks.

What does a category value look like? How many characters does it contain? Is there some particular arrangement of the characters?

When a specific filter for the type of field to be validated doesn’t exist you can build your own using a regular expression.

Treat validation as a separate step - you need to validate all user inputs as much as possible before you do anything else at all with the values.

1 Like

Category values (category_name) are in the following format:

[a-z0-9-]

They can have small letters, numbers and dash only. So, you mean that I should use a preg_match to make sure the input fits that criteria before using it in the prepare statement? And do the same thing for the post value too.

Yes - that’s safest.

It provides protection for if you revisit the code in a few years time having forgotten why you wrote the code the way you did and want to do something else with those fields before you call the database.

1 Like