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/