I have been working on an online **[newspaper/blogging application][1]** with *CodeIgniter 3.1.8* and Twitter Bootstrap 4.

The latest feature added is *lazy loading* of posts.

From the **Static_model** (`application\models\Static_model.php`), which is responsible with the general settings, I can set `$data['is_ajax_loading']` to `true` and enable loading posts via AJAX.

In the Posts controller,  I have:

    class Posts extends CI_Controller
    {
      public function __construct()
      {
        parent::__construct();
      }
    
      private function _initPagination($path, $totalRows, $query_string_segment = 'page')
      {
        // load and configure pagination 
        $this->load->library('pagination');
        $config['base_url']             = base_url($path);
        $config['query_string_segment'] = $query_string_segment;
        $config['enable_query_strings'] = TRUE;
        $config['reuse_query_string']   = TRUE;
        $config['total_rows']           = $totalRows;
        $config['per_page']             = 12;
    
        if ($this->Static_model->get_static_data()['has_pager']) {
          $config['display_pages'] = FALSE;
          $config['first_link'] = FALSE;
          $config['last_link'] = FALSE;
          $config['prev_tag_open'] = '<li class="prev">';
          $config['prev_tag_close'] = '</li>';
          $config['next_tag_open'] = '<li class="next">';
          $config['next_tag_close'] = '</li>';
        }
    
        if (!isset($_GET[$config['query_string_segment']]) || $_GET[$config['query_string_segment']] < 1) {
          $_GET[$config['query_string_segment']] = 1;
        }
        $this->pagination->initialize($config);
    
        $limit  = $config['per_page'];
        $offset = ($this->input->get($config['query_string_segment']) - 1) * $limit;
    
        return array(
          'limit' => $limit,
          'offset' => $offset
        );
      }
    
      public function index()
      {
        //call initialization method
        $config = $this->_initPagination("/", $this->Posts_model->get_num_rows());
        $data                  = $this->Static_model->get_static_data();
        $data['base_url']      = base_url("/");
        $data['pages']         = $this->Pages_model->get_pages();
        $data['categories']    = $this->Categories_model->get_categories();
        $data['search_errors'] = validation_errors();
        $data['posts'] = $this->Posts_model->get_posts($config['limit'], $config['offset']);
        $data['max_page'] = ceil($this->Posts_model->get_num_rows() / 12);
    
        $this->twig->addGlobal('pagination', $this->pagination->create_links());
    
        // Featured posts
        if ($data['is_featured']) {
          $data['featured'] = $this->Posts_model->featured_posts();
          $this->twig->addGlobal('featuredPosts', "themes/{$data['theme_directory']}/partials/hero.twig");
        }
    
        $this->twig->display("themes/{$data['theme_directory']}/layout", $data);
      }
    
      public function search()
      {
        // Force validation since the form's method is GET
        $this->form_validation->set_data($this->input->get());
        $this->form_validation->set_rules('search', 'search term', 'required|trim|min_length[3]', array(
          'min_length' => 'The search term must be at least 3 characters long.'
        ));
        $this->form_validation->set_error_delimiters('<p class = "error search-error">', '</p>');
        // If search fails
        if ($this->form_validation->run() === FALSE) {
          $data['search_errors'] = validation_errors();
          return $this->index();
        } else {
          $expression           = $this->input->get('search');
          $posts_count          = $this->Posts_model->search_count($expression);
          $query_string_segment = 'page';
          $config               = $this->_initPagination("/posts/search", $posts_count, $query_string_segment);
          $data                 = $this->Static_model->get_static_data();
          $data['base_url']     = base_url("/");
          $data['pages']        = $this->Pages_model->get_pages();
          $data['categories']   = $this->Categories_model->get_categories();
          //use limit and offset returned by _initPaginator method
          $data['posts']        = $this->Posts_model->search($expression, $config['limit'], $config['offset']);
          $data['expression']   = $expression;
          $data['posts_count']  = $posts_count;
          $data['max_page'] = ceil($posts_count / 12);
          
          $this->twig->addGlobal('pagination', $this->pagination->create_links());
          $this->twig->display("themes/{$data['theme_directory']}/layout", $data);
        }
      }
    
      public function byauthor($authorid)
      {
        //load and configure pagination 
        $this->load->library('pagination');
        $config['base_url']             = base_url('/posts/byauthor/' . $authorid);
        $config['query_string_segment'] = 'page';
        $config['total_rows']           = $this->Posts_model->posts_by_author_count($authorid);
        $config['per_page']             = 12;
    
        if ($this->Static_model->get_static_data()['has_pager']) {
          $config['display_pages'] = FALSE;
          $config['first_link'] = FALSE;
          $config['last_link'] = FALSE;
          $config['prev_tag_open'] = '<li class="prev">';
          $config['prev_tag_close'] = '</li>';
          $config['next_tag_open'] = '<li class="next">';
          $config['next_tag_close'] = '</li>';
        }
    
        if (!isset($_GET[$config['query_string_segment']]) || $_GET[$config['query_string_segment']] < 1) {
          $_GET[$config['query_string_segment']] = 1;
        }
    
        $limit  = $config['per_page'];
        $offset = ($this->input->get($config['query_string_segment']) - 1) * $limit;
        $this->pagination->initialize($config);
    
        $data                 = $this->Static_model->get_static_data();
        $data['base_url']     = base_url("/");
        $data['pages']        = $this->Pages_model->get_pages();
        $data['categories']   = $this->Categories_model->get_categories();
        $data['posts']        = $this->Posts_model->get_posts_by_author($authorid, $limit, $offset);
        $data['posts_count']  = $this->Posts_model->posts_by_author_count($authorid);
        $data['posts_author'] = $this->Posts_model->posts_author($authorid);
        $data['max_page'] = ceil($data['posts_count'] / $limit);
        $data['tagline']      = "Posts by " . $data['posts_author']->first_name . " " . $data['posts_author']->last_name;
    
        $this->twig->addGlobal('pagination', $this->pagination->create_links());
        $this->twig->display("themes/{$data['theme_directory']}/layout", $data);
      }
    
      public function post($slug)
      {
        $data                 = $this->Static_model->get_static_data();
        $data['base_url']     = base_url("/");
        $data['pages']        = $this->Pages_model->get_pages();
        $data['categories']   = $this->Categories_model->get_categories();
        $data['authors']      = $this->Usermodel->getAuthors();
        $data['posts']        = $this->Posts_model->sidebar_posts($limit = 5, $offset = 0);
        $data['post']         = $this->Posts_model->get_post($slug);
        $data['next_post']    = $this->Posts_model->get_next_post($slug);
        $data['prev_post']    = $this->Posts_model->get_prev_post($slug);
        $data['author_image'] = isset($data['post']->avatar) && $data['post']->avatar !== '' ? $data['post']->avatar : 'default-avatar.png';
    
        if ($data['categories']) {
          foreach ($data['categories'] as &$category) {
            $category->posts_count = $this->Posts_model->count_posts_in_category($category->id);
          }
        }
    
        if (!empty($data['post'])) {
          // Overwrite the default tagline with the post title
          $data['tagline']  = $data['post']->title;
          // Get post comments
          $post_id          = $data['post']->id;
          $data['comments'] = $this->Comments_model->get_comments($post_id);
          $this->twig->addGlobal('singlePost', "themes/{$data['theme_directory']}/templates/singlepost.twig");
        } else {
          $data['tagline'] = "Page not found";
          $this->twig->addGlobal('notFound', "themes/{$data['theme_directory']}/templates/404.twig");
        }
        $this->twig->display("themes/{$data['theme_directory']}/layout", $data);
      }
    }

