PEAR::Pager Tutorials - Article Pagination and Navigation

Article pagination, or how to navigate through the paragraphs of an article with Pager

You've probably seen a lot of websites featuring long, detailed articles, which are split into paragraphs, each of them in a separate page. Users often prefer reading short chunks of text instead of scrolling a very long page (unless they want to print the page, then the opposite applies). In this tutorial, we're going to see how we can build an article pagination system, with a little help from PEAR::Pager.

The database structure

We'll need two tables to store our articles: one with the basic article info (author, title, abstract, submission date) and one containing the paragraphs (one per record, with title and contents). Technically, we could use just one table, using some special delimiters embedded within the text (such as "====END OF PAGE====") to split the paragraphs, but I believe in the long run this is a much better solution.

CREATE TABLE articles (
  id INTEGER NOT NULL,
  title VARCHAR(250) NOT NULL,
  abstract TEXT,
  submission_date TIMESTAMP NOT NULL,
  author_id INTEGER NOT NULL,
  CONSTRAINT articles_pkey PRIMARY KEY(id)
);
 
CREATE TABLE paragraphs (
  article_id INTEGER NOT NULL,
  paragraph_id INTEGER NOT NULL,
  title VARCHAR(250),
  content TEXT,
  CONSTRAINT paragraphs_pkey PRIMARY KEY(article_id, paragraph_id),
  CONSTRAINT paragraphs_fk FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE
);

Sample data

Here's some sample data if you want to play with it while reading this tutorial:

-- First Article
INSERT INTO articles (id, title, abstract, submission_date, author_id) VALUES (
  1,
  'How to navigate through the paragraphs of an article with Pager',
  'You\'ve probably seen a lot of websites featuring long, detailed articles, which are split into paragraphs, each of them in a separate page. Users often prefer reading short chunks of text instead of scrolling a very long page (unless they want to print the page, then the opposite applies). In this tutorial, we\'re going to see how we can build an article pagination system, with a little help from PEAR::Pager.',
  CURRENT_TIMESTAMP,
  1
);
INSERT INTO paragraphs (article_id, paragraph_id, title, content) VALUES (1, 1, 'The database structure', 'First paragraph here');
INSERT INTO paragraphs (article_id, paragraph_id, title, content) VALUES (1, 2, 'Sample data', 'Second paragraph here');
INSERT INTO paragraphs (article_id, paragraph_id, title, content) VALUES (1, 3, 'Showtime', 'Third paragraph here');
INSERT INTO paragraphs (article_id, paragraph_id, title, content) VALUES (1, 4, 'Alternate navigation', 'Fourth paragraph here');
INSERT INTO paragraphs (article_id, paragraph_id, title, content) VALUES (1, 5, 'Article summary', 'Fifth paragraph here');
INSERT INTO paragraphs (article_id, paragraph_id, title, content) VALUES (1, 6, 'Printer friendly version', 'Sixth paragraph here');
 
-- Second Article
INSERT INTO articles (id, title, abstract, submission_date, author_id) VALUES (
  2,
  'PEAR::Pager tutorials',
  'New series of tutorials about PEAR::Pager',
  CURRENT_TIMESTAMP,
  1
);
INSERT INTO paragraphs (article_id, paragraph_id, title, content) VALUES (2, 1, 'Articles index', '1. How to efficiently paginate database results.  2. Create pretty links with Pager and mod_rewrite.  3. Navigation with Pager and AJAX (or simple Javascript).  4. Article pagination.');

Showtime!

Now that we're done with the database structure, we can display the articles in our site, one paragraph per page. For this task, we're going to use the PEAR::MDB2 DBAL and the handy Pager_Wrapper we've already seen in a previous tutorial:

<?php
//copy the Pager_Wrapper file where you can include it
require_once 'Pager_Wrapper.php';
require_once 'MDB2.php';

$article_id = 1; //if you fetch this parameter via GET/POST, remember to validate it!

//skipped the db connection code...
//let's just suppose we have a valid db connection in $db.

$pager_options = array(
    'mode'       => 'Sliding',
    'perPage'    => 1, //we want only ONE paragraph per page
    'delta'      => 3,
);

$query = 'SELECT articles.title AS article_title,
                 articles.submission_date,
                 articles.abstract,
                 paragraphs.title AS paragraph_title,
                 paragraphs.content
            FROM paragraphs
       LEFT JOIN articles ON articles.id = paragraphs.article_id
           WHERE articles.id = '. (int)$article_id .'
        ORDER BY paragraphs.paragraph_id;

$paged_data = Pager_Wrapper_MDB2($db, $query, $pager_options);

