PHP — язык очень интересный и очень эффективный, если речь идет о web-программировании (далее, для простоты общения, я не буду употреблять приставку web). Но, к моему глубокому изумлению, обнаружилось, что очень многие программисты на том же perl продолжают игнорировать PHP только потому, что не могут найти помощи в освоении нового языка. Действительно, perl очень распространенный и очень удобный язык. Но во многих случаях гораздо удобнее, быстрее и функциональнее писать на PHP. Мои выпуски, посвященные PHP, направлены в сторону тех, кто уже знаком с каким-либо языком программирования. Это не обязательно, но крайне желательно. Ибо я не собираюсь учить вас писать
<? echo "Hello world!"; ?>
но и постараюсь не использовать слов и терминов, пугающих нормального человека, не ведущего тяжелую жизнь системного программиста. Если дать краткую характеристику читателю, на которого нацелена серия выпусков "Пишем на PHP", то это будет человек, увлекающийся созданием сайтов, не боящийся экспериментировать и знакомый с основами верстки html.
Все, закончим со вступлениями и займемся делом. Терпеть не могу лишних слов в таком деле, как программирование. Всегда хочется замочить автора, который размазывает сопли по тарелке, вместо того, чтобы говорить суть. Что забыл - скажу в процессе.
Несколько ответов на вопросы.
Вопрос: Как читать курс?
Ответ: Подряд. Это обычный текст. Я стараюсь делать его последовательным. Важное я буду отмечать красным, лирические отступления серым, а полезные замечания — зеленым.
Вопрос: А если что-то непонятно?
Ответ: Спроси. Где? В форуме. Адрес форума: http://forum.21.ru
Вопрос: Какое программное обеспечение понадобится для работы?
Ответ: На сервере: Apache, PHP, MySQL. Можно еще добавить старенькую графическую библиотеку GD, вдруг меня занесет в сторону издевательства над GIF-ами.
На рабочей машине вам понадобится любой текстовый редактор. Лично я предпочитаю работать в FAR, подключив к нему colorer. Ничего удобнее я все равно так и не нашел.
Вопрос: А как собрать Apache с PHP и MySQL?
Ответ: Спросите об этом своего админа, спросите на форуме или у google.
Чем PHP отличается от других языков?
По большому счету, ничем. Лично я считаю основным отличием PHP от других языков для web — удобство использования в html-текстах. Нет почти никаких ограничений в использовании PHP-вставок.
Не надо использовать PHP и SSI в одном файле. Apache, а я надеюсь, что вы пользуетесь именно им в качестве web-сервера, не будет интерпретировать вам и то и другое в одном флаконе. Да это и не надо. Обращаю на сей факт внимание только потому, что многие поначалу пытаются подружить SSI и PHP. Не нужно этого. Не тратьте время.
В любом месте html-кода вы можете смело вставить заветную комбинацию:
<? ... ?>
где вместо многоточия может быть любой сложности скрипт.
Дам несколько наглядных примеров "прямого программирования", а затем перейдем сразу к написанию какого-нибудь цельного сайта со всеми примочками. Так вам будет проще все понять.
Цвет строки
Создаем файл example-1.php и пишем в него простейший код:
<font color=#<? echo $color; ?>>Подопытная строка</font>
<form action="" method=GET>
<input type="text" name=color>
<input type="submit" value="сменить цвет">
</form>
Все. Программа готова. Если ваш Apache собран с PHP, то можете попробовать ввести в окошко разные цвета и нажать на кнопочку. Попробуйте у меня:
Подопытная строка
Напомню, что цвет задается шестнадцатеричным числом (RGB) от 0000000(черный) до FFFFFF(белый).Видите, как все просто. PHP сам распознал переменную $color, в которой вы передали код цвета. Вам не пришлось делать парсинг строки (разбирать строку на составляющие), чтобы выяснить, какие переменные были переданы в html-запросе и какие значения они содержат.
Я специально дал форме method=GET, чтобы вы в строке URL увидели свою переменну
В нашем коде, из PHP используется всего одна функция echo. Она выводит все, что вы ей дадите: строки, цифры, значения переменных. В других языках она чаше называется print.
В PHP print тоже есть, но мне более по душе echo. Теперь вы смело можете экспериментировать. Например, добавим еще один вывод той же переменной.
<font color=#<? echo $color; ?>>Наш цвет: <? echo $color; ?></font>
<form action="" method=GET>
<input type="text" name=color>
<input type="submit" value="сменить цвет">
</form>
Теперь наша программа выводит нам наш код цвета, окрашенный в этот самый цвет. Забавно. Попробуем:
Наш цвет:
Если у на вашем сервере вместо результата использования кода вы видите сам код, значит вам надо трясти вашего админа или менять хостинг: ваш веб-сервер не поддерживает PHP.
Мне думается, что описывать общие принципы — это не совсем то, чего вы от меня ждете. Я предлагаю несколько иное. Давайте просто возьмем, да и напишем вместе цельный web-проект! По всем правилам. Да так, чтобы был он полезен многим, да чтобы на него ложился любой дизайн, да чтобы все у нас было как у взрослых: замысел, план, блок-схема и все остальное. С использованием PHP, MySQL и особенностей Apache. Договорились? Ну вот и ладненько.
Я сегодня посоветовался с Александром Малюковым и решил принять его идею. Пишем систему персонального паблишинга. Или "колонку обозревателя", как назвал ее Саша.
Задача. Написать комплект скриптов на языке PHP(4), предоставляющие следующие возможности пользователю:
В общем, у нас должно получиться нечто, напоминающее данный сайт (http://kurepin.ru). Только созданный не в виде десятка примитивных функций, а в виде солидного пакета скриптов со всеми современными примочками: back office, mail-информер, полный контроль над ошибками, и прочей фигней, без которой крупный проект просто не сможет эффективно существовать.
Начнем мы с планирования. Но-но, попрошу без соплей в этом месте! Планирование — важнейшая часть создания большого проекта. Мы же пишем большой проект, а не фигню какую-то.
Для начала спланируем место в нашей директории, чтобы правильно сохранять то, что у нас будет рождаться. Предположим, что вы выбрали правильного провайдера, который хостит ваши страницы на одной из UNIX-систем. Например, на FreeBSD. И полный путь к вашему каталогу выглядит так:
/home/roma/ Мне почему-то показалось, что вас непременно зовут Романом.
А директория (или "папка", — как сейчас принято говорить), в которой должны лежать html-файлы сайта, называется:
/home/roma/WWW/
А что, так оно чаше всего и бывает. Еще эту папку называют html или public_html.
Теперь, нам нужна папка, где мы будем хранить все наши скрипты. Это должна быть папка НЕ в директории WWW, иначе все желающие смогу посмотреть ваши скрипты и найти в них лазейки для кряка. Нам это надо? Нет, нам это не надо. Поэтому, для необходимых нам файлов-скриптов мы создаем:
/home/roma/req/
req — это от слова require (нуждаться).
Еще нам понадобится папка, в которую мы сложим куски html для динамической сборки страниц. Назовем ее inc (от include).
/home/roma/inc/
Еще папка для хранения данных:
/home/roma/data/
И папка для хранения периодически выполняемых программ службой cron:
/home/roma/cron/
Давайте до кучи создадим в папке WWW папку pic (от picture) для хранения всякой графики.
Все. Наш каталог /home/roma/ выглядит вот так:
./req
./data
./inc
./WWW
./WWW/pic
Следующим этапом я предлагаю создать базу данных для хранения данных, простите за тавтологию.
Если вы не знакомы с языком SQL, то это вас не должно пугать. Во-первых, я постараюсь прокомментировать то, что будет написано на этом языке. А во-вторых, если вы потратите час на прочтение любого общего руководства по языку запросов к БД SQL, то сможете без каких-либо проблем читать запросы на этом языке и писать свои. Там и надо-то знать с десяток ключевых слов и несколько стандартных конструкций запросов. Все остальное — ваша фантазия (я преклоняюсь перед изобретателями этого языка. Ничего более совершенного, созданного человеком в области программирования, я в жизни не встречал). Найти документацию по стандартным SQL-возможностям можно на том же www.citforum.ru.
Базу назовем так же, как и account у провайдера — ROMA. Во-первых, чтобы не путаться, а во-вторых, обычно так провайдер и выдает доступ к SQL-серверу. Если база еще не создана, то мы ее создаем:
create database roma;
и переходим в нее:
use roma;
Нам потребуются три таблицы: таблица разделов, таблица текстов и таблица статистики. Создадим пока две. Третья подождет.
Информацию по работе с MySQL-сервером вы найдетет на сайте производителя: www.mysql.com. Или поищите FAQ-и при помощи поисковых серверов. Их полным-полно.
Таблица tbl_cats (категории, разделы):
create table tbl_cats(
c_id int not null auto_increment primary key,
c_name varchar(50) not null default ""
);
Поясняю. Тут написано, что таблица tbl_cats имеет два поля: c_id — уникальный целочисленный идентификатор записи и c_name — буквенное название раздела, собственно. Можем сразу добавить в нашу таблицу первый раздел:
insert into tbl_cats(c_name) values('web-обзоры');
Теперь вторая таблица:
create table tbl_texts(
t_id int not null auto_increment primary key,
t_name varchar(200) not null default "",
t_dt datetime not null
);
в которой написано почти тоже самое: идентификатор, название текста и дата+время публикации работы (t_dt).
У нас уже есть спланированные директории на диске и база данных для наших будущих обзоров. Теперь пора перейти к определению функций нашей будущей системы и разделению их на тематические группы.
Сразу хочу вас обрадовать. Писать мы будем все в ООП (Объектно-Ориентированном Программировании). Бояться тут нечего. В PHP ООП развито не на самом высоком уровне, никакими страшными ООП-конструкциями я вас пугать не собираюсь. Но и без ООП нам не обойтись, если мы пишем большой серьезный проект.
Для тех, кто писал когда-нибудь на каком-нибудь языке, но не писал в ООП, я расскажу о ключевых отличиях ООП от обычного "прямого" программирования. В ООП все функции и переменные объединяются в классы (class). Классы между собой объединяются "наследованием" (extends). Наследование происходит в одну сторону: от отца к сыну. Причем, сын наследует все возможности отца. Предположим, что класс "first" имеет в своем теле две переменные и две функции, работающие с этими переменными:
class first { var $a1; var $b2; function f_increment($x) { settype($x,"integer"); $this->$a1=$this->a1+$x; return(0); } function f_ab() { $this->$a1=$this->a1+$this->$b2; return(0); } }
Это мы объявили переменные $a1 и b2, и две функции. Первая получает в качестве параметра некое значение x, которое приводит к целому числу, и прибавляет его (число) к глобальной переменной $a1, возвращает "0" в качестве кода ошибки (0 — нет ошибок). Вторая функция просто плюсует наши две объявленные переменные и результат записывает в переменную $a. Тоже возвращает код ошибки. Привыкайте всегда возвращать код ошибки выполнения любой функции!
Напомню, что я стараюсь писать понятно, а не эффективно. Эффективность пусть каждый для себя ищет сам. Это замечание для снобов, которые могут сейчас сказать, что мой демонстрационный код можно было написать намного короче. Да это так. Но кто меня поймет, если я напишу все тело первой функции вот так:
$this->$a+=(int)$x;
Можно так написать. Но это не очень понятно для тех, кто слабо знаком с синтаксисом PHP или C. Давайте уважать психику других.
Обратите внимание на конструкцию $this->. Таким образом внутри класса мы обращаемся к любому объявленному объекту класса. Будь то переменная или функция.
ООП тем и приятно, что нам в конструкциях вызова процедур или переменных не надо ничего менять. Функция отличается от переменной во время вызова только тем, что содержит у себя "на хвосте" дополнительные параметры или пару пустых скобок, если параметров нет.
$this->labuda — это переменная.
$this->labuda() — это функция.
echo "Результаты работы: ".$this->labuda()." и ".$this->labuda; — вывод обоих значений: значение переменной будет выведено как есть, а функция отработает и выдаст результат своей работы не хуже переменной. Точка "." в PHP используется для слияния двух строк:
"123"."abc" = "123abc"
Обращаю внимание: переменная и функция вполне могут иметь одинаковые имена, интерпретатор PHP сам разберет "кто есть who", если вы не будете забывать про скобки на хвосте у функций. В больших проектах я этим часто пользуюсь, чтобы не путаться.
Возвращаясь к наследованию. Мы создаем класс "second", от класса "first":
class second extends first
{
...
}
Даже если на месте двоеточия (в теле класса) мы не определим ни одной переменной или функции, мы можем пользоваться теми, которые были описаны в классе first.
Таким образом, в ООП создают классы и наследуют их, двигаясь от общего к частному. Получается четкая древовидная структура классов, которую мы и создадим для нашего проекта. В PHP не реализовано множественное наследование. Т.е. каждый новый класс может рождаться только от одного родителя. Нам этого будет вполне достаточно. Это пока основное, что вам надо знать про отличия ООП от обычного.
Вернемся к планированию. Скажу вам по секрету, что это почти все мои веб-проекты делятся на следующие тематические группы скриптов (я эти группы буду сразу называть классами, пользуясь терминологией ООП. А каждый класс мы будем хранить в отдельном файле с окончанием ".class"):
1. Класс переменных описывающих свойства проекта (class_var). Этот класс я обычно делаю изначальным, в котором описываю глобальные переменные, влияющие на работу всего проекта. Такие, как: пути на диске, время кеширования страниц, стандартные названия, форматы отображения времени и даты, и так далее.
2. Класс class_mysql наследуется от class_var. По названию класса не сложно догадаться, что он посвящен работе с базой данных MySQL. Это будет небольшой класс.
3. Класс class_util содержит все глобальные утилиты, которые могут нам понадобиться на любом из этапов выполнения нашей задачи. Кроме того, в этом классе мы опишем важнейшую часть проекта: обработку ошибок.
Эти три класса можно было бы объединить в один, т.к. все остальные классы будут наследоваться от class_util, но я всегда держу их именно в разных файлах. Так проще вносить изменения в нужное место.
Давайте думать далее. У нас есть три основные группы скриптов. Группа, отвечающая за добавление данных в базу сайта. Группа, отвечающая за вывод данных из базы. И группа, отвечающая за выполнение периодических (cron) задач. Вот мы и определим три основных класса:
class_in
class_out
class_cron
Надеюсь, не надо пояснять, какой класс за что будет отвечать. В качестве домашнего задания рекомендую графически отобразить генеалогическое древо наших классов. Это даст вам возможность понять красоту планирования.
С чего начать писать? Начинать надо с самого начала. Что мы там, в планировании писали? Если мне не изменяет память, то первым у нас шел класс, описывающий основные переменные и параметры нашей системы. Вот с этого класса и начнем. Тем более, что в нем не будет сложных языковых конструкций, попривыкните к синтаксису PHP.
Класс: class_vars.
Файл: vars.class
Место расположения: /home/roma/req/
Итак, создаем новый файл с указанным названием, открываем начало PHP-кода (<?) и начинаем писать.
Шапка:
<? class class_vars {
Давайте подумаем, какие переменные могут нам быть интересны. Прежде всего, надо описать все абсолютные пути к данным. Тогда, в случае переезда нашего проекта в другую папку или вообще на другой сервер, нам надо будет всего лишь заменить значения этих переменных.
В PHP комментарий начинается с двойного слеша "/". А в именах переменных учитывается регистр (высота букв), будьте осторожны. Сразу выработайте для себя схему использования... строчные, прописные:никогда не знал, что как называется... БОЛЬШИХ и обычных букв. Лично я предпочитаю глобальные писать БОЛЬШИМИ буквами, а все остальные переменные - обычными.
Пишем:
// Пути к папкам. var $PATH="/home/roma"; // основной путь к проекту var $PATH_INC="/home/roma/inc"; var $PATH_REQ="/home/roma/req"; var $PATH_DATA="/home/roma/data"; var $PATH_WWW="/home/roma/WWW"; var $PATH_WWW_PIC="/home/roma/WWW/pic";
Понятно, что я сделал? Я определил путь для каждой папки. И этими переменными мы будем пользоваться в скриптах, вместо того, чтобы писать каждый раз реальный путь. Если вам вдруг понадобится перенести какие-то данные в другое место (а в больших проектах подобное случается), то вам надо будет просто поменять вот эти самые пути. А не изучать все скрипты, в поисках обращений к нужным директориям. А если ваш друг Дима захочет запустить у себя копию вашего сайта, то ему будет достаточно скопировать ваши данные и отредактировать класс vars. Это может сделать даже человек, не пишущий на PHP.
Теперь я предлагаю описать виртуальные пути к вашему сайту. Т.е. URL-ы. А заодно опишем сразу полное название нашего проекта, короткое его название и некоторые другие понятные параметры.
Открываем наш vars.class и пишем дальше:
// Основной URL var $PATH_HTTP="http://roma.21.ru/"; // Полное название и короткое var $NAME_FULL="Персональная страница Ромы-обозревателя"; var $NAME_SHORT=''Рома-обозреватель"; // адрес хозяина страницы var $EMAIL_ADMIN=array("roma@21.ru"); // техническая служба сайта var $EMAIL_NOC=array("roma@21.ru","noc@21.ru"); // Время кеширования страниц "Expires" (в секундах) var $CACHE_TIME=300; // Максимальный размер подгружаемого в базу текста var $TEXT_SIZE_MAX= 1048576; // это мегабайт // Минимальный размер подгружаемого в базу текста var $TEXT_SIZE_MIN=100; // сто байт // Формат вывода времени (из SQL-базы) var $TIME_FORMAT="%H:%i:%S"; // ЧЧ:ММ:СС // Формат вывода даты (из SQL-базы) var $DATE_FORMAT="%d.%m.%Y"; // ДД.ММ.ГГГГ
Ну вот, хватит нам пока переменных. В процессе дальнейшей работы мы будем обращаться к этому классу, чтобы пополнить его новыми и новыми данными.
Сохраняем файл, не забыв предварительно закрыть класс и закрыть главный PHP-тэг (?>):
} ?>
Я прошу вас, пользуйтесь нормальными текстовыми редакторами для работы с PHP. Редакторами, в которых понятно, где заканчивается строка и где заканчивается весь текст. Если вы не хотите ошибок "непонятного происхождения", -- проверьте: после закрывающего PHP-тэга не должно быть никаких символов, включая символ перевода строки. Закрывающая треугольная скобка (>) должна быть последним символом файла.
Вот, мы и написали с вами первый, базовый класс нашего проекта. Правда, это не сложно? На мой взгляд -- проще не бывает. Теперь от этого класса мы будем растить древо нашего проекта. Следующим классом у нас будет класс, обслуживающий доступ к базе MySQL.
Теперь перечитайте вашу программу, которая сложилась у вас в файле vars.class и найдите там слова и фрагменты, которые вам не понятны. Я отвечу на все ваши вопросы в форуме.
На данный момент файл vars.class выглядит вот так:
<? class class_vars { // Пути к папкам. var $PATH="/home/roma"; // основной путь к проекту var $PATH_INC="/home/roma/inc"; var $PATH_REQ="/home/roma/req"; var $PATH_DATA="/home/roma/data"; var $PATH_WWW="/home/roma/WWW"; var $PATH_WWW_PIC="/home/roma/WWW/pic"; // Основной URL var $PATH_HTTP="http://roma.21.ru/"; // Полное название и короткое var $NAME_FULL="Персональная страница Ромы-обозревателя"; var $NAME_SHORT=''Рома-обозреватель"; // адрес хозяина страницы var $EMAIL_ADMIN=array("roma@21.ru"); // техническая служба сайта var $EMAIL_NOC=array("roma@21.ru","noc@21.ru"); // Время кеширования страниц "Expires" (в секундах) var $CACHE_TIME=300; // Максимальный размер подгружаемого в базу текста var $TEXT_SIZE_MAX= 1048576; // это мегабайт // Минимальный размер подгружаемого в базу текста var $TEXT_SIZE_MIN=100; // сто байт // Формат вывода времени (из SQL-базы) var $TIME_FORMAT="%H:%i:%S"; // ЧЧ:ММ:СС // Формат вывода даты (из SQL-базы) var $DATE_FORMAT="%d.%m.%Y"; // ДД.ММ.ГГГГ } ?>
Будем изучать самый бестолковый класс нашего проекта - класс, обслуживающий контакты с базой данных.
На самом деле, все три первых наших класса (vars, mysql, utils) можно один раз внимательно и с расстановкой написать, а потом просто пользоваться ими во всех остальных своих проектах. Но если в переменных окружения могут быть какие-то перемены, как и в обслуживающих утилитах, то в классе mysql нам вряд ли что-то когда-то придется менять, разве что реквизиты доступа к базе (логин, пароль...). Вы можете вообще не напрягаться и особенно не разбираться в том, что я сейчас напишу. То есть, понять все это, безусловно, вам придется, но сразу после прочтения вы имеете полное право забыть все это как страшный сон - не так важно.
Итак. Давайте разбираться, какие основные функции нам нужны при работе с базой MySQL?
Во-первых, нам нужна процедура подключения к базе. Ее еще можно назвать процедурой авторизации или активации соединения. Или, как говорят программисты, "открытие соединения с базой".
Во-вторых, нам нужна возможность оформить запрос в базу и механизм исполнения этого запроса.
В-последних, за собой надо прибирать. Закрытие соединения.
Все, по большому счету.
Вот на этом месте я вас должен предупредить. Дело в том, что тот метод общения с базой, который напишем мы, не универсален. Благодаря нашим функциям, можно будет одновременно работать только с одним запросом к базе. Есть случаи, когда это не очень удобно. Но мне это никогда не мешало. В конце концов, всегда можно за несколько минут добавить недостающую функцию.
Итак, давайте перейдем к делу. Я бы не стал повторять, как оформлять новый класс, но сегодняшний класс имеет одно малозаметное, но очень важное отличие от предыдущего (vars): класс class_mysql рождается от класса class_vars. То есть class_vars, являясь родительским к классу class_mysql, передает ему все свои возможности. В нашем случае, это значения тех переменных, которые мы описали на прошлом занятии.
Открываем (создаем) новый файл class_mysql и пишем в нем:
<? require("/home/roma/req/vars.class"); class class_mysql extends class_vars
Видите, прежде всего, мы должны указать, что для нам для работы необходим файл "/home/roma/req/vars.class", т.к. в нем содержится фрагмент нашей программы, описывающий родительский class_vars.
Затем, мы объявляем новый класс. Объявляем его точно так же, как и class_vars, только дописываем в конце строки ссылку на родительский класс: extends class_vars.
Помните, я рассказывал вам, как наследуются классы в PHP? Вот это тот самый случай. Теперь перейдем к телу класса, и сначала добавим нужные нам переменные.
var $sql_login="roma"; var $sql_passwd="parol"; var $sql_database="roma"; var $sql_host="127.0.0.1"; var $conn_id; var $sql_query; var $sql_err; var $sql_res;
В первых четырех переменных мы определили реквизиты доступа к базе MySQL: регистрационное имя (логин), пароль, название нашей базы и IP-адрес, по которому физически располагается MySQL.
Эти четыре переменные мы вполне можем перенести в класс vars, но я этого предпочитаю не делать. Почему? Ну, бывают такие случаи, когда мы доверяем управление нашим сайтом кому-то еще. При этом, мы можем дать этому человеку отдельный доступ именно к файлу с переменными, чтобы он мог влиять на манеру поведения и внешний вид нашего сайта. Но не всегда при этом мы даем ему доступ к самим скриптам и не сообщаем наших реквизитов для доступа к БД.
Вторые четыре переменные мы будем активно эксплуатировать в нашей работе.
Первая из них ($conn_id)- идентификатор соединения. Он несет в себе информацию о полученном соединении к базе.
Вторая (sql_query) - строка запроса. В нее мы будем помешать наш запрос к БД, прежде чем его активировать.
Третья (sql_err) - код ошибки. Сервер MySQL, как и любая друга программа, может давать различные сбои как по своей вине, так и по нашей: когда наш запрос написан с ошибками. Вот эта переменная и будет работать у нас "флагом", который будет "подниматься", если произошла какая-то ошибка.
А четвертая переменная (sql_res) будет содержать в себе полученную информацию из базы данных после выполнения запроса. Из этой переменной мы будем извлекать эту информацию удобным нам способом.
Других переменных нам тут пока не понадобится, переходим к функциям.
Функция подключения к базе данных.
function sql_connect() { $this->conn_id=mysql_connect($this->sql_host,$this->sql_login,$this->sql_passwd); mysql_select_db($this->sql_database); }
Поясню построчно:
Понятно, что мы сделали? Мы подключились к серверу MySQL, используя свои реквизиты, после чего, выбрали интересующую нас базу данных. В $conn_id останется идентификатор нашего соединения, он нам пригодится в следующих двух функциях.
Функция, выполняющая наш запрос к БД:
function sql_execute() { $this->sql_res=mysql_query($this->sql_query,$this->conn_id); $this->sql_err=mysql_error(); }
Первая строка (в теле функции) выполняет отправку запроса (функция PHP mysql_query), а результат помещает в объявленную нами переменную $sql_res.
Вторая строка запрашивает код ошибки выполнения SQL-запроса (функция PHP mysql_error) и записывает ее в другую переменную, которую мы с вами тоже определили заранее — $sql_err.
Вот такая простая функция. Как видите, PHP очень много делает за нас. На языке C это выглядело бы настолько страшно, что даже показывать это не стану, чтобы не отбить у вас вкус к изучению программирования.
Нам осталось реализовать последнюю, на текущий момент, функцию класса class_mysql. Нам осталось зарыть наше соединение с сервером MySQL.
Закрываем.
function sql_close() { mysql_close($this->conn_id); }
Функция языка PHP mysql_close получает в качестве параметра идентификатор открытого соединения и закрывает это самое соединение.
Закрывайте оставшиеся тэги и сохраняете файл. Он у вас должен выглядеть вот так:
<? require("/home/roma/req/vars.class"); class class_mysql extends class_vars { var $sql_login="roma"; var $sql_passwd="parol"; var $sql_database="roma"; var $sql_host="127.0.0.1"; var $conn_id; var $sql_query; var $sql_err; var $sql_res; function sql_connect() { $this->conn_id=mysql_connect($this->sql_host,$this->sql_login,$this->sql_passwd); $this->conn_log_id=mysql_connect($this->sql_host,$this->sql_login,$this->sql_passwd); mysql_select_db($this->sql_database); } function sql_close() { mysql_close($this->conn_id); } function sql_execute() { $this->sql_res=mysql_query($this->sql_query,$this->conn_id); $this->sql_err=mysql_error(); } } ?>
За исключением, разумеется, реквизитов подключения к _вашей_ базе данных MySQL.
Прежде чем переходить к следующему классу (utils), предлагаю вам совершить небольшой экскурс в язык запросов SQL.
Это необходимо по двум причинам. Во-первых, не хочу, чтобы вы смотрели на SQL-фрагменты последующих скриптов как парнокопытные животные на только что построенное заградительное сооружение. А во-вторых, мне хочется донести до вас красоту этого языка, скрытую в его простоте и "человечности".
Что такое база данных? Это хранилище информации. Структурированное хранилище. В базе данных можно хранить любой тип информации: от цифр до видеороликов.
А все запросы к базе данных можно разделить на три основные группы:
Как создаются таблицы мы уже видели. На этом этапе я останавливаться не буду, ибо тут ничего интересного нет. Как и стандартного. В разных БД создание таблиц, функций и индексации может производиться по-разному. Это не так важно. Мы остановимся на двух других группах. Добавление, изменение, удаление информации производится почти человеческим языком. Поэтому все команды SQL так легко запомнить.
Предположим, у нас есть таблица books (книги), в которой мы храним информацию о своей домашней библиотеке. Для этого мы создали таблицу books в следующем виде:
create table books( book_id bigint not null auto_increment primary key, book_name varchar(100) not null default "", book_date datetime not null, book_comm varchar(200) not null default "" );
где book_id -- порядковый номер книги (создает автоматически, при добавлении новой записи в таблицу, путем прибавления единицы к внутреннему счетчику), book_name -- автор и название книги, book_date -- дата (и время) приобретения книги. А book_comm -- свободный комментарий до двухсот символов. Например, мы можем в это поле записывать имя человека, взявшего у нас книгу, или интересную подробность, связанную с ее приобретением. Давайте перейдем непосредственно к добавлению информации.
По-хорошему, надо было создать как минимум две таблицы: для книг и для авторов. И связать их внешним ключом для целостности. Но сейчас наша задача состоит в ином, поэтому простите мне мой топорный подход.
Нам надо добавить в нашу электронную библиотеку книгу "Алексей Толстой. 'Золотой ключик или приключения Буратино'". Как бы мы это сделали, если бы база данных управлялась человеческим голосом? Наверное, что-то вроде: "добавить в таблицу КНИГИ название и дату покупки: А. Толстой. 'Золотой ключик...', 27-е августа 2001 года". Вот мы так и запишем, а SQL с удовольствием нас поймет. Только по-английски.
insert into books(book_name, book_date) values("А.Толстой. 'Золотой ключик или приключения Буратино'","2001-08-27 00:00:00");
Вот так. Почти по-человечески. Следующей книгой для контраста добавим "А.Платов. 'Котлован'", неизвестно когда подаренную нам любимым преподавателем.
insert into books (book_name, book_comm) values("А.Платов. 'Котлован'", "Подарена Пустоваловым Петром Семеновичем");
Ну и еще одну...
insert into books(book_name) values('Поваренная книга');
Вот так. Теперь у нас в базе есть аж три книги. Попробуем в этом убедиться? Как мы попросим нашу базу выбрать нам все, что в ней есть, без каких-либо условий? По-русски это бы могло звучать так: "Выбери все из таблицы книг". Так и напишем (звездочкой в SQL обозначается любой набор параметров, символов и прочего, как почти во всем компьютерном):
select * from books;
И получаем список наших книг. Я не буду приводить тут полностью ответ MySQL, т.к. слишком широкая табличка получилась. Мы сейчас это подправим. Попросим выбрать только названия книг:
select book_name from books; +------------------------------------------------------+ | book_name | +------------------------------------------------------+ | А.Толстой. 'Золотой ключик или приключения Буратино' | | А.Платов. 'Котлован' | | Поваренная книга | +------------------------------------------------------+ 3 rows in set (0.00 sec)
Понятно, не так ли? А теперь присовокупим их порядковые номера:
select book_id, book_name from books; +---------+------------------------------------------------------+ | book_id | book_name | +---------+------------------------------------------------------+ | 1 | А.Толстой. 'Золотой ключик или приключения Буратино' | | 2 | А.Платов. 'Котлован' | | 3 | Поваренная книга | +---------+------------------------------------------------------+ 3 rows in set (0.01 sec)
Вот так просто. Теперь давайте попросим нашу понятливую БД выдать нам книгу под номером 2:
select book_id, book_name from books where book_id=2; +---------+----------------------+ | book_id | book_name | +---------+----------------------+ | 2 | А.Платов. 'Котлован' | +---------+----------------------+ 1 row in set (0.01 sec)
Ты смотри, нашел! А если мы хотим все книги от номера 2 и больше?
select book_id, book_name from books where book_id>1; +---------+----------------------+ | book_id | book_name | +---------+----------------------+ | 2 | А.Платов. 'Котлован' | | 3 | Поваренная книга | +---------+----------------------+ 2 rows in set (0.00 sec)
Ну хорошо, давайте зададим в запросе что-нибудь похитрее. Например, нам нужна книга, которую мы купили на днях, но не помним про нее буквально ничего. Для этого так и попросим:
select book_id, book_name from books where book_date between subdate(now(), interval 3 day) and now();
Поясню.
Зарезервированное слово between используется тут дословно - "между".
Функция now() дает текущую дату и время.
Функция subdate уменьшает указанную дату на указанный период времени.
Т.е. по-русски наш запрос звучал: "выбрать номер книги и название из базы книги, где дата книги находится между датой трехдневной давности и текущей датой". И что же нам выдала база?
select book_id, book_name from books where book_date between subdate(now(), interval 3 day) and now(); +---------+------------------------------------------------------+ | book_id | book_name | +---------+------------------------------------------------------+ | 1 | А.Толстой. 'Золотой ключик или приключения Буратино' | +---------+------------------------------------------------------+ 1 row in set (0.01 sec)
Как в сказке! Еще один примерчик хочу продемонстрировать. Попробуйте понять его сами, без подсказок.
select book_name from books where book_name like "%Толстой%"; +------------------------------------------------------+ | book_name | +------------------------------------------------------+ | А.Толстой. 'Золотой ключик или приключения Буратино' | +------------------------------------------------------+ 1 row in set (0.01 sec)
Давайте разберем еще один мощный select, затем посмотрим методы удаления и на этом закончим наше отступление в сторону MySQL.
select date_format(book_date,'%d.%m.%Y') as date, book_name from books where book_name like "А.__%" && book_id<100 && book_date<"2002-01-01 00:00:00" order by book_date desc limit 0,2; +------------+------------------------------------------------------+ | date | book_name | +------------+------------------------------------------------------+ | 28.08.2001 | А.Толстой. 'Золотой ключик или приключения Буратино' | | 00.00.0000 | А.Платов. 'Котлован' | +------------+------------------------------------------------------+ 2 rows in set (0.00 sec)
Запрос, конечно, не самый сложный, но на такой ущербной базе данных особенно не разгуляешься. Ну, да ладно, перевожу строку запроса на русский язык.
Выбрать даты в формате "дд.мм.гггг" и называния книг из базы данных "книги", в тех записях, которые удовлетворяют условиям:
1. Название книги начинается на "А.", имеет еще минимум два символа после этого "__" и продолжается как угодно до конца"%".
2. Порядковый номер в базе не превышает цифры 100.
3. Дата меньше, чем 2002-й год.
Полученные данные отсортировать по дате, в обратном порядке: от большего к меньшему. Из полученных данных взять только две строки, начиная с первой же позиции (0 — первая запись). Вот так расшифровывается наш запрос.
Интереснее поиграть с запросами сразу из нескольких таблиц, конечно. Будем надеяться, что наша задача предоставит нам такую возможность. Посмотрим. Все же по SQL написано много умных книг, и я пока не планировал писать учебника по нему. Эту главу и предыдущую я посвятил сему языку, чтобы вы могли нормально читать те несложные запросы, которые мы будем составлять по мере создания нашего проекта.
Теперь об удалении из базы данных.
Удалять из базы записи так же просто, как и выбирать их select-ом. Все то же самое, только вместо слова select используется слово delete. При этом не указываются названия полей (т.к. удаление производится по целым записям), и не используются сортировки и группировки.
Например:
delete from books where b_id>2 && book_name like "___"
"Удалить из таблицы books записи, id которых больше 2 и имя книги состоит из трех любых символов". Надеюсь, других примеров удаление приводить не надо, и так должно быть все понятно. Если возникли попутные вопросы — Добро пожаловать на форум.
Возвращаемся к нашей программе. Если я ничего не напутал, то у нас на очереди наименее нудный класс, который мы назвали utils. Почему наименее нудный? А потому, что состоять он у нас будет из большого количества процедур, имеющих самое разное предназначение: обработка ошибок, система оповещения, контроль кеширования страниц, преобразование и приведение форматов дат и времени, и так далее. В течение всего нашего "учебного курса" мы будем возвращаться к этому классу, чтобы добавить в его тело очередную нужную нам функцию.
А для начала я предлагаю определить три функции работы с ошибками: получение ошибки по номеру, получение html-сообщения об ошибке для пользователя и отправка сообщения об ошибке модератору сайта. Думаю, вы уже сами догадались, что надо создать файл utils.class и породить в нем новый класс class_utils от класса class_mysql. Эту процедуру мы неоднократно повторяли, не будем задерживаться, перейдем к "ошибкам".
Позвольте дать вам один совет. Многие начинающие программисты, пишущие на C, perl или php часто совершают одну небольшую ошибку, приводящую порою к страшной нервотрепке. Это проблема открывающих и закрывающих элементов конструкций. Чаще всего это выражается в том, что открыв очередную конструкцию той же фигурной скобкой, они не закрывают ее сразу, а откладывают это дело на потом. Но в процессе написания функции накапливается столько открывающих и закрывающих скобок, что не сложно запутаться самому и запутать интерпретатор. Последний может запутаться так, что укажет вам место ошибки очень далеко от ее настоящего места, совсем в другом месте вашего листинга. Старайтесь всегда ставить скобки, теги, кавычки и прочих "парных тварей" сразу парами. Поставил пару кавычек, а уж потом вписывай между ними свой текст. Это сэкономит вам кучу времени, уверяю.
Мы еще не знаем, какие ошибки у нас будут возникать в процессе работы наших скриптов. Поэтому создадим саму процедуру с несколькими ошибками, а в процессе работы над проектом будем добавлять в нее новые данные.
Для наглядности будем использовать в качестве базы хранения ошибок обычный массив переменных. Для больших проектов это не самый разумный способ, но он очень нагляден и понятен. А место хранения описания ошибок мы всегда можем изменить, если того потребуют обстоятельства.
В общем, функция возврата текста ошибки по ее номеру феноменально примитивна. Ее можно разделить на две составляющие: описание базы ошибок и собственно возврат описания ошибки по ее номеру. Смотрим.
function err_to_str($num) { // Фатальные ошибки $err[1]="Ошибка управления или попытка взлома системы. Администратору отправлено сообщение!"; $err[2]="Ошибка авторизации."; // Ошибки работы с SQL $err[11]="Ошибка выполнения SQL-запроса"; // Ошибки для web-серфера $err[101]="Ошибка выполнения SQL-запроса"; // Отправка ошибки администратору if($this->DEBUG_LEVEL>=$num) $this->mail_to_noc($err[$num]); // Возврат текста ошибки return($err[$num]); }
Почти все понятно без объяснений.
Мы передаем номер ошибки нашей функции, которая хранит этот номер в переменной $num.
$err[2]="Ошибка авторизации."; — это мы занесли троку "Ошибка авторизации" в ячейку массива под номером 2. Т.е. 2 — это у нас уже зафиксированный номер ошибки, которые будет возбуждаться при нарушении правил авторизации: при неверной попытке доступа в защищенную часть сайта.
return($err[$num]); — собственно, возврат текста ошибки. То, ради чего создана эта функция.
И только одна строка требует более детальных пояснений:
if($this->DEBUG_LEVEL>=$num) $this->mail_to_noc($err[$num]);
Дело в том, что есть ошибки, о появлении которых необходимо знать администратору айта: ошибки базы данных, попытки взлома, попытки подбора паролей и тому подобное. А есть такие, которые будут возникать в больших количествах для посетителей страниц, но не являющиеся при этом нарушениями работы системы: "вы забыли ввести ваше имя", "по этому материалу нет комментариев", "по вашему запросу ничего не найдено" и прочее. Вот, чтобы администратор получал только те ошибки, которые ему интересны, мы вводим в наш первый класс class_vars еще одну переменную, определяющую текущий уровень отладки скриптов (добавьте в файл vars.class):
// уровень отладки var $DEBUG_LEVEL=100;
Вы обратили внимание, что я разделил все наши ошибки на несколько групп? И не просто так, а с некоторой особенностью. Чем серьезнее и фатальнее ошибка, тем ниже ее порядковый номер. "Страшные" ошибки нумеруются 1, 2, 3..., ошибки попроще (вроде MySQL) нумеруются с номера 11. А совсем неинтересные администратору сообщения — начинают нумероваться с числа 101.
Это сделано для того, чтобы удобно нам было выставлять уровень отладки. Смотрим еще раз на строку:
if($this->DEBUG_LEVEL>=$num) $this->mail_to_noc($err[$num]);
Из нее не сложно понять, что мы вызываем некую функцию mail_to_noc() (она отсылает администратору сообщение об ошибке по почте), но только в том случае, если номер активной ошибки меньше или равен той, что мы указали в нашей переменной $DEBUG_LEVEL.
Сейчас мы установили DEBUG_LEVEL в состояние 100. Это значит, что администратор получит сообщение об ошибке по почте только в том случае, когда эта ошибка не имеет отношения к сообщениям для web-серфера.
При отладке наших скриптов мы можем поднять уровень отладки до любой большой цифры, чтобы получать полную информацию о том, что творится на сайте, но после отладки этот уровень вполне можно уменьшить обратно до 100 и спасть спокойно.
Очень подробно я разжевал эту простейшую функцию только для того, чтобы вы поняли важность "ошибки" для программы. Если мы построим грамотную систему самоанализа программы, то никогда не будем иметь проблем со взломами, добавлением ошибочных данных и другими неприятностями, которые не только могут подпортить нам нервы, но и подпортить результат многолетнего труда. Кстати, по уровню отношения к ошибкам и самоанализу системы можно судить о серьезности программиста. Научиться кодировать для web — совсем не сложно. Гораздо сложнее сделать это так, чтобы система могла за себя постоять, чтобы пользователь знал — что он сделал не так, чтобы администратор сайта знал, что на нем творится, чтобы другие программисты могли после тебя разобраться в том, что ты написал. Хотя последнее — не обязательно. У каждого программиста своя манера, как у художника. Поэтому, не всегда удается дописать талантливый фрагмент к чужому творению. Художникам это и не надо, а вот программистам — наоборот.
Нам осталось разобрать еще две функции.
Совсем коротенькая функция получения html-версии ошибки:
function err_to_html($num) { $text="<b>Error:</b> <font color=#FF0000>".$this->err_to_str($num)."</font> <p>Если у вас есть время, пожалуйста, пришлите по адресу <a href=mailto:noc@21.ru>noc@21.ru</a> подробное описание ситуации, при которой возникла данная ошибка. Будем вам за это очень признателены! <br><br><div align=right>Рома</div><br><br><br>"; return $text; }
Тут даже пояснять нечего. Мы вызываем написанную нами err_to_str с номером ошибки, чтобы получить ее текст. А затем оформляем все это html-тэгами и просьбой прислать описание ситуации, при которой произошла данная ошибка. Все просто, даже языком тут почесать не обо что.
Лучше освоим новую для нас функцию обращения с почтовыми сообщениями.
В PHP известно два способа отправлять почтовые сообщения: при помощи стандартной почтовой службы сервера и отправка сообщения без посторонней помощи через socket. Мы остановимся на первом варианте. В 99.9% случаев его оказывается достаточно. Кроме того, у меня просто не хватит преподавательской квалификации, чтобы разжевать вам второй способ.
Давайте напишем сразу две функции. Первая будет у нас заниматься отправкой почты как таковой, а вторая будет вызывать первую, чтобы отправить именно письмо с ошибкой.
Первую функцию назовем mailer. Вот так она выглядит.
function mailer($from, $to, $subj, $body) { $from="From: $from\nReply-To: $from\nX-Priority: 1\nContent-Type: text/plain; charset=\"koi8-r\"\nContent-Transfer-Encoding: 8bit"; $from=convert_cyr_string($from,"w","k"); $to=convert_cyr_string($to,"w","k"); $subj=convert_cyr_string($subj,"w","k"); $body=convert_cyr_string($body,"w","k"); mail($to,$subj,$body,$from); }
В качестве аргументов этой функции мы передаем параметры:
$from — от кого
$to — кому
$subj — тема сообщения
$body — само сообщение (тело сообщения — так правильно это называется)
Далее мы производим магические пассы над нашими аргументами. Я не буду объяснять, что означают эти странные добавления, вроде "nContent-Type: text/plain; charset=\"koi8-r\"\nContent-Transfer-Encoding: 8bit", вам это знать не так уж обязательно. Скажу только, что это служебная информация для вашего outlook или TheBat, уж не знаю, чем вы там пользуетесь для чтения почты. Если этой информации в заголовке письма не будет, то письмо вы увидите в жутком формате, русский язык в виде всяких "зюков" и так далее.
А функция convert_cyr_string() просто преобразует текст из формата win-1251 в формат KOI-8, который является стандартом для почтовых сообщений в русскоязычном Интернете (к сожалению, далеко не все это знают).
Опустив все эти "пассы", мы подходим к функции PHP mail(), которая и отправит наше письмо при помощи почтовой службы сервера. Ей мы и предаем подготовленные нами параметры: отправителя, получателя, тему и сообщение.
Ну вот. А теперь можно и послать администратору сообщение об ошибке.
// Отправка сообщения об ошибке function mail_to_noc($message) { global $REQUEST_URI; for($i=0;$iEMAIL_NOC);$i++) { $this->mailer("Robot ", $this->EMAIL_NOC[$i], "Fatal error!", "Error: $message\nDateTime:".$this->today_date()." ".$this->today_time()."\n"."Remote IP:".$this->remote_ip()."\n\nURI: $REQUEST_URI\n\n$SQL_QUERY: ".$this->sql_query."\nSQL_ERROR:".$this->sql_err); usleep(100000); } }
В качестве аргумента мы передаем функции mail_to_noc сообщение об ошибке, которое необходимо отправить в службу поддержки сайта. Адреса службы поддержки мы описали среди наших переменных, помните?
// техническая служба сайта var $EMAIL_NOC=array("roma@21.ru","noc@21.ru");
В качестве вспомогательной информации мы возьмем некоторые данные, которые сопровождают прочти любую ошибку. Эти данные помогут нам (службе поддержки сайта) сориентироваться в ситуации и принять правильное решение. Какая же информация нам не помешает? Я думаю, что нам пригодится знать URL, по которому произошла ошибка. Вдруг какой-то кретин нагородил в строке вызова страницы всякого бреда, что и привело к ошибке (обратите внимание, если пришла ошибка, значит сработала защита от этого самого бреда).
В PHP есть масса зарезервированных переменных, к которым можно обратиться из любого места наших скриптов. Просмотреть полный список, так называемых, переменных окружения PHP можно очень просто. Достаточно написать в файле (назовем его phpinfo.php):
<? echo phpinfo(); ?>
И обратиться вызвать в браузере эту страницу.
Зарезервированная функция phpinfo() покажет нам в красивой табличке все переменные окружения и особенности настройки конкретной версии PHP, другую полезную информацию.
Посмотрите сами: http://kurepin.ru/phpinfo.php
Среди них есть и та, о которой мы только что говорили — REQUEST_URI. Это переменная, в которой храниться URL запрашиваемой страницы. В нашем примере вы видите ее значение — "/phpinfo.php" — что мы и запрашивали.
Так вот, возвращаясь к нашей функции, в первой строке ее вы видите как можно заполучить эту самую информацию.
global $REQUEST_URI (обратите внимание, последняя буква "I", а не "L").
Тут надо заметить, что в обычном скрипте в мы могли бы обраться к данной переменной напрямую, но в PHP принято, что глобальная еременная (распространяемая на весь скрипт) не известна в отдельной функции, пока ее к этому не принудят. Принуждение (объявление/вызов) производится при помощи конструкции global имя глобальной переменной. После этого к ней можно обращаться точно так же, как и к другим переменным, определенным в данной функции. Запомните, что в разных функциях и в глобальном скрипте вы можете пользоваться одинаковыми именами для переменных: и каждый раз это будут разные переменные.
Теперь, каждое сообщение об ошибке мы будем сопровождать строкой URL. Это полезная информация, уверяю вас. Особенно в больших проектах (надеюсь, вы еще не забыли, что мы пишем БОЛЬШОЙ проект), когда один вид ошибки может возникнуть на любой из тысяч страниц. Мне почему-то кажется, что кроме места, нам понадобится еще и время происхождения ошибки. В письме всегда указывается время отправления письма. Но мы не будем полагаться на совесть почтового сервера и сами проставим время с точностью до секунды. Вообще, дата и время — полезная штука. Давайте сразу напишем в коллекцию наших утилит две коротенькие функции — приведения даты и времени к приличному виду.
function today_date() { $ret=date("d.m.Yг.",time()); return($ret); }и
function today_time() { $ret=date("G:i:s",time()); return($ret); }
Очень просто. Мы запрашиваем у PHP дату и время в том виде, что предоставляет нам сервер (число секунд, прошедших с 1970-го года) и преобразуем их в удобоваримый формат: "дд.мм.гггг" и "чч:мм:сс".
Теперь мы можем в любом месте наших скриптов вставить текущую дату и текущее время.
Что нам понадобится еще? У нас уже есть место "преступления", его дата и время, имеется текстовое его определение. Чего не хватает? Любой следователь вам без запинки ответит, что нам необходимо узнать самого нарушителя спокойствия. Правильно. Это единственное, что нам недостает.
Мы уже позаботились об этом, помните? Мы вставили в html-сообщение об ошибке просьбу описать ситуацию, при которой возникла эта самая ошибка. Хорошо. Но не каждый посетитель захочет нам отписать такое письмо, это во-первых. А во-вторых, уж точно не станет писать нам письмо, если он сознательно пытается нанести урон нашему сайту, или просто прощупывает сайт на предмет "дыр". Поэтому мы, без специального на то разрешения, обнаружим IP-адрес посетителя, нарушившего спокойствие нервного админа, и приложим его к письму (адрес, конечно, а не админа).
Получить IP посетителя так же просто, как и URL: адрес содержится в переменной окружения $REMOTE_ADDR.
Уж простите мне мое занудство, но я предпочитаю определить это в отдельную функцию.
function remote_ip() { global $REMOTE_ADDR; return($REMOTE_ADDR); }
Почему я поступаю так "неразумно"? Дело в том, что запрашивать эту информацию можно из разных мест скрипта. А что, если потребуется присовокупить к адресу еще какую-то информацию? Или произвести его регистрацию в логах? А есть добавить проверку на наличие прокси-сервера и попытаться получить реальный IP-адрес? В общем, может произойти все, что угодно, и необходимо забронировать себе удобное расширение возможностей в любом направлении. Если вы не догадались, то дату и время я заключил в отдельные функции по той же причине: нам не составит труда заменить разделители разрядов даты и времени с точек и двоеточий на слеши или что-то иное. Хоть на слова.
Ну что, мне кажется, что мы собрали всю минимально необходимую сопроводительную информацию: мы знаем время, место, адрес возмутителя и описание ошибки. Пора все это уже отправить адресату.
Вот, мы и натыкаемся на первую конструкцию типа цикл. Не думаю, что следует останавливаться на подробном объяснении цикла for: этот вид цикла существует во всех языках, начиная с Бейсика, и называется везде так же. Скажу только , что тело цикла выполняется до тех пор, пока выполняется условие, заключенное в скобки. В данном случае, это количество email-адресов, указанных нами в переменной EMAIL_NOC. У нас, насколько я вижу, их два. А функция PHP подсчета количества ячеек массива count() поможет нам сделать количество наших адресов произвольным. Т.е. нам не надо будет ничего менять в скриптах, когда потребуется добавить или убрать какие-то адреса из списка службы поддержки.
Таким образом тело нашей конструкции for выполняется до тех пор, пока не кончатся адреса. При каждом проходе функции к переменной $i прибавляется единица, указывая не следующую ячейку массива.
Все тело функции состоит из двух строк.
Первая функция вызывает написанную до этого mailer(), передавая ей нужные параметры. А именно:
"Robot
$this->EMAIL_NOC[$i] — это to (кому мы отправляем письмо. Обратите внимание: это как раз обращение к очередной ячейке массива email-адресов).
"Fatal error!" — это поле письма subj (тема сообщения)
"Error: $message\nDateTime:".$this->today_date()." ".$this->today_time()."\n"."Remote IP:".$this->remote_ip()."\n\nURI: $REQUEST_URI\n\n$SQL_QUERY:".$this->sql_query."\nSQL_ERROR:".$this->sql_err — само сообщение, составленное из подготовленных нами данных.
Теперь, я уверен, вам не составит труда самостоятельно разобрать на части тело сообщения и без ошибки определить, как оно будет выглядеть в виде письма. Осталось только подсказать, что сочетание символов "\n" — не что иное, как символ перевода строки (enter).
Кто возмущен тем, что я не дал финального листинга класса utils? А зачем? Вы вполне можете составить его сами и дополнять по мере продвижения к финишу. А там и сравним наши результаты.
Кстати, вы заметили, что мы закончили с "общими" классами? Т.е. мы написали основной набор функций, который пригодится практически для любой задачи. А теперь переходим уже к более специфическим классам и функциям. У нас есть две основные ветви скриптов. Два основных направления программирования. Первое, — это возможность ввода данных в недра сайта, а второе — вывод данных для зрителя-посетителя. Почему ввод данных идет первым? А потому, что прежде чем показать что-то, надо это что-то добавить. Вот разработкой этой ветви и займемся. Давайте соберем все данные, которыми мы оперируем.
Есть:
1. Рубрики нашего сайта
2. Материалы, размещаемые на сайте.
Рубрики состоят только из их названий и уникального номера. Помните таблицу, которую мы для них создали?
А Материалы — более сложные данные. Они состоят из названия,текста и уникального номера. И не только. Но об этом позже.
Для начала займемся рубриками.
Необходимо создать функцию добавления рубрик, редактирования рубрик и их удаления.
Честно говоря, добавление и редактирование рубрик — настолько редкая задача, что можно было бы не писать ничего подобного, а ручками отправлять в SQL-запросы, когда надо что-то изменить в таблице рубрик. Но поскольку мы пишем большой и самодостаточный проект, то пропустить даже такую мелкую задачу не приходится.
Для начала, я вас прошу, создайте новый класс. Класс, который будет отвечать за добавление данных. По нашей с вами терминологии он называется class.in. А файл, соответственно, in.class.
Создали? Все заголовки для нового класса оформили? Все парные теги и скобочки закрыли? Ай, молодцы. Приятно работать с внимательными учениками.
Ввод данных.
Прежде чем броситься писать функцию, пихающую в базу какие-то данные, давайте поразмыслим о том, какие подготовительные этапы могут нам понадобиться.
"Ну какие еще подготовительные этапы", — скажет нетерпеливый программист. Написать один запрос в базу, запихивающий строку-название раздела, и всего делов.
Так-то оно так... да не так. А если сайтом управляет полный неуч? Или, того хуже, — в руки к злоумышленнику попала панель управления сайтом, и он начнет искать уязвимые места, чтобы нанести урон системе?
Вот именно! Первым этапом мы обязаны проверить те данные, которые собираемся заносить в базу. Это не сложно. Надо для себя описать условия, которым должна удовлетворять строка текста, которую мы добавляем в качестве названия раздела. Я говорю добавляем, так как сначала пишем функцию добавления нового раздела. Если я не ошибаюсь, то наша строка-название должна удовлетворять следующим параметрам: быть не короче одного символа, быть не длиннее пятидесяти символов и не должна дублировать уже существующую рубрику. Вот так и напишем (готовьте файл utils, будем добавлять в него первые сообщения об ошибках).
function in_cat_add_check($name) { // добавление слешей перед управляющими символами . $name=AddSlashes($name); // проверка на кол-во символов if(strlen($name)==0) return(21); if(strlen($name)>50) return(22); // проверяем на существование такой рубрики $this->sql_res="select c_id from tbl_cats where c_name='$name'"; $this->sql_execute(); if($this->sql_err) return(11); if(mysql_num_rows($this->sql_res)) return(23); return(0); }
И сразу, чтобы ничего не забыть, добавляем в список ошибок (файл utils.class) наши новые ошибки.
// Ошибки работы с рубриками $err[21]="Вы не задали название рубрики"; $err[22]="Длинна рубрики превышает допустимые 50 символов"; $err[23]="Такая рубрика уже есть в базе";
Вот так. Теперь, уже по традиции, пережевываю написанное в кашку.
$name=AddSlashes($name); — это очень важная и очень удобная функция PHP, которая добавляет слеши "\" ко всем символам, которые MySQL может интерпретировать как управляющие команды, превращая их в обычные текстовые символы.
Более всего, это относится к кавычкам. Если в названии нашей рубрики встретится кавычка, то интерпретатор MySQL воспримет эту кавычку как управляющую (открывающую или закрывающую) и наш запрос к базе данных будет иметь совсем не тот вид, на который мы рассчитываем. К сожалению, многие забывают об этом (или не обращают на это внимание), за что и расплачиваются. В Сети добрая половина скриптов не имеет подобных превентивных проверок, что приводит к частым взломам сайтов и проникновению в их базы данных. >Я бы мог показать вам серию несложных приемов, с помощью которых можно прощупать систему и нарушить работу стандартных функций сайтов: гостевых книг, форумов, форм оправки почты и получения чужих паролей, содержащихся в базах. Но я вас учу не взламывать чужие сайты, а защищать свои. Поэтому, будем заострять внимание на том, как надо оформлять безопасность скриптов, а не наоборот.
Вам не надо помнить весь набор управляющих символов. Функция PHP AddSlashes() сама знает, куда надо добавить слеши.
Обращаю ваше внимание на то, что в конфигурационном файле php.ini, где лежат настройки правил работы php-интерпретатора, есть параметр, отвечающий да добавление слешей в данные, передаваемые скрипту через web-форму. Проконсультируйтесь в службе поддержки вашего провайдера: включен ли этот параметр управления. Я предпочитаю его выключать и самостоятельно заботиться о безопасности. В противном случае мои скрипты могут в миг оказаться "дырявыми", если этот параметр по чьей-то воле изменит свое значение.
Далее мы производим проверку на длину строки, которую получили в качестве параметра. Функция PHP strlen() вернет нам количество символов в той строке (или переменной), которую мы передали ей в качестве параметра. Тут все очевидно и пояснять нечего, только обратите внимание, что в PHP знак сравнения обозначается в виде сдвоенного "равно". Ибо одинарное "=" — во всех ситуациях выполняет присваивание. Будьте с эти внимательны.
И, наконец, мы производим проверку на существование такого раздела в базе. В четыре строки.
1. Оформляем запрос в базу. Запрос совсем простой — комментировать его не стану.
2. Вызываем нашу функцию активации запроса (кто забыл, как она работает, обратитесь к главе "PHP: mysql.class").
3. Проверяем, не случилось ли ошибки. Обратите внимание, в классическом виде строка в скобках if() должна была бы иметь условие:
if($this->sql_err>0) return(11);
Но, так как 0 — это "правда" по логике PHP, а любое другое число — "ложь", мы опускаем знак "больше", будучи уверенными, что условие сработает и без этого, если значение переменной sql_err не равно 0 — нормальному завершению операции.
4. И четвертой строкой мы проводим ту самую проверку на дублирование строки в базе. Функция PHP mysql_num_rows(), получая в качестве параметра ссылку на результат запросу в БД, возвращает нам количество найденных запросом строк. И если возвращенное значение не равно 0, значит такая строка в базе найдена, и нам остается только вернуть соответствующий код ошибки — 23. Поэтому, тут мы тоже можем смело опустить условие ">0".
Не знаю, догадались ли вы уже или нет, команда PHP return() не только возвращает из нашей функции указанное в скобках значение, но и немедленно прекращает выполнение данной функции, возвращая управление той, из которой произошло обращение. Другими словами, в цепочке наших проверок остановка выполнения скрипта произойдет на той строке, где прежде всего сработает команда return. Это очень удобно.
Мы с вами написали функцию проверки корректности добавляемого названия раздела и приведения строки к безопасному виду, готовому к употреблению в MySQL.
На предыдущем уроке мы научились подготавливать названия рубрик для добавления их в базу. Давайте теперь их добавим. Только прежде я хочу немного изменить функцию in_cat_add_check(), которою мы написали. Давайте не будем передавать указанной функции в качестве параметра название рубрики. Лучше мы для нее объявим глобальную переменную, которой и будем пользоваться для хранения названия. И будем так поступать и впредь -- каждое значение будет у нас иметь свою обособленную переменную. Так будет удобнее для понимания и красивее, что тоже приятно. То есть, функция должна выглядеть вот так:
var $in_cat_name; function in_cat_add_check() { // добавление слешей перед управляющими символами . $this->in_cat_name=AddSlashes($this->in_cat_name); // проверка на кол-во символов if(strlen($this->in_cat_name)==0) return(21); if(strlen($this->in_cat_name)>50) return(22); // проверяем на существование такой рубрики $this->sql_res="select c_id from tbl_cats where c_name='$name'"; $this->sql_execute(); if($this->sql_err) return(11); if(mysql_num_rows($this->sql_res)) return(23); return(0); }
Немного изменений. Мы добавили перед описанием функции декларацию новой переменной in_cat_name, которая будет содержать название рубрики, убрали из объявления функции локальную переменную name, а в теле функции заменили все $name на $this->in_cat_name. Не сложно.
Вообще, я сам так делаю, и вам рекомендую: во-первых, старайтесь называть ваши функции и переменные по какой-то одной схеме, чтобы не запутаться. А во-вторых, декларируйте переменные в начале того класса, для которого они декларируются. Вот переменные, начинающиеся на in_... -- относятся к классу class_in, поэтому они так начинаются и декларировать их надо в начале этого класса, чтобы всегда было видно -- какие переменные уже объявлены и как они называются. Кстати, следующая часть за in_ указывает на то, к какой группе функций переменная относится. В нашем случае речь идет о рубриках, поэтому переменная имеет продолжение cat_. Не ленитесь давать функциям и переменным длинные понятные имена. Поверьте, лучше потратить время на написание длинных понятных имен, чем потом на вспоминание их имен и их предназначений.
Так вот, давайте теперь воспользуемся переделанной функцией для проверки данных, и добавим эти самые данные в базу.
function in_cat_add() { // Проверяем данные на корректность $err=$this->in_cat_add_check(); if($err) return($err); // Формируем запрос в БД $this->sql_query="insert into tbl_cats(c_name) values('".$this->in_cat_name."')"; $this->sql_execute(); if($this->sql_err) return(11); return(0); }
Мне уже начинает надоедать это... ни одной сложной функции пока не написано. Все как-то очень просто и примитивно. Я уж и запутываю вас, и заставляю переписывать функции на новый лад, добрались уже до самого сердца, если можно так сказать, нашего движка, а все ничего сложного...
Итак, из чего же состоит функция добавления в базу названия рубрики. Она состоит из:
1. Вызова функции проверки на правильность названия. Видите, мы сразу вызываем in_cat_add_check(), чтобы получить от нее ошибку выполнения. Если название, содержащееся в переменной in_cat_name пройдет все проверки, и в базе не окажется такого же названия, мы получим в нашу переменную err номер ошибки -- ноль. Если же это не так, то мы получим какой-то другой номер ошибки. В последнем случае мы его тут же возвращаем дальше по цепочке, не пытаясь ничего более делать: нам надо донести до пользователя информацию об ошибке. Мы еще увидим, как работает эта система передачи номера ошибки.
2. Формируем примитивный запрос в БД: добавление новой записи. Убежден, разбирать строку запроса мне не придется, вы уже отлично с эти справляетесь сами. Если это не так, обратитесь к главе язык SQL.
3. Возвращаем 0 в качестве ошибки -- подтверждение того, что функция наша отработала успешно.
Вот и все, собственно. Мы умеем добавлять в базу данных новую рубрику. Теперь ее удалим. Только удалять из базы рубрику надо по ее идентификационному номеру, а не по названию. Для того он и нужен, этот самый идентификатор, чтобы обращаться к нужной записи в базе.
Добавьте еще одну переменную, и написать функцию удаления записи из таблицы рубрик не составит никакого труда.
var $in_cat_id; function in_cat_delete() { // Приводим номер рубрики к целочисленному виду $this->in_cat_id=(int)$this->in_cat_id; // Формируем запрос в БД $this->sql_query="delete from tbl_cats where c_id='".$this->in_cat_id."'"; $this->sql_execute(); if($this->sql_err) return(11); return(0); }
Все. Удалили.
Или не все? Или надо было произвести проверку на существование такой записи в базе, прежде чем ее удалять? Да, так и надо было сделать, если бы эта возможность была публичной. Например, когда посетитель сайта удаляет что-то из своей регистрационной карточки. Тогда важно лишний раз проверить и доложить пользователю: операция удаления произошла с ошибкой, ошибка заключается в том, что нет такой записи в базе. Но нам этого не нужно. Рубрики редко будут удалять, а функция удаления будет вызываться из защищенного паролем раздела сайта, куда вредный хакер еще должен как-то добраться, прежде чем попытаться удалить что-то не то.
Но необходимую защиту мы все же поставили -- преобразовав содержимое переменной in_cat_id в целое число. И если по ошибке или по причине вредительства в данной переменной окажется какая-то вредоносная строка, то она в процессе преобразования попросту превратится в значение "0". И функция нормально отработает запрос в базу: "удалить из таблицы рубрику под номером 0". Такого номера рубрики у нас быть не может, поэтому ничего страшного и не произойдет.
Другое дело -- переименование рубрики. Вот тут нам опять придется проверять название на соответствие нашим правилам. Но это не страшно. Не зря же я вынес проверку в отдельную функцию. Мы просто ее вызовем, как вызывали в in_cat_add.
function in_cat_rename() { // Приводим номер рубрики к целочисленному виду $this->in_cat_id=(int)$this->in_cat_id; // Проверяем наличие в базе изменяемой рубрики $this->sql_query="select c_id from tbl_cats where c_id='".$this->in_cat_id."'"; $this->sql_execute(); if($this->sql_err) return(11); if(mysql_num_rows($this->sql_res)==0) return(24); // нет такой рубрики в базе // Проверяем данные на корректность $err=$this->in_cat_add_check(); if($err) return($err); // Формируем запрос в БД $this->sql_query="update tbl_cats set c_name='".$this->in_cat_name."' where c_id='".$this->in_cat_id."'"; $this->sql_execute(); if($this->sql_err) return(11); return(0); }
Готово.
1. Приводим номер рубрики к целому числу.
2. Запрашиваем из базы запись с этим номером. Если такого нет, то возвращаем соответственную ошибку. Не забудьте добавить в файл utils.class ошибку под номером 24:
$err[24]="Нет такой рубрики в базе";
Хотя, если быть честным, можно было бы тут возвратить номер ошибки "1". Ведь это больше походит на сбой в системе или на ее взлом, чем на обычную ошибку. Ну да ладно, распишем ее отдельно, пусть будет все подробно.
3. Проверяем данные на корректность, воспользовавшись уже написанной проверкой.
4. И запрашиваем базу на предмет обновления данных (update).
Вот мы и написали первую группу функций, которая позволяет нам работать с базой рубрик. Причем, вызывать эти функции мы можем как с сайта, так и из других скриптов. Например, добавлять рубрики в пакетном режиме. Но это уже тема других уроков.
Совсем не лишним оказалось описание базовых возможностей SQL, которым я немного прервал наш курс несколькими уроками назад.
Сегодня я хочу опять чуть-чуть отклониться от "курса партии" и поговорить о конструкциях. Но уже не SQL, а самого PHP. Это поможет вам легче читать листинги программ, а я смогу писать более лаконично последующие функции, не боясь, что вы меня не поймете. Не пугайтесь, это совсем не значит, что я собираюсь дальше писать все сложно и туманно. Нет, ничего подобного. Просто хочу, чтобы у вас появилось понимание того, что PHP - не Бейсик, где можно писать только так, как можно писать. И никак иначе.
Знакомьтесь -- точка.
Точку "." вы уже знаете, она объединяет строки. Плюс "+" -- складывает цифры, а точка объединяет строки.
$a1="100"; $a2="200"
Что будет, если сложить $a1 и $a2? А это как сложить...
$a1+$a2=300 $a1.$a2="100200"
...и никогда об этом не забывайте.
А вот как еще можно записать сложение.
Предположим, что мы составляем какую-то сложносочиненную строку из различных фрагментов. Мы можем добавлять в конец нашей переменной новые данные вот так:
$this->string_about_letters = $this->string_about_letters."несколько букв..."; $this->string_about_letters = $this->string_about_letters." еще несколько букв..."; $this->string_about_letters = $this->string_about_letters." и еще несколько букв..."; $this->string_about_letters = $this->string_about_letters." снова несколько букв...";
Несколько длинновато, не так ли? Мешает повторение длинной переменной $this->string_about_letters. Поэтому, запись сделаем иначе:
$this->string_about_letters .= "несколько букв..."; $this->string_about_letters .= " еще несколько букв..."; $this->string_about_letters .= " и еще несколько букв..."; $this->string_about_letters .= " снова несколько букв...";
Удобнее, не так ли?
Тоже самое, касается и математического сложения:
$abc+=1;
Прибавить 1 к содержимому переменной $abc. А как еще можно прибавить единицу? Обычно, по С-шному:
$abc++; или ++$abc;
Два этих выражение обычно используются в циклических конструкциях. И не сами по себе, а в каком-то более сложном выражении. Отличие же этих двух выражений состоит в том, что в первом случае значение переменной учитывается в выражении, а затем уже производится прибавление единицы, а во втором все наоборот -- сначала происходит увеличение значения переменной, а затем уже получившееся значение используется в выражении. На примере станет понятно:
$a=1; echo $a++; echo ++$a;
Первое echo распечатает нам "1", а второе распечатает?.. а вот не правильно! Не 2 оно распечатает, а "3". Почему? Догадайтесь сами.
Еще я хочу разложить для вас один фрагмент кода, который мы будем часто использовать в нашей программе. Да и вообще в любой программе, где данные черпаются из SQL-базы. Речь идет, конечно же, о получении данных посредством select-а.
Вы уже знаете, как сделать запрос в базу, но мы толком не говорили о том, как разбирать полученные данные. А между тем, это можно делать различными способами. Давайте сразу на примере. Предположим, мы выбираем из базы видеокассет наименования видеофильмов, выпущенных в 2000 году, и хотим распечатать их в виде таблицы (с ID кассеты, конечно). Вот, можно было бы оформить процедуру так (во многих учебниках вы и найдете нечто похожее):
$this->sql_query="select * from film where f_date between '2000-01-01 00:00:00' and '2000-12-31 23:59:59 order by f_id'"; $this->sql_execute(); $str="<table>"; $row_count=mysql_num_rows($this->sql_res); for($i=0;$i<$row_count;$i++) { $film=mysql_fetch_array($this->sql_res); $str=$str."<tr><td>".$film['f_id']."</td><td>".$film['f_name']."</td></tr>\n"; } $str=$str."</table>";
Поясняю.
Мы делаем запрос в базу (кстати, обратите внимание на новое для нас условие в запросе: between date and date, такая форма часто используется для указания диапазона дат).
Инициируем текстовую переменную для занесения в нее html-кода, добавив первый фрагмент текста -- начало таблицы.
Далее выясняем: сколько же строк(записей) из базы мы должны получить. Для этого используем функцию mysql_num_rows().
Затем мы открываем цикл с количеством повторений, равным количеству полученных строк из запроса.
В теле цикла мы вынимаем из нашего запроса очередную строку в переменную-массив $film. Функция mysql_fetch_array раскладывает данные в ассоциативный массив, используя в качестве ключей имена полей из таблицы.
Ассоциативный массив -- это тоже самое, что и обычный (нумерованный) массив, только в качестве имен для ячеек используются не цифры, а цепочки символов. И обращаться к ячейкам такого массива следует соответствующе: $abc['first'], $abc['mama']...
Далее мы приплюсовываем к нашей текстовой переменной очередную строку таблицы, используя полученные из базы данные. Для обращения к данным, как вы видите, используются названия полей таблицы. Это свойство функции mysql_fetch_array, как я уже сказал.
Обратите особое внимание: обращаться к ячейкам ассоциативных массивов в текстовых строках нельзя! Надо обязательно разрывать строку и "вклеивать" значения при помощи точек(.), как показано.
Цикл повторяется нужное количество раз, после чего мы замыкаем $str последним html-тэгом. Готово. Но не слишком ли это все громоздко? По-моему, очень даже. Предлагаю записать это все иначе: все тоже самое, но короче.
$this->sql_query="select f_id, f_name from film where f_date between '2000-01-01 00:00:00' and '2000-12-31 23:59:59 order by f_id'"; $this->sql_execute(); $str="<table>"; while(list($f_id,$f_name)=mysql_fetch_row($this->sql_res)) { $str=$str."<tr><td>$f_id</td><td>$f_name</td></tr>\n"; } $str=$str."</table>";
Вот так. Что изменилось?
Во-первых, мы в запросе сразу конкретизировали: какие поля из базы нам нужны, и в каком порядке нам их подать: f_id, f_name.
Во-вторых, мы избавились от подсчета кол-ва полученных записей из базы. Зачем они нам нужны, когда функция mysql_fetch_row сама сообщит, когда они закончатся. А цикл по условию while тут же прервет свою работу и передаст управление следующей строке программы.
В-третьих, мы в самом условии цикла while получаем в отдельные переменные нужные нам значения: list() изображает из себя массив, где ячейками массива являются переменные, которые мы укажем. То есть мы сразу получаем в переменную $f_id номер видеофильма, а в f_name получаем его название.
И эти же переменные попросту вставляем в очередную строку таблицы, не разрывая ее. И все у нас хорошо и все у нас понятно.
Посмотрите еще раз на второй вариант записи этой функции. Краткость -- сестра таланта, не забывайте об этом даже тогда, когда пишите большой web-проект. Лучше десять раз подумать, как написать короче, чем плодить километровые листинги. Заказчик, правда, не всегда оказывается доволен маленьким объемом исходных текстов, не понимая, за что же он платит деньги. Но тут уже вы должны ему объяснить, что платит он не за машинопись, а за талант! Чем короче программа, тем она быстрее выполняется и тем меньше занимает оперативную память, что тоже бывает важно...
Если вы начинающий программист, то наверняка привыкли работать так: строчку добавил — проверил выполнение, еще строчку добавил — опять проверил, как программа работает. А мы тут пишем, но не проверяем. Все верно, не проверяем. Более того, я вам обещаю, что в тех листингах, что мы уже написали, достаточно много ошибок. В основном, конечно, синтаксических. Где-то забыли точку с запятой поставить, где-то $this-> к переменной забыли приписать. Да мало ли что еще... Но это так и надо. Я не кривлю душой, я действительно все пишу прямо в текстах курса, без какой-либо проверки их в работе.
Так вот, друзья мои. Привыкайте писать именно так! Рождайте в голове задумки и выкладывайте их в виде программного кода. Проверить и отладить вы их всегда успеете. А как показывает практика, в процессе написания нового модуля очень даже часто возникает "задним числом" какая-то новая идея, заставляющая нас возвращаться назад и что-то переделывать. И что, снова тестировать и выискивать глюки? Да ничего подобного - так можно всю жизнь писать один проект. Но я вам обещаю. Еще несколько занятий, и мы перейдем к классу class_out, перейдем к визуализации данных и добавления их через web-форму, с использованием написанных функций. Готовьте дизайн для вашего нового сайта!
А сегодня мы займемся файловыми процедурами. Нам же надо уметь сохранять тексты и другие файлы. Вот и поучимся.
В PHP существует достаточно много разных механизмов для работы с файлами различных типов. Мы разделим все файлы на два основных типа: текстовые файлы и файлы данных (картинки, музыка, исполняемый код и все остальные типы файлов). Отличие между текстовыми файлами и остальными состоит в том, что текстовые файлы можно разделить на строки (окончанием строки служит символ EOL — "\n" — Enter). И по этим строкам файл можно читать, писать и все остальное. Для начала вспомним, что во всех системах файл необходимо открыть, прежде чем что-то с ним сделать.
Открыть файл нам поможет функция PHP fopen(). Оформляется открытие файла так:
$r=fopen('path_to_file','mode');
где:
$r — указатель на открытый файл. Он нам нужен, чтобы обращаться к нужному файлу, когда их открыто более одного.
path_to_file — абсолютный путь к файлу на диске сервера.
mode — режим, в котором открывается файл.
В PHP можно открыть файл в следующих режимах:
'r' — только для чтения.
'r+' — для чтения и записи
'w' — только для записи
'a-' — только для записи. То есть файл открывается для записи, но при этом курсор устанавливается в конец файла. Можно сказать, что это открытие файла для дозаписи.
'a+' — тоже, что и a-, но еще доступно и чтение.
В каждом режиме, где присутствует возможность записи, PHP создаст вам новый файл, если такового не существует в момент открытия. При условии, конечно, что у вас есть на это права в системе.
На самом деле, я не рекомендую вам забивать себе голову всеми этими режимами. Достаточно запомнить два:
'r' — читать (read)
'w' — писать (write)
А к остальным режим нужно обращаться только в том случае, когда не получается реализовать задачу этими двумя, которых почти всегда достаточно.
После того, как файл был открыт, с ним можно проводить операции по записи и чтению данных.
Помните, я как-то писал, что все структуры, имеющие открывающие и закрывающие формы, надо сразу закрывать, как только их открыли, а уж потом писать тело структуры? Вот к файлам это так же относится.
Прежде чем перейти к чтению и записи содержимого файла, давайте его закроем:
fclose($r);
Не сложно. $r — это указатель на открытый файл (вспомните функцию открытия файла).
После выполнения скрипта PHP сам закроет все файлы, которые вы забыли закрыть. Но если вам важно сохранить в файлах именно то, что надо — сделайте это сами.
Теперь приступим к тирании содержимого файла. Для начала, я хочу вам показать, как читается и записывается обычный файл с данными (я пронумерую строки для простоты комментирования).
$file_name='/home/roma/address.txt'; // 1 $r=fopen($filename,'r'); // 2 $text=fread($r,filesize($file_name)); // 3 fclose($r); // 4 $text=ereg_replace('213-','670-',$text); // 5 $w=fopen($file_name,'w'); // 6 fwrite($w,$text); // 7 fclose($w); // 8
По строчкам:
1. Определили в переменную путь к файлу. Представим, что в этом файле содержится копия вашей записной книжки.
2. Открываем этот файл для чтения.
3. Читаем в переменную $text содержимое всего файла. Функция filesize(), как раз, сообщает нам размер файла, который мы собрались читать. Зная, что файл не очень большой, мы решаем прочесть в переменную все его содержимое разом.
4. Закрываем файл.
5. А почему мы все это делаем? А потому, что у массы наших друзей сменились первые три цифры телефона: наконец сменили старую АТС на новую цифровую. Функция PHP preg_replace поможет нам заменить все 213- на 670- по всему содержимому переменной $text. А измененный вариант мы записываем обратно в $text.
6. Открываем все тот же файл, но теперь для записи.
7. Записываем в файл содержимое переменной $text.
8. Закрываем файл.
Вот так. Тоже все не сложно.
Только я прошу не забывать, что работа с файлами чревата множеством самых разных ошибок. Поэтому, как минимум, все открытия файлов должны обязательно сопровождаться проверкой на результат попытки открытия или создания файла. На его существование, если вы его собрались читать и так далее. Я сегодня не стану останавливаться на ошибках — будем рассматривать идеальный безошибочный вариант, но в дальнейшем (в нашей программе) вы обязательно увидите все необходимые проверки.
А теперь напишем ту же самую процедуру, но будем работать с файлом, зная, что это текстовый файл, разбитый на строки. При этом, необходимо предположить, что длина одной строки не более nnn символов (байт). Я думаю, что у нас не более 1024 символов в одной текстовой строке(1K).
$file_name='/home/roma/address.txt'; // 1 $file_new_name='/home/roma/address_new.txt'; // 2 $r=fopen($filename,'r'); // 3 $w=fopen($file_new_name,'w'); // 4 while($str=fgets($r,1024)) // 5 { $str=ereg_replace('213-','670-',$str); // 6 fputs($w,$str); // 7 } fclose($r); // 8 fclose($w); // 9
1. Определяем путь к файлу для чтения
2. Определяем путь к файлу для записи
3. Открываем файл для чтения
4. Открываем другой файл для записи
5. Начинам читать по одной строке в переменную $str из файла $r до тех пор, пока не достигнем конца файла (EOF — End Of File). Причем, строка читается либо до знака конца строки (EOL — End Of Line), либо до 1024-го символа. Это свойство функции чтения строки fgets().
6. Проводим замену 213 на 670.
7. Записываем строку $str в файл $w.
8, 9. После окончания цикла закрываем оба файла.
Немного длиннее получилось, но насколько правильнее!
Есть разные причины, по которым этот метод можно считать более правильным, чем предыдущий, но я сообщу вам самые веские. Представьте, что у вас случайно файл оказался размером во много мегабайт: скрипт попросту не выполнится, так как для каждого скрипта отводится ограниченное пространство в оперативной памяти (по умолчанию это 5Mb, если не ошибаюсь). Или подобную функцию вызвали сразу сто посетителей страницы (например, у вас сто модемных пользователей не спеша тянут с сайта mp3-музыку): у вас просто случится переполнение оперативной памяти сервера, и вы получите по мозгам от вашего администратора. А еще мы во втором примере прочитали один файл, а записали результат обработки в другой: может случиться так, что произойдет ошибка во время записи данных в файл. В этом случае у нас останется исходная копия данных и нам не придется рвать на голове волосы, разыскивая данные на кассетах с бэкапом. И это все касается не только обработки текстовых файлов, но файлов данных. Особенно файлов данных, так как текстовые файлы обычно имеют не такой уж большой размер, как картинки или те же mp3-файлы.
Кстати, в PHP есть замечательная функция readfile(), которая просто читает файл и отдает его в стандартный output (в нашем случае это может быть посетитель сайта) небольшими порциями, не перегружая оперативную память, с требуемой скоростью. Мы ей тоже воспользуемся где-нибудь, я надеюсь.
Пишем процедуру добавления в базу нашего сайта нового текста. По крайней мере — начнем писать. На какие этапы логично разбить эту задачу? Мне думается, что на такие:
1. Проверка данных на корректность.
2. Приведение текста к нужному виду
3. Сохранение текста на диске
4. Добавление необходимых данных в БД.
Согласны? Вот и начнем с самого начала. Функции можно добавлять в class_in, он ведь у нас отвечает за добавление данных. Нам понадобятся переменные.
var $this->in_text; // сам текст var $this->in_text_name; // название текста var $this->in_text_id; // id текста var $this->in_text_dt; // дата добавления var $this->in_text_cat; // рубрика
Вах! Ну-ка, обратимся к главе, где мы создавали базу данных. Ошибку видите? Нет, не видите. Подсказываю: а где же мы будем хранить номер рубрики? В таблице tbl_texts нет поля для номера рубрики, соответствующей данному тексту. Какая оплошность! Ничего, сейчас исправим:
alter table tbl_texts add column t_cat int not null;
Все. Теперь есть! А те, кто читают данный курс не подряд — пожалеют об этом, и не один раз (тут злобная ухмылка). Для тех, кто читает подряд, перевожу: изменить таблицу tbl_texts, добавив в нее целочисленное поле t_cat. Свою ошибку удачно исправили, теперь будем проверять на ошибки данные пользователя, управляющего нашей системой (готовьте class_utils — добавлять описания новых ошибок). Нам много не потребуется:
function in_text_data_check() { $this->in_text_name=AddSlashes($this->in_text_name); if(strlen($this->in_text_name)==0) return(24); if(strlen($this->in_text_name)>200) return(25); return(0); }
Тут и пояснять нечего. Добавьте в class_ulils две новые ошибки:
$err[24]="Вы не задали название тексту";
$err[25]="Длинна названия текста превышает допустимые 200 символов";
и поехали дальше...
Теперь надо серьезно подумать над тем, в каком виде мы будем хранить тексты.
Лично я предпочитаю хранить их в html-формате. Почему? В данной задаче этот формат будет чаще использоваться — запрашиваться с сайта. А если надо будет "выгрызть" html-тэги по какой-то нужде — выгрызем, не впервой.
На самом деле, храните тексты просто в двух видах. Тогда ничего выгрызать или заменять не придется. При сегодняшних объемах дискового пространства это вполне доступно. В данном случае я просто хочу оставить за собой повод вернуться к "парсингу", когда дойдем до почтовых рассылок или чего-то подобного.
А что нужно, чтобы привести текст к html-виду? Для это надо заменить управляющие спецсимволы html-тэгами.
Хочу вам дать еще один совет. Не сильно важный, но может пригодиться. Прежде чем превратить символы А в символы Б, попробуйте превратить символы Б в символы А — вдруг они уже есть и могут где-то спутать нам карты.
Предлагаю такие условия для добавляемого текста:
1. Текст не должен превышать 100Кб.
2. Текст не должен быть короче 100 символов.
3. Подгружаемый текст не должен иметь html-тэгов и спецсимволов, кроме: <a>, <b>, <i>, <u>, <img>... м-м-м-м... <div>. Хватит пока. Все остальные символы мы просто выкинем в помою. Попробуем реализовать задуманное....
function in_text_adapt() { $this->in_text=strip_tags($this->in_text,"<a><b><u><img><div>"); $this->in_text=nl2br($this->in_text); if(strlen($this-in_text)<201) return(26); if(strlen($this->in_text)>102400) return(27); return(0); }
В общем-то, это все. Только не забывайте описывать ошибки:
$err[26]="Текст слишком короткий";
$err[27]="Текст слишко длинный";
На что следует обратить внимание в этой функции.
Первой строкой мы выкидываем все html-тэги из текста, кроме тех, что указали в кавычках. Это очень удобная функция PHP. Ибо моделировать этот процесс обычным регекспом — не самая простая задача.
Второй строкой мы перед каждым переносом строки (EOL — "\n" — Enter) добавили тэг <br>, указывающий браузеру на перенос строки.
Вынужден признаться: никогда в жизни не пользовался функцией nl2br(). И вам не советую. Она вам вместо <br> наставит XHTML-совместимых <br />. Оно вам надо? Замените эту функцию простейшим регекспом:
$this->in_text=preg_replace("/\n/","<br>\n",$this->in_text);
и всего делов.
И вот только теперь (прошу обратить на это особое внимание) мы замеряем размер нашего текста. Только после того, как мы все выкинули и все добавили. Ну что ж, данные мы приготовили, можно теперь добавить их в базу сайта.
Если я ничего не путаю, то мы приготовили данные для того, чтобы добавить их в базу нашего сайта. Just do it!..
function in_text_add() { // Проверяем данные для БД $err=$this->in_text_data_check(); if($err) return($err); // Адаптируем текст для сохранения $err=$this->in_text_adapt(); if($err) return($err); // Сначала заносим в базу данные $this->sql_query="insert into tbl_texts(t_name, t_dt, t_cat) values('".$this->in_text_name."', now(), '".$this->in_text_cat."')"; $this->sql_execute(); if($this->sql_err) return(11); // Получаем сгенерированный базой ID добавленного текста $this->sql_query="select last_insert_id"; $this->sql_execute(); if($this->sql_err) return(11); list($this->in_text_id)=mysql_fetch_row($this->sql_res); // Теперь пишем текст в директорию data, в файл с номером ID if($w=fopen($this->PATH_DATA."/".$this->in_text_id,'w')) { fwrite($w,$this->in_text); fclose($w); } else { $this->sql_query="delete from tbl_texts where t_id='".$this->in_text_id."'"; $this->sql_execute(); return(31); } return(0); }
Зачитываю собственные комментарии.
Вызываем функцию проверки данных для БД и функцию адаптации текста. Если одна из них вернет ошибку, то мы останавливаемся с возвратом этой самой ошибки.
Заносим в БД новую запись: название текста, дату, номер рубрики.
Помните, как в таблице было декларировано поле t_id? Оно было декларировано как auto_increment. А это значит, что при добавлении новой записи содержимое этого поля определяется автоматически -- плюс один к последнему добавленному в базу значению этого поля.
Вот нам это самое число и надо выдернуть из базы. Это же уникальный идентификатор добавленного текста! Он вполне может служить нам и уникальным именем файла для хранения текста на диске.
В этом нам поможет SQL-запрос select last_insert_id. Очень короткий запрос, который возвратит нам последний сгенерированный номер.
Можно даже ничего более не уточнять, так как мы не закрывали сессию с MySQL, и база знает, что именно мы от нее хотим.
Записали идентификационный номер в переменную $this->in_text_id.
Теперь можно записать и файл на диск.
Вот тут, мы делаем проверку на открытие файла.
if(fopen(:.
А имя файла мы формируем из переменной $this->PATH_DATA (которую описали в классе class_vars) и идентификационного номера текста, который получили из базы (in_text_id).
Если открытие файла для записи прошло удачно, то мы записываем в файл содержимое переменной $this->in_text и закрываем файл.
Если же открытие прошло с ошибкой, то положительное тело if выполняться не будет, а выполнится тело false, в котором мы возвращаем все на круги своя: удаляем из таблицы tbl_texts запись, которую только что сделали: "удалить из таблицы tbl_texts запись, где идентификационный номе равен $this->in_text_id". И затем уже возвращаем код ошибки 31.
Рекомендую этот номер ошибки записать в class_utils как:
$err[31]="Не могу открыть файл для записи: (".$this->PATH_DATA."/".$this->in_text_id.")";
Понятно, почему так? В случае ошибки мы получим не только соответствующее сообщение, но и узнаем имя файла, который не удалось открыть, и полный путь к этому файлу. Этой информации более чем достаточно, чтобы разобраться в возникшей проблеме.
Вот так. Мы уж умеем добавлять, изменять и удалять данные в таблице рубрик. Умеем добавлять новый текст в базу сайта. Что нам еще надо? Я так думаю, что надо уметь удалять из базы ошибочный или устаревший текст. И снабдить текст атрибутом "hidden", чтобы прятать его от посторонних глаз, когда он не нужен на сайте, или просто еще не готов к публикации.
По-моему, пора вам давать небольшие домашние задания. Темы для заданий я буду черпать из прошлых упражнений -- для новичков, и буду давать одну задачку для "гурманов": она тоже будет основана на том, что мы уже знаем, но будет требовать некоторой сообразительности.
Первое задание. Как надо переписать функцию in_text_data_check() из прошлого урока, чтобы избежать присваивания нового текста к несуществующей рубрике?
Второе задание (для гурманов). Как должна выглядеть функция in_text_adapt() из прошлого урока, чтобы текст, добавленный через браузер в кодировке KOI-8, отлавливался, и преобразовывался в win-1251 (в win-1251 мы записываем текст на диск).
Свои варианты решений присылайте на форум, или мне лично (я их тогда сам выложу в форум). Там их и обсудим.
Вы знаете, что в классическом процессе создания программного продукта должно быть три звена: постановщик задачи — программист — кодировщик?
Постановщик — ставит задачу. Ставит ее на родном языке, да так, чтобы она всем была понятна. Особенно программисту.
Программист — ломает голову над алгоритмами решения тех задач, которые поставил постановщик задач. А потом вдалбливает эти алгоритмы в голову кодировщику.
Кодировщик — пишет всякие тексты, понятные только компьютеру. Это последнее звено в приготовлении программного продукта.
Сегодня, после того, как языки программирования поднялись до уровня обычного английского (а в некоторых случаях и родного, локального) языка, эти три ипостаси как-то незаметно слились воедино. Лично мне кажется, что это плохо. Но с этим ничего не поделаешь, поэтому и я тоже стал "сам себе постановщик–программист–кодировщик", хотя программировать (в чистом понимании этого процесса) мне больше нравится. Именно поэтому, кстати, у меня очень много заброшенных проектов, которые решены в голове, но не реализованы на бумаге. Ну что поделать... остается только надеяться, что у вас таких проектов будет меньше. Ну да ладно, что-то я разболтался сегодня. Перейдем к делу.
Прежде всего, давайте вот какой вопрос решим. Как сделать так, чтобы мы могли снимать и ставить материалы на сайт быстро и с легкостью?
Способов решения такой задачи настолько много, насколько много программистов на этой земле, я думаю. Но мы воспользуемся самым традиционным, как мне кажется, способом.
Раз у нас есть база данных, значит надо добавить в таблицу текстов флажок, указывающий на "видимость" материала. Этот флажок можно назвать, например, t_enable. Или t_on. Мне больше нравится t_enable.
Модифицировать таблицу мы уже умеем, поэтому разжевывать следующий запрос в SQL я не буду:
alter table tbltexts add column t_enable enum("Y","N") not null default "N";
Кто такой enum() и с чем его едят — вы можете прочесть тут: 6.2.3.3 The ENUM Type.
Теперь каждый добавленный в базу текст будет иметь флаг t_enable, который будет изначально устанавливаться в N (No).
Теперь, о чем вы уже наверняка догадались, надо написать функцию, которая будет включать или выключать текст на сайте. Назовем эту функцию in_text_enable(). Добавьте в class_in переменную in_text_enable:
$this->in_text_enable;
и приступим к написанию коротенькой функции.
function in_text_enable() { if($this->in_text_enable!='Y') $this->in_text_enable='N'; $this->in_text_id=(int)$this->in_text_id; $this->sql_query="update tbl_texts set t_enable='".$this->in_text_enable." ' where t_id='".$this->in_text_id."'"; $this-sql_execute(); if($this->sql_err) return(11); return(0); }
Как видите, ничего особенно умного тут придумывать не надо. Достаточно присвоить переменной in_text_id номер текста, переменной in_text_enable статус (Yes/No) и вызвать нашу свежеиспеченную функцию in_text_enable().
Какую проверку на ошибочные данные я не сделал в этой переменной? Правильно — не сделал проверку на существование текста, которому мы хотим сменить статус. И ничего страшного, я вам скажу. Это как раз тот случай, когда запрос в БД построен так, что никакого вреда случиться не может.
Посудите сами. Переменная in_text_enable может нести в себе только значение "N" или "Y": все другие значения напрочь отметаются первой строкой тела функции. Либо переменная равна 'Y', либо мы ей тут же присваиваем значение "N".
Честно говоря, эту проверку можно было бы тоже не производить — MySQL сам не пропустит значений, не входящих в перечислимый тип enum(). Но! Нам тогда надо было бы "прослешить" эту переменную, чтобы не допустить подстановки в нее взламывающего кода, а потом еще обрабатывать сообщение об ошибке от mysql, если значение окажется не 'Y' и не 'N'. Поэтому, проще привести данные к их точному виду и ни о чем не волноваться.
Переменная же in_text_id после второй строки тела функции не может быть ничем иным, кроме как целым числом. Это так же защитит нас от взлома.
Если же значение переменной in_text_id окажется ложным, и такого текста в нашей базе нет, то SQL-запрос обычно отработается, не внеся в базу никаких изменений и не возвратив никаких ошибок.
Так что, функция вполне защищена и полностью выполняет свою работу, не смотря на то, что состоит всего из нескольких строк. Теперь на нашем сайте будут видны только те тексты, которые должны быть видны, а не все подряд. Можно было бы на этом и закончить с текстами. Но мы для полноты сервиса добавим еще и функцию удаления текста из БД и физического удаления файла с диска.
Для удаления файла с диска в PHP (для UNIX) используется функция unlink(), которой в качестве аргумента передается полный путь к файлу, подлежащему уничтожению.
function in_text_remove() { $this->in_text_id=(int)in_text_id; $this->sql_query="delete from tbl_texts where t_id='".$this->in_text_id; $this->sql_execute(); if($this-sql_err) return(11); if(!unlink($this->PATH_DATA."/".$this->in_text_id)) return(32); return(0); }
Тоже простая функция, комментировать построчно я ее не стану. Лучше обратите внимание на порядок выполнения: сначала мы удаляем текст из БД, а только потом с диска, а не наоборот! Почему? А представьте себе, что будет, если мы сначала удалим файл с диска, а потом по какой-нибудь случайной причине не получится удалить файл из БД? В этом случае документ будет по-прежнему отображаться на сайте, а при его запросе пользователем будет выскакивать ошибка открытия файла на диске. Фу, как это не красиво!
В нашем порядке вполне допустимо, что из базы данные удаляются, а с диска — нет. В этом случае текст с сайта все равно пропадет, а нам придет сообщение об ошибке удаления файла, с которой мы можем не спеша разобраться.
Особенно надо следить за порядком добавления и удаления данных в тех функциях, которые могут выполняться по cron-у, т.е. автоматически. Ночью или в любое другое время, когда администратора может не быть рядом с компьютером. Никакие ошибки не должны повлиять на внешний вид сайта и его работоспособность.
Именно поэтому, мы должны были на нашем прошлом занятии сначала записать файл на диск, а потом уже добавлять его в базу данных. Но мы сделали наоборот, и я обязан объяснить почему. Мы не можем записать файл на диск, не узнав его ID. А узнать его ID мы можем только после добавления записи в базу. Казалось бы, что это заколдованный круг, но не тут-то было! У нас же теперь есть флаг t_enable, который защитит нас от ошибки. Даже если произошла ошибка записи на диск, то на сайте никаких изменений не произойдет, так как по умолчанию флаг t_enable установлен в 'N' и добавленный текст на сайте не показывается, до его подтверждения при помощи функции in_text_enable(). Вот такая военная хитрость!
И не забудьте добавить в файл class_utils описание новой ошибки:
$err[32]="Не могу удалить файл: ".$this->PATH_DATA."/".$this->in_text_id;
Предлагаю сегодня создать новый файл, в который мы поместим новый класс. Класс, отвечающий за визуализацию содержимого базы нашего сайта. Называться этот класс будет class_out, а храниться он будет в файле out.class. Мы его породим от класса class_utils, как и класс class_in.
Обратите внимание, только сейчас появилась новая ветка в нашем древе классов. До этого каждый последующий класс порождался от предыдущего, наследуя все его переменные и функции. А теперь мы открываем новую ветвь, которая унаследует функции от класса утилит, не имея уже никакого отношения к классу class_in -- другая ветвь нашего программного древа.
Думаю, что оформить новый класс для вас не составит никакого труда.
Теперь опять надо отложить в сторону кодирование и подумать над тем, что потребуется -- какие функции нам нужны для работы видимой части web-проекта. Прежде всего, нам потребуется вывод на сайт списка рубрик. Причем, каждое название рубрики должно быть ссылкой на страницу, которая показывает список всех текстов, имеющихся в данном разделе (рубрике).
Далее. Из первого вытекает, что нам нужна функция, способная скомпоновать названия всех текстов раздела в нужном порядке и выложить их на сайт в виде ссылок на сами документы, не так ли? Это будет вторая функция, которую мы напишем для класса class_out.
Пользуясь той же логикой, становится очевидно, что нам нужна функция, показывающая сам текст. Эту функцию напишем в последнюю очередь. Забегая немного вперед, хочу сказать, что после этих трех функций мы бросим программировать в чистом PHP и начнем "собирать" наш сайт уже с использованием html, styles и дры. Того, что будет написано -- уже достаточно для создания жизнеспособного сайта.
Уже после того, как заработает наш сайт, мы займемся его техническим украшательством: подписка и рассылка материалов, голосовалки, гостевая, всевозможная статистика, напишем свой счетчик посещений -- поработаем с графикой, и так дальше. Почему я все это сообщаю сейчас? Потому, что хочу поднять одну не очень важную, но достаточно интересную тему. Я хотел ее коснуться чуть позже, но посетитель форума по имени Const затронул ее в своем сообщении, и я обещал раскрыть подробнее свою позицию по этому вопросу.
В чем же, собственно, проблема. А в том, что классы можно рождать друг от друга хоть каждый раз, когда надо написать новую функцию. Удобно ли это? Или наоборот: разместить все перечисленные возможности сайта в один файл и тупо обращаться к нему с каждой html-страницы? Любая крайность -- плохо. Тут надо искать золотую середину. И будем мы ее искать вместе. Что предлагаю я. У меня уже выработалась своя техника составления древа файлов и классов. Она сильно похожа на ту, что я вам преподношу. Мы разделили функции общего назначения на три класса: vars, mysql, utils. Для удобства. Конечно, можно было их собрать в один файл и даже в один класс. Но это вы сможете сделать сами, если пожелаете. Тут никаких весомых доводов "за" или "против" привести, как мне кажется, не получится. "У каждого свой вкус: один любит арбуз, а другой -- свиной хрящик..."(с).
Далее мы написали класс добавления данных. Он у нас получился небольшой, но если бы мы на самом деле писали огромный проект, то функций добавления данных и функций управления данными было бы заметно больше. Стоило ли бы, в этом случае, разнести их на подклассы? Не думаю. Я бы не стал. Почему? Объясню. Когда мы пишем систему добавления и управления данными (back office это обычно называется), то никогда до конца не можем знать, в каком виде будут поступать данные и в каких комбинациях. И если мы начнем дробить систему добавления данных на разные классы, то рано или поздно начнем наталкиваться на ситуации, когда функции из одной ветки классов будут требоваться в классах других веток. К чему это все приведет? Правильно, приведет все это к большому бардаку!
Но при всем при этом, не следует забывать, что чем больше код программы, тем дольше она интерпретируется и выполняется. Универсальность, в любых ее видах, приводит к громоздкости. Как быть с этим? А очень просто. Достаточно оценить: как часто класс добавления или управления данными будет вызываться. Если это частота не превышает запроса в несколько секунд, то можно ничего не дробить на подклассы. Даже слабенький сервер спокойно обработает ваши запросы. Если же идет речь о более интенсивном использовании данного класса, но можно подумать об оптимизации.
А вот класс out мы как раз будем оптимизировать, путем разнесения некоторых функций в разные классы-файлы.
Как это будет выглядеть. Берем за точку отсчета class_out, в который добавим функции, требуемые для любой страницы сайта. Затем, породим от него class_out_title, в который занесем функции, вызываемые только на титульной странице сайта. От того же class_out породим еще ветвь, например, -- class_out_news, которая будет нести в себе функции, связанные с новостной страницей. И так далее. Для каждого типа страницы мы создадим свой подкласс, рожденный от общего class_out, который и будем вызывать на этих самый страницах. Я не сильно запутал вас?
Надеюсь, понятно, чего таким образом добьемся? Очевидно, что при запросе пользователем одной страницы, PHP придется интерпретировать только данные, имеющие непосредственное отношение к этой самой странице. Другие классы при этом -- отдыхают. Это очевидный плюс. А есть еще и не такая очевидная польза, но реальная, как показывает жизнь.
Представьте, что вы изменили функцию вывода новостей на странице. И при внесении изменений, совершили ряд синтаксических или логических ошибок. Разумеется, PHP будет ругаться на ваши ошибки, и вместо страницы сайта посетитель будет видеть ругань какого-нибудь MySQL. Но, в нашем случае, это будет видеть посетитель только новостной страницы. Все остальные страницы будут продолжать показываться корректно, пока вы чините новости. Согласитесь, это тоже важно, когда вы правите сайт "по живому", как это обычно делаю я. Кстати, ничего в этом хорошего нет -- в переделке сайта по живому, но это здорово экономит время, когда речь идет о каких-то мелочах.
Ничего, что я так долго не продолжал писать курс? Ну, у меня появилась небольшая надежда на то, что вы хорошенько осмыслили все написанное до сего выпуска и уже попытались сами предпринять какие-то действия.
Прежде чем перейти к визуализации нашего грандиозного проекта, хотелось бы отойти на пару занятий в сторону и отдельно рассмотреть еще одну интересную процедуру, которая, несомненно, нам тоже очень пригодится.
Как оказалось, мало кто умеет при помощи простой команды PHP mail() отправлять письма с вложенными файлами. Это очень популярный на сегодня вопрос по PHP, как оказалось. А между тем, все не так сложно, как кажется.
Давайте разберем этот интересный вопрос.
Что такое файл с вложениями?
Вы видели как выглядит исходный текст любого письма? Нет, не только текст, написанный человеком, а полностью -- со всей служебной информацией? Давайте посмотрим.
Я оправляю сам себе письмо с вложенным файлом MS Word, содержащим данный текст на данной стадии его написания. Закрываю файл...
Открываю обратно... Вот, я уже получил свое письмо.
В моем любимом TheBat посмотреть исходник письма можно по кнопке F9. И что же мы видим? Уж простите, для полноты ощущений я привожу почти весь исходный текст письма.
Return-Path:Received: from by atlanta.ru (CommuniGate Pro RULES 3.4.5) with RULES id 393466; Thu, 20 Dec 2001 14:20:36 +0300 X-Autogenerated: Mirror X-Mirrored-by: Received: from 195.58.33.146 ([195.58.33.146] verified) by atlanta.ru (CommuniGate Pro SMTP 3.4.5) with ESMTP id 393465 for atos@21.ru; Thu, 20 Dec 2001 14:20:28 +0300 Date: Thu, 20 Dec 2001 14:17:31 +0300 From: Ruslan Kurepin X-Mailer: The Bat! (v1.53bis) Personal Reply-To: Ruslan Kurepin X-Priority: 3 (Normal) Message-ID: <36657745688.20011220141731@21.ru> To: atos@21.ru Subject: text MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----------11552613C2F043A" ------------11552613C2F043A Content-Type: text/plain; charset=koi8-r Content-Transfer-Encoding: 8bit рТЙЧЕФУФЧХА! ч ЬФП РЙУШНП ЧМПЦЕО ЖБКМ "РЙУШНБ У ЧМПЦЕОЙСНЙ.doc" чЕУПН Ч 20 480 bytes. -- у хЧБЦЕОЙЕН, тХУМБО лХТЕРЙО mailto:atos@21.ru http://caricatura.ru http://www.21.ru ------------11552613C2F043A Content-Type: application/msword; name==?koi8-r?B?0MnT2M3BINMg18zP1sXOydHNyS5kb2M=?= Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename==?koi8-r?B?0MnT2M3BINMg18zP1sXOydHNyS5kb2M=?= 0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAABAAAAIwAAAAAAAAAA EAAAJQAAAAEAAAD+////AAAAACIAAAD///////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////s pcEAOSAZBAAA8BK/AAAAAAAAEAAAAAAABAAAGgsAAA4AYmpiav3P/c8AAAAAAAAAAAAAAAAAAAAA AAAZBBYAIhIAAJ+lAACfpQAAjQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//w8AAAAA AAAAAAD//w8AAAAAAAAAAAD//w8AAAAAAAAAAAAAAAAAAAAAAGwAAAAAAKgAAAAAAAAAqAAAAKgA AAAAAAAAqAAAAAAAAACoAAAAAAAAAKgAAAAAAAAAqAAAABQAAAAAAAAAAAAAALwAAAAAAAAA3gEA AAAAAADeAQAAAAAAAN4BAAAAAAAA3gEAAAwAAADqAQAADAAAALwAAAAAAAAATQMAALYAAAACAgAA AAAAAAICAAAAAAAAAgIAAAAAAAACAgAAAAAAAAICAAAAAAAAAgIAAAAAAAACAgAAAAAAAAICAAAA AAAAzAIAAAIAAADOAgAAAAAAAM4CAAAAAAAAzgIAAAAAAADOAgAAAAAAAM4CAAAAAAAAzgIAACQA AAADBAAAIAIAACMGAADCAAAA8gIAABUAAAAAAAAAAAAAAAAAAAAAAAAAqAAAAAAAAAACAgAAAAAA AAAAAAAAAAAAAAAAAAAAAAACAgAAAAAAAAICAAAAAAAAAgIAAAAAAAACAgAAAAAAAPICAAAAAAAA RAIAAAAAAACoAAAAAAAAAKgAAAAAAAAAAgIAAAAAAAAAAAAAAAAAAAICAAAAAAAABwMAABYAAABE AgAAAAAAAEQCAAAAAAAARAIAAAAAAAACAgAACgAAAKgAAAAAAAAAAgIAAAAAAACoAAAAAAAAAAIC AAAAAAAAzAIAAAAAAAAAAAAAAAAAAEQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
...можно я тут вырежу десяток страниц этого мусора? ;-)
BAQAAAAYAAAD///// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoAAAAAAAAATwBiAGoAZQBj AHQAUABvAG8AbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYA AQD///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAANBMFaNHicEB0EwVo0eJwQEAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAP///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAEAAAD+//////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// ////////////////////////AQD+/wMKAAD/////BgkCAAAAAADAAAAAAAAARhgAAADE7urz7OXt 8iBNaWNyb3NvZnQgV29yZAAKAAAATVNXb3JkRG9jABAAAABXb3JkLkRvY3VtZW50LjgA9DmycQAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAA= ------------11552613C2F043A--
Мы видим некий бардак, который, как это не странно, легко читается...
Не буду строить из себя умника, а отошлю вас к статье по этому адресу: http://www.bryansk.ru/pismo.html, в которой коротко и очень внятно расписывается все, что нужно знать для чтения письма в исходнике. А умение читать исходник -- неминуемо приводит к умению создавать подобное самостоятельно.
Не стану упрощать задачу. Будем писать утилиту (если быть точным, то -- набор функций), которая даст нам возможность с легкостью создавать и отсылать из наших скриптов письма любой сложность и "мультипАртовости".
Набор наших функций мы помещаем в класс utils, договорились?
Определимся с переменными и константами.
// Статические значения var $mail_boundary = "----_=_NextPart_000_01C1.94F.653432C1"; var $mail_boundary2="----_=_NextPart_001_01C1.94F.653432C1"; // может пригодиться var $mail_priority=3 // Приоритет письма // А это вполне переменные значения var $mail_from; // Отправитель var $mail_to; // Получатель var $mail_subj; // Тема письма // Еще нам потребуется: var $mail_body_plain // plain-text -- обычный текст письма var $mail_body_html // html-формат письма var $mail_body; // все письмо целиком var $attach // вложенные файлы var $attach_type // типы (форматы) вложенных файлов
Надеюсь, понятно, что перед отправкой письма надо заполнить поля: от кого, кому, тему и так дальше? Если вы планируете использовать все время одни и те же заголовки, то можете их прописать изначально, как мы сделали с boundary.
Теперь разберемся с функциями, которые будут нам "собирать" письмо в один такой корявый текстовый файл.
Нам для этого надо суметь: создать заголовок письма, преобразовать присоединяемые файлы и соединить все получившееся. Итак, создание заголовка.
function mail_header() { $header="Reply-To: ".$this->mail_from. "\n"; $header.="MIME-Version: 1.0\n"; $header.="Content-Type: multipart/mixed; boundary=\"".$this->mail_boundary. "\"\n"; $header.="X-Priority: ".$this->mail_priority. "\n"; return($header); }
По-моему, тут пояснять нечего. Про каждый параметр создаваемого заголовка вы только что прочли тут: http://www.bryansk.ru/pismo.html.
Бросили заголовок, перешли к телу письма. Тело современного электронного письма с вложениями (attachments) не редко состоит из трех(!) видимых частей. И если вы пользуетесь правильным почтовым клиентом, то вы их всегда видите:
Создать первые две части можно одной функцией, т.к. обе они представляют собой обычный текст.
function mail_body($html, $plain) { $this->mail_body_html=$html; $this->mail_body_plain=$plain; }
Честно говоря, можно было этой функции вообще не писать, т.к. она производит всего лишь простое присваивание, которое можно делать точно так же перед отправкой каждого письма. Никакой разницы, вроде? Действительно, зачем вызывать для прямого присваивания дополнительную функцию. Но я предпочитаю подобные вещи оформлять отдельной функцией для того, чтобы в последствии можно было единым махом добавить что-то в систему отправки писем -- фильтры на запрещенные слова, например. Или, если ваши скрипты обслуживают клиентов, то можно в эту функцию добавить сквозную нумерацию писем -- прицеплять к тексту что-то вроде: "Идентификационный номер данного письма 00012424 от 02.12.02. Если вы нуждаетесь в дополнительных пояснениях, напишите в нашу службу поддержки, указав ID письма...", -- да и снабдить еще эту функцию логированием всех писем, чтобы знать -- когда кому и что было отправлено. И так далее. Как видите -- маленький "перезаклад" сегодня может выйти солидной экономией времени в будущем.
Переходим к следующему пункту -- форматируем файлы-вложения. Сейчас у вас головка-то закипит... Я, конечно, попробую все разжевать подробно, но уж как получится. В крайнем случае -- плюньте на непонятное слюной и просто используйте то, что написано.
Присоединенные файлы могут создаваться двумя путями -- генерироваться скриптом или читаться из файла. Нам надо предусмотреть для себя оба этих случая.
Для хранения аттачей будем использовать ассоциативный массив. Это массив, где в качестве номера ячейки используется не порядковое число, а строка символов.
function mail_attach($name, $type, $data) { $this->mail_attach_type[$name]=$type; $this->mail_attach[$name]=$data; }
Этой функцией мы загоняем в два разных массива: тип данных и сами данные, имена ячеек в этих массивах при этом совпадают, что нам будет очень полезно. Удачный случай показать эффективное применение ассоциативных массивов.
Эта функция удобна, когда мы на ходу генерируем данные для вложенного файла. Тогда вызов функции звучит логично, например:
начало цикла
$this->mail_attach($err[$n],"text/html", "<b>Ошибка такая-то, </b><br><br>примите меры!");конец цикла
Т.е. наши вложенные файлы будут иметь формат html и нести небольшой текст с номером ошибки. Особенно это удобно, когда делаешь "на лету" какое-нибудь преобразование. Например, я бы захотел высылать подписчиками маленькие варианты карикатур с caricatura.ru. Зачем же мне после преобразования карикатуры или нанесения на нее защитной надписи сохранять файл на диске, генерируя уникальное имя, потом читать его, а потом за собой удалять? Вот тут удобно данные передать сразу в функцию отправки.
А как быть с файлами с диска? Да очень просто.
function mail_fileattach($path, $type) { $name=ereg_replace("/(.+/)/","",$path); if(!$r=fopen($path,'r')) return(1); $this->mail_attach($name, $type, fread($r,filesize($path))); fclose($r); return(0); }
Идея понятна?
Мы передаем функции mail_fileattach путь к файлу на диске (или в Сети) и его тип (формат).
Первой строкой мы отцепляем от пути только название. Вернее -- удаляем все символы до названия файла, чтобы получить $name.
Второй строкой открываем файл на чтение, не забыв поставить проверку на открытие -- зачем нам надо высылать пустой аттач, мы лучше администратору вышлем самое грозное из наших предупреждений -- 1.
А третьей строкой вызываем написанную до этого функцию, которой передаем знакомые нам параметры, только в качестве третьего параметра зачитываем данные из файла.
Не забываем закрыть файл!
Вот тут очень важно заметить следующее. Размер памяти, в которой выполняется скрипт, может сильно не совпадать с размером файла или файлов, которые вы засовываете в письмо! Если не ошибаюсь, то по умолчанию объем выделяемой памяти под каждый скрипт не превышает 5Мб. Это зависит от вашего провайдера. Конечно, может возникнуть желание обойти это ограничение. Сие вполне возможно: открываешь сокет, соединяешься с почтовым сервером, договариваешься с ним, закидываешь в него хидеры и начинаешь методично зачитывать ему все содержимое жесткого диска. Но этот метод -- не для этого занятия.
Теперь надо преобразовать эти данные из массивов в понятный почтовым службам текстовый формат. И отправить письмо адресату.
Чтобы совсем уж не размениваться на мелочи, в следующей функции мы полностью сформируем текст нашего письма. Следите за мыслью...
function mail_body_create() { $this->mail_body="\n\n"; $this->mail_body.=$this->body_plain; if(strlen($this->body_html)>0) // html-версия текста письма есть! { $this->mail_body.="--".$this->boundary."\n "; $this->mail_body.="Content-Type: multipart/alternative; boundary=".$this->mail_boundary2."\n\n"; $this->mail_body.=$this->mail_body_plan."\n"; $this->mail_body.="--".$this->mail_boundary2."\n"; $this->mail_body.="Content-Type: text/plain\n"; $this->mail_body.="Content-Transfer-Encoding: quoted-printable\n\n"; $this->mail_body.=$this->body_plain."\n\n"; $this->mail_body.="--".$this->boundary2."\n"; $this->mail_body.="Content-Type: text/html\n"; $this->mail_body.="Conent-Transfer-Encoding: quoted-printable\n\n"; $this->mail_body.=$this->mail_body_html\n\n"; $this->mail_body.="--$boundary2--\n"; } else // html-версии письма нет! { $this->mail_body.="--".$this->boundary."\n"; $this->mail_body.="Content-Type: text/plain\n"; $this->mail_body.="Content-Transfer-Encoding: quoted-printable\n\n"; $this->mail_body.=$this->body_plain."\n\n--"; $this->mail_body.=$this->boundary. "\n"; } reset($this->attach_type); while(list($name, $content_type)=each($this->attach_type) ) { $this->mail_body.="\n--".$this->boundary."\n"; $this->mail_body.="Content-Type: $content_type\n"; $this->mail_body.="Content-Transfer-Encoding: base64\n"; $this->mail_body.="Content-Disposition: attachment;"; $this->mail_body.="filename=\"$name\"\n\n"; $this->mail_body.=chunk_split(base64_encode($this->atcmnt[$name]))."\n"; } $this->mail_body.= "--".$this->boundary. "--\n"; return(0); }
Ну вот, собственно, мы и сформировали наше письмо в переменной $this->mail_body.
Что в этой функции интересного? Да ничего интересного. Можно ее даже не разбирать на составляющие. Она очень понятна, если ее внимательно прочесть. Лучше попробуйте создать с ее помощью письмо и посмотрите, что получится. Результат очень интересно будет сравнить с табличкой составляющих почтового сообщения, на которую я давал ссылку в предыдущих главах.
Осталось письмо отправить. Ну, это проще простого. Где-то в глубине наших скриптов присваиваем значения переменным:
$my->mail_from="roma"; $my->mail_to="roma "; $my->mail_subj="test message";
и отправляем письмо с помощью стандартной функции PHP mail().
На этом я заканчиваю повествование об отправки файлов по e-mail, но убежден, что к этому вопросу еще вернусь. Так как остался совершенно не доволен качеством рассказа. Придется переписать функцию mail в удобоваримый формат, но это сделаю чуть позже, когда сам обкатаю ее и найду все неудобства ее использования.
А напоследок приведу весь текст со страницы http://www.bryansk.ru/pismo.html, на случай ее пропажи с указанного URL. Изучайте!
Что есть в письмах кроме писем? Много ли можно почерпнуть из обычного письма? Если отправитель вам давно и хорошо известен, то, вероятно, ничего существенно нового не узнать, но если отправитель - неизвестный аноним, можно попробовать определить его географическое положение, реальное имя, используемое программное обеспечение, имя его компьютера и т.д. Для этого необходимо всего-то лишь проанализировать заголовок его письма. Заголовок письма всегда идет в начале письма. Признаком конца заголовка является пустая строка. Всё идущее после этой строки является текстом письма и, как правило, не обрабатывается почтовыми службами. Заголовок состоит из нескольких полей, а поле, в свое очередь, - из двух частей: названия и значения, разделяемые между собой двоеточием, например field:value. Займемся теперь детальным изучением наиболее часто встречающихся полей:
Приведенная краткая таблица не претендует на абсолютную полноту. Существует масса устаревших или, напротив, новых (экспериментальных) полей заголовка e-mail. Например, Microsoft использует поля "X-MimeOLE" и "X-MSMail-Priority", которые являются нестандартными. Строго говоря, все поля, начинающиеся с "X-" (так называемые, private-поля), не являются стандартными, а используются по внутрифирменным соглашениям. Существуют редко используемые стандартные поля, например: "Keywords", "Comments", "Content-Description" - служащие для описания содержимого письма. Но в подавляющем большинстве случаев вышеприведенной таблицы вполне достаточно для прочтения и понимания содержания заголовка. Впрочем, для полноты картины эту таблицу желательно дополнить более подробным описанием стандарта MIME (Multipurpose Internet Mail Extensions), поскольку многие поля так или иначе относятся именно к MIME, информацию по которому можно найти в RFC 1521 и RFC 1522. Итак, MIME предназначен для задания способов передачи посредством электронной почты различной нетекстовой информации, а также больших объемов информации, которые необходимо посылать в нескольких письмах, разбив на кусочки. Следует заметить, что под нетекстовой информацией надо понимать не только бинарные файлы, но и тексты в нестандартной кодировке, то есть в отличной от ASCII, описанной в ANSI X3.4-1986. Одно из основных полей, касающихся MIME, это "Content-Type". Возможные его значения приведены в таблице.
В поле "Content-Type" нужно указать подтип информации. Например, для поля "text" используются в основном подтипы "plain" - означающий, что текст обычный и неформатированный, и "html" - указывающий на то, что сообщение в формате HTML. Если отправляется сообщение в формате HTML, то правильная почтовая программа создаст две части письма: в "text/plain" и "text/html", чтобы в случае непонимания подтипа "html" на получающей стороне не возникло неудобств. С другой стороны, правильная почтовая программа получателя должна показать письмо в максимально информативном виде и поэтому, пропустив "text/plain"-часть сообщения, отобразит "text/html". В общем случае, текст может быть форматирован любым способом, понимаемым обеими сторонами. Это декларировано в RFC 1341. В поле "text" также указывается кодовая таблица. Например: "Content-type: text/plain; charset=us-ascii". Для поля "multipart" можно указывать подтипы: "alternative" - на случай, если несколько частей представляют одно и то же, а надо выбрать одну из них для отображения; "parallel" - если надо отобразить части одновременно; "digest" - если каждая часть имеет тип "message"; и некоторые другие подтипы. Для поля "message" основной подтип - "rfc822". Еще пара используемых подтипов: "partial" - для посылки части сообщения и "External-body" - для передачи, например, объемной информации путем ссылки на внешний источник данных. Подтип поля "image" задает графический формат, в котором пересылается изображение. Основные форматы - jpeg и gif. Поле "audio" имеет основной подтип "basic". Поле "video" - основной подтип "mpeg". Поле "application" имеет основной подтип "octet-stream" в случае обычных бинарных данных. Если посылается файл известного типа, то указывается его тип. Так, для MS Word документов Outlook пишет "application/msword", а для MS Excel "application/vnd.ms-excel". В соответствии с RFC 822, все письма, по умолчанию, передаются простым текстом в кодировке US-ASCII, что соответствует "Content-type: text/plain; charset=us-ascii". Следующее важнейшее поле, касающееся MIME это "Content-Transfer-Encoding". Служит оно для указания способа кодирования, в случаях, когда посылаемое письмо содержит что-либо не умещающееся в рамках US-ASCII. Стандартизованы следующие способы:
Подробно касаться методов "quoted-printable" и "base64", равно как "uuencoding" и некоторых других, в этой статье касаться не станем, поскольку тема это довольно объемистая. Разработчики почтовых систем могут использовать свои (оригинальные) способы кодирования, с тем лишь условием, чтобы принимающая сторона смогла корректно декодировать информацию. Вторая задача MIME - это стандартизация разбиения больших писем на несколько кусочков. Для этого в поле "Content-Type" после значения "multipart/<subtype>" указывается строка - уникальный ограничитель фрагментов "boundary=<boundary string>". А затем перед каждым фрагментом пишется эта строка, предваренная двумя минусами, а в конце фрагментации еще одна, завершающаяся такими же двумя минусами. Не вдаваясь в дальнейшее описания стандартов, коснемся нескольких практических вещей. Как добраться до заголовка письма?
В других почтовых программах читатель может попытаться разыскать заголовок письма самостоятельно. Скорее всего, ему это удастся. Маленькое резюме. Что же действительно интересного можно увидеть в заголовке письма?
Но самое главное - изучая заголовки, приходящих писем, вы разберетесь в механизме работы одной из старейших интернет-служб. Чего я вам и желаю! |
С чего начать визуализацию сайта? С начала начать! И пусть заглохнут те, кто считает разбор будущего дизайна последним делом. Ничего подобного! Дизайн (я имею в виду только его функциональную часть) — вещь наиважнейшая. Код можно всегда поправить, не взирая на лица. А вот с дизайном это сделать всегда сложнее.
Ну, рисовать дизайн я вас учить не буду, т.к. сам не умею. А вот как сделать дизайн текстовый — это мы попробуем. Заранее прошу прощения у всех, кто прислал мне свои варианты дизайна для нашего тестового сайта, но мне подумалось, что не стоит опираться на чей-то вкус: о графическом дизайне так много всегда спорят. А текстовые дизайны я делаю давно и, как мне кажется, они вполне у меня получаются.
"Итак, начало... начало — это встреча со зрителем...", — как говорил главный герой "Мы из джаза", — "Зритель — наш старый знакомый. Мы же не орем ему ЗДОРОВО! Мы говорим мягкое — привет!".
Это я к чему? А, это я к тому, что любой сайт начинается всегда с заголовка. И хоть законы жанра заставляют нас влупить в него неимоверного размера графическую бляшку, которую все помпезно называют фирменным знаком или еще как-то, но надо при этом стремиться к тому, чтобы заголовок был как можно более функциональным.
Лично я всегда в душе хвалю сайты, которые позволяют передвигаться по страницам и разделам, не дожидаясь полной загрузки текущей страницы. Честно говоря, я сам далеко не всегда следую всем правилам, но мы будем стараться их придерживаться.
Стоп! Я не прав. Я же совсем забыл вам рассказать, что у нас каждая страничка сайта должна собираться из кирпичиков. Вернее, из блоков. И эти блоки должны храниться в разных файлах, чтобы в любой момент мы могли из этих блоков построить очередную страницу или внести изменения во все страницы сайта одновременно.
Мы будем действовать по принципу SSI. Кто не знает что такое SSI — отправляю читать про SSI. Про эту технологию написаны сотни статей, хотя она вся состоит из нескольких команд. В двух словах: SSI диктует http-серверу — какие файлы нужно подключить (или подлинковать, как говорят некоторые программисты) в текущий, прежде чем отдать файл пользователю. Сама технология SSI нам не понадобится, мы легко заменим функции SSI функциями PHP.
Можно достаточно мелко дробить сайт на составляющие, но мы это сделаем по мере необходимости. Пока можно остановиться на основных составляющих почти любого сайта:
Заголовок — это для нас просто: текстовая строка с названием сайта и/или текстовый блок. Для примера можно посмотреть на сайт http://caricatura.ru/parad/. Видите, слева логотип, справа пояснительный текст, ниже отчеркивающая строка. Это и есть заголовок, который лежит в папочке inc под названием top.inc. У нас тоже есть папочка inc, в которую мы положим свой такой заголовок.
Хочу еще раз напомнить, что папку с подключаемыми файлами (в отличие технологии SSI) мы храним вне директории WWW или как называется папка с доступными по http страницами. Иначе злоумышленник сможет прочесть содержимое inc-файлов и найти способ взломать сайт.
Что вообще хорошо иметь в файле заголовка? Я предпочитаю иметь там четыре основные составляющие сайта:
В принципе, в заголовок можно включить и описание стилей (CSS), но я предпочитаю хранить стили в отдельном файле и подключать их стандартной строкой в описании html-заголовка:
<link rel=stylesheet type="text/css" href="/style.css">
Так вот. В папочку inc кладем файл top.inc с нашим заголовком. Пусть для начала он выглядит просто:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>php.kurepin.ru</title> <link rel=stylesheet type="text/css" href="/style.css"> </head> <body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="#ffffff"> <table width="100%" cellspacing="0" cellpadding="3" bgcolor=#cccccc> <tr><td align=center> <b>Программируем на PHP.</b> Проект номер один! </td></tr></table>
потом этот файл не раз подвергнется модификации, поэтому сильно сейчас на него не стоит обращать внимание.
Перейдем к следующему фрагменту сайта — последнему. Почему последнему? А потому, что: "поставив открывающую скобку — сразу поставь закрывающую", — помните мой принцип? Вот и тут он должен сработать без осечки. Тем более, что написать фрагмент с копирайтом еще проще. Назовем файл bottom.inc, раз у нас верхний назывался top.inc.
<table width="100%" cellspacing="0" cellpadding="3"> <tr><td align=center> copyright (c) 2001-2002 <a href=http://kurepin.ru>Руслан Курепин</a> </td></tr></table> </body></html>
В общем, сложив эти два файла мы уже получим полноценную http-страницу, оформленную по всем правилам языка html. Но мы пока ничего складывать не будем, нам еще надо "впендюрить" кучу других фрагментов, прежде чем переходить к сборке нашей первой страницы.
Продолжим формирование типовой страницы сайта. Следующим "кирпичиком" предлагаю создать полоску быстрой навигации по сайту.
На мой взгляд, очень удобно, когда сайт снабжен быстрой навигацией, не смотря на любые меню и подсказки. Особенно это удобно для постоянных посетителей и для сайтов, перемещение по которым производится достаточно часто. Предлагаю следующий inc-файл так и назвать — navigator.inc. И представим мы его в виде горизонтальной полосы.
<table width="100%" cellspacing="0" cellpadding="3" bgcolor=#eeeeee> <tr> <td align=center> <a href=/>начало</a> </td></tr></table>
Как видите, горизонтальный навигатор быстрого перемещения по сайту у нас содержит пока только одну ссылку "начало", но постепенно он обрастет у нас всевозможными функциями, а может еще и трансформируется в полноценное меню с выпадающими подменю. Посмотрим.
Поскольку, навигация важна не только вверху страницы, но и внизу — чтобы после прочтения текста не листать страницу наверх — мы этот же навигатор будем вставлять и внизу страницы, сразу над копирайтом.
Теперь сборку нашего сайта можно представить вот так:
То есть, каждая страница собирается из четырех обязательных частей и одной изменяющейся, в зависимости от ситуации. Конечно, это все не жестко, и каждую страницу можно спланировать по-своему, но во всем должен быть свой основополагающий принцип.
Чего у нас еще не хватает, помните? Еще нужны места под подробное/контекстное меню и рекламные блоки. Давайте создадим еще несколько (пока пустых) файлов:
adv_top.inc — для горизонтальной рекламы вверху
adv_bottom.inc — для горизонтальной рекламы внизу
main_menu.inc — для главного меню
Ну, куда вставить рекламу — догадались сразу, не так ли? Теперь наша схема выглядит уже вот так:
А куда же мы денем меню? Придется нам распланировать и серединку — "рабочее пространство".
Хочу только напомнить, что расписываю подробно процесс создания дизайна не для того, чтобы научить делать web-дизайн, а чтобы показать процесс планирования и "рубки" сайта на самостоятельные составляющие для последующего их эффективного использования.
Мудрить не будем, разделим вертикально рабочую площадь на две неравные части: правую отдадим под меню, определив ей фиксированную ширину а левую оставим для сменяющего контента.
<table width="100%" cellspacing="0" cellpadding="3"> <tr><td> текст страницы </td><td width=200 bgcolor=#eeeeee> главное меню<br> </td></tr></table>
Определим пока ширину меню в 200 точек, а дальше видно будет.
В какой же файл нам положить этот текст... а ни в какой! Этот текст мы будем заряжать в каждый новый html-файл. Как это будет — скоро увидите.
Хотелось бы обратить ваше внимание на одну интересную тонкость web-дизайна. Наверняка вы обращали внимание, что на некоторых сайтах страница появляется только после полной загрузки, а на других — постепенно. Это связано с очень многими тонкостями. Попробую перечислить те, что знаю я.
Браузер.
Каждый браузер обладает своими особенностями. Скажем, Netscape Navigator (NN) очень любит всосать в себя все, что отдает web-сервер, и только после получения последнего байта сгенерировать страницу. Это относится к тем версиям NN, которые мне были известны на момент написания этого текста.
Microsoft Internet Explorer (MSIE) ведет себя иначе. Начиная с четвертой или пятой версии он достаточно легко производит рендеринг страницы во время ее загрузки, изменения размера браузера и даже (чего не делает NN) реагирует на изменения свойств объектов уже загруженной страницы, вызываемые, скажем, JavaScript (JS).
Верстка сайта.
В конце концов, от верстки зависит большая часть свойств поведения страницы при загрузке ее в браузер. Об этом не следует забывать и желательно оттестировать загрузку страниц как на медленном, так и на быстром каналах связи.
Что над помнить при форматировании страницы? Главное — помните о таблицах! Таблицы определяют 99% манеры появления страницы в окне браузера. Вряд ли браузер начнет показывать таблицу на сайте, прежде чем получит ее заключительный тэг. Только наличие заключительного тэга </table> дает браузеру понять, что других ячеек таблицы не ожидается и можно приступить к правильному отображению таблицы.
Из вышеизложенного не трудно догадаться, что сложность распознавания браузером контента сильно зависит от уровня вложенности таблиц друг в друга.
Первое — старайтесь не устраивать из таблиц русских матрешек. Это может сильно сказаться на слабых компьютерах.
Второе — если ваша таблица имеет много колонок — старайтесь указывать ширину колонки. Лучше всего в абсолютных цифрах. На худой конец — в процентном соотношении. Браузеру будет легче вычислить ширину каждого столбца, отформатировать содержание каждой ячейки и произвести разумную деформацию таблицы, если содержание не влезает в заявленную ширину.
Функции PHP.
Да-да, друзья мои. Очень даже можно повлиять на порядок загрузки страницы при помощи функций PHP. Возможностей не так много, но они есть. И первая из них — функция flush().
По умолчанию, PHP сначала формирует в памяти всю html-страницу, а потом уже отдает ее apache, который в свою очередь отправляет ее пользователю. Данный порядок выполнения можно отключить в конфигурационном файле PHP, но делать это крайне не рекомендуется. По той же причине, по которой браузер не хочет показывать таблицы до полной ее загрузки: кто знает, что там дальше в этом файле написано.
Вот функция flush() как раз и позволяет форсировать отдачу уже созданного html-фрагмента пользователю. Это особенно удобно, когда страница долго формируется. Поясню на примере.
Скажем, сайт представляет собой базу данных по всем автомобильным запасным частям фирмы Mercedes Bens. Сайт предлагает возможность поиска нужной запчасти по ее описанию в огромной базе данных, находящейся в другом городе. На это требуется время & mdash; несколько секунд. Чтобы сэкономить время пользователю и избежать эффекта "зависания сайта", мы читаем из файла заголовок страницы, после чего вызываем принудительную его отправку функцией flush(). Пока пользователь тянет по модему заголовок, рисунок, меню и другую муру, база успевает найти деталь, кою мы и отправляем следом пользователю. Идея понятна?
Этот пример можно смоделировать. Напишем код.
<div align=center> Заголовок<br> (ждем 10 секунд)<p> <br> Основной Текст<p> Копирайт (c)2002<p> </div>
И другой код
<div align=center> Заголовок<br> (ждем 10 секунд)<p> <br> <? flush(); ?> <? sleep(10); ?> <br> Основной Текст<p> Копирайт (c)2002<p> </div>
Как видите, они отличаются только наличием вызова flush().
Как отработает страница с первым кодом? Она появится у вас сразу и целиком через 10 секунд после обращения к серверу.
А вторая? А вторая напишет вам "Заголовок", а через 10 секунд покажет остальное. Не верите? Смотрите сами (обновите несколько раз каждую страницу, чтобы убедиться в эффекте):
http://kurepin.ru/php/functions/no_flush.php
http://kurepin.ru/php/functions/flush.php
Во избежании неприятностей не вставляйте flush() без надобности, и уж тем более не вызывайте эту функцию посреди таблиц. Это поставит раком половину версий NN и ваша страница будет похожа на японский иероглиф, а то и вообще не догрузится.
Вот на такие особенности открытия страниц желательно обращать внимание.
Если вы ничего не упустили из предыдущих глав, то у вас в директориях лежит все, чтобы сложить первую страничку нашего наиграндиознешего проекта.
Честно говоря (обратите внимание на название выпуска), я сегодня собирался посвятить выпуск некоторым особенностям такого удобного и современного html-инструмента, как стили (CSS), но подумалось мне, что измотал я вас уже крепко -- надо бы и результат какой-то продемонстрировать. Да? Да! Значит, на вашей улице сегодня праздник: собираем первую страницу нашего проекта! Кстати, хочу сразу предупредить всех, кто будет читать этот курс уже после его написания -- вам повезло меньше. Дело в том, что все последующие шаги построения сайта и модернизации скриптов вы воочию не увидите, вам достанется только конечный результат. Ну что поделать, это тоже будет не так мало, как мне кажется.
Итак. Нам для построения страницы потребуется:
1. Подключиться к подходящему классу (думаю, что out.class нам подойдет);
2. Активировать соединение с mysql;
3. Сгенерировать невидимую "шапку" для браузера;
4. Подключить все нужные нам статические файлы из папки inc;
5. Выложить файл в Сеть и полюбоваться результатом;
В общем, у нас все для этот есть, кроме пункта 3 -- шапки для браузера.
Что же это такое? А дело в том, что браузер, прежде чем принимать данные, должен сначала получить информацию о том, какие данные вы собираетесь ему передать. Ведь если вы были внимательны, то видели, что браузер может показывать не только html-документы, но и работать просмотрщиком графических файлов, звуковых, может показывать содержимое диска, ftp-каталога, показывать plain-text и выполнять другие различные функции. И если мы хотим быть вежливыми мальчиками и девочками, то нам надо научиться предупреждать браузер о том, что мы в него хотим впихнуть.
Надо заметить, что современных браузеры на 90% умеют сами различать тип передаваемой информации. Но, во-первых, 90% -- это не 100%, во-вторых, существуют правила хорошего тона даже у программистов, а в-третьих, заголовок для браузера может говорить не только о типе данных, но и передавать другие команды. Например, редирект (переход на другую страницу) или время кеширования страницы.
Вот последним мы сейчас быстренько и разберемся. Почему быстренько? А потому, что хидеры (как их проще всего называть -- от headers) проще запомнить, чем понять. Особенно что связано с кешированием. Смотрите сами.
В классе utils.class создадим функцию, которая будет заряжать каждую страницу стандартными заголовками кеширования. Помните, в файле vars.class мы когда-то запаслись переменной $CACHE_TIME? Вот ее мы сейчас и используем.
function html_headers() { header( "Cache-Control: max-age=". $this->CACHE_TIME_ALL.", must-revalidate" ); header( "Last-Modified: " . gmdate("D, d M Y H:i:s", time()-3600) . " GMT"); header( "Expires: " . gmdate("D, d M Y H:i:s", time()+$this->CACHE_TIME_ALL) . " GMT"); header( "Content-type:text/html"); }
Поясняю. Первые три строки говорят браузеру (а частенько и proxy-серверу), что страница наша была создана еще при царе горохе, но хранить ее и считать актуально надо $CACH_TIME секунд. Например, 300 -- 5 минут. Это значит, что посетитель нашего сайта может беспрепятственно посещать знакомые ему страницы, не выкачивая их из Сети повторно, сколько угодно раз в течение заданного времени. По истечение заявленного времени браузер вытянет новую версию страницы, т.к. мы решили, что за прошедшее время страница могла потерять свою актуальность.
Кеширование страниц -- очень удобный инструмент, которым можно облегчить или страшно усложнить жизнь веб-серферу. Я бы рекомендовал всем, даже тем у кого контент на странице обновляется часто или даже генерируется уникальный при каждом заходе, создавать хоть небольшой временной буфер кеширования, чтобы пользователь мог беспрепятственно вернуться на предыдущие страницы браузерной кнопкой назад, не выкачивая повторно уже полученный материал.
Прежде чем перейти непосредственно к собиранию страницы, позвольте пояснить еще буквально пару распространенных моментов, связанных с кешированием.
Первое. Если у вас есть форма для заполнения чего-либо (например форма заказа или регистрации), данные которой проверяются на ошибки, кешировать подобную форму просто необходимо. И чем дольше и сложнее форму заполняют, тем больше должно быть время кеширования. Это нужно для того, чтобы пользователь, получивший от вас сообщение об ошибке, мог нажать "назад" и попасть обратно на страницу с формой, где все поля уже им заполнены. Конечно, если вы не возвращаете пользователя сами на нужную страницу, подставив в заполненные поля соответствующие им данные.
Второе. Если вы используете на своем сайте баннерную рекламу, надо иметь в виду, что разные рекламные движки по-разному обновляют/подгружают новые баннеры при хождении по кешированным страницам. Например, сети на основе движка Rotabanner не загружают новые баннеры, если вы вернулись на закешированную страницу. А сети на основе движка BannerBank ведут себя полностью противоположно. И то и другое имеет свои преимущества и большой проблемы не представляет, но это надо иметь в виду, чтобы потом не говорить: "А вот такая-то сеть плохая, она засчитывает мне только 50% показов".
Ну что, лирика кончилась, пора за дело?
<? require("/home/atos/php.kurepin.ru/req/out.class"); $my=new class_out; $my->sql_connect(); $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr><td> текст страницы<br> </td><td width=200 bgcolor=#eeeeee> главное меню<br> </td></tr></table> <? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>
Файл называется index.php и лежит в корневой директории нашего сайта. Не верите? Смотрите сами: http://php.kurepin.ru.
Давайте разберем нашу первую драгоценную страничку.
require... -- это мы подключили класс
$my=new class_out -- объявили экземпляр класса. Теперь ко всем процедурам и функциям мы будем обращаться не $this->, как раньше, а $my->.
$my->sql_connect() -- а вот и первая наша функция отработала: подключились к базе данных. Отлючимся мы теперь только в самом конце файла.
$my->html_headers() -- зарядили хидеры. Это надо делать до того, как хоть одна буковка видимого текста попадет в браузер, потом заряжать хидеры нам никто не позволит.
Далее, как вы уже сами догадались, подкачиваем заголовок страницы, рекламный блок (пока пустой) и навигатор. Обратите внимание, мы уже во всю используем данные из наших классов. В этих трех вызовах мы пользуемся ссылкой на папку inc в виде переменной из класса vars.
Поскольку все таблицы у нас закрыты на данный момент выдачи страницы, мы смело можем сделать flush(); -- пусть шапка отдается пользователю, пока у нас формируется тело страницы. Тело страницы у нас пока крайне простое, текстовое, мы его as is и выдаем.
Ну вот, дошли и до финала страницы. Первым делом "выдавливаем" пользователю основное содержимое (тело) страницы посредством flush();, затем подключаем нижний навигатор, нижний рекламный блок, копирайтную строку с финальными html-тэгами и отключаемся от БД -- $my->sql_close(); Все!
Поздравляю вас, друзья мои, дело пошло в гору. Теперь у на сайт будет расти как на дрожжах.
В заключение мне в голову пришла замечательная идея. Под именем index.php у нас будет храниться, конечно, актуальная версия сайта. Но каждый шажок мы будем запоминать в отдельных файлах в специальной папочке step. А чтобы не мучиться с поиском нужного примера, каждый набор файлов будет иметь номер своего выпуска. Вот этот выпуск имеет номер 144 (видите, в строке URL написано: ...&id=144), значит состояние сайта на текущий момент лежит в папочке http://php.kurepin.ru/step/144/. Проверьте обязательно! ;-)
Ну что, господа грызуны науки программирования? Как настроение? Надеюсь, лучше моего... Сегодня один урод на дороге помял моего любимого Мерседеса, от чего я в страшном гневе (даже подлокотник в порыве ярости вырвал с корнем). Так что, злой я сегодня на редкость. Рекомендую слабонервным, женщинам и детям не читать этого выпуска, пока он не отлежится и не остынет! А для тех, кто не боится ни ножа не #уя, мы продолжаем нашу неторопливую беседу...
Чего вы ждете? Вероятно, вы ждете начала очередного нудного повествования о том, как и что нам надо дальше написать? Ничего подобного! У нас уже столько всего написано, что пора бы уж этим воспользоваться.
Если я не ошибаюсь, то у нас уже написаны функции добавления в базу разной всячины, вот их-то мы и задействуем следующим шагом.
То есть, поработаем сначала над backoffice-ом. Бэкофис (не сильно режет глаз такое написание? Ничего, потерпите) должен располагаться в какой-нибудь отдельной директории, которую лучше закрыть паролем. Надо рассказывать, как закрыть директорию паролем? Будет надо — пишите, расскажу. А мы тут не апач изучаем, а PHP.
Создаем папку /admin/ в директории WWW, в которой вся эта песня и расположится. Доступ в эту папку я закрою логином и паролем в честь моего покалеченного коня: login/passwd: mercedes/e320. А выделять это я никак не буду — читайте курс, чтобы попасть в святая святых админа!
Сделаем главный файл — index.php
Кстати, не рекомендую обзывать файлы с расширениями .php3/.php4, как многие это делают. Лучше называйте .php или .phtml, это избавит вас от необходимости менять расширения после перехода на новую версию языка. Менять — не обязательно, конечно, но это не совсем по-людски — когда в расширении указана одна версия языка, а в теле файла используются функции из арсенала более новой версии.
Этот файл выглядит почти так же, как и index.php в корне сайта. За одним исключением: он будет иметь у нас другое главное меню. Или не другое, а дополнительное, содержащее "админские штучки". Целиком файл можно представить так:
<? require("/home/atos/php.kurepin.ru/req/out.class"); $my=new class_out; $my->sql_connect(); $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr><td> текст страницы<br> </td><td width=200 bgcolor=#eeeeee> <b>АДМИНИСТРАТОР</b><br> <? include($my->PATH_INC."/menu_admin.inc");?><br> <br> главное меню<br> <br> </td></tr></table> <? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>
Вот так. Теперь создадим файл админского меню в директории inc:
<b>тексты</b><br> <a href=<? echo $PATH_SITE; ?>/admin/text/add.php>добавить</a><br> <a href=<? echo $PATH_SITE; ?>/admin/text/delete.php>удалить</a><br> <a href=<? echo $PATH_SITE; ?>/admin/text/enable.php>активность</a><p> <b>рубрики</b><br> <a href=<? echo $PATH_SITE; ?>/admin/cat/add.php>добавить</a><br> <a href=<? echo $PATH_SITE; ?>/admin/cat/delete.php>удалить</a><br> <a href=<? echo $PATH_SITE; ?>/admin/cat/rename.php>переименовать</a>
Кстати, а знаете что будет, если сделать include несуществующего файла? Будет WARNING, который PHP выкинет прямо вам на сайт. А знаете, как от этот избавиться? Есть масса способов, самый распространенный из которых — поставить перед вызовом функции гыгу — @:
$r=@fopen(...);
Эта гадость — @ — распространяется почти на все функции PHP. Интерпретатор проглотит ваш неправильный инклуд или любую другую операцию с файлом или чем-то другим, если перед вызовом функции стоит эта гадость.
Почему я ее называю гадостью? Потому, что надо самому обрабатывать ошибки и варнинги, а не маскировать их, как это делают некоторые "разработчики". Ну если ты не уверен в существовании файла, ну сделай предварительную проверку на существование файла, базы или к чему ты там обращаешься. Все лучше, чем показывать пользователю "поплывшую" от недостатка фрагментов страницу.
Есть и более грамотный способ попрятать все варнинги и ошибки. Это делается настройками в файле конфигурации PHP. В результате все ошибки будут аккуратненько складываться в специальный файл на сервере, который надо просто периодически просматривать на наличии проблем. Но этот вариант можно использовать только после полной отладки всех сайтов, работающих на данном комплекте PHP. Нам пока это не грозит.
Так, все у нас готово? Да, все готово. Вот наша страница: http://php.kurepin.ru/admin/. Логин и пароль я уже сообщил.
Что дальше? А дальше будем создавать первую рубрику и постить в нее первый текст. Но это уже завтра, сегодня я пошел с горя водку пить...
До завтра!
P.S. Прошу прощения за столь короткий выпуск, а копии файлов легли в /step/145/, как договаривались.
Странно, но никто не спросил, почему я везде ставлю относительные пути (URL): которые начинаются с корня сайта /, как обычно и делают, а в меню администратора добавил еще и переменную с абсолютным путем $PATH_HTTP. Может у кого-то возникнут на этот счет предположения? Если возникнут -- милости просим их озвучить на форуме: http://forum.21.ru.
Продолжаем разговор... Давайте создадим страницу добавления новой рубрики. Нам для этого потребуется:
1. Страница с формой добавления.
2. Функция вывода всех имеющихся рубрик
3. Страница вывода существующих рубрик (чтобы мы могли увидеть результат нашей деятельности)
Начнем со второго пункта, если позволите. Напишем короткую функцию для класса out:
var $out_cat_list; function out_cat_list() { $this->sql_query="select c_id, c_name from tbl_cats order by c_name"; $this->sql_execute(); if($this->sql_err) return(11); while(list($id, $name)=mysql_fetch_row($this->sql_res)) { $this->out_cat_list.="<a href=/cat/$id>$name</a><br>\n"; } return(0); }
Я предпочитаю давать переменным, содержащим результат работы функции, такое же имя, как и самой функции. Так никогда не запутаешься в принадлежностях, главное -- не забывать, что вызов функции отличается от обращения к переменной наличием скобок для переменных ().
echo $this->list_all_users; -- печатать содержимое переменной
echo $this->list_all_users(); -- печатать результат работы функции
Теперь перейдем к пункту 3 нашего плана.
Страницу вывода результата функции $out_cat_list нам делать не надо. Достаточно небольшого файла. Этот будет .inc-файл, который мы сможем вызывать в любом месте нашего проекта. Но есть и еще одна причина, по которой лучше сохранить эту функцию отдельным файлом. Дело в том, что меню рубрик -- величина достаточно постоянная. Поэтому нет особой необходимости формировать список рубрик из базы при каждом обращении к странице (хотя это дело мгновения для MySQL). Т.е. мы пока будем формировать список рубрик "на лету", но когда займемся оптимизацией, можно будет заменить этот файл простым текстовым содержимым. А пока файл cat_list.inc в директории inc, разумеется, будет выглядеть следующим образом:
<? $err=$my->out_cat_list(); if($err) { echo $my->err_to_html($err); }else { echo $my->out_cat_list; } ?>
Вот так. Расшифровываю на всякий случай.
Вызываем функцию формирования списка рубрик.
Если функция возвратила номер ошибки отличный от нуля, высвечиваем сообщение об ошибке в html-формате.
Если возвращен код ошибки 0, значит в переменной $out_cat_list содержится то, что нас интересовало. Его и распечатываем...
Теперь можно воткнуть эту функцию куда-нибудь, чтобы увидеть результат ее работы. Предлагаю пока прицепить ее к файлу menu_main.inc, все равно главное меню у нас пока пустое. А рубрики -- важная часть навигации по сайту.
Итак, файл menu_main.inc теперь выглядит так:
<br> <b>рубрики</b><br> <? include($my->PATH_INC."/cat_list.inc"); ?>
и хранится в папке inc.
Теперь и его надо в свою очередь подключить уже к html-файлам вывода. Замените фразу "главное меню" во всех наших php-файлах на строку
<? include($my->PATH_INC."/menu_main.inc"); ?>
и посмотрите как они теперь смотрятся в браузере.
Что, ничего интересного не увидели? Просто пропала надпись "главное меню"... а что вы ждали, у нас же ничего в базе рубрик нет! На этом этапе главное -- увидеть ничего на том месте, где должен быть список рубрик. Если вы все же что-то там увидели, значит произошла какая-то ошибка.
Вам кажется, что у нас получается слишком большая вложенность подключения файлов? Ведь сложилось такое мнение, не так ли? Так вот, ничего подобного! Вы это поймете выпусков через 10-20, когда наш проект обрастет большим количеством всяческих наворотов и фитюлек. Более того, нам придется еще дробить и дробить наши файлы на составляющие, дабы чувствовать себя комфортно среди огромного количества кода php и html.
Вот теперь можно приступать к реализации пункта 1 нашего плана на сегодня.
Создадим файл add.php в директории /admin/cat/ с формой добавления новой рубрики.
<? require("/home/atos/php.kurepin.ru/req/in.class"); $my=new class_in; $my->sql_connect(); if($post=="Y") { $my->in_cat_name=$cat_name; $err=$my->in_cat_add(); if($err) { $my->error=$my->err_to_html($err); }else { $my->error=$my->ok_to_html("Рубрика '".$my->in_cat_name."' успешно добавлена в базу"); unset($cat_name); } } $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr> <td valign=top> <b>Добавление новой рубрики</b><br> <br> <form action="<? $PHP_SELF; ?>" method="post"> <input type="hidden" name="post" value="Y"> <input type="text" name="cat_name" value="<? echo $cat_name; ?>"><input type="submit" value="Добавить"> </form> </td> <td width=200 bgcolor=#eeeeee valign=top> <b>АДМИНИСТРАТОР</b><br> <br> <? include($my->PATH_INC."/menu_admin.inc");?><br> <br> <b>ГЛАВНОЕ МЕНЮ</b><br> <b><? include($my->PATH_INC."/menu_main.inc");?></b><br> <br> </td> </tr></table> <? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>
Чего интересного в этом файле. Прежде всего, мы подключаемся не к классу out, а к классу in, т.к. именно в нем расположены интересующие нас функции. Но, при этом мы пользуемся и вызовом списка существующих рубрик -- функцией, расположившейся в классе out. Как быть?
Можно пойти тремя разными путями:
1. Скопировать функцию в класс in. Но зачем нам две идентичные функции?
2. Можно перенести функцию в родительский класс utils, от которого порождаются классы in и out, но зачем нам функция в классе, который не отвечает за визуализацию.
3. Можно породить класс in не от класса утилит, а от класса out, унаследовав все его возможности. Вот так мы и поступим. Учитывая, что функции класса in не могут выполняться со скоростью 10 запросов в секунду, учитывая специфику класса, мы можем в нем унаследовать хоть слона. Понятно, о чем я говорю? Не бойтесь наследовать административные классы от любых других.
Дальше, мы видим фрагмент кода, который отрабатывается не при каждой загрузке страницы, а только когда в форме подтверждается добавление новой рубрики.
Тут все просто. Если скрытая переменная $post имеет значение "Y", значит в форме была нажата кнопка submit -- надо обработать запрос.
Переменной присваивается значение -- имя новой рубрики, после чего вызывается функция добавления рубрики.
Смотрим на возвращенную ошибку. Если ошибка есть, то переменной $my->error мы присваиваем текст ошибки, если же ошибки нет, то той же переменной присваиваем текст положительного ответа системы.
Заметили? У нас прибавилась новая функция ok_to_html() и новая переменная $html_error. Функция выглядит вот так:
var $html_error; function ok_to_html($text) { return "<font color=green>$text</font><br><br>"; }
и располагается в классе utils рядом с функцией вывода ошибки err_to_html. Тут же объявлена и переменная. А вывод содержимого переменной мы вставим в файл top.inc, сразу под заголовком.
<? echo $my->html_error; ?>
Это позволит нам более не думать о том, куда выводить сообщения об ошибках: присвоив переменной $html_error текст сообщения в любом месте наших файлов, мы всегда будем уверены, что увидим его там, где положено. А вызов функции ok_to_html можно будет снабдить рассылкой почтовых сообщений или журналом регистрации событий, если мы захотим фиксировать все удачные операции на сайте.
Уф... пишу, пишу, а функция добавления рубрик уже работает, друзья мои. Я и не заметил, что подобрался к концу очередной главы повествования о современном языке PHP и его друзьях.
Товарищи офицеры, сверим часы...
Я вынужден дать небольшое пояснение по курсу, а потом будем сверяться. Для тех, то не верит, совершенно официально повторяю: я пишу этот курс из головы, каждый выпуск рождается прямо на листе MS Word и даже потом особенно не вычитывается на ошибки. Тема каждого выпуска приходит в голову в момент открытия MS Word и продиктована настроением, программерской логикой и левой пяткой.
Все это так, потому, что:
1. У меня нет желания писать очередную заумную книгу — их и так полно.
2. У меня нет времени писать очередную заумную книгу.
3. Сайт и скрипты создаются в том порядке, в каком бы они создавались, если бы я писал это для себя или под заказ. Т.е. все ошибки, опечатки, переделки и все остальное — следствие нормального процесса создания сайта. Единственное отличие — стараюсь писать более подробно и внятно: меньше выеживаться и умничать.
Исходя из сказанного, не сложно сделать выводы:
1. Ошибки и опечатки неизбежны
2. Изменения исходных текстов неизбезны
3. Изменения в направлении мыслей и в методах реализации — вполне допустимы.
Вот так.
А для тех, кто пытается писать свой сайт параллельно с курсом, мы будем устраивать глобальные сверки: я буду выкладывать все свои скрипты и html-файлы as is: как сегодня.
Итак, скачивайте SFX-архив php- и html- файлов, которые полностью идентичны тем, что работают сегодня по адресу http://php.kurepin.ru
Не забудьте поправить их под себя...
Ну вот. Появился у нас работающий сайт. Теперь можно вернуться на несколько уроков назад и продолжить работу над сайтом в том порядке, в котором надо было ее вести. Если помните, я чуть форсировал визуализацию, чтобы вы не умерли от удовольствия.
Теперь мы чуток откатимся назад и пройдем еще один подготовительный этап -- создание таблицы стилей (CSS). Для чего нам эти стили и почему надо этим заниматься на начальной стадии создания проекта? Да потому, что на начальной стадии проекта надо планировать буквально все: базу, классы, стили, директории, почтовые адреса, журналы операций и все такое прочее. Мы не будем заниматься доскональным изучением CSS, мы просто создадим костяк таблицы стилей, чтобы правильно к ней обращаться.
Что же такое CSS? Это набор правил отображения в браузере, устанавливаемых для любого стандартного html-объекта: шрифта, слоя, элемента таблицы или формы и так далее. Была б моя воля -- я бы запретил использование браузеров, не умеющих правильно отображать правила CSS. Очень уж много возможностей у CSS по сравнению со стандартными возможностями форматирования языка html. Я не буду рассказывать о синтаксисе и правилах создания CSS, об этом и так очень много написано: любой поисковик выдаст вам тонну статей и учебников по CSS. Сегодня мы остановимся на другом, на планировании.
Когда мы создаем текстовый дизайн, то весь арсенал наших визуальных инструментов составляют: размеры, цвета и отступы. И вот тут CSS равных нет. Чтобы иметь возможность менять оформление дизайна полностью или частично нам надо грамотно заложить в html-код употребление разных классов CSS. Причем, эти классы распланировать и описать надо тоже нам. Посмотрим, как это делается.
Давайте попробуем перечислить все основные элементы, выделяющиеся на html-странице.
Прежде всего, это ЗАГОЛОВКИ. Заголовки могут быть основные (на всю страницу), могут быть заголовками, разделов, меню, таблиц и так далее.
Далее, у нас выделяется выделенный текст. Он может у нас тоже выделяться в разных ситуация и по-разному.
Далее -- основной текст. Он может изменяться в зависимости от выполняемых функций: основной текст, меню, сноска, комментарий и так далее.
А таблицы? Таблицы же тоже могут иметь разного цвета и размера ячейки, столбцы, рамки и отступы... я уж молчу про элементы форм (form)...
Что же делать? Кажется, что всего учесть нельзя, что проще описывать прямо на месте тот или иной элемент кода. Ничего подбного!
Теория CSS гласит, что каждый элемент наследует свои свойства от прародителя, изменяя только то, что было явно изменено. Т.е. CSS -- это такое же ООП, как и наш PHP-код. Так давайте этим пользоваться в меру наших потребностей. Предлагаю разбить первый уровень классов на:
1. Базовый текст
2. Таблицы
3. Формы
И для каждого описать свойства текста, фона и прочего, чего нам захочется.
В свою очередь, текст можно подразделить на:
1. Заголовок (title)
2. Основной (normal)
3. Важный (urgent)
4. Яркий (ошибка)
5. Мелкий (small)
6. Очень мелкий (tiny)
Если этих шести нам не хватит -- добавим уже потом. Вот из этих двух списков и составим нашу CSS.
Предлагаю создать базовый набор названий, а со свойствами каждый из вас сможет поиграть самостоятельно, выбрав наиболее подходящие для себя.
Предлагаю вот такой вариант для начала.
<style> BODY { COLOR: #000000; FONT-FAMILY: Verdana, Arial; FONT-SIZE: 9pt } A { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 9pt; TEXT-DECORATION: none } A:link { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 9pt; TEXT-DECORATION: none } A:visited { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 9pt; TEXT-DECORATION: none } A:hover { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 9pt; TEXT-DECORATION: underline } TABLE { COLOR: #000000; FONT-FAMILY: Verdana, Arial; FONT-SIZE: 8pt } .text_ { font-family: Verdana, Arial Cyr; font-size: 8pt; font-weight: normal; color: #000000; } A.text_ { COLOR: #000000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.text_:link { COLOR: #000000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.text_:visited { COLOR: #000000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.text_:hover { COLOR: #FF0000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: underline } .text_title { font-family: Verdana, Arial Cyr; font-size: 10pt; font-weight: bold; color: #666666; } .text_urgent { font-family: Verdana, Arial Cyr; font-size: 8pt; font-weight: bold; color: #FF0000; } .text_error { font-family: Verdana, Arial Cyr; margin-left: 10pt; font-size: 10pt; font-weight: bold; color: #FF0000; } .text_small { font family: Verdana, Arial Cyr; font size: 7pt; color: #000000; font-weight: normal; } A.text_small { COLOR: #000000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 7pt; TEXT-DECORATION: none } A.text_small:link { COLOR: #000000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 7pt; TEXT-DECORATION: none } A.text_small:visited { COLOR: #000000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 7pt; TEXT-DECORATION: none } A.text_small:hover { COLOR: #FF0000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 7pt; TEXT-DECORATION: underline } .text_tiny { font family: Verdana, Arial Cyr; font size: 6pt; color: #000000; font-weight: normal; } A.text_tiny { COLOR: #000000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 6pt; TEXT-DECORATION: none } A.text_tiny:link { COLOR: #000000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 6pt; TEXT-DECORATION: none } A.text_tiny:visited { COLOR: #000000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 6pt; TEXT-DECORATION: none } A.text_tiny:hover { COLOR: #FF0000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 6pt; TEXT-DECORATION: underline } .form_ { font-family: Verdana, Arial Cyr; border: solid 1 #999999; background-color: #eeeeee; padding : 4; font-size: 8pt; } .form_text_title { font-family: Verdana, Arial Cyr; font-size: 10pt; font-weight: bold; color: #006600; } .form_text_ { font-family: Verdana, Arial Cyr; border: solid 1 #333333; background-color: #ffffff; font-size: 8pt; } .form_text_urgent { font-family: Verdana, Arial Cyr; border: solid 1 #ff3333; background-color: #ffffcc; font-size: 8pt; } .form_drop_normal { font-family: Verdana, Arial Cyr; border: solid 1 #333333; background-color: #ffffff; font-size: 8pt; } .form_drop_urgent { font-family: Verdana, Arial Cyr; border: solid 1 #ff3333; background-color: #ffffcc; font-size: 8pt; } .form_textarea_normal { font-family: Verdana, Arial Cyr; border: solid 1 #333333; background-color: #ffffff; font-size: 8pt; } .form_textarea_urgent { font-family: Verdana, Arial Cyr; border: solid 1 #ff3333; background-color: #ffffcc; font-size: 8pt; } .form_radio_normal { font-family: Verdana, Arial Cyr; width: 8pt; height:8pt; } .form_check_normal { font-family: Verdana, Arial Cyr; width: 9pt; height:9pt; } .form_submit_normal { font-family: Verdana, Arial Cyr; border: outset 1; font-size: 8pt; } .tbl_ { font family: Verdana, Arial Cyr; border: 1 inset #000066; background-color: #eeeeee; padding : 4; font size: 8pt; border-top : none; border-left : none; border-right : none; border-bottom : none; } A.tbl_ { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.tbl_:link { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.tbl_:visited { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.tbl_:hover { COLOR: #FF0000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: underline } .tbl_text_ { font family: Verdana, Arial Cyr; font size: 8pt; color: #000000; background: #FFFFFF; } A.tbl_text_ { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.tbl_text_:link { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.tbl_text_:visited { COLOR: #000066; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.tbl_text_:hover { COLOR: #FF0000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: underline } .tbl_text_title { font family: Verdana, Arial Cyr; font size: 8pt; color: #006600; font-weight: bold; } .tbl_text_urgent { font family: Verdana, Arial Cyr; font size: 8pt; color: #FF0000; background: #FFFFFF; } .tbl_row_hl { font family: Verdana, Arial Cyr; font size: 8pt; color: #000000; background: #FFFFCC; } .tbl_text_small { font family: Verdana, Arial Cyr; font size: 7pt; color: #000000; font-weight: normal; } .tbl_text_tiny { font family: Verdana, Arial Cyr; font size: 6pt; color: #000000; font-weight: normal; } </style>
Вот, что у меня получилось.
Это _очень_ далеко от хорошего и правильно CSS, но нам пока его хватит. Тем более, что я не увлекаюсь изучением возможностей CSS, хотя уверен, что возможности его поистине безграничны.
На что хочется обратить внимание. Пожалуй, только на то, что в качестве единицы величины я использовал pt, а не px. Мне кажется это наиболее правильным, учитывая, что люди имеют разное зрение и совсем разные разрешения мониторов. Для тех, кто не знает разницы между pt и px, я поведаю, что pt пляшет от дюйма или какой-то другой реальной меры длины, а px -- от видимой на экране точки. Поэтому, 10pt на всех мониторах будет иметь один физический размер, а 10px от невидимого текста на огромных разрешениях до здоровенных букв на низком разрешении.
Я выбираю pt.
Давайте теперь для примера переделаем одну из наших страниц. Например, /admin/cat/add.php
Вот так она теперь выглядит:
<? require("/home/atos/php.kurepin.ru/req/in.class"); $my=new class_in; $my->sql_connect(); if($post=="Y") { $my->in_cat_name=$cat_name; $err=$my->in_cat_add(); if($err) { $my->html_error=$my->err_to_html($err); }else { $my->html_error=$my->ok_to_html("Рубрика '".$my->in_cat_name."' успешно добавлена в базу"); unset($cat_name); } } $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr> <td valign=top> <br> <font class="text_small">Тут можно пустить пояснительный текст для модератора/напоминалку/подсказку.</font><br> <form action="< ? $PHP_SELF; ?>" method="post" class="form_"> <div align=center class="form_text_title">Добавление новой рубрики</div><br> <input type="hidden" name="post" value="Y"> <input type="text" name="cat_name" value="< ? echo $cat_name; ?>" class="form_text_urgent"> — название рубрики<br> <br> <input type="submit" value="Добавить" class="form_submit_normal"> </form> </td> <td width=200 bgcolor=#eeeeee valign=top> <b>АДМИНИСТРАТОР</b><br> <br> <? include($my->PATH_INC."/menu_admin.inc");?><br> <br> <b>ГЛАВНОЕ МЕНЮ</b><br> <b><? include($my->PATH_INC."/menu_main.inc");?></b><br> <br> </td> </tr> </table> < ? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>
Сравните:
http://php.kurepin.ru/step/146/admin/cat/add.php -- это было (хотя было гораздо хуже, ибо даже тут через заголовок мы подхватили контроль за гарнитурой и кеглем шрифта).
http://php.kurepin.ru/step/148/admin/cat/add.php -- а это стало.
И нам для этого надо было просто добавить несколько class="" в некоторые тэги нашей страницы.
Отныне мы будем стараться каждому видимому элементу прописывать свой класс. Это позволит нам без лишней мороки менять вид сайта в любом направлении: хоть в размерах, хоть в шрифтах, хоть в цветах.
И еще одна замечательная возможность плывет прямо к нам в руки. Угадайте!
Дело в том, что нам теперь ничего не стоит сделать настраиваемый под клиента дизайн. Если у данного клиента слабое зрение или LCD монитор, ядовито излучающий стандартные цвета или еще какое отклонение от стандарта -- что нам мешает позволить ему выбрать другой набор отображения сайта или внести коррективы в существующий? Возможно, мы это и сделаем когда-то. А для нетерпеливых я в двух словах поясню, как это сделать:
1. Подключаем CSS прямо в текст каждой страницы, для чего в файле top.inc заменяем
<link rel=stylesheet type="text/css" href="/style.css">
на
<? include(...style.inc); ?>
А в файле style.inc заменяем изменяемые элементы (цвета, размеры и др.) обычными переменными, на которые можно влиять как нам будет угодно.
Пользователь будет выбирать тип дизайна или его типоразмер, а мы помним об этом в его же куках или у себя в базе. Клиент всегда может вернуться на наш сайт, получив тот вид дизайна, к которому привык.
Сегодня это уже не роскошь в Сети, а вполне пользуемая функция многих сайтов. Впервые я увидел это на сайте webclub.ru. Не знаю, какими средствами они этот эффект реализовывали, но полагаю, что какими-то схожими.
Эх, что творится в нашей многострадальной России... НТВ закрыли, ТВ-6 закрыли, в армию всех забирают, чиновники беспредельничают... о чем это я? А! Это я о новом своем проекте — voices.ru.
Узнаете? Что напоминает своим внешним видом? Правильно! Напоминает наш родимый php.kurepin.ru.
Пусть voices послужит реальным примером использования разрабатываемых нами приемов программирования. Конечно, если у меня хватит сил и времени тащить еще и этот проект. Кстати, если у кого есть желание помочь — пишите. Для работы над voices.ru надо уметь находить горячие темы и материалы по ним.
А мы возвращаемся к нашим баранам и продолжаем писать backoffice. У нас уже добавляются и показываются рубрики. Теперь настала очередь научиться их удалять и переименовывать. В общем-то, это не чуть не сложнее добавления.
Для начала копируем файл add.php в rename.php. Нам не много надо изменить в файле rename.php, чтобы он выполнил функцию, соответствующую своему названию. Если бы этот был не backoffice, то следовало бы вывести на экран список всех рубрик и снабдить их какими-нибудь кнопочками, чтобы пользователь мог выбрать нужный ему раздел и перейти к его редактированию. Но мы с вами — люди серьезные, мы же можем подвести мышь к названию рубрики в общем их списке и по URL посмотреть номер интересующей нас рубрики, не так ли? Полагаю, что именно так. Поэтому, нам надо снабдить форму переименования рубрики только дополнительными полем типа text, в которое мы впишем номер изменяемой рубрики, а в поле для названия — новое название рубрики. Все очень логично по-моему.
<? require("/home/atos/php.kurepin.ru/req/in.class"); $my=new class_in; $my->sql_connect(); if($post=="Y") { $my->in_cat_id=$cat_id; $my->in_cat_name=$cat_name; $err=$my->in_cat_rename(); if($err) { $my->html_error=$my->err_to_html($err); }else { $my->html_error=$my->ok_to_html("Рубрика '".$my->in_cat_name."' успешно переименована"); unset($cat_name,$cat_id); } } $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr> <td valign=top> <br> <font class="text_small">Тут можно пустить пояснительный текст для модератора/напоминалку/подсказку.</font><br> <form action="<? $PHP_SELF; ?>" method="post" class="form_"> <div align=center class="form_text_title">Переименование рубрики</div><br> <input type="hidden" name="post" value="Y"> <input type="text" name="cat_id" value="<? echo $cat_id; ?>" class="form_text_urgent"> - ID изменяемой рубрики<br> <input type="text" name="cat_name" value="<? echo $cat_name; ?>" class="form_text_urgent"> - новое название рубрики<br> <br> <input type="submit" value="Сохранить" class="form_submit_normal"> </form> </td> <td width=200 bgcolor=#eeeeee valign=top> <b>АДМИНИСТРАТОР</b><br> <br> <? include($my->PATH_INC."/menu_admin.inc");?><br> <br> <b>ГЛАВНОЕ МЕНЮ</b><br> <b><? include($my->PATH_INC."/menu_main.inc");?></b><br> <br> </td> </tr> </table> <? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>
Да, так и есть — работает: http://php.kurepin.ru/step/150/admin/cat/rename.php
Что изменилось, по порядку:
Как видите, вся процедура создания новой возможности backoffice заняла не более минуты. Полагаю, что комментировать порядок создания файла delete.php нет никакой надобности? Вот так он выглядит:
<? require("/home/atos/php.kurepin.ru/req/in.class"); $my=new class_in; $my->sql_connect(); if($post=="Y") { $my->in_cat_id=$cat_id; $err=$my->in_cat_delete(); if($err) { $my->html_error=$my->err_to_html($err); }else { $my->html_error=$my->ok_to_html("Рубрика '".$my->in_cat_id."' удалена"); unset($cat_id); } } $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr> <td valign=top> <br> <font class="text_small">Тут можно пустить пояснительный текст для модератора/напоминалку/подсказку.</font><br> <form action="<? $PHP_SELF; ?>" method="post" class="form_"> <div align=center class="form_text_title">Удаление рубрики</div><br> <input type="hidden" name="post" value="Y"> <input type="text" name="cat_id" value="<? echo $cat_id; ?>" class="form_text_urgent"> - ID удаляемой рубрики<br> <br> <input type="submit" value="Удалить" class="form_submit_normal"> </form> </td> <td width=200 bgcolor=#eeeeee valign=top> <b>АДМИНИСТРАТОР</b><br> <br> <? include($my->PATH_INC."/menu_admin.inc");?><br> <br> <b>ГЛАВНОЕ МЕНЮ</b><br> <b><? include($my->PATH_INC."/menu_main.inc");?></b><br> <br> </td> </tr> </table> <? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>
Проверяем... http://php.kurepin.ru/step/150/admin/cat/delete.php ...работает!
Единственное, на что прошу обратить внимание, это на изменение сообщения об удачном удалении: вместо $my->in_cat_name надо поставить $my->in_cat_id, т.к. тут мы оперируем только с номером рубрики.
Ну что, можно поздравить вас окончанием работы над администрированием рубрик. Осталось проверить их устойчивость к нетипичным данным (обычно проверяют наличием в кавычек и апострофов в текстовых полях и наличием букв и спецсимволов в цифровых полях. А так же — выходы за пределы допустимых размеров) и можно отправляться пить пиво.
А я сегодня тестирую очередной Интернет-магазин. На днях закончил тестирование наиболее популярных книжных интернет-лавок и перешел на продуктовые. Сегодня под мою критику попадает онлайн-магазин сети гипермаркетов Рамстор. Они уже мне отзвонили и сообщили, что из 25 заказанных наименований у них в наличии только 17, но еще не привезли...
Пора перейти к добавлению текстов в базу. Для чего создадим папку text в админской директории. В общем-то, нам достаточно скопировать файл add.php из папки рубрик в папку текстов и чуть подправить его, заменив названия переменных и добавив пару элементов в форму. Но мы же не просто делаем сайт. Мы же еще и чему-то учимся, не так ли? Поэтому, предлагаю усложнить самим себе жизнь и добавить еще мелких задач.
Какие параметры мы вписываем в базу для регистрации нового текста:
С первыми двумя полями точно ничего не сделаешь, а вот к третьему давайте хоть выпадающее меню напишем, а? Чтобы реже ошибаться в номере рубрики. Предлагаю создать эту функцию в файле out.class, так как она может нам понадобиться в любом последующем классе.
В классе out у нас уже есть одна функция, выводящая в формате html список рубрик -- out_cat_list. Предлагаю новую назвать по аналогии -- out_cat_droplist. Ага?
Настоящий программист всегда ленив до рутиной работы: мы просто скопируем первую функцию чуть ниже, переименуем ее в out_cat_droplist и чуть иначе оформим html-вывод рубрик. Не сложно. Если вы сейчас посмотрите на out_cat_list и вспомните, как оформляется в html выпадающее меню, то тут же поймете, как новая функция будет выглядеть.
Вот только не надо на меня кряхтеть: "Что-то у него все как-то просто получается. Даже когда он сознательно усложняет себе жизнь. Все сводится к тому, что надо что-то во что-то скопировать и чуточку поправить. Прямо не работа, а халява сплошная получается...", -- не надо, не тратьте на меня свои нервы. Так все и есть: при правильном подходе программировать весело и интересно. Особенно в таких проектах, где не надо искать хитроумных алгоритмов для решения сложных задач. У нас пока все просто и легко. Наслаждайтесь.
Но одно существенное изменение мы все-таки внесем в новую функцию. Дело в том, что думать надо чуть дальше, чем решаемая в данный момент задача. Остановись, подумай над тем, как эта функция будет использоваться... Какие могут возникнуть с ней сложности или неудобства?.. Остановились? Подумали? Да куда там...
А я, вот, подумал. И вот, что я надумал. Добавление текста -- занятие не однозначное. Помните, сколько мы описали ситуаций, когда скрипт даст отказ на проведение добавления? При этом надо дать пользователю возможность исправиться. А для этого мы вместе с сообщением об ошибке всегда заполняем форму теми данными, которые он уже ввел.
Ну, представьте. Пользователь (а в данном случае это мы с вами) заполняет форму данными (а форма может быть большой и иметь много разных полей для заполнения) и ошибается в какой-нибудь ерунде. Мы сообщаем ему об этом. Пользователь поправляется. Но все остальные поля стоят уже в "сброшенном" положении. Это так не удобно, так бесит, ей богу!
Как сохранить данные в форме? Очень просто: надо в значение формы подставить вывод значения переменной, совпадающей с именем поля формы. Вы это уже могли видеть на добавлении рубрики (если название написать длиннее положенных 50 символов, вы увидите сообщение об ошибке и свой текст на месте его редактирования. А я получу еще одно письмо-уведомление об ошибке выполнения операции добавления рубрики :).
Но в случае с выпадающими меню, селекторами типа "radio button" и "чекбоксами" ситуация совершенно иная. Вот мы ее сейчас и обработаем. Смотрите, как будет выглядеть функция вывода наших рубрик в виде выпадающего меню:
var $out_cat_droplist; function out_cat_droplist($num) { $this->sql_query="select c_id, c_name from tbl_php_cats order by c_name"; $this->sql_execute(); if($this->sql_err) return(11); // Приводим переменную к ее разумному виду $num=(int)$num; while(list($id, $name)=mysql_fetch_row($this->sql_res)) { $selected=""; if($id==$num) $selected="selected "; // не это ли значение было выбрано прежде? $this->out_cat_droplist.="<option value=\"$id\" $selected>$name</option>\n"; } return(0); }
То есть, мы на входе принимаем значение, которое должно быть выбрано изначально. Если такое значение в базе находится, то так тому и быть. А коли не найдется, так на "нет" и суда нет...
Если позволите, я не буду сегодня задействовать это меню, т.к. уверен, что оно заработает. В этом мы убедимся в завтрашнем выпуске. А сегодня я предлагаю вернуться аж ко второй главе повествования о PHP, которая закончилась фразой: "Добавили в свою базу данных эти две таблицы? Отлично. Можете пока подумать, как будет выглядеть третья таблица. А я тем временем прощаюсь с вами до следующего выпуска".
Речь шла о том, что мы создали таблицу для текстов и таблицу для рубрик. Но должна быть и третья таблица. Я все ждал, пока кто-нибудь спросит: "А какая же -- эта третья таблица?!". Но никто так и не спросил. Будем считать, что все догадались -- третья таблица нужна для связки рубрик с текстами.
И не надо строить такие умные моськи, я вас умоляю! В процедуре добавления текста, в SQL-запросе написана откровенная лажа, которую вы сожрали -- как так и надо. Там даже количество передаваемых параметров не совпадает. Внимательные вы мои... Я ставлю всем по "двойке" за наблюдательность и начинаем разбираться в этой ситуации.
Связать текст с рубрикой можно и без третьей таблицы. Достаточно добавить поле t_cat в таблицу текстов и в это поле писать номер рубрики, к которой относится текст. Так все обычно и делают. Но мы пойдем другим путем; у нас большой проект, нам надо закладываться в максимальную гибкость. Что мы будем делать, когда начнут появляться тексты, имеющие отношение к разным рубрикам. Например, куда поместить текст с условным названием "PHP и MySQL, популярные решения"? В рубрику "Базы данных", "Программирование на PHP" или "PHP для чайников"?
Так вот, чтобы не ломать голову над подобными проблемами, мы просто создадим таблицу связей между текстами и рубриками. Моя база вот так сожрала запрос на создание связующей таблицы:
-> create table tbl_cat_text -> ( -> ct_id bigint not null auto_increment primary key, -> ct_cat int not null, -> ct_text int not null -> ); Query OK, 0 rows affected (0.00 sec)
И в эту базу мы будем заносить все соответствия текстов рубрикам. Сможем привязывать один текст с любым количеством рубрик, или рубрику с любым количеством текстов -- называйте это как хотите.
Вот так всегда. Вместо того, чтобы по-человечески лежать на диване, смотреть в телевизор и отпиваться пивом с креветками после вчерашнего пьянствования моего дня рождения, я сижу за компьютером, пью зеленый чай и пишу очередной выпуск PHP. А все почему? А все потому, что в жизни все всегда перевернуто с ног на голову: пиво пить -- почки бунтуют, водку -- печень. В телевизоре, после закрытия ТВ-6, все каналы сразу превратились в однояйцовых близнецов и вещают только о том, какой у нас классный президент и о том, что за время его правления мы все стали жить еще лучше.
Вот и приходится -- пить чай, читать новости в Сети. Пока еще импорт зеленого чая не прикрыли, а модем не отняли. Значит надо успеть закончить курс PHP, пока у меня не отобрали лицензию на доступ в Сеть из-за всяких нелицеприятных проектов, вроде voices.ru.
Если мне не изменяет память, то нам надо адаптировать наши скрипты к появлению новой таблицы в базе данных, отвечающей за связку текстов с рубриками.
Как мы это сделаем? Для начала уберем все упоминания рубрики в функции добавления текста (у нас там номер рубрики якобы сразу добавлялся в таблицу текстов), а заодно подправим опечатки и ошибки, если попадутся. Теперь функция in_text_add выглядит вот так:
// Добавление текста в базу function in_text_add() { // Проверка данных для базы $err=$this->in_text_data_check(); if($err) return($err); // Адаптация текста для сохранения $err=$this->in_text_adapt(); if($err) return($err); // Заносим данные в базу $this->sql_query="insert into tbl_php_texts(t_name, t_dt) values('".$this->in_text_name."', now())"; $this->sql_execute(); if($this->sql_err) return(11); // Получаем сгенерированный базой id добавленного текста $this->sql_query="select last_insert_id()"; $this->sql_execute(); if($this->sql_err) return(11); list($this->in_text_id)=mysql_fetch_row($this->sql_res); // Пишем текст в директорию data, в файл с номером ID if($w=fopen($this->PATH_DATA."/".$this->in_text_id,'w')) { fwrite($w,$this->in_text); fclose($w); } else { $this->sql_query="delete from tbl_php_texts where t_id='".$this->in_text_id."'"; $this->sql_execute(); return(31); } return(0); }
Ошибок не заметил.
Эта функция добавляет текст на диск, в директорию /data/ и в таблицу tbl_text добавляется название текста.
Теперь давайте разбираться с рубриками...
По-хорошему, на странице добавления текста надо давать не выпадающее меню для рубрик, а список рубрик, с возможностью выбора из списка любого набора рубрик. Но я тут сознательно отступаю от логики. Нам же надо не просто создать web-проект "страничка обозревателя", или как мы его там назвали. А есть еще и необходимость рассмотреть наибольшее количество приемов программирования, не так ли? Поэтому, мы пойдем таким путем. Будем считать, что новый текст добавляется в базу со ссылкой на одну рубрику. А далее, при редактировании текста и его свойств, мы добавим возможность изменения списка рубрик, к которым привязан данный текст.
Итак. Надо снабдить функцию добавления нового текста привязкой текста к рубрике. Можно было бы просто добавить еще один SQL-вызов в функцию in_text_add, но мы поступим дальновиднее, создав отдельную функцию связи текста с рубрикой. Она нам еще пригодится, увидите. Она маленькая, мы добавим ее в класс in.
// Функция связи ID-рубрики с ID-текста function in_link_text_cat($text, $cat) { $text=(int)$text; $cat=(int)$cat; $this->sql_query="insert into tbl_php_cat_text(ct_cat, ct_text) values('$cat','$text')"; $this->sql_execute(); if($this->sql_query) return(11); return(0); }
Как видите, функция принимает в качестве параметров: id рубрики и id текста, и добавляет в таблицу связи новую запись.
А как же быть с дубликатами -- спросите вы? Конечно, таблица связей может уже иметь подобную связку и мы сделаем просто дубль, так?
Так, да не так. Держите меня, сейчас опять понесет в философию... Дело в том, что при добавлении новых текстов, ID текста будет уникально, а это значит, что и запись в таблице связей тоже будет уникальна. Это что касается нашей с вами ситуации. Далее, подобное "повторение" может возникнуть во время сохранения свойств текста (когда мы сделаем редактирование текста). Но и в этом случае проверка нам не нужна, так как мы будем добавлять данные по новой, а это значит, что перед фиксированием новых связей текста с рубриками мы попросту будем очищать таблицу от всех записей, содержащих ссылку на данный документ. А значит, что и дубликатов у нас не будет.
Вот такая сложная мысль. Подобные мысли должны рождаться в вашей голове мгновенно. Безусловно, лишняя проверка данных на корректность никогда не помешает. Но и паранойей страдать тоже не следует. Если же вы в себе не уверены, то можете просто объявить в базе данных связку id_текста+id_рубрики -- уникальной и SQL сам не станет добавлять повторных записей. Кстати, такой ключ нам еще понадобится, когда мы начнем делать выборки текстов из базы по номеру рубрики. Но это тема другого выпуска, не стану сейчас отвлекать вас на это.
Возвернемся к нашим баранам и добавим в функцию добавления текста вызов связки in_text_id с in_text_cat. На сегодняшний момент функция добавления текста будет выглядеть так:
// Добавление текста в базу function in_text_add() { // Проверка данных для базы $err=$this->in_text_data_check(); if($err) return($err); // Адаптация текста для сохранения $err=$this->in_text_adapt(); if($err) return($err); // Заносим данные в базу $this->sql_query="insert into tbl_php_texts(t_name, t_dt) values('".$this->in_text_name."', now())"; $this->sql_execute(); if($this->sql_err) return(11); // Получаем сгенерированный базой id добавленного текста $this->sql_query="select last_insert_id()"; $this->sql_execute(); if($this->sql_err) return(11); list($this->in_text_id)=mysql_fetch_row($this->sql_res); // Пишем текст в директорию data, в файл с номером ID if($w=fopen($this->PATH_DATA."/".$this->in_text_id,'w')) { fwrite($w,$this->in_text); fclose($w); } else { $this->sql_query="delete from tbl_php_texts where t_id='".$this->in_text_id."'"; $this->sql_execute(); return(31); } $err=$this->in_link_text_cat($this->in_text_id, $this->in_text_cat); if($err) return($err); return(0); }
Все, вынужден проститься, меня ждут друзья в боулинге. Они там отмечают мой день рождения и звонят каждые десять минут. Надо ехать, нельзя обидеть.
А вам задание: подумайте, какие проверки в какие функции надо будет добавить, чтобы по ходу работы у нас не возникало коллизий с таблицей связей. Подскажу, что речь идет о изменении списка рубрик, например.
----
Файлы этого выпуска: /step/154/
----
Ох, что-то мне стало скучно писать этот курс. Как-то вяло вы принимаете в нем участие. Проект читают около тысячи человек, а явные ошибки и опечатки замечают единицы. Я даже не исправляю их. Зачем? У меня сложилось такое впечатление, что 99% читателей курса попросту ждут его завершения, чтобы запользовать готовые стрипты для своих задач. А программирование на PHP их интересует просто по ходу дела. Это так? Если это так, ответьте. Я буду меньше времени уделять болтовне и буду публиковать больше готовых стриптов с минимумом пояснений. Мне ж не жалко. Наоборот, писать скрипты -- это для меня намного проще, чем объяснять, как они работают. Но мне дороже те, кто пришел ко мне за умением, за соревнованием, за сложностями. Вот для таких работать по-настоящему интересно. Именно они -- "умники и умницы".
Погода сегодня какая-то тяжелая, что ли... Перед тем, как вернуться к скриптованию наших баранов, предлагаю задачку для Умников и Умниц по SQL. Ответы, как всегда, жду в форуме (ссылка на форум внизу и вверху страницы).
Итак. Вполне реальная задача.
Есть у меня робот, занимающийся рассылками всякой всячины: http://www.21.ru/post/. Рассылки проводятся по разным правилам, но с соблюдением одного из них: ни один текст не должен попасть в рассылку дважды. Для этого ведется специальный лог в SQL-таблице -- когда в какую рассылку какой текст был отправлен. И при поиске следующего текста обязательно учитываются данные из этого лога.
Дальше. У MySQL, в отличие от более продвинутых диалектов SQL, нет такого понятия, как select from table where var in select... То есть, не поддерживаются вложенные запросы. Жаль. Как бы было удобно, запросить следующий текст для рассылки примерно так:
select [данные текста] from [таблица] where [id текста] not in select [id текста] from [таблица логов]Но я этого сделать не могу. Конечно, можно вложенный запрос смоделировать каким-то простым способом. Например, запросить все интересующие ID из лога, сложить их в конструкцию (1,2,5,73...) и запросить из таблицы текст, где id текста not in (список). Но боже мой, как это не красиво!
Вот мы и подошли к задаче. "Как выбрать верный следующий текст из таблицы текстов при помощи одного SQL-запроса?"
Вот такая задачка. Поверьте, она не сложная. Тем более, что я дал только 30% реальной задачи. Но меня интересуют именно эти 30%. Если вы решите эту задачку -- считайте, что диалект MySQL вы знаете на хорошем уровне.
А теперь вернемся к нашему проекту. Я вижу, что вы мне там уже каких-то текстов в базу насували. Молодцы! Ща будем разбираться в них. Кстати, мне никто так и не рассказал, какие поправки мы должны внести в существующие функции, чтобы учесть появившуюся в нашем проекте таблицу связи рубрик и текстов.
Речь идет об удалении рубрик. Как мы можем удалить рубрику, если с ней связан какой-то текст? Никак не можем мы этого допустить. И я очень жалею, что в MySQL объявление внешних ключей сделано только для совместимости с другими диалектами SQL. Очень удобно, когда за целостностью базы следит сама база. Но что поделать, у каждого свои плюсы и минусы. Поехали доделывать in_cat_delete(). Теперь эта функция выглядит вот так:
function in_cat_delete() { // Приведение номера рубрики к целочисленному виду $this->in_cat_id=(int)$this->in_cat_id; // Проверка на связь данной рубрики с текстами $this->sql_query="select from tbl_php_cat_text where ct_cat='".$this->in_cat_id."'"; $this->sql_execute(); if($this->sql_err) return(11); if(mysql_num_rows($this->sql_res)) return(29); // Формирование запроса в базу $this->sql_query="delete from tbl_php_cats where c_id='".$this->in_cat_id."'"; $this->sql_execute(); if($this->sql_err) return(11); return(0); }А в список ошибок добавляем:
$err[29]="Вы не можете удалить рубрику, она связана с текстами";
Что теперь? Есть два пути. Можно довести до ума добавление текстов, их редактирование, или заняться выводом информации. Мы займемся....ммм-м-м... выводом информации. А то вы мне весь сайт за#рете, прежде чем я его открою. Шутка.
Что нам нужно для вывода? Я так думаю, что нужно:
С чего начнем? Давайте по порядку, не так уж много надо написать. За полчаса управимся.
Чуть-чуть правим out_cat_list() (видите, как удобны префиксы в названии функций, совпадающие с названием класса: сразу видно, в каком классе искать функцию): заменяем
$this->out_cat_list.="<a href=/cat/$id>$name</a><br>\n";на
$this->out_cat_list.="<a href=/cat/?cat=$id>$name</a><br>\n";И пишем функцию вывода названия текстов по номеру рубрики. Очень просто:
var $out_text_by_cat; function out_text_by_cat() { $this->out_cat_id=(int)$this->out_cat_id; // Получаем название рубрики $this->sql_query="select c_name from tbl_php_cats where c_id='".$this->out_cat_id."'"; $this->sql_execute(); if($this->sql_err) return(11); if(!mysql_num_rows($this->sql_res)) return(30); // нет такой рубрики list($this->out_cat_name)=mysql_fetch_row($this->sql_res); // Получаем названия текстов $this->sql_query="select t_id, t_name, date_format(t_dt,'%d.%m.%Y') as dt from tbl_php_texts, tbl_php_cat_text where ct_cat='".$this->out_cat_id."' && t_id=ct_text group by ct_text order by t_dt"; $this->sql_execute(); if($this->sql_err) return(11); while(list($id, $name, $dt)=mysql_fetch_row($this->sql_res)) { $this->out_text_by_cat.="$dt. <a href=/text/?text=$id>$name</a><br>\n"; } return(0); }Как видите, мы выбираем из базы название рубрики, а затем и все тексты, имеющие связь с номером интересующей нас рубрики. Названия текстов сопровождаем ссылкой и датой публикации.
И не забудьте в начало файла добавить новые переменные out_cat_id и out_cat_name.
Так-с... вывод рубрик и названия текстов в них у наc теперь есть. Не хватает вывода текста. Давайте сделаем.
function out_text() { $this->out_text_id=(int)$this->out_text_id; $this->sql_query="select t_name, date_format(t_dt,'%d.%m.%Y') from tbl_php_texts where t_id='".$this->out_text_id."'"; $this->sql_execute(); if($this->sql_err) return(11); if(!mysql_num_rows($this->sql_res)) return(33); // нет такого текста list($this->out_text_name, $this->out_text_dt)=mysql_fetch_row($this->sql_res); return(0); }Это мы сделали не вывод самого текста, а проверку на его существование и получение данных по нему.
Не забудьте добавить в utils новый номер ошибки, а в начало текста новые переменные: out_text_id, out_text_name и out_text_dt.
Ну что, давайте теперь сформируем рабочие файлы и проверим мою писанину на работоспособность?
Итак, список рубрик у нас выводился так же, как и раньше, только немного изменен формат ссылок. Проверяем на /step/154/. Работает.
Раз рубрики ссылаются на /cat/?cat=, значит нам нужно создать поддиректорию /cat/ в директории WWW. И положить туда файл index.php, содержащий вывод всех работ в запрашиваемой рубрике.
Вот так он выглядит:
<? if(!isset($cat)) { header("location:/"); exit(); } require("/home/atos/php.kurepin.ru/req/out.class"); $my=new class_out; $my->sql_connect(); $my->out_cat_id=$cat; $err=$my->out_text_by_cat(); if($err) $my->html_error=$my->err_to_html($err); $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr> <td valign=top> Рубрика "<b>< ? echo $my->out_cat_name; ?></b>"<br> <br> <? echo $my->out_text_by_cat; ?><br> </td> <td width=200 bgcolor=#eeeeee valign=top> <? include($my->PATH_INC."/menu_main.inc");?><br> </td></tr></table> <? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>Как я его сделал? Взял корневой файл index.php и добавил несколько строк. Каких -- догадайтесь сами.
Познакомлю вас только с самой первой. По-моему, такой у нас еще не было. Она гласит: "Если посетитель зашел в директорию /cat/ без указания интересующей его рубрики, он прямиком отправляется на головную страницу".
Все, рубрикатор готов. Можете справа щелкать по рубрикам и видеть их содержимое. Осталось вывести сам текст. Это тоже будет просто. Мы скопируем файл index.php из папки cat в папку text и чуть его подправим. Вот, что у меня получилось.
if(!isset($text)) { header("location:/"); exit(); } require("/home/atos/php.kurepin.ru/req/out.class"); $my=new class_out; $my->sql_connect(); $my->out_text_id=$text; $err=$my->out_text(); if($err) $my->html_error=$my->err_to_html($err); $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr> <td valign=top> <div align=center>"<b>< ? echo $my->out_text_name; ?></b>"</div> <br> <div align=justify> <? if(file_exists($my->PATH_DATA."/".$my->out_text_id)) include($my->PATH_DATA."/".$my->out_text_id); ?><br> </div> <div align=right> <? echo $my->out_text_dt; ?><br> </div> <br> <br> </td> <td width=200 bgcolor=#eeeeee valign=top> <? include($my->PATH_INC."/menu_main.inc");?><br> </td></tr></table> <? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>В центре выводим название, затем сам текст. Внизу справа -- дата. Все прилично, вроде. Побродите по нашему сайту, попробуйте ввести несуществующие номера рубрик или причинить еще какой-нибудь вред сайту. Проверять не буду, но полагаю, что вести себя он будет достойно.
Здравствуйте, мои недешевые читатели!
Несколько дней я вам не писал. Был занят. Учился. Да-да, учился. Плох тот препод, который не стремится к новым знаниям. К сожалению, вынужден констатировать, что в наших ВУЗах дела в области современных компьютерных технологий обстоят так, что ученики зачастую намного "продвинутее" своих преподавателей. Преподаватели, в свою очередь, не желают это принимать и попросту "гасят умников".
Ну да ладно. Я не препод, но очень благодарный ученик. Люблю учиться, особенно, когда надо не только слушать кого-то, но и самому делать что-то.
Чему я учился? Я учился рисовать на Macromedia flash. Потратил на это почти полные три дня и чему-то научился. Нарисовал несколько интерактивных роликов и несколько баннеров. Баннеры можно посмотреть тут: http://caricatura.ru/agency/.
Технология flash мне очень понравилась. Не только за то, что с ее помощью можно создавать качественные презентации и красивые заставки, но и главным образом за то, что с ее помощью можно разрешать сложные задачи html-программирования, связанные с общением сайта и посетителя. Например, с помощь flash можно обновлять некую информацию не перегружая сайта и не создавая фреймов. И другие возможности, связанные с удачной реализацией интерактива. Как-нибудь, обязательно посвящу статью использованию флеша в создании сайта. Конечно, с точки зрения уникальной функциональности, а не красоты.
Теперь подступаем ближе к нашим парнокопытным (или бараны -- непарнокопытные?). Вот, значит, какие мысли меня все больше стали посещать...
Довольно широко мой курс по PHP стали читать во всевозможных рассылках и дры. Аудитория растет. Причем, среди читателей стали появляться люди достаточно продвинутые в программировании. Может быть без особого опыта в PHP, но быстро осваивающие этот язык, так как он не сильно отличается от того же C или Perl. Разумеется, вопросы пошли более серьезные, интересные предложения в области алгоритмов и так далее. Так вот, дорогие мои читатели! Чего вы мне в рот-то смотрите? Предлагаю вам не стесняться, а приниматься за дело. Каждый из вас может написать собственный класс к нашему проекту, посвященный той или иной конкретной задаче: поиску, ротации баннеров, рассылкам, хелпу, трансляции новостей, голосованию, гостевым книгам, форуму, рейтингам, статистике и так далее!
Давайте делать проект вместе! Только оставим за мной функции координатора, чтобы не было накладок. Сообщайте мне о том, что в решили сделать то-то и то-то, а я буду иметь это в виду и предостерегу других от дублирования работ по одной теме.
Объектно-ориентированное программирование (ООП) тем и прелестно, что позволяет разрабатывать проекты большими группами, заботясь только о системе взаимоотношений между модулями. Мне кажется, что я достаточно четко задал тон формату классов и его вполне можно придерживаться. Разумеется, наиболее сложные и интересные моменты в каждом таком направлении мы будем решать вместе. Код будем публиковать и доводить до совершенства только сообща.
Итак, жду от вас предложений!
Теперь я обращаюсь к тем, кто не может или не хочет взваливать на себя бремя того или иного раздела сайта. Отныне вы не остаетесь читателями-наблюдателями. От вас требуются идеи! Интересные идеи по направлению развития проекта. Мне не хочется решать выдуманные самому для себя задачи. Давайте договоримся, что каждый следующий урок я буду черпать из предложений, составленных и обсужденных вами в моем форуме на forum.21.ru. Договорились? Я надеюсь, что договорились. Кстати, из того же обсуждения желающие присоединиться к проекту смогут черпать и для себя идеи.
На этом позвольте раскланяться. Жду идеи в форум. Тема беседы будет называться... "Куда дальше?!!".
А чтобы данный выпуск не показался вам совсем скучным -- предлагаю скачать очередной SFX-zip всего кода сайта на текущий момент.
Согласно триду "Куда дальше", что на форуме forum.21.ru, сегодня мы уделяем внимание внешнему виду сайта.
Надо вспомнить, что мы изначально ориентировали наш проект на создание "странички обозревателя". То есть, сайт должен выполнять функции управляемого сайта с текстами, рубриками, новостями, рассылками. Вот и будем внешний вид ориентировать на это. Но перед тем, как перейти к скриптам, хочу ответить на популярный вопрос: "Почему мы храним тексты в файлах на диске, а не в базе данных?".
Хранение текстов в базе, текстовых файлах или еще где-то -- дело программиста. Лично я могу назвать несколько десятков причин в пользу того или иного способа. Но в нашем случае это связано только с тем, что мне хочется, как можно шире охватить возможности PHP -- показать максимум приемов программирования и способов хранения и обработки информации.
И еще. Прошлую задачу по SQL успешно решили, ответы можно найти, как всегда, на forum.21.ru, а я задаю следующую.
На том же форуме недавно вставал вопрос о том, как передавать данные из формы в PHP, если в форме присутствуют "чекбоксы". Как определить -- отмечен был чекбокс или нет? Как правильно давать имена чекбоксам при формировании их списка из базы. Этот вопрос мы быстро разрешили.
И на основе этой беседы я задаю задание более сложное. Представьте, что вы программируете систему документооборота. У вас, помимо всего, существует условие доступа нескольких уровней для нескольких групп. И вам надо написать web-функцию редактирования доступа пользователю. Пример. Как это на экране.
Управляющий выбирает из списка пользователей одну персону и на экран выводится таблица:
[доступ] [название группы]
[доступ] [название группы]
[доступ] [название группы]
[доступ] [название группы]
где:
доступ -- выпадающее меню с вариантами (нет доступа, только чтение, редактирование, администрирование);
название группы -- есть название группы, к которой применяется данный доступ.
Количество групп динамично, они берутся из отдельной таблицы.
Управляющий выставляет напротив каждой группы уровень доступа и нажимает "Сохранить". Данные сохраняются.
"Как правильно сгенерировать такую форму?" -- это первый вопрос. То есть, как правильно назвать все переменные формы и какие значения им присвоить.
И вопрос второй -- более интересный: "Как обработать эти данный в PHP, после нажатия кнопки "Сохранить", напишите оптимальный и/или красивый код".
Подумаете над вопросом чуть позже, а мы возвращаемся к нашим кудрявым... Переделаем навигатора. Это полоска сверху и снизу, если вы уже забыли. Я посмотрел, подумал... и решил, что навигатор у нас будет выполнять функцию "зеркала местонахождения". То есть, он будет показывать путь, который прошел пользователь от головной страницы сайта. Это модная и очень удобная "фича". Для этого немного переделаем файл navigator.inc, в котором хранится сам навигатор. Что переделаем?
1. Установим выравнивание к левому краю.
2. Вместо готового текста сделаем присваивание текста переменной $NAVIGATOR в vars.classи ее вывод в navigator.inc. Это делается для того, чтобы в любом html-файле нашего проекта мы могли как угодно переопределить вывод этой строки: дополнить, изменить, стереть ее.
3. Добавим отдельный стиль файл style.css для навигатора.
Вот, что у меня получилось:
1. В vars.inc дописал:
// Переменные var $NAVIGATOR="<font class=text_navigator><b>Смотрим:</b></font> <a class=text_navigator href=/>начало</a>"; // верхний и нижний навигатор2. Файл navigator.inc теперь выглядит вот так:
<table width="100%" cellspacing="0" cellpadding="3" bgcolor=#eeeeee> <tr> <td> <? echo $my->NAVIGATOR; ?> </td> </tr></table>3. А в style.css добавил:
.text_navigator { font family: Verdana, Arial Cyr; font size: 8pt; color: #FF6600; font-weight: normal; } A.text_navigator { COLOR: #FF6600; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.text_navigator:link { COLOR: #FF6600; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.text_navigator:visited { COLOR: #FF6600; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: none } A.text_navigator:hover { COLOR: #FF0000; FONT-FAMILY: Verdana, Arial Cyr; FONT-SIZE: 8pt; TEXT-DECORATION: underline }Теперь можно управлять навигатором, показывая пользователю место его расположения в древе сайта.
Мы будем это делать путем добавления к переменной навигатора необходимый текст. Делать это желательно на каждой странице и обязательно до начала вывода страницы. Например, в файл /cat/index.php следует добавить где-нибудь в начале, но уже после создания экземпляра класса и получения названия рубрики:
$my->NAVIGATOR.="<font class=text_navigator> -> рубрики -> \"<a href=/cat/?cat=$cat class=text_navigator>" .$my->out_cat_name."</a>\"</font>";А вывод названия рубрики в тексте уже можно убрать. А можно и не убирать. Давайте лучше его отцентруем и придадим вид заголовка при помощи CSS. Для этого у нас в CSS определен тип text_title. Только цвет мы ему изменим с серого на коричневый -- так веселее.
Таким образом, файл /cat/index.php у нас выглядит теперь следующим образом:
<? if(!isset($cat)) { header("location:/"); exit(); } require("/home/atos/php.kurepin.ru/req/out.class"); $my=new class_out; $my->sql_connect(); $my->out_cat_id=$cat; $err=$my->out_text_by_cat(); if($err) $my->html_error=$my->err_to_html($err); $my->NAVIGATOR.="<font class=text_navigator> -> рубрики -> \"<a href=/cat/?cat=$cat class=text_navigator>" .$my->out_cat_name."</a>\"</font>"; $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr> <td valign=top> <div align=center class=text_title><? echo $my->out_cat_name; ?></div> <br> <? echo $my->out_text_by_cat; ?><br> </td> <td width=200 bgcolor=#eeeeee valign=top> <? include($my->PATH_INC."/menu_main.inc");?><br> </td> </tr></table> <? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>Можно было бы и не брать в тэг <a> ссылку на данную рубрику. Все равно, она ведет на эту же страницу. Но пользователя надо приучать посетителя к тому, что навигатор позволяет кликать по любому слову в его строке. Кроме того, заход на эту же страницу бывает удобен, когда пользователь на странице что-то натворил и хочет ее обновить. В этом случае он и будет кликать в эту ссылку.
Слово "рубрики" я добавил в навигатор на будущее. Надо будет сделать страницу, посвященную рубрикам, на которой каждому названию рубрики будет дана своя аннотация или просто расшифровка короткого названия рубрики. Мне кажется, что это не помешает.
Теперь давайте немного причешем заглавную страницу. Или "морду", как ее часто называют web-дизайнеры.
Предлагаю для начала разместить на морде информацию о сайте и подключить новости. Краткую информацию о сайте можно вписать прямо файл index.php, так как этот текст вряд ли нам еще где-то понадобится. Пишу...
<font class="text_"><font color=red><b>Внимание!</b></font> Данный сайт является практическим комментарием к курсу web-программирования на PHP. Сайт обновляется и пополняется новыми возможностями параллельно появлению новых выпусков курса на сайте <a href=kurepin.ru class="text_">kurepin.ru</a>. <br><br><div align=right> <a href=mailto:atos@21.ru class="text_">Руслан Курепин</a><br> </div><br> </font>... и копирую это в index.php
Теперь новости. Честно говоря, я нахожусь в некотором замешательстве по поводу новостей. Дело в том, что я всю жизнь новости хранил в текстовом файле и просто подключал его в нужном месте сайта. Но для нас создать новый текстовый файл и подключить его к сайту -- не задача давно. Но и усложнять этот процесс не очень бы хотелось, дабы меня не обвинили в чрезмерных заморочках на пустом месте.
Давайте так! Мы будем хранить новости в текстовом файле, но саму работу с новостями овеем особым подходом и особой обработкой.
Я говорю о том, что новости мы будем добавлять, сортировать, вырезать, составлять дайджесты и прочее -- из текстового файла, а не из базы данных. Таким образом, мы сможем хорошенько потренироваться в работе с файлами. А работы с базой у нас и так будет предостаточно.
В начале сегодняшнего выпуска я и писал о том, что хранение данных в файлах и в БД имеют свои достоинства и недостатки. Поэтому, уметь работать с файлами -- это тоже очень важно. Может быть даже важнее, чем с SQL-базой. Вам не лишнее будет узнать, что все огромные проекты в Рунете делаются на файлах гораздо чаще, чем на SQL. В качестве примера можно привести такие известные всем проекты, как: счетчик Rambler, баннерная сеть RLE, издание Lenta.ru и другие. И тому есть свои причины, поверьте.
Я опять отвлекся. Значит так. Нам надо разработать форму хранения новостей в текстовых файлах. Предлагаю хранить следующим образом. Каждая новость располагается в одной строке и состоит из четырех составляющих:
1. Дата
2. Время
3. Анонс
4. Новость
Все четыре составляющие записываются в одной строке, а в качестве уникального разделителя предлагаю использовать двойную тильду "~~". Такое сочетание крайне редко используется в тексте. Разве что в графическом "украшательстве текстов", поэтому нам не светит включать в новость такое сочетание символов: мы смело можем считать это сочетание разделителем составляющих новости.
Строки хранятся в файле -- одна за другой.
Пустые строки игнорируются.
Строки, начинающиеся с "#", считаем комментариями и внимания не обращаем.
Кстати, этот знак имеет тоже несколько неофициальных названий, среди которых самым распространенным среди программистов является "тюрьма".
Чуть позже, когда приступим к программированию работы с новостями, мы определим несколько глобальных переменных для новостей, а пока просто включим новости в головную страницу из файла. Новости будем хранить в отдельной директории /news/ на одном уровне с другими основными папками: inc, req и другими. В этой директории будут хранится файлы-исходники, из которых будут формироваться другие файлы, видимые на сайте. Те файлы, что видны на сайте, временные файлы, файлы-архивы.
Основной/активный файл с кратким списком новостей назовем main.inc, его и прицепим на главную страницу. Вот так выглядит файл index.php -- морда нашего проекта:
<? require("/home/atos/php.kurepin.ru/req/out.class"); $my=new class_out; $my->sql_connect(); $my->html_headers(); include($my->PATH_INC."/top.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/navigator.inc"); flush(); ?> <table width="100%" cellspacing="0" cellpadding="3"> <tr> <td valign=top> <font class="text_"><font color=red><b>Внимание!</b></font> Данный сайт является практическим комментарием к курсу web-программирования на PHP. Сайт обновляется и пополняется новыми возможностями параллельно появлению новых выпусков курса на сайте <a href=kurepin.ru class="text_">kurepin.ru</a>. <br><br><div align=right> <a href=mailto:atos@21.ru class="text_">Руслан Курепин</a><br> </div> <br> </font> <!-- news --> <font class=text_title>Новости</font> <p><? if(file_exists($my->PATH_NEWS."/main.inc")) include($my->PATH_NEWS."/main.inc"); ?> <!-- /news --> <br> </td> <td width=200 bgcolor=#eeeeee> <? include($my->PATH_INC."/menu_main.inc");?><br> </td> </tr></table> <? flush(); include($my->PATH_INC."/navigator.inc"); include($my->PATH_INC."/adv_top.inc"); include($my->PATH_INC."/bottom.inc"); $my->sql_close(); ?>Интересный момент -- обратите внимание -- все фрагменты в этом файле подключаются без проверки на существование файла, а новости я предварил проверкой. Как думаете, почему?
И не забудьте в vars.class добавить новую переменную. Догадались какую? Ну, PHP вам укажет на ошибку, если забудете.
Как и обещал, сегодня работаем с новостями. Если я еще помню вчерашний выпуск, мы придумали формат для хранения новости. Исходя из этого формата нам надо бы получить краткий вариант новостей для "морды" и полный вариант для раздела "новости" с разбиением всего списка новостей на страницы. Начнем с новостей на титульную страницу (морду).
У нас для анонсов предусмотрено специально поле строке записи новости. Кроме нее возьмем дату новости, если новости у нас выходят не часто (раз в день или реже) или время новости, если новости у нас идут как из пулемета. Еще нам понадобится "ограничитель" количества коротких новостей -- еще одна переменная в vars -- NEWS_MAIN_MAX -- мы ее назовем. А имя файла для титульных новостей мы определили в прошлом выпуске: /news/main.inc. Этого будет достаточно, мне кажется.
Теперь большой и философский вопрос о том, в какой класс нам запихнуть функции работы с новостями.
Для добавления новостей и их редакции вполне подойдет class_in, а вот для вывода... надо рассудить здраво. Давайте рассуждать вместе... Новости у нас могу вызываться в нескольких местах:
1. На главной странице
2. На странице "новости"
3. На любой другой странице может вызываться анонс в виде текстового рекламного блока.
Напрашивается класс out... Согласны? Вижу, что согласны. Ох, люблю я вас поводить за нос! Ну какой может быть вывод новостей через класс? Вы что, собираетесь выдергивать новости из архива для каждого пользователя? Конечно же нет. Тут разговора быть не может -- новостные файлы должны формироваться на диск в момент какого-либо изменения в главном новостном файле и подключаться на сайт простым includ-ом.
А это значит, что все (почти все) функции работы с новостями у нас пойдут в класс in. Вернее, я предлагаю породить от класса in класс class_in_news и в нем хранить все функции обработки новостей. Итак. Создаем файл /req/in_news.class.
И... снова ударяемся в рассуждения. Рассуждения, друзья мои, -- в нашем деле далеко не последняя вещь, если не первая.
Когда нам надо обращаться к формированию файлов новостей? Очевидно, что только после внесения каких-то изменений в основной новостной файл. Это значит, что нам нужна функция, создающая новостные файлы. Эту функцию мы будем вызывать изо всех функций, вносящих какое-либо изменение в основной файл новостей. Перечитывать этот абзац до полного понимания.
Вы можете сейчас сказать, сколько и каких файлов нам надо будет создавать? Я, честно говоря, сильно затрудняюсь это сделать. Поэтому, создадим первую функцию in_news_files_create(), которую и будем вызывать для формирования новостных файлов. То есть это будет некий аккумулятор функций, создающей структуру новостных файлов на диске.
Первой "рабочей" функцией станет in_news_main(). Именно в ней мы сделаем выборку нужных нам строк в файл /news/main.inc.
В одном из выпусков я уже рассказывал о работе с файлами, поэтому не стану заострять внимания на используемых ключевых словах языка, скажу лишь только, что использовать мы будем самые заурядные команды.
Как происходит сборка файла для морды. Алгоритм прост как барабан. Надо прочесть от начала файла $NEWS_MAIN_MAX строк, выдрать из них дату и анонсную часть текста и записать это в файл. Проделаем это в реале:
Да, чуть не забыл! Исходный файл для хранения новостей так и будем звать -- source.inc -- исходник.
Создали файл /news/source.inc:
# Новости 18.02.02~~12:30~~Новый выпуск PHP на <a href=http://kurepin.ru class="text_">kurepin.ru</a> ~~Новый выпуск PHP на <a href=http://kurepin.ru class="text_">kurepin.ru</a>. В этот раз речь пойдет о внешних(видимых) фрагментах сайта: форматирование строки навигатора, добавление новостей на главную страницу 17.02.02~~12:30~~Новый выпуск PHP на <a href=http://kurepin.ru class="text_">kurepin.ru</a> ~~Новый выпуск PHP на <a href=http://kurepin.ru class="text_">kurepin.ru</a>. В этот раз речь пойдет о внешних(видимых) фрагментах сайта: форматирование строки навигатора, добавление новостей на главную страницуНу... ничего страшного, пусть будет такой убогий для начала.
Далее создаем в class_in_news функцию in_news_main(). Вот такой файл у меня получился:
<? require("/home/atos/php.kurepin.ru/req/in.class"); class class_in_news extends class_in { // Класс работы с новостями function in_news_main() { // открываем файлы для чтения и записи if(!$r=fopen($this->PATH_NEWS."/source.inc","r")) return(41); if(!$w=fopen($this->PATH_NEWS."/main.inc","w")) return(42); $i=$this->NEWS_MAIN_MAX; // устанавливаем счетчик while((!feof($r)) and ($i>0)) { $str=fgets($r, 10240); if((substr($str,0,1)!="#") and (substr_count($str,"~~")>2)) { list($dt, $tm, $anons, $text)=explode("~~",$str); fputs($w,"<font class=\"text_\"><b>$dt</b> $anons<br>\n"); $i--; } } fclose($r); fclose($w); return(0); } function in_news_files_create() { $err=$this->in_news_main(); if($err) return($err); return(0); } } ?>Пояснять надо? Поясняю.
Открыли файл для записи/перезаписи и для чтения.
Установили убывающий счетчик на нужное количество строк новостей.
Начали цикл while по условию счетчика или конца файла
Все, как всегда, просто и логично. Как и должно быть в коде программы.
Если вы внимательно просмотрели код, то заметили два новых номера ошибки. Добавим их в класс утилит:
$err[41]="Не могу открыть файл ".$this->PATH_NEWS."/source.inc для чтения"; $err[42]="Не могу открыть файл ".$this->PATH_NEWS."/main.inc для записи";Все, новости на морду у нас теперь создаются! Ура! Только эта... как выполнить-то функцию, а? Она же у нас предназначена для автоматического выполнения из других функций...
Вот мы и подошли к созданию еще одной важной директории нашего проекта. Даже не одной, а сразу двух! Дело в том, что в жизни любого, даже самого продуманного сайта возникают ситуации, требующие ручного запуска той или иной автоматической функции. Это может потребоваться и для наших новостей. Кроме этого, существуют функции, которые необходимо запускать как из скриптов, так и с некоторой периодичностью из cron-а.
В директорию /utils/ мы будем складывать файлы для ручного выполнения каких-либо операций, а в директорию /cron/ будем складывать скрипты для периодического выполнения. Пожалуйста, создайте обе эти директории в своих проектах.
Файлы, живущие в этих директориях имеют, как правило, всего несколько строк кода -- они всего лишь подключают нужный класс и вызывают нужную функцию.
Положив в папку /utils/ файл create_news.php такого содержания:
<? require("/home/atos/php.kurepin.ru/req/in_news.class"); $my=new class_in_news; $my->sql_connect(); $err=$my->in_news_files_create(); if($err) $my->err_to_str($err); $my->sql_close(); ?>Этот файл мы можем выполнить из командной строки, придав ему вид выполняемой программы. Лично я этого никогда не делаю. Мне приятнее запускать подобные скрипты, подставляя их имена в качестве параметра к php, собранному для командной строки. Но это всего лишь дело привычки.
Главное что? Главное, что скрипт этот я запустил и получил на сайте новостную колонку из коротких анонсов. Потом надо будет снабдить их ссылкой на полную новость, но это уже после того, как будет создана страница трансляции полных новостей.