Веб-программирование на PHP. (Часть III)

Руслан Курепин
http://kurepin.ru/main.phtml

Веб-программирование на PHP (Часть II)

Готовим дизайн (начало)
Готовим дизайн (продолжение)
Готовим дизайн (окончание)
BackOffice (Начало)
BackOffice (Продолжение-1)
Глобальная сверка
Таблицы стилей (CSS)
BackOffice (Продолжение-2)
BackOffice (Продолжение-3)
BackOffice (Продолжение-4)
BackOffice (Продолжение-5)

Веб-программирование на PHP (Часть IV)


^^^   Готовим дизайн (начало)

С чего начать визуализацию сайта? С начала начать! И пусть заглохнут те, кто считает разбор будущего дизайна последним делом. Ничего подобного! Дизайн (я имею в виду только его функциональную часть) — вещь наиважнейшая. Код можно всегда поправить, не взирая на лица. А вот с дизайном это сделать всегда сложнее.

Ну, рисовать дизайн я вас учить не буду, т.к. сам не умею. А вот как сделать дизайн текстовый — это мы попробуем. Заранее прошу прощения у всех, кто прислал мне свои варианты дизайна для нашего тестового сайта, но мне подумалось, что не стоит опираться на чей-то вкус: о графическом дизайне так много всегда спорят. А текстовые дизайны я делаю давно и, как мне кажется, они вполне у меня получаются.

"Итак, начало... начало — это встреча со зрителем...", — как говорил главный герой "Мы из джаза", — "Зритель — наш старый знакомый. Мы же не орем ему ЗДОРОВО! Мы говорим мягкое — привет!".

Это я к чему? А, это я к тому, что любой сайт начинается всегда с заголовка. И хоть законы жанра заставляют нас влупить в него неимоверного размера графическую бляшку, которую все помпезно называют фирменным знаком или еще как-то, но надо при этом стремиться к тому, чтобы заголовок был как можно более функциональным.

Лично я всегда в душе хвалю сайты, которые позволяют передвигаться по страницам и разделам, не дожидаясь полной загрузки текущей страницы. Честно говоря, я сам далеко не всегда следую всем правилам, но мы будем стараться их придерживаться.

Стоп! Я не прав. Я же совсем забыл вам рассказать, что у нас каждая страничка сайта должна собираться из кирпичиков. Вернее, из блоков. И эти блоки должны храниться в разных файлах, чтобы в любой момент мы могли из этих блоков построить очередную страницу или внести изменения во все страницы сайта одновременно.

Мы будем действовать по принципу SSI. Кто не знает что такое SSI — отправляю читать про SSI. Про эту технологию написаны сотни статей, хотя она вся состоит из нескольких команд. В двух словах: SSI диктует http-серверу — какие файлы нужно подключить (или подлинковать, как говорят некоторые программисты) в текущий, прежде чем отдать файл пользователю. Сама технология SSI нам не понадобится, мы легко заменим функции SSI функциями PHP.

Можно достаточно мелко дробить сайт на составляющие, но мы это сделаем по мере необходимости. Пока можно остановиться на основных составляющих почти любого сайта:

  1. Заголовок
  2. Быстрая навигация по основным разделам
  3. Меню
  4. Рекламные блоки
  5. Основной текст
  6. Низ страницы (копирайт)

Заголовок — это для нас просто: текстовая строка с названием сайта и/или текстовый блок. Для примера можно посмотреть на сайт http://caricatura.ru/parad/. Видите, слева логотип, справа пояснительный текст, ниже отчеркивающая строка. Это и есть заголовок, который лежит в папочке inc под названием top.inc. У нас тоже есть папочка inc, в которую мы положим свой такой заголовок.

Хочу еще раз напомнить, что папку с подключаемыми файлами (в отличие технологии SSI) мы храним вне директории WWW или как называется папка с доступными по http страницами. Иначе злоумышленник сможет прочесть содержимое inc-файлов и найти способ взломать сайт.

Что вообще хорошо иметь в файле заголовка? Я предпочитаю иметь там четыре основные составляющие сайта:

  1. html-заголовок
  2. java script-функции ("жалкий скрип", как я называю этот язык)
  3. Сам заголовок — текст и графику
  4. Невидимые фрагменты всевозможных счетчиков и других "жучков", которым ладнее грузиться в первую очередь