//show the results
echo '<h1>'.$paged_data['data'][0]['article_title'].'</h1>';
echo '<p><i>'.$paged_data['data'][0]['submission_date'].'</i></p>';
if ($paged_data['page_numbers']['current'] == 1) {
    // also show the abstract on the first page
    echo '<p>'.$paged_data['data'][0]['abstract'].'</p>';
}
echo '<h2>'.$paged_data['data'][0]['paragraph_title'].'</h2>';
echo '<p>'.$paged_data['data'][0]['content'].'</p>';

//show the links
echo $paged_data['links'];
?>

Since we told Pager to split the items (the paragraphs, in our case) into groups of one, it will return only one paragraph, and we can navigate through the other paragraphs with the links built by Pager.
This is the output of the script:
Pager with links example

Alternate navigation

If you don't like normal links for the navigation, and prefer a <select> menu instead, you have to add a call to getPageSelectBox() in the Pager_Wrapper_MDB2() function, before returning the paged data array, since it's not included in the default version:

function Pager_Wrapper_MDB2(&$db, $query, $pager_options = array(), $disabled = false, $fetchMode = MDB2_FETCHMODE_ASSOC)
{
  // Pager_Wrapper_MDB2() body omitted. Add the following lines before the return call:

  // ===== START ADDED CODE ======
  $selectbox_options = array(
      'optionText' => 'page %d',
      'autoSubmit' => true,
  );
  $page['select_menu'] = $pager->getPageSelectBox($selectbox_options);
  // ===== END ADDED CODE =====

  return $page;
}

Now you can display the navigation menu:

<?php
// [snip: same code as in the previous paragraph]

//show the menu
echo 'Select page: '. $paged_data['select_menu'];
?>

And here's how it will look like:
Pager with select menu example

Article summary

Sometimes, you may want to display an article summary, with a list of the paragraph titles, each one pointing to the complete paragraph. No problem,you can do that.

<?php
$query = 'SELECT title
            FROM paragraphs
           WHERE article_id = '. (int)$article_id
     .' ORDER BY paragraph_id';
$paragraph_titles = $db->queryCol($query);
//error checking omitted

echo '<h2>Summary</h2>';
echo '<ul>';
foreach ($paragraph_titles as $k => $title) {
    if ($k == ($paged_data['page_numbers']['current'] -1)) {
        // current page: don't show the link
        echo '<li>' . $title . '</li>';
    } else {
        echo '<li><a href="article.php?id='. (int)$article_id .'&pageID='. ($k+1) .'">'. $title . '</a></li>';
    }
}
echo '</ul>';
?>

The result is a summary of the paragraph titles, with links:
article summary example

Printer friendly version

If you want to offer a printer-friendly version of the complete article, you can fetch all the paragraphs and simply print them one by one:

<?php
$query = 'SELECT title,
                 submission_date,
                 abstract
            FROM articles
           WHERE id = '. (int)$article_id;
$article = $db->queryRow($query, null, MDB2_FETCHMODE_ASSOC);

$query = 'SELECT title,
                 content
            FROM paragraphs
           WHERE article_id = '. (int)$article_id
     .' ORDER BY paragraph_id';
$paragraphs = $db->queryAll($query, null, MDB2_FETCHMODE_ASSOC);

//show main article data:
echo '<h1>'.$article['title'].'</h1>';
echo '<p><i>'.$article['submission_date'].'</i></p>';
echo '<p>'.$article['abstract'].'</p>';

foreach ($paragraphs as $paragraph) {
    echo '<h2>'.$paragraph['title'].'</h2>';
    echo '<p>'.$paragraph['content'].'</p>';
}

I hope this tutorial was useful. If you need some clarifications, or have some suggestions about other topics, drop me a mail.

« Go back to PEAR::Pager tutorials index.




Lorenzo Alberton

Lorenzo Alberton Lorenzo PHP5 ZCE - Zend Certified Engineer has been working with large enterprise UK companies for the past years and is now Chief Tech Architect at DataSift. He's an international conference speaker and a long-time contributor to many open source projects. Lorenzo Alberton's profile on LinkedIN View Lorenzo Alberton's Twitter stream

Lorenzo Alberton - Sun Certified MySQL 5 Developer

Tags

AJAX, Apache, Book Review, Charset, Cheat Sheet, Data structures, Database, Firebird SQL, Hadoop, Imagick, INFORMATION_SCHEMA, JavaScript, Kafka, Linux, Message Queues, mod_rewrite, Monitoring, MySQL, NoSQL, Oracle, PDO, PEAR, Performance, PHP, PostgreSQL, Profiling, Scalability, Security, SPL, SQL Server, SQLite, Testing, Tutorial, TYPO3, Windows, Zend Framework

Buy me a book - Data Structures And Algorithm Analysis In C