*Важно: везде в коде, где производится вычитание, замените—
на-
Рассмотрим постраничную навигацию. Объемный список неудобно отображать на странице целиком, т. к. это требует значительных ресурсов.
Гораздо нагляднее выводить список, например, по 10 элементов, предоставляя ссылки на оставшиеся страницы. В большинстве случаев такая задача
решается без привлечения объектно-ориентированного подхода и тем более без наследуемой иерархии и полиморфизма.
Однако в Web-приложении источником списка элементов, к которому следует применить постраничную навигацию, могут выступать база данных, файл со строками, директория с
файлами изображений и т. п. Каждый раз создавать отдельную функцию для
реализации постраничной навигации не всегда удобно, т. к. придется дублировать значительную часть кода в нескольких функциях.
В данном случае удобнее реализовать постраничную навигацию в методе базового класса pager
, а методы, работающие с конкретными источниками, переопределить в производных классах:
- pager_mysql
— постраничная навигация для СУБД MySQL;
- pager_file
— постраничная навигация для текстового файла;
- pager_dir
— постраничная навигация для файлов в директории.
Иерархия классов постраничной навигации представлена на рис. 4.5.
Рис. 4.5. Иерархия классов постраничной навигации
Создадим базовый класс pager
, который при помощи перегруженного метода __toString()
будет выводить ссылки постраничной навигации в следующем формате: [1-10]… [281-290] [291-300] [301-310] [311-320] [321-330] … [511-517]
Номера отображенных на экране элементов [301—310]
выделяются жирным шрифтом. Крайняя левая и крайняя правая ссылки позволяют перейти в начало и конец списка, а вокруг текущей страницы выводятся ссылки на соседние
страницы.
Класс pager()
“не знает”, каким образом производные классы будут узнавать общее количество позиций в списке, сколько позиций будет отображаться на
одной странице, сколько ссылок находится слева и справа от текущей страницы. Поэтому помимо метода __ToString()
класс будет содержать четыре
защищенных (protected) метода, которые возвращают:
- get_total()
— общее количество позиций в списке;
- get_pnumber()
— количество позиций на странице;
- get_page_link()
— количество позиций слева и справа от текущей страницы;
- get_parameters()
— строку, которую необходимо передать по ссылкам на другую страницу (например, при постраничном выводе результатов поиска по ссылкам придется передавать результаты поиска).
В листинге 4.28 представлена одна из возможных реализаций класса pager
.
Следует обратить внимание, что перечисленные выше методы не несут никакой функциональности: они необходимы лишь для того, чтобы обращение к ним из метода __toString()
не приводило к ошибке. Сами методы должныбыть реализованы в производных классах.
Конструктор класса pager объявлен защищенным, т. е. из внешнего кода не будет никакой возможности объявить объект классаpager
: такая предосторожность необходима, поскольку сам по себе классpager
не является работоспособным, он лишь задает функциональность производных классов.
Листинг 4.28. Реализация класса pager
get_total()/$this->get_pnumber()); if((float)($this->get_total()/$this->get_pnumber()) — $number != 0) { $number++; } // Проверяем, есть ли ссылки слева if($page — $this->get_page_link() > 1) { $return_page .= "get_parameters()}> [1-{$this->get_pnumber()}] ... "; // Есть for($i = $page — $this->get_page_link(); $i<$page; $i++) { $return_page .= " get_parameters()}> [".(($i — 1)*$this->get_pnumber() + 1). "-".$i*$this->get_pnumber()."] "; } } else { // Нет for($i = 1; $i<$page; $i++) { $return_page .= " get_parameters()}> [".(($i — 1)*$this->get_pnumber() + 1). "-".$i*$this->get_pnumber()."] "; } } // Проверяем, есть ли ссылки справа if($page + $this->get_page_link() < $number) { // Есть for($i = $page; $i<=$page + $this->get_page_link(); $i++) { if($page == $i) $return_page .= " [". (($i — 1) * $this->get_pnumber() + 1). "-".$i*$this->get_pnumber()."] "; else $return_page .= " get_parameters()}> [".(($i — 1)*$this->get_pnumber() + 1). "-".$i*$this->get_pnumber()."] "; } $return_page .= " ... ". "get_parameters()}> [".(($number — 1)*$this->get_pnumber() + 1). "-{$this->get_total()}] "; } else { // Нет for($i = $page; $i<=$number; $i++) { if($number == $i) { if($page == $i) $return_page .= " [". (($i — 1)*$this->get_pnumber() + 1). "-{$this->get_total()}] "; else $return_page .= " get_parameters()}> [".(($i — 1)*$this->get_pnumber() + 1). "-{$this->get_total()}] "; } else { if($page == $i) $return_page .= " [". (($i — 1)*$this->get_pnumber() + 1). "-".$i*$this->get_pnumber()."] "; else $return_page .= " get_parameters()}> [".(($i — 1)*$this->get_pnumber() + 1). "-".$i*$this->get_pnumber()."] "; } } } return $return_page; } } ?>
Заранее не известно, для какой страницы будет выводиться навигация, поэтому в ссылках используется элемент суперглобального массива $_SERVER['PHP_SELF']
, который возвращает номер текущей страницы. Нумерация страниц начинается с 1; номер текущей страницы передается через GET-параметр page и доступен в суперглобальном массиве $_GET['page']
.
Суперглобальные массивы ($_GET
, $_POST
, $_SESSION
, $_COOKIE
, $_GLOBAL
, $_REQUEST
) доступны в любой части скрипта, поэтому не обязательно предусматривать передачу их элементов объекту.
Реализуем постраничную навигацию при помощи производного класса
pager_file
, который будет работать с источником и перегрузит методы
get_total()
, get_pnumber()
, get_page_link()
и get_parameters()
базового
класса pager
.
Рассмотрим простейший случай, когда класс pager_file
читает
строки из текстового файла и выводит 10 строк на одной странице, предоставляя ссылки на другие страницы (листинг 4.29).
Листинг 4.29. Класс pager_file
filename = $filename; $this->pnumber = $pnumber; $this->page_link = $page_link; $this->parameters = $parameters; } public function get_total() { $countline = 0; // Открываем файл $fd = fopen($this->filename, "r"); if($fd) { // Подсчитываем количество записей // в файле while(!feof($fd)) { fgets($fd, 10000); $countline++; } // Закрываем файл fclose($fd); } return $countline; } public function get_pnumber() { // Количество позиций на странице return $this->pnumber; } public function get_page_link() { // Количество ссылок слева и справа // от текущей страницы return $this->page_link; } public function get_parameters() { // Дополнительные параметры, которые // необходимо передать по ссылке return $this->parameters; } // Возвращает массив строк файла // по номеру страницы $index public function get_page() { // Текущая страница $page = $_GET['page']; if(empty($page)) $page = 1; // Количество записей в файле $total = $this->get_total(); // Вычисляем число страниц в системе $number = (int)($total/$this->get_pnumber()); if((float)($total/$this->get_pnumber()) — $number != 0) $number++; // Проверяем, попадает ли запрашиваемый номер // страницы в интервал от 1 до get_total() if($page <= 0 || $page > $number) return 0; // Извлекаем позиции текущей страницы $arr = array(); $fd = fopen($this->filename, "r"); if(!$fd) return 0; // Номер, начиная с которого следует // выбирать строки файла $first = ($page — 1)*$this->get_pnumber(); for($i = 0; $i < $total; $i++) { $str = fgets($fd, 10000); // Пока не достигнут номер $first, // досрочно заканчиваем итерацию if($i < $first) continue; // Если достигнут конец выборки, // досрочно покидаем цикл if($i > $first + $this->get_pnumber() — 1) break; // Помещаем строки файла в массив, // который будет возвращен методом $arr[] = $str; } fclose($fd); return $arr; } } ?>
Конструктор класса page_file принимает четыре параметра:
- $filename
— имя текстового файла, выступающего источником строк;
- $pnumber
— количество позиций на странице, необязательный параметр;
если не указывается при создании объекта, принимает значение 10;
- $page_link
— количество ссылок слева и справа от текущей страницы, необязательный параметр; если не указывается при создании объекта, принимает значение 3;
- $parameters
— параметры для передачи по ссылке на другие страницы,
необязательный параметр; если не указывается при создании объекта,
принимает значение 3.
Данные четыре параметра передаются соответствующим закрытым членам
класса. Реализация методов get_pnumber()
, get_page_link()
и get_parameters()
сводится к возвращению значений соответствующих закрытых членов. Метод
get_total()
открывает файл с именем $filename
, переданным в конструкторе,
и подсчитывает количество строк в файле при каждом обращении.
С точки
зрения производительности было бы разумно завести закрытую переменную
$total
и присваивать ей значение только один раз в конструкторе, т. к. операция сканирования файла достаточно трудоемка. Однако это не всегда
удобно, поскольку количество записей в файле может изменяться на всем
протяжении существования объекта.
Подсчет строк в файле осуществляется путем чтения строк функцией fgets()
в цикле while()
. До тех пор, пока конец файла не достигнут, функция feof()
возвращает значение false
, и цикл продолжает работу. Функция fgets()
читает из файла количество символов, указанное во втором параметре; чтение
символов заканчивается, если функция встречает символ перевода строки.
Обычно строки в текстовых файлах гораздо короче 10 000 символов, поэтому
чтение всегда выполняется корректно.
Для работы с текстовыми файлами можно использовать функциюfile()
, которая возвращает содержимое текстового файла в виде массива, каждый элемент которого соответствует отдельной строке файла. Однако для файлов большого объема функцияfile()
не всегда подходит.
Дело в том, что содержимое файла приходится полностью загружать в память скрипта, которая зачастую ограничена 8 или 16 Мбайт, а это приводит к аварийному завершению работы функцииfile()
. При использовании функцииfgets()
в циклеwhile()
для хранения содержимого файла скрипт в каждую секунду времени использует не больше 10 Кбайт (переменная$str
).
Помимо указанных выше методов, объект снабжен методом get_page()
, который возвращает массив строк файла, соответствующих текущей позиции.
Функция ориентируется на GET-параметр page, который используется для
указания номера страницы в постраничной навигации. Если этот параметр не
установлен, считается, что текущей является первая страница. Перед использованием параметра проверятся, не выходит ли его значение за пределы допустимого интервала; если выходит, параметр принимает значение 0.
Далее
вычисляется номер строки $first
, начиная с которой следует выбирать стро-
ки из файла и помещать их в массив $arr
, возвращаемый методом get_page()
в качестве результата.
Пока счетчик цикла for()
не достиг величины $first
,
итерация цикла может быть прекращена досрочно при помощи ключевого
слова continue
.
Если счетчик цикла превысил величину, равную $first
плюс
количество позиций на странице, цикл прекращает работу при помощи ключевого слова break
.
Следует отметить, что ни классpager
, ни классpager_file
не используют ни одного оператораecho
; результат возвращается при помощи ключевых словreturn
. Очень важно помещать в классы как можно меньше информации о дизайне и месте вывода информации на странице.
Именно такой подход позволяет повторно использовать классы в других проектах. Местоположение вывода — это задача либо клиентского кода, либо специального класса вывода; абстрактный тип данных должен решать только свойственные ему задачи.
Теперь, когда готов первый производный класс постраничной навигации,
можно воспользоваться им для представления файла большого объема —
стандартного словаря операционной системы Linux, который можно найти
в /usr/share/dict/linux.words
.
В листинге 4.30 приводится пример построения постраничной навигации по
словарю linux.words
.
Листинг 4.30. Постраничная навигация по файлу
get_page(); for($i = 0; $i < count($arr); $i++) { echo "{$arr[$i]}
"; } // Выводим ссылки на другие страницы echo $obj; ?>
Результат работы скрипта из листинга 4.30 представлен на рис. 4.6.
Рис. 4.6. Постраничная навигация
Классы не следует воспринимать как нечто статическое и завершенное.
Основное преимущество объектно-ориентированного подхода заключается в
том, что уже существующие классы легко расширяются.
Например, пользователю может понадобиться возможность вывести в окно браузера все слова
из словаря, начинающиеся с введенных им символов, а результаты поиска
разбить на страницы. Разработанный ранее класс pager_file
не подходит для
решения этой задачи, т. к. не учитывает дополнительное ограничение.
Однако не стоит спешить его модифицировать, поскольку это может отразиться на
коде, уже использующем этот класс. Более правильным будет унаследовать
от класса pager_file
новый класс pager_file_search
, модифицировав его отдельные методы (рис. 4.7).
Рис. 4.7. Расширение функциональности существующей системы путем наследования
В листинге 4.31 представлена возможная реализация класса pager_file_search
, позволяющая устанавливать фильтры на позиции из словаря.
Листинг 4.31. Класс pager_file_search
search = $search; } public function get_total() { $countline = 0; // Открываем файл $fd = fopen($this->filename, "r"); if($fd) { // Подсчитываем количество записей // в файле while(!feof($fd)) { $str = fgets($fd, 10000); if(preg_match("|^".preg_quote($this->search)."|i", $str)) { $countline++; } } // Закрываем файл fclose($fd); } return $countline; } // Возвращает массив строк файла // по номеру страницы $index public function get_page() { // Текущая страница $page = $_GET['page']; if(empty($page)) $page = 1; // Количество записей в файле $total = $this->get_total(); // Вычисляем число страниц в системе $number = (int)($total/$this->get_pnumber()); if((float)($total/$this->get_pnumber()) — $number != 0) $number++; // Проверяем, попадает ли запрашиваемый номер // страницы в интервал от 1 до get_total() Ãëàâà 4 if($page <= 0 || $page > $number) return 0; // Извлекаем позиции текущей страницы $arr = array(); $fd = fopen($this->filename, "r"); if(!$fd) return 0; // Номер, начиная с которого следует // выбирать строки файла $first = ($page — 1)*$this->get_pnumber(); while(!feof($fd)) { $str = fgets($fd, 10000); if(preg_match("|^".preg_quote($this->search)."|i", $str)) { $countline++; // Пока не достигнут номер $first, // досрочно заканчиваем итерацию if($countline < $first) continue; // Если достигнут конец выборки, // досрочно покидаем цикл if($countline > $first + $this->get_pnumber() — 1) break; // Помещаем строки файла в массив, // который будет возвращен методом $arr[] = $str; } } // Закрываем файл fclose($fd); return $arr; } } ?>
Как видно из листинга 4.31, перегрузке подвергся конструктор, который первым параметром $search
теперь принимает искомое словосочетание, вторым
параметром $filename
— имя файла-источника и лишь затем необязательные
параметры, среди которых отсутствует $parametrs
.
Последний параметр формируется явно при вызове конструктора базового класса — именно с его помощью передается искомое словосочетание $search
.
Помимо конструктора перегрузке подверглись методы get_total()
и
get_page()
, которые при помощи регулярного выражения, выстраиваемого на
основе члена $search
, проверяют каждую строку файла на соответствие заданному условию.
В листинге 4.32 приводится пример использования объекта класса pager_file_search
, в котором задается вывод всех слов из словаря,
начинающихся с последовательности "ab" по пять позиций на странице.
Листинг 4.32. Использование объекта класса pager_file_search
get_page(); for($i = 0; $i < count($arr); $i++) { echo "{$arr[$i]}
"; } // Выводим ссылки на другие страницы echo $obj; ?>
Теперь не представляет сложности создать небольшое Web-приложение по поиску слов в Linux-словаре (листинг 4.33).
При поиске для передачи параметров из HTML-формы обработчику лучше
пользоваться методом GET, чем POST. Во-первых, это позволяет ссылаться на
результаты поиска, во-вторых, для организации постраничной навигации можно
легко передавать все параметры формы через URL, не помещая их в сессию
(т. к. массив для хранения данных сессий $_SESSION
является суперглобальным и не сбрасывается автоматически, это зачастую приводит к созданию
сложных систем, в которых легко допустить ошибку).
Листинг 4.33. Поиск по файлу linux.words
get_page(); for($i = 0; $i < count($arr); $i++) { echo "{$arr[$i]}
Результат работы скрипта из листинга 4.33 представлен на рис. 4.8.
Рис. 4.8. Поиск по словарю linux.words
В качестве источника позиций, нуждающихся в постраничной навигации,
может выступать директория с файлами. Сами файлы могут иметь разную
природу — это может быть фотография, путь к которой следует передать атрибуту src
тега img
, или текстовый файл с сообщением для гостевой книги.
Поэтому класс постраничной навигации pager_dir
должен, по аналогии с
pager_file
, возвращать массив путей к файлам, а выводить их в окно браузера.
Это позволит, не сужая области применения класса, использовать его для
организации постраничной навигации по любой произвольной директории.
В листинге 4.34 представлен класс pager_dir
, который перегружает методы
get_total()
, get_pnumber()
, get_page_link()
и get_parameters()
базового
класса pager
.
Конструктор класса принимает в качестве первого параметра
имя директории с файлами $dirname
, количество позиций на текущей страни-
це $pnumber
, количество ссылок слева и справа от текущей страницы
$page_link
и строку с GET-параметрами $parameters
, которую следует передавать по ссылкам.
От классаpager_dir
можно, в свою очередь, унаследовать классpager_dir_sort
, позволяющий задавать критерий сортировки файлов, или классpager_dir_recurse
, который осуществляет рекурсивный спуск и учитывает файлы вложенных поддиректорий.
Листинг 4.34. Класс pager_dir
dirname = trim($dirname, "/"); $this->pnumber = $pnumber; $this->page_link = $page_link; $this->parameters = $parameters; } public function get_total() { $countline = 0; // Открываем директорию if(($dir = opendir($this->dirname)) !== false) { while(($file = readdir($dir)) !== false) { // Если текущая позиция является файлом, // учитываем ее if(is_file($this->dirname."/".$file)) $countline++; } // Закрываем директорию closedir($dir); } return $countline; } public function get_pnumber() { // Количество позиций на странице return $this->pnumber; } public function get_page_link() { // Количество ссылок слева и справа // от текущей страницы return $this->page_link; } public function get_parameters() { // Дополнительные параметры, которые // необходимо передать по ссылке return $this->parameters; } // Возвращает массив строк файла // по номеру страницы $index public function get_page() { // Текущая страница $page = $_GET['page']; if(empty($page)) $page = 1; // Количество записей в файле $total = $this->get_total(); // Вычисляем количество страниц в системе $number = (int)($total/$this->get_pnumber()); if((float)($total/$this->get_pnumber()) — $number != 0) $number++; // Проверяем, попадает ли запрашиваемый номер // страницы в интервал от 1 до get_total() if($page <= 0 || $page > $number) return 0; // Извлекаем позиции текущей страницы $arr = array(); // Номер, начиная с которого следует // выбирать строки файла $first = ($page — 1)*$this->get_pnumber(); // Открываем директорию if(($dir = opendir($this->dirname)) === false) return 0; $i = -1; while(($file = readdir($dir)) !== false) { // Если текущая позиция является файлом if(is_file($this->dirname."/".$file)) { // Увеличиваем счетчик $i++; // Пока не достигнут номер $first, // досрочно заканчиваем итерацию if($i < $first) continue; // Если достигнут конец выборки, // досрочно покидаем цикл if($i > $first + $this->get_pnumber() — 1) break; // Помещаем пути к файлам в массив, // который будет возвращен методом $arr[] = $this->dirname."/".$file; } } // Закрываем директорию closedir($dir); return $arr; } } ?>
Класс pager_dir
отличается от класса pager_file
только реализацией метода
get_total()
, предназначенного для возвращения количества файлов в директории, и метода get_page()
, возвращающего массив путей к файлу для текущей страницы.
В основе обоих методов лежит использование функций для
работы с директориями.
Порядок работы с директорией точно такой же, как и
с файлом: директория открывается при помощи функции opendir()
, функция
принимает в качестве единственного параметра имя директории и возвращает
дескриптор $dir
, который затем используется в качестве первого параметра
для всех остальных функций, работающих с директорией.
В цикле while()
осуществляется последовательное чтение элементов директории при помощи
функции readdir()
.
Функция возвращает лишь имя файла, поэтому при обращении к файлу необходимо формировать путь, добавляя перед его названием имя директории $this->dirname
.
Пользователь класса может задавать имя директории как со слэшем на конце — "photo/", так и без него — "photo". Чтобы не создавать специальной обработки подобной ситуации, в конструкторе классаpager_dir
косая черта в конце строки удаляется при помощи функцииtrim()
.
По умолчанию данная функция удаляет пробелы в начале и в конце строки, однако при помощи второго параметра можно указать удаляемый символ, отличный от пробела.
Следует отметить, что результат присвоения переменной $file
значения
функции readdir()
сравнивается со значением false
.
Обычно в качестве аргумента цикла while()
используется выражение $file = readdir($dir)
, однако в данном случае это недопустимо, т. к. имя файла может начинаться с 0,
что в контексте оператора while()
будет рассматриваться как false
и приводить к остановке цикла.
В директории могут находиться как файлы, так и поддиректории; обязательно учитываются как минимум две директории: текущая "." и родительская "..".
Поэтому при подсчете количества или при формировании массива файлов для текущей страницы важно подсчитывать именно файлы, избегая директорий. Для этой цели служит функция is_file()
, которая возвращает true
,
если переданный ей в качестве аргумента путь ведет к файлу, и false
— в противном случае.
В листинге 4.35 приводится пример использования класса pager_dir
для вывода фотографий из папки photo по три штуки на странице.
Конструктор
класса принимает в качестве первого параметра имя директории, в качестве
второго, задающего количество позиций на странице, по умолчанию используется число 3.
Листинг 4.35. Использование класса pager_dir
get_page(); for($i = 0; $i < count($arr); $i++) { echo " "; } echo "
"; // Выводим ссылки на другие страницы echo $obj; ?>
Результат работы скрипта из листинга 4.35 может выглядеть так, как это представлено на рис. 4.9.
Рис. 4.9. Постраничная навигация для файлов в директории
Помимо файлов и директорий, постраничная навигация часто применяется
для вывода позиций из базы данных.
Реализуем производный класс
pager_mysql
, который расширит класс pager
для работы с СУБД MySQL.
Очень часто для доступа к СУБД MySQL организуют специальный класс. Это не оправданно, как и в случае с классом доступа к файлу: если приложение работает только с одним типом СУБД, незачем подменять стандартный прозрачный интерфейс доступа собственным объектно-ориентированным интерфейсом, который не будет знаком никому, кроме самого разработчика.
Однако когда приложение проектируется для работы сразу с несколькими базами данных, управляемыми разными СУБД, полезно создать класс, который будет адаптировать запросы для доступа к разным типам СУБД.
Для доступа к СУБД MySQL следует воспользоваться новой библиотекойphp_mysqli
, которая предоставляет объектно-ориентированный интерфейс.
К сожалению, данная библиотека пока не получила столь же широкого распространения, как и библиотекаphp_mysql
.
В листинге 4.36 представлена одна из возможных реализаций класса
pager_mysql
.
В задачу класса не входит установка соединения с базой данных — этим будет заниматься внешний код.
Конструктор класса принимает в
качестве параметров имя таблицы $tablename
, WHERE
-условие $where
и критерий сортировки $order
.
Конструктор класса не пытается предотвратить SQL- инъекции, т. к. предполагается, что объекту передаются уже сформированные фрагменты SQL-запроса, и внешний программист позаботился о предотвращении данного вида атаки.
SQL-запросы зачастую формируются динамически, т. е. включают в себя одну или несколько переменных. Если значение таких переменных может быть умышленно искажено с целью изменения логики SQL-запроса, имеет место SQL-инъекция.
Представленный в листинге 4.36 классpager_mysql
работает лишь с одной таблицей, именно поэтому в нем не предусмотрены параметры для группировки при помощи ключевого словаGROUP BY
и условийON
иHAVING
. Для реализации многотабличных запросов потребуется унаследовать отpager
новый классpager_mysql_multi
.
Листинг 4.36. Реализация класса pager_mysql
tablename = $tablename; $this->where = $where; $this->order = $order; $this->pnumber = $pnumber; $this->page_link = $page_link; $this->parameters = $parameters; } public function get_total() { // Формируем запрос на получение // общего количества записей в таблице $query = "SELECT COUNT(*) FROM {$this->tablename} {$this->where} {$this->order}"; $tot = mysql_query($query); if(!$tot) exit("Ошибка подсчета количества позиций
{mysql_error()}
$query"); return mysql_result($tot, 0); } public function get_pnumber() { // Количество позиций на странице return $this->pnumber; } public function get_page_link() { // Количество ссылок слева и справа // от текущей страницы return $this->page_link; } public function get_parameters() { // Дополнительные параметры, которые // необходимо передать по ссылке return $this->parameters; } // Возвращает массив строк файла // по номеру страницы $index public function get_page() { // Текущая страница $page = $_GET['page']; if(empty($page)) $page = 1; // Количество записей в файле $total = $this->get_total(); // Вычисляем количество страниц в системе $number = (int)($total/$this->get_pnumber()); if((float)($total/$this->get_pnumber()) — $number != 0) $number++; // Проверяем, попадает ли запрашиваемый номер // страницы в интервал от 1 до get_total() if($page <= 0 || $page > $number) return 0; // Извлекаем позиции текущей страницы $arr = array(); // Номер, начиная с которого следует // выбирать строки файла $first = ($page — 1)*$this->get_pnumber(); // Извлекаем позиции для текущей страницы $query = "SELECT * FROM {$this->tablename} {$this->where} {$this->order} LIMIT $first, {$this->get_pnumber()}"; $tbl = mysql_query($query); if(!$tbl) exit("Ошибка обращения к таблице позиций
{mysql_error()}
$query"); // Если имеется хотя бы один элемент, // заполняем массив $arr if(mysql_num_rows($tbl)) { while($arr[] = mysql_fetch_array($tbl)); } // Удаляем последний нулевой элемент // массива $arr unset($arr[count($arr) — 1]); return $arr; } } ?>
Точно так же, как и в предыдущих классах, наиболее серьезной модификации
подвергаются метод get_total()
, возвращающий количество записей в
таблице $tablename
, и метод get_page()
, который возвращает двумерный массив: каждый элемент массива представляет собой массив полей одной из
записи.
Для подсчета количества записей таблицы в методе get_total()
используется
функция COUNT(*)
:
SELECT COUNT(*) FROM tbl
В методе get_page()
, возвращающем записи для текущей страницы, для получения ограниченного объема записей используется конструкция LIMIT
: так,
запрос с конструкцией LIMIT 0, 10
возвращает первые 10 элементов таблицы,
LIMIT 10, 10
возвращает следующие 10 элементов, LIMIT 20, 10
— следующие и т. д.
SELECT COUNT(*) FROM tbl LIMIT 0, 10
Для демонстрации возможностей класса pager_mysql
создадим таблицу
position
, предназначенную для хранения заголовков разделов сайта (листинг 4.37).
Листинг 4.37. SQL-дамп таблицы position
CREATE TABLE position ( id_position INT(11) NOT NULL AUTO_INCREMENT, name TEXT NOT NULL, PRIMARY KEY (id_position) ); INSERT INTO position VALUES (1, 'C++'); INSERT INTO position VALUES (2, 'Pascal'); INSERT INTO position VALUES (3, 'Perl'); INSERT INTO position VALUES (4, 'PHP'); INSERT INTO position VALUES (5, 'C#'); INSERT INTO position VALUES (6, 'Visual Basic'); INSERT INTO position VALUES (7, 'BASH'); INSERT INTO position VALUES (8, 'Python'); INSERT INTO position VALUES (9, 'SQL'); INSERT INTO position VALUES (10, 'Fortran'); INSERT INTO position VALUES (11, 'JavaScript'); INSERT INTO position VALUES (12, 'HTML'); INSERT INTO position VALUES (13, 'UML'); INSERT INTO position VALUES (14, 'Java');
Таблица position
состоит из двух полей:
- id_position
— первичный ключ таблицы, снабженный атрибутом
AUTO_INCREMENT
;
- name
— название раздела.
Перед началом работы класса pager_mysql
необходимо установить соединение с базой данных под управлением СУБД MySQL. Так как задача установки соединения с базой данных возникает достаточно часто, код, осуществляющий соединение, выделяют в отдельный файл (листинг 4.38).
Листинг 4.38. Установка соединения с базой данных под управлением СУБД MySQL (файл config.php)
В настоящий момент сервер базы данных не доступен, поэтому корректное отображение страницы невозможно." ); } // Выбираем базу данных if (! @mysql_select_db($dbname, $dbcnx) ) { exit( " В настоящий момент база данных не доступна, поэтому корректное отображение страницы невозможно.
" ); } // Устанавливаем кодировку соединения; следует выбрать кодировку, // в которой данные будут отправляться серверу MySQL @mysql_query("SET NAMES 'cp1251'"); ?>
Теперь, когда весь вспомогательный код готов, выведем содержимое таблицы
position
, отсортировав ее по полю name (листинг 4.39).
Листинг 4.39. Постраничный вывод содержимого таблицы position
get_page(); for($i = 0; $i < count($arr); $i++) { echo "". "{$arr[$i][name]}
"; } // Выводим ссылки на другие страницы echo $obj; ?>
Пожалуй, основным неудобством при использовании класса pager_mysql
является раздельное формирование SQL-запроса. Большинство программистов будут вынуждены затратить значительное время для того, чтобы выяснить, что запись
$obj = new pager_mysql("position", "", "ORDER BY name");
эквивалентна SQL-запросу
SELECT * FROM position ORDER BY name
Такова плата за возможность повторного использования кода, которая снижает гибкость всей системы.
Результат работы скрипта из листинга 4.39 про-
демонстрирован на рис. 4.10.
Рис. 4.10. Постраничный вывод информации
из таблицы position базы данных под управлением MySQL
Пусть требуется изменить формат вывода ссылок постраничной навигации
или предоставить альтернативную форму постраничной навигации, например, вместо номеров позиции выводить номера страниц:
<< ... < ... 3 4 5 6 7 8 9 ... > ... >>
Символ <<
является ссылкой на первую страницу, символ >>
— на послед-
нюю, а ссылки <
и >
перемещают пользователя с текущей страницы на одну
позицию влево или вправо соответственно.
Для осуществления такого способа постраничной навигации создадим в базовом классе pager
новый метод print_page()
(листинг 4.40).
В листинге 4.40 с целью экономии места базовый класс pager
приводится не
полностью.
Листинг 4.40. Альтернативный способ реализации постраничной навигации
get_total()/$this->get_pnumber()); if((float)($this->get_total()/$this->get_pnumber()) — $number != 0) { $number++; } // Ссылка на первую страницу $return_page .= "". "<< ... "; // Выводим ссылку "Назад", если это не первая страница if($page != 1) $return_page .= " ". "< ... "; // Выводим предыдущие элементы if($page > $this->get_page_link() + 1) { for($i = $page — $this->get_page_link(); $i < $page; $i++) { $return_page .= "$i "; } } else { for($i = 1; $i < $page; $i++) { $return_page .= "$i "; } } // Выводим текущий элемент $return_page .= "$i "; // Выводим следующие элементы if($page + $this->get_page_link() < $number) { for($i = $page + 1; $i <= $page + $this->get_page_link(); $i++) { $return_page .= "$i "; } } else { for($i = $page + 1; $i <= $number; $i++) { $return_page .= "$i "; } } // Выводим ссылку "вперед", если это не последняя страница if($page != $number) $return_page .= " ... ". ">"; // Ссылка на последнюю страницу $return_page .= " ... ". ">>"; return $return_page; } } ?>
Важно отметить, что новый метод автоматически начинает работать во всех
производных классах pager_file
, pager_file_search
, pager_dir
, pager_mysql
, и
для объектов каждого класса автоматически выбираются правильные методы
get_total()
, get_pnumber()
, get_page_link()
и get_parameters()
.
Метод print_page()
можно вызывать вместо содержимого метода
__toString()
или строку
echo $obj
в листингах 4.30, 4.32, 4.33, 4.35 и 4.39 заменить на строку
echo $obj->print_page()
На рис. 4.11 представлен внешний вид постраничной навигации при вызове
метода print_page()
для объекта класса pager_dir
.
Рис. 4.11. Альтернативный вариант постраничной навигации
На примере постраничной навигации было продемонст-
рировано построение типичной иерархии классов с применением полиморфизма. Однако объект класса pager
не имеет смысла и не может быть использован по назначению, и для того, чтобы предотвратить объявление объекта
класса pager
, пришлось снабдить его конструктор спецификатором protected
,
запрещающим объявление объекта из внешнего кода.
Однако объектно-ориентированная модель PHP предоставляет специальные инструменты для
классов, появление объектов которых нежелательно: класс можно объявить
абстрактным.
Для этого объявление класса предваряют ключевым словом
abstract
(листинг 4.41).
Листинг 4.41. Объявление абстрактного класса
Теперь попытка объявить экземпляр класса pager
(листинг 4.42) будет приводить к возникновению ошибки: Fatal error: Cannot instantiate abstract class
pager (Невозможно объявить объект абстрактного класса).
Листинг 4.42. Попытка объявления объекта абстрактного класса
Рис. 4.12. Схема транспортной сети
Абстрактными следует объявлять классы, которые не существуют в реальности и объекты которых не понадобятся.
Например, в схеме транспортной сети очевидно, что объекты "транспорт", "автомобиль", "воздушный транспорт", "водный транспорт" и "железнодорожный транспорт" не понадобятся, и их можно сделать абстрактными.
В то же время конкретные классы ("лодка", "баржа", "теплоход", "катер") могут понадобиться для оценки грузо- и пассажиропотока, расчета налогообложения и т. п. (рис. 4.12).
pager
новый производный класс.
Однако продемонстрированный подход обладает некоторым изъяном: программист может забыть реализовать один из методов, который используется
базовым классом. Напомним, что в базовом классе pager
находятся только
заглушки методов get_total()
, get_pnumber()
, get_page_link()
и get_parameters()
.
Заглушками в программировании называют методы, которые не выполняют никакой работы.
Для решения этой задачи в объектн-ориентированной модели PHP предусмотрены абстрактные методы, объявление которых предваряется ключевым
словом abstract
.
Для абстрактных методов задают их описание и список параметров без реализации.
Каждый класс, который наследуется от базового
класса, содержащего абстрактные методы, обязан их реализовать.
Если хотя бы один метод не реализован — работа PHP-скрипта будет остановлена, а интерпретатор сообщит о необходимости реализации абстрактного метода в
производном классе.
При помощи абстрактных методов исключается воз- можность неумышленного нарушения полиморфизма для производных классов.
В листинге 4.43 методы get_total()
, get_pnumber()
, get_page_link()
и get_parameters()
класса pager
объявляются абстрактными.
Листинг 4.43. Объявление абстрактных методов
Наличие в классе абстрактного метода требует, чтобы класс также был объявлен абстрактным.
Невозможно объявить класс не абстрактным, если он содержит абстрактные методы.
Важно понимать, что требования реализации абстрактных методов относится лишь к обычным классам; абстрактный класс может их не реализовывать.
В листинге 4.44 приводится пример класса pager_abstract
, который является
абстрактным производным классом, унаследованным от класса pager
.
Листинг 4.44. Абстрактный класс может не реализовывать абстрактные методы
dirname = trim($dirname, "/"); $this->pnumber = $pnumber; $this->page_link = $page_link; $this->parameters = $parameters; } public function get_pnumber() { // Количество позиций на странице return $this->pnumber; } public function get_page_link() { // Количество ссылок слева и справа // от текущей страницы return $this->page_link; } public function get_parameters() { // Дополнительные параметры, которые // необходимо передать по ссылке return $this->parameters; } } ?>
Если класс, унаследованный от класса pager_abstract
, не является абстрактным, он должен реализовать все абстрактные методы всех абстрактных классов, которые предшествуют ему в иерархии классов.
Например, абстрактный
класс pager_abstract
реализует три абстрактных метода класса pager — get_pnumber()
, get_page_link()
и get_parameters()
, поэтому производным
классам достаточно реализовать лишь метод get_total()
(листинг 4.45).
Листинг 4.45. Использование класса pager_abstract
dirname = trim($dirname, "/"); $this->pnumber = $pnumber; $this->page_link = $page_link; $this->parameters = $parameters; } public function get_total() { $countline = 0; // Открываем директорию if(($dir = opendir($this->dirname)) !== false) { while(($file = readdir($dir)) !== false) { // Если текущая позиция является файлом, // учитываем ее if(is_file($this->dirname."/".$file)) $countline++; } // Закрываем директорию closedir($dir); } return $countline; } // Возвращает массив строк файла // по номеру страницы $index public function get_page() { // Текущая страница $page = $_GET['page']; if(empty($page)) $page = 1; // Количество записей в файле $total = $this->get_total(); // Вычисляем количество страниц в системе $number = (int)($total/$this->get_pnumber()); if((float)($total/$this->get_pnumber()) — $number != 0) $number++; // Проверяем, попадает ли запрашиваемый номер // страницы в интервал от 1 до get_total() if($page <= 0 || $page > $number) return 0; // Извлекаем позиции текущей страницы $arr = array(); // Номер, начиная с которого следует // выбирать строки файла $first = ($page — 1)*$this->get_pnumber(); // Открываем директорию if(($dir = opendir($this->dirname)) === false) return 0; $i = -1; while(($file = readdir($dir)) !== false) { // Если текущая позиция является файлом if(is_file($this->dirname."/".$file)) { // Увеличиваем счетчик $i++; // Пока не достигнут номер $first, // досрочно заканчиваем итерацию if($i < $first) continue; // Если достигнут конец выборки, // досрочно покидаем цикл if($i > $first + $this->get_pnumber() — 1) break; // Помещаем пути к файлам в массив, // который будет возвращен методом $arr[] = $this->dirname."/".$file; } } // Закрываем директорию closedir($dir); return $arr; } } ?>