5

I am working on a blogging application in Laravel 8.

The ArticlesController controller I have this method to display the single article and its comments:

class ArticlesController extends FrontendController {

    // More code

    public function show($slug) {
        // Single article
        $article = Article::firstWhere('slug', $slug);
        $old_article = Article::where('id', '<', $article->id)->orderBy('id', 'DESC')->first();
        $new_article = Article::where('id', '>', $article->id)->orderBy('id', 'ASC')->first();

        // Comments
        $commentsQuery = Comment::where(['article_id' => $article->id, 'approved' => 1])->orderBy('id', 'desc');
        $comments = $commentsQuery->paginate(10);
        $comments_count = $commentsQuery->count();

        return view('themes/' . $this->theme_directory . '/templates/single', 
            array_merge($this->data, [
                'categories' => $this->article_categories,
                'article' => $article,
                'old_article' => $old_article,
                'new_article' => $new_article,
                'comments' => $comments,
                'comments_count' => $comments_count,
                'tagline' => $article->title,
                ])
            );
    }

}

In the view I have this for the comments list:

<div id="commentsList">
  <ol class="commentlist {{ boolval($is_infinitescroll) ? 'infinite-scroll' : '' }}">
    @foreach ($comments as $comment)
    <li class="depth-1 comment">
      <div class="comment__avatar">
        <img class="avatar" src="{{ asset('images/avatars/' . $comment->user->avatar) }}" alt="" width="50" height="50">
      </div>
      <div class="comment__content">
        <div class="comment__info">
          <div class="comment__author">{{ $comment->user->first_name }} {{ $comment->user->last_name }}</div>
          <div class="comment__meta">
            <div class="comment__time">{{ date('jS M Y', strtotime($comment->created_at)) }}</div>
            <div class="comment__reply">
              <a class="comment-reply-link" href="#0">Reply</a>
            </div>
          </div>
        </div>
        <div class="comment__text">
          <p>{{ $comment->body }}</p>
        </div>
      </div>
    </li>
    @endforeach
  </ol>
  
  <div class="ajax-load text-center is-hidden">
    loading...
  </div>
</div>

The routes related to the article(s):

// Article routes
Route::get('/', [ArticlesController::class, 'index'])->name('homepage');
Route::get('/category/{category_id}', [ArticlesController::class, 'category'])->name('category');
Route::get('/author/{user_id}', [ArticlesController::class, 'author'])->name('author');
Route::get('/show/{slug}', [ArticlesController::class, 'show'])->name('show');

The goal

I want to replace the comments pagination with an "infinite scroll".

For this purpose, I have:

/* Infinite comments */
function infiniteComments() {
    var page = 1;
    $(window).scroll(function() {
      if ($(window).scrollTop() + $(window).height() >= $(document).height() - $('.s-footer').height()) {
        page++;
        loadMoreData(page);
      }
    });
  }

  function loadMoreData(page){
    var base_url = window.location.href.split('?')[0];
    $.ajax({
        url: `${base_url}?page=${page}`,
        type: "get",
        beforeSend: function() {
          $('.ajax-load').show();
        }
      })
      .done(function(data) {
        if (data.html == "") {
          $('.ajax-load').hide();
          return;
        }
        $('.ajax-load').hide();
        $(".infinite-scroll").append(data.html);
      })
      .fail(function(jqXHR, ajaxOptions, thrownError) {
        console.log('The server is not responding...');
      });
 }

 $(document).ready(function(){
    infiniteComments();
 });

The problem

While accessing https://larablog.com/show/deserunt-qui-exercitationem?page=2 shows the comments on page 2 correctly, the Chrome console shows these 500 (Internal Server Error) errors:

https://larablog.com/show/deserunt-qui-exercitationem?page=65 500 (Internal Server Error)
The server is not responding...

https://larablog.com/show/deserunt-qui-exercitationem?page=76 500 (Internal Server Error)
The server is not responding...

The error can be tracked back to this error message in ArticlesController, at line 70 - $article = Article::firstWhere('slug', $slug):

Trying to get property 'id' of non-object.

This is strange because $article = Article::firstWhere('slug', $slug) works fine without Ajax.

Questions

  1. What causes this bug?
  2. What is the easiest fix?
9
  • What does your server's error log tell you about this? What have you tried to resolve the problem? Commented Aug 28, 2022 at 12:23
  • 1
    In addition to the above, what is the value of thrownError from your fail response handler method? Commented Aug 28, 2022 at 12:27
  • @Peppermintology console.log(thrownError) throws Internal Server Error. Commented Aug 28, 2022 at 13:30
  • And what's the root error? Try to enable a higher error reporting level in your app, or have a look at the server's error log Commented Aug 28, 2022 at 13:44
  • @NicoHaase There is this error message in ArticlesController: Trying to get property 'id' of non-object, but the log does not say on what line. Commented Aug 29, 2022 at 11:28

3 Answers 3

3

firstWhere returns the first record which meets the passed criteria, defaulting to null. So, your line of

$article = Article::firstWhere('slug', $slug);

will return the first article whose slug matches the $slug, or null, if no such record exists. Now, whenever you reference $article->id, you assume that $article is a proper Article and you wonder about the value of its id. This will yield the error you have experienced if there is no matching article.

So, it is wise to check for empty($article) just after $article was initialized and handle the edge-case when it is empty indeed.

Sign up to request clarification or add additional context in comments.

6 Comments

How would you do that and where exactly in the code? And... does this solve the problem? :)
@RazvanZamfir I would (temporarily) do a throw new Exception(json_encode($article)); just after your line of $article = Article::firstWhere('slug', $slug);, generating an error which would communicate what $article is. If it is null, then the error is that firstWhere returned with a null. This would narrow down your problem-space and strongly suggest that the solution is to either fix what slug is being passed (if that always needs to be valid), or handle the lack of matching slugs gracefully.
@RazvanZamfir as about the last question, this answer does not provide a ready-made solution for you yet, because at this point the culprit was not identified yet and it is unclear whether it is valid to expect your action to handle lacks of matches or, instead, the passing of the slug's value is the culprit, I need more contextual information in order to suggest a solution at this depth. So, this answer suggests how you can determine whether the (probable) issue is that the slug was unmatched and, as a general practice, this answer suggests defensively checking for the result of firstWhere.
The error can be tracked back to this error message in ArticlesController, at line 70 - $article = Article::firstWhere('slug', $slug): Trying to get property 'id' of non-object.
We do see the article, so the variable $article = Article::firstWhere('slug', $slug) does not return null. What we do not see is the comments, stating from the 11th. I think Laravel makes empty($article) unnecessary.
|
1
+50

The error can be due to the following issues

  1. Missing CSRF token ( Which seems not being sent using your AJAX )
  2. Wrong Route or Conflict with another route
  3. Route Param value not passing Correctly
  4. Database Record missing or wrong field name.

Try the below example to debug to get your issue fixed

Always try to use the try-catch block to make Debugging easy for your self like the below example of your ArticlesController Code Please use the below code and then check your Logs file under the directory

storage/logs/laravel.log or file with laravel-28-08-2022.log

Then use that error log to find the actual cause of the 500 Internal Server Error

class ArticlesController extends FrontendController {

// More code

public function show($slug) {
    try{
    // Single article
    $article = Article::firstWhere('slug', $slug);
    $old_article = Article::where('id', '<', $article->id)->orderBy('id', 'DESC')->first();
    $new_article = Article::where('id', '>', $article->id)->orderBy('id', 'ASC')->first();

    // Comments
    $commentsQuery = Comment::where(['article_id' => $article->id, 'approved' => 1])->orderBy('id', 'desc');
    $comments = $commentsQuery->paginate(10);
    $comments_count = $commentsQuery->count();

    return view('themes/' . $this->theme_directory . '/templates/single', 
        array_merge($this->data, [
            'categories' => $this->article_categories,
            'article' => $article,
            'old_article' => $old_article,
            'new_article' => $new_article,
            'comments' => $comments,
    'comments_count' => $comments_count,
            'tagline' => $article->title,
            ])
        );
    }
    catch(\Exception $e){
       \Log::error("Error in file: ".$e->getFile()." , Error Message: ".$e->getMessage());
       return abort(500);
   }
}

}

13 Comments

There is no form, so what shall I o with the CSRF token? Where shall I add it?
did you try the try-catch block and got any errors? @RazvanZamfir
I did try it. No errors.
If you want, look in the repo.
I have this thing in the logs: [2022-08-29 10:47:50] production.ERROR: SQLSTATE[HY000] [1045] Access denied for user 'forge'@'localhost' (using password: NO) (SQL: select * from settings` limit 1) {"exception":"[object] (Illuminate\\Database\\QueryException(code: 1045): SQLSTATE[HY000] [1045] Access denied for user 'forge'@'localhost' (using password: NO) (SQL: select * from settings limit 1) at E:\\wamp64\\www\\larablog\\vendor\\laravel\\framework\\src\\Illuminate\\Database\\Connection.php:712)`
|
1

Here is the solution I have:

In routes\web.php this new route was added:

Route::post('/load_comments', [ArticlesController::class, 'get_comments_ajax'])->name('load_comments');

In the ArticlesController:

/**
 * AJAX Call for Loading extra comments
 *
 * @param Request $request
 *
 * @return void
 */
public function get_comments_ajax( Request $request ) {
    if ( ! $request->ajax() ) {
        // Redirect to Home Page or just BOMB OUT!
        exit();
    }

    $more_comments_to_display = TRUE;

    /** @todo - 5 - This should\could be a setting */

    $article_id  = $request->post( 'article_id' );
    $page_number = $request->post( 'page' );
    $offset      = $this->comments_per_page * $page_number;

    $data['comments'] = $this->get_commentQuery( $article_id, $this->comments_per_page, $offset )->get();
    $content          = '';
    if ( $data['comments']->count() ) {
        $content .= view('themes/' . $this->theme_directory . '/partials/comments-list',
            array_merge( $data, [
              'is_infinitescroll' => $this->is_infinitescroll
            ])
        );
    } else {
        $more_comments_to_display = FALSE;
    }
    echo json_encode( [ 'html' => $content, 'page' => $page_number, 'more_comments_to_display' => $more_comments_to_display, 'article_id' => $article_id ] );
    exit();
}

/**
 * get_commentQuery
 *
 * @param int $article_id
 * @param int $limit
 * @param int $offset
 *
 * @return object
 */
private function get_commentQuery( int $article_id, int $limit = 0, int $offset = 0 ): object {
    $commentQuery = Comment::where( [ 'article_id' => $article_id, 'approved' => 1 ] )->orderBy( 'id', $this->comments_orderby_direction );
    if ( $offset > 0 ) {
        $commentQuery = $commentQuery->offset( $offset );
    }
    if ( $limit > 0 ) {
        $commentQuery = $commentQuery->limit( $limit );
    }

    return $commentQuery;
}

I only load the Ajax script if there are more then 10 comments:

@if ($is_infinitescroll && $comments_count > $comments_per_page)
    @section('custom_js_files')
        <script src="{{ asset('themes/' . $theme_directory . '/js/infinite-comments.js') }}"></script>
    @endsection
@endif

The script:

$(document).ready(function () {

    let flagMoreCommentsToDisplay = true;
    let flagCommentsBlockNewRequest = false;
    let domInfiniteScroll = $(".infinite-scroll");

    infiniteComments();

    function infiniteComments() {
        let page = 0;
        $(window).scroll(function () {
            if (flagCommentsBlockNewRequest === false) {
                if ($(window).scrollTop() + $(window).height() >= $(document).height() - $('.s-footer').height()) {
                    if (flagMoreCommentsToDisplay) {
                        flagCommentsBlockNewRequest = true;
                        page++;
                        loadMoreData(page);
                    }
                }
            }
        });
    }

    function loadMoreData(page) {
        let base_url = window.location.origin
        $.ajax({
            url: base_url + '/load_comments',
            type: 'POST', dataType: 'json',
            data: {'_token': token, 'page': page, 'article_id': article_id},
            beforeSend: function () {
                $('.ajax-load').show();
            }
        })
        .done(function (data) {
            $('.ajax-load').hide();
            let commentHtml = data.html;
            flagMoreCommentsToDisplay = data.more_comments_to_display;
            if (flagMoreCommentsToDisplay) {
                if (commentHtml !== '') {
                    domInfiniteScroll.append(commentHtml);
                }
            }
            flagCommentsBlockNewRequest = false;
        })
        .fail(function () {
            flagCommentsBlockNewRequest = false;
        });
    }
});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.