The script that does the loading:

    (function($) {
    
        var currentPage = 2,
            maxPage = $('#postsContainer').data('max-page'),
            posts = null,
            pageUrl = $(location).attr('href'),
            pageBaseUrl = pageUrl.split('?')[0],
            searchStr = pageUrl.split('?')[1];
    
        $('.pagination').hide();
    
        $(window).scroll(function() {
            var toBottom = $(window).scrollTop() >= $(document).height() - $(window).height() - 25;
    
            if (toBottom && currentPage <= maxPage) {
                loadMore();
            }
        });
    
        function loadMore() {
    
            $.ajax({
                    url: `${pageBaseUrl}?${typeof searchStr === 'string' ? searchStr : ''}&page=${currentPage}`,
                    type: 'GET',
                    beforeSend: function() {
                        if (typeof posts != 'undefined') {
                            $('.loader').show();
                        }
                    }
                })
                .done(function(data) {
                    $('.loader').hide();
                    posts = $(data).find('#postsContainer').html();
    
                    if (typeof posts != 'undefined') {
    
                        $('#postsContainer').append(posts);
                        currentPage = currentPage + 1;
    
                        if (currentPage > maxPage) {
                            $('#postsContainer').append('<p class="text-center text-muted">No more posts to load</p>');
                        }
                    }
                });
        }
    
    })(jQuery);

In the `posts-lists.twig` view:

    {% if posts %}
    	{% if is_ajax_loading %}
    	<div id="postsContainer" data-max-page="{{max_page}}">
    	{% endif %}
    		{% for post in posts %}
    			<div class="post-preview">
    				<a href="{{base_url}}{{post.slug}}">
    					<h2 class="post-title">{{post.title}}</h2>
    					<h3 class="post-subtitle">{{post.description}}</h3>
    				</a>
    				<p class="post-meta">
    					Posted {% if category_name is null %}in <a href="{{base_url}}categories/posts/{{post.cat_id}}" title="All posts in {{post.post_category}}">{{post.post_category}}</a>,{% endif %} on {{post.created_at | date("M d, Y")}}
                   </p>
    			</div>
          <hr class="posts-separator">
    		{% endfor %}
    
    	{% if is_ajax_loading %}
    	</div>
    	{% endif %}	
    {% else %}
    	{% if posts_author is not defined and expression is not defined %}
    		<p class="mt-0 text-muted text-center">There are no posts {% if category_name is defined %} in this category {% endif %} yet.</p>
    	{% endif %}
    {% endif %}	
    
    {% if is_ajax_loading %}
    	<div class="loader"><span></span></div>
    {% endif %}

My main concerns are security and code redundancy.


#### Questions:

 1. Is the code DRY enough?
 2. Are there any security risks?
 3. Improvement opportunities? 

  [1]: https://github.com/Ajax30/Bravecms/