В принципе, в заголовок можно включить и описание стилей (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 (Начало)

Ну что, господа грызуны науки программирования? Как настроение? Надеюсь, лучше моего... Сегодня один урод на дороге помял моего любимого Мерседеса, от чего я в страшном гневе (даже подлокотник в порыве ярости вырвал с корнем). Так что, злой я сегодня на редкость. Рекомендую слабонервным, женщинам и детям не читать этого выпуска, пока он не отлежится и не остынет! А для тех, кто не боится ни ножа не #уя, мы продолжаем нашу неторопливую беседу...

Чего вы ждете? Вероятно, вы ждете начала очередного нудного повествования о том, как и что нам надо дальше написать? Ничего подобного! У нас уже столько всего написано, что пора бы уж этим воспользоваться.

Если я не ошибаюсь, то у нас уже написаны функции добавления в базу разной всячины, вот их-то мы и задействуем следующим шагом.

То есть, поработаем сначала над 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/, как договаривались.


^^^   BackOffice (Продолжение-1)

----
Файлы этого выпуска: /step/146/
----

Странно, но никто не спросил, почему я везде ставлю относительные пути (URL): которые начинаются с корня сайта /, как обычно и делают, а в меню администратора добавил еще и переменную с абсолютным путем $PATH_HTTP. Может у кого-то возникнут на этот счет предположения? Если возникнут -- милости просим их озвучить на форуме: http://forum.kurepin.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

php_kurepin_ru_180102.exe

Не забудьте поправить их под себя...


^^^   Создание таблицы стилей (CSS)

----
Файлы этого выпуска: /step/148/
----

Ну вот. Появился у нас работающий сайт. Теперь можно вернуться на несколько уроков назад и продолжить работу над сайтом в том порядке, в котором надо было ее вести. Если помните, я чуть форсировал визуализацию, чтобы вы не умерли от удовольствия.

Теперь мы чуток откатимся назад и пройдем еще один подготовительный этап -- создание таблицы стилей (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. Не знаю, какими средствами они этот эффект реализовывали, но полагаю, что какими-то схожими.


^^^   BackOffice (Продолжение-2)

----
Файлы этого выпуска: /step/150/
----

Эх, что творится в нашей многострадальной России... НТВ закрыли, ТВ-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

Что изменилось, по порядку:

  1. добавилась строка
    $my->in_cat_id=$cat_id;
    присваивающая глобальной переменной номер изменяемой рубрики;
     
  2. поменяли название вызываемой функции с add на rename;
     
  3. немного изменили вывод сообщения об удачном завершении операции;
     
  4. в процедуру очистки переменных unset() добавили новую переменную $cat_id, несущую из формы номер рубрики;
     
  5. переименовали заголовок формы с "добавление" на "переименование";
     
  6. добавили в форму еще одно поле типа text с именем cat_id, для указания номера изменяемой функции;
     
  7. кнопку добавить переименовали в сохранить;

Как видите, вся процедура создания новой возможности 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, но еще не привезли...


^^^   BackOffice (Продолжение-3)

Пора перейти к добавлению текстов в базу. Для чего создадим папку text в админской директории. В общем-то, нам достаточно скопировать файл add.php из папки рубрик в папку текстов и чуть подправить его, заменив названия переменных и добавив пару элементов в форму. Но мы же не просто делаем сайт. Мы же еще и чему-то учимся, не так ли? Поэтому, предлагаю усложнить самим себе жизнь и добавить еще мелких задач.

Какие параметры мы вписываем в базу для регистрации нового текста:

  1. Название текста
  2. Сам текст
  3. Номер рубрики

С первыми двумя полями точно ничего не сделаешь, а вот к третьему давайте хоть выпадающее меню напишем, а? Чтобы реже ошибаться в номере рубрики. Предлагаю создать эту функцию в файле 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)

И в эту базу мы будем заносить все соответствия текстов рубрикам. Сможем привязывать один текст с любым количеством рубрик, или рубрику с любым количеством текстов -- называйте это как хотите.


^^^   BackOffice (Продолжение-4)

Вот так всегда. Вместо того, чтобы по-человечески лежать на диване, смотреть в телевизор и отпиваться пивом с креветками после вчерашнего пьянствования моего дня рождения, я сижу за компьютером, пью зеленый чай и пишу очередной выпуск 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);
 }

Все, вынужден проститься, меня ждут друзья в боулинге. Они там отмечают мой день рождения и звонят каждые десять минут. Надо ехать, нельзя обидеть.

А вам задание: подумайте, какие проверки в какие функции надо будет добавить, чтобы по ходу работы у нас не возникало коллизий с таблицей связей. Подскажу, что речь идет о изменении списка рубрик, например.


^^^   BackOffice (Продолжение-5)

----
Файлы этого выпуска: /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 и чуть его подправим. Вот, что у меня получилось.

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();
?>
В центре выводим название, затем сам текст. Внизу справа -- дата. Все прилично, вроде. Побродите по нашему сайту, попробуйте ввести несуществующие номера рубрик или причинить еще какой-нибудь вред сайту. Проверять не буду, но полагаю, что вести себя он будет достойно.


Веб-программирование на PHP (Часть IV)