Razvan Zamfir
Razvan Zamfir

Reputation: 4616

What makes the AJAX call in this Laravel application fail?

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?

Upvotes: 5

Views: 631

Answers (3)

Razvan Zamfir
Razvan Zamfir

Reputation: 4616

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;
        });
    }
});

Upvotes: 1

Lajos Arpad
Lajos Arpad

Reputation: 76554

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.

Upvotes: 3

Abdul Rehman
Abdul Rehman

Reputation: 192

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);
   }
}

}

Upvotes: 1

Related Questions