Июнь-Июль '05 No 13
2005
Киев, Май 2005
Современные технологии эффективной разработки веб-приложений с использованием PHP PHP::SOAP и XForms на АвтоВАЗ-е MySQL: репликация и кластер издается с февраля 2004 года, www.phpinside.ru
PHP Inside №13
Содержание В фокусе Свой проект свободно распространяемого программного обеспечения.................................. 5 Две стороны документирования: phpDocumentor и DocBook..................................................14 Разработка современной CMS. Преимущества которые дает PHP 5.......................................24 MySQL – просто о сложном........................................................................................................ 37 Оптимизация PostgreSQL.............................................................................................................48 Построение поисковых систем. Принципы и реализация........................................................60 Доклад по целесообразности тестирования...............................................................................68 XML Sapiens как универсальная концепция сайтостроения в разрезе XML/PHP................. 77 XML в PHP 5................................................................................................................................. 88 Поддержка нескольких СУБД в проекте....................................................................................95 PHP::SOAP и Xforms.................................................................................................................. 104 Платежные системы – это не страшно..................................................................................... 133 Влияние TDD на дизайн кода....................................................................................................157 Введение TDD в существующий проект.................................................................................. 172
Человек с обложки. Как это было В этом номере мы совместно с организаторами конференции "Современные технологии эффективной разработки веб-приложений с использованием PHP" публикуем ее доклады и мастер-классы. На страницах журнала вы сможете найти не только материалы конференции, но и отзывы ее участников - почувствуете, так сказать, царившую там атмосферу. В дополнение к этому выпуску PHP Inside мы так же публикуем некоторые видео-материалы и презентации "as is". Теперь перейдем к вопросу, который записан в качестве заголовка этого раздела: кто же этот человек с обложки? Здесь, уважаемые коллеги, следует мысленно вернуться в тот весенний вечер 13-го Мая в Киев и, тем кто там был, вспомнить банкет после конференции (а тем кто не был - просто представить). А вспомнить нужно не то, что в заведении было выпито ВСЕ пиво, да и не то как все веселились, а аукцион, профессионально проведенный Александром Смирновым [admin], Еленой Теслей [Lenka], Александром Войцеховским [young] и некоторыми другими клубчанами. Одним из лотов была эксклюзивная кружка с логотипом PHP Inside и...! Замрите! Нет, не горшок, что в правой руке, а фото победителя торгов на обложку этого спецвыпуска (креативная идея исходила от admin'a)..
Команда номера
Авторы и переводчики Андрей Козак Борис Безруков Константин Погорелов Дмитрий Магунов Александр Войцеховский Алексей Борзов Юрий Логвинов Сергей Юдин Дмитрий Шейко Денис Жолудов Денис Соловьев Александр Анохин Владимир Булов Кирилл Литвинов Евгений Бондарев Павел Щеваев
Редакционная коллегия Александр Смирнов Александр Войцеховский Андрей Олищук [nw] Антон Чаплыгин Елена Тесля
2
PHP Inside №13
Победителем стал человек с сегодняшней обложки - Евгений Синица из украинского города Харькова (за фотосессию спасибо Lenke). Именно он увез эксклюзив. Евгению настолько полюбилась символика журнала и клуба, что он победил еще в нескольких торгах и вместе с кружкой "PHP Inside" получил пару других экспонатов, уже с атрибутикой PHPClub'а. Фотоотчеты с конференции, банкета, как и прочих событий PHPClub'а можно найти на сайте http://party.phpclub.net/search.php А состоялась конференция благодаря неисчерпаемому энтузиазму активистов из веб-сообщества и отеческой поддержке спонсоров и организаторов, чьи логотипы вы можете видеть справа. Внимание, еще остались раздаточные материалы в изначальном печатном виде. Приобрести их можно в Киеве: ул. Исаакяна, 18, офис 211, тел. +380 44 230 47 77, Андрей Зинченко.
Команда номера (продолжение)
Выпуск номера
Андрей Олищук [nw] Роман Толкачев [rammstein] Антон Чаплыгин Денис Зенькович
Контактные данные http://phpinside.ru nw@phpinside.ru
Если вам интересно узнать отзывы участников, то в полном объеме отзывы представлены на сайте PHPClub'а: http://phpclub.ru/talk/showthread.php?s=&threadid=67108&rand=59 а здесь мы приведем только некоторые из них. syfisher: Почти все понравилось! Плюсы: организация отличная, публика тоже очень хорошая. Мы думали по началу, что это будет некий официоз. Я на деле - очень дружеская атмосфера разработчиков. Очень понравилось место проживания активистов. Минусы: некоторые доклады были провальными или имели слишком рекламный характер. С этим нужно бороться или же выносить такие доклады в отдельные секции, идущие параллельно. Я бы посмотрел сколько бы человек пошло на AWWCMS, если бы мы начали рассказывать про LIMB (ладно, шучу :-)). На будущее: получать доклады в сокращенном виде (до 5 страниц) еще до голосования по темам. То есть ввести элемент диктатуры по этапе отбора докладчиков. Я думаю, что такого пренебрежительного отношения в конференции, как после 3-й, больше не будет. Мне как докладчику по TDD очень было приятно, что сообщество экстремалов развивается и достаточно резво. Это придает уверенности и является хорошим стимулом чтобы продолжать то, что мы начали. Конечно жаль, что не все активисты клубы выбрались на конференцию, а мы так рано уехали, но для первого раза (для меня лично) - очень доволен. Fiva: Конкретно разочаровали вопросы не по существу а с целью завалить докладчика, показать что весь его доклад - полная фуйня. Я думаю этого делать было не обязательно, плохие доклады от хороших можно было отличить не только невооруженным взглядом, но и сразу же после получения раздатки. kdk: Добавлю свои три капли. Уровень докладчиков несомненно стал выше. Если на прошлой конференции мне вспоминаются 2-3 интересных доклада, то на этой вспоминаются 2-3 плохих. Т.е. Ситуация в корне изменилась!!! Хороших докладов и грамотных докладчиков, умеющих общаться с аудиторией стало на порядок больше!!! 3
PHP Inside №13
voituk: Долго распыляться по поводу своих впечатлений не буду (хотя очень хотелось) скажу только что конференция прошла под лозунгом "TDD в массы!". Вторым (хотя скорее первым) ярким эпизодом конференции был отличный доклад Александра Анохина про XFroms+SOAP. P.S. конечно же очень приятно и познавательно было пообщаться с ребятами из Пензы и Анохиным в неформальной обстановке пивного бара. young: тема php не раскрыта :) mariroz: большое спасибо организаторам за хорошо подготовленную конференцию! Понравилась достаточно неформальная обстановка и свободный стиль изложения, разнообразность тем докладов. Хотя, действительно, были и рекламные выступления и слабые, но в целом время (надеюсь, не только для меня) потрачено недаром. Для того, чтобы были сильные, интересные доклады должны быть и слабые . И, кстати, глупые вопросы тоже. Мальчики из Пензы: вы просто молодцы ). Разочаровали конечно доклады о AWWCMS и siteMETA. Здесь не ругают, но мне не понравился также доклад о поддержке нескольких субд в проекте. Трудно, конечно, было выступать после доклада от автоваз, но мне показалось, что докладчик и сам чувствовал слабость своего доклада, отсюда и странноватый (имхо) стиль выступления. Из пожеланий: материалы конференции хорошо бы получить заранее; очень поддерживаю идею общего банкета . Да, и спасибо за отдельное внимание к женской части публики. Мелочь - но приятно. Yur@: Почитал отзывы, и немного удивился. Многие говорят, что конфы прогрессируют. Мне сравнивать не с чем, но осталось какой-то неприятный осадок... Если конкретно - организацию я бы оценил на 5, может с минусом. Т.к. никакого дискомфорта в этом плане не ощущал, а придирки к еде, бейджикам и указателям не столь серьезны (т.е. опять же, я не чувствовал по этому поводу дискомфорта). А вот по поводу докладов есть серьезное замечание. Не буду останавливаться на каком-то конкретном докладе, но 1й день мне не понравился абсолютно (окромя доклада по MySQL), второй день был куда интереснее (слабым звеном был разве что доклад по платежным системам). Но теперь о самом главном. Мне абсолютно неинтересно сидеть 45 минут и слушать, как люди сидят и пиарят свой программный продукт!!! Я НЕ ЖЕЛАЮ СЛУШАТЬ РЕКЛАМУ ЗА СВОИ ЖЕ ДЕНЬГИ. Самый яркий тому пример - Разработка современной CMS, отчасти сюда же можно отнести доклад по "Мете"... Я считаю, что такие доклады даже не должны допускаться на конфу. Я абсолютно ничего не услышал из доклада для себя полезного, а сам стиль доклада был пиарным, бездоказательственным, и необоснованным. screjet: Конфа прошла отлично! Есть предложение следующую провести в лесу с палатками, гитарами и фаейрволом.
PHP Inside №13
Свой проект свободно распространяемого программного обеспечения
В фокусе Свой проект свободно распространяемого программного обеспечения Каждый из вас слышал об операционных системах Linux. Автор: Андрей Козак Многие знают, что эти ОС в большинстве являются бесплатными и имеют открытые исходные коды. В настоящее время существуют десятки тысяч Open Source проектов. Эти проекты написаны на самых различных языках под самые различные платформы. Что же такое Open Source?
Вступление Open Source Software (ПО с открытым исходными текстами) представляет собой методологию разработки программного обеспечения, при котором исходный код программ либо является общественным достоянием (public domain), либо защищён с помощью одной или нескольких из многочисленных "открытых" лицензий - как, например, GNU General Public License (GPL). Такие лицензии, как правило, требуют, чтобы исходный код распространялся вместе с ПО, мог свободно модифицироваться и использоваться с минимальными ограничениями. Таким ограничением может быть требование о сохранении имен авторов, упоминание об их авторских правах. В некоторых случаях (напр., Apache или FreeBSD) эти ограничения очень малы. Заметьте, что это права пользователя, но не обязанность производителя - "открытая" лицензия не требует, чтобы само ПО предоставлялось бесплатно. Многие из наиболее успешных проектов "открытого" ПО, однако, доступны совершенно бесплатно. Подавляющее большинство открытых программ является одновременно свободным программным обеспечением и наоборот. Это вызвано тем, что определения открытого и свободного ПО очень близки, и большинство лицензий либо удовлетворяют обоим определениям сразу, либо не удовлетворяют ни одному из них.
Зачем нужен свой проект в Open Source? Причин, по которым люди начинают писать Open Source программы, существует несколько. Одна из них - с идеологической точки зрения. Такие люди часто используют Open Source ПО. Вторая из причин -поделиться с другими программистами какой-либо идеей (когда проект только зарождается), развивать её и соответственно реализовывать. Третья из возможных причин - это желание сделать что-то полезное, удобное. Четвертая - просто прославиться и стать народным героем. Пятая - поднять свой уровень программирования. Есть другие причины, но на них мы останавливаться не будем.
5
PHP Inside №13
Свой проект свободно распространяемого программного обеспечения
Создание своего проекта Итак, по какой-то из причин вы решили все таки создать свой проект Open Source и разместить его в сети. Теперь придется решить несколько проблем: где разместить проект, под какой лицензией распространять, как лучше писать документацию и т.п.
Хостинг Возможно, первая проблема, с которой сталкивается разработчик Open Source, - как заявить о проекте, как сделать так, чтобы люди могли про него узнать, а возможно, и скачать. Самый простой способ -разместить сообщение на форуме, а затем интересующимся высылать информацию на e-mail. Но способ этот самый неудобный, т.к. существуют более гуманные средства для распространения Open Source. Начнем с различных каталогов скриптов. Довольно интересные сервисы, но малофункциональные. Полную документацию разместить в online некуда, дополнительные страницы создать невозможно и т.п. Поэтому сразу перейдем ко второму варианту. Вторым вариантом являются системы для размещения OpenSource проектов. На данный момент самые популярные - это Sourceforge и Freshmeat. В общем-то, они выполняют приблизительно одинаковые задачи, но Sourceforge является более популярным ресурсом. По последним данным там зарегистрировано около 1,1 миллиона пользователей и около 100 000 проектов, среди которых около 11 000 - проекты, написанные на PHP. Регистрация и размещение на этом сайте (как, впрочем, и на многих других) бесплатные. После создания своего проекта вы сможете пользоваться такими инструментами, как добавление на сервер файлов, логическое разделение их на релизы, ведется подробная статистика скачиваний по каждому из файлов, ведение своего Bug-track, CVS, форумов, возможность бесплатно разместить сайт проекта, что, несомненно, является большим плюсом, т.к. бесплатного хорошего хостинга не бывает. Есть еще один, но более специфический вариант для размещения проектов РНР Open Source - сайт PHPClasses. На этом сайте можно размещать РНР-проекты и просто библиотеки, которые написаны с использованием ООП. Сервис, если учесть специфику, довольно удобный. Кроме того, каждый месяц проходит конкурс на лучший проект месяца с призовым фондом (от спонсоров, например, книга либо лицензионная версия Zend Studio). Представим, что проблема, где размещать проект, решена, и вы выбрали для этого сервис, либо просто пользуетесь обычным платным/бесплатным хостингом. Теперь возникает другая проблема - как писать и оформлять документацию, чтобы другим было удобно ей пользоваться.
Документация Чаще всего документация появляется в относительно немаленьких проектах, т.к. реально появляется необходимость объяснить пользователям и, возможно, будущим разработчикам, что для чего служит. 6
PHP Inside №13
Свой проект свободно распространяемого программного обеспечения
Документация обычно может состоять из двух основных частей: это описание пользовательской/администраторской части и описание для разработчиков (в отличие от обычных проектов, в библиотеках обычно одно описание - список и описание функций, классов и т.п.). Когда документируется пользовательская часть, не следует описывать все кропотливо и подробно, т.к. люди, которые интересуются проектом, уже имеют минимальный опыт для работы с такими вещами, но и просто перечислять кнопки тоже не стоит, т.к. это может затруднить понимание того, как работать с программой. При документировании самого кода я могу посоветовать программу под названием phpDocumentor. О ней подробно рассказано в докладе Бориса Безрукова "Две стороны документирования: PHPdocumentor и DocBook". Документация играет очень важную роль в поддержке проекта: лучше посидеть над документацией, чем потом отвечать на простые вопросы в форуме или по e-mail. Кроме того, будущим разработчикам все же удобнее читать документацию кода, чем разбирать чей-то код.
Выбор лицензии (GPL, LGPL, BSD) При создании своего проекта рано или поздно встает проблема лицензирования, т.к. необходимо как-то защитить свое право на интеллектуальную собственность, а также по другим причинам. Существует большое количество лицензий для программного обеспечения. Но чаще всего для проектов PHP Open Source пользуются тремя типами лицензий: GPL, LGPL и BSD. Для наглядности и более простого понимания лицензии GPL и BSD помещены в таблицу. Требования
GPL
BSD
Требуется указывать имя автора
Да
Да
Измененные файлы должны быть помечены
Да
Нет
Наименование производного ПО должно отличаться от наименования продукта создателей лицензии
Нет
Нет
Производные произведения должны распространяться на условиях первоначальной лицензии
Да
Нет
Указана территория, на которую предоставляется лицензия
Нет
Нет
Отсутствие гарантий на ПО
Да
Да
Предоставляется право применить другую лицензию
Нет
Не указано
Отдельно следует сказать о лицензии LGPL. Эта лицензия носит ограниченное применение: она может применяться только к библиотеке, и произведение, производное от первоначальной библиотеки, также должно быть библиотекой.
7
PHP Inside №13
Свой проект свободно распространяемого программного обеспечения
Немного подробнее о лицензии BSD. Как известно, существует два варианта ее текста: с оговоркой о рекламе и без этой оговорки. Лицензия, которая одобрена для применения как Open Source, так и FreeSoftware, - это лицензия без оговорки о рекламе. Эта оговорка была официально отменена директором департамента технологического лицензирования Калифорнийского университета 22 июля 1999г. В 2001г. появился еще один вариант лицензии BSD - лицензия корпорации Intel "BSD+Patent License". Она специально разработана для того, чтобы позволить модифицировать и распространять ПО, которое может защищаться патентами на программное обеспечение корпорации Intel. Лицензии LGPL, BSD являются совместимыми с лицензией GPL. Совместимость с GPL означает, что разработчик вправе объединить модуль, который распространяется на условиях совместимой с GPL лицензии с модулем, распространяемым на условиях GPL, чтобы получить одну программу. Дальнейшее распространение полученной программы должно осуществляться в соответствии с условиями. Если создатель библиотеки решит заменить лицензию LGPL на GPL, то такая замена будет окончательной и повлечет за собой применение GPL ко всем последующим копиям данного экземпляра библиотеки и произведениям, производным от нее. LGPL содержит ряд специфических условий, в частности, в отношении произведений, возникающих в результате связывания ПО, использующего библиотеку, с библиотекой. Такие произведения могут распространяться на любых условиях с соблюдением определенных требований LGPL.
Контроль версий CVS (Concurrent Versions System) является системой контроля версий: она хранит историю изменений определённого набора файлов, как правило, исходного кода программного обеспечения, и позволяет нескольким (порой весьма удалённым друг от друга) разработчикам совместно работать над одним проектом. CVS популярна в мире Open Source. Система разрабатывается по лицензии GNU GPL. CVS использует архитектуру клиент-сервер: сервер хранит текущую версию (версии) проекта и историю изменений, а клиент соединяется с ним, чтобы получить рабочую копию (данная процедура называется check-out), затем проделать необходимые изменения и позже залить эти изменения (check-in). Обычно клиент и сервер соединяются через локальную сеть или через Интернет, но могут работать и на одной машине, если необходимо вести историю версий локального проекта. Серверное ПО обычно работает под управлением Unix (хотя существует CVS-сервер и для Windows NT), тогда как CVS клиенты доступны во всех популярных операционных системах.
8
PHP Inside №13
Свой проект свободно распространяемого программного обеспечения
Клиенты также могут сравнить различные версии файлов, запросить полную историю изменений или получить исторический образ проекта на определённое число или по номеру ревизии. Многие Open Source проекты разрешают анонимный доступ на чтение, который впервые был применён OpenBSD. Это означает, что клиенты могут запрашивать и сравнивать версии файлов без пароля; только операции check-in, ведущие к изменению данных в репозитории, требуют пароль. CVS также может содержать различные ветки проекта. Например, стабильная версия проекта может составлять одну ветвь (branch), в которую вносятся только исправления ошибок, тогда как активная разработка может вестись в параллельной ветке, которая включает значительные улучшения или изменения с момента выхода стабильной версии. Для проектов, которые ведутся на сайте www.sourceforge.net, есть возможность использовать CVS (как анонимный доступ, так и доступ для разработчиков через SSH).
Стиль кода При создании и ведении своего проекта необходимо придерживаться определенных правил в написании программ, функций, классов, именования переменных и т.д. Эти правила вы должны утвердить сами. Если проект задумывается с учетом того, что разработчиков в будущем будет много (крупный, серьезный проект), то лучше писать код по общепринятым стандартам. Например, если идут какие-либо разветвления (операторы условий или циклов), то перед строками, находящихся между фигурными скобками этих операторов, следует ставить отступ (табуляция или пробелы). При написании классов их имена лучше составлять не из пары символов, а из слова (двух слов), которые будут приблизительно говорить о том, что этот класс делает. То же самое с функциями и переменными. Так код будет намного читабельнее для других программистов, которые захотят просмотреть исходники. И если какойлибо программист захочет помочь вам в разработке, то попросите его так же следовать этим правилам из этических соображений. Кроме того, было бы нелишним ставить комментарии, особенно перед объявлениями новых переменных и функций, где вкратце объяснять, для чего служит функция или переменная. Также при несинтаксических ошибках (скорее, пользовательских) следует выводить предупреждающее сообщение, а не просто завершать программу. Небольшой пример, демонстрирующий читабельность кода. Две программы, выполняющие абсолютно одинаковые задачи - вывод содержимого таблицы из СУБД MySQL. Пример 1. Нечитабельный код. <? $dbs="localhost"; $dbl="root"; $dbp="gjekrlg"; $dbn="base"; $d = mysql_connect("$dbs", "$dbl", "$dbp"); if (!$d) die ("ERROR1");
9
PHP Inside №13
Свой проект свободно распространяемого программного обеспечения
mysql_select_db($dbn, $d) or die("ERROR2"); $rs=mysql_query("SELECT * FROM table"); if(!$rs) die ("ERROR3"); $nr = mysql_num_rows($rs); while($r=mysql_fetch_row($rs)) { $ar[]=$r;} print "<table>"; for($i=0; $i<count($ar); $i++) { print "<tr>"; for($k=0; $k<count($ar[$i]); $k++) { if($ar[$i][$k]==TRUE) {print "<td>".$ar[$i][$k]."</td>";} else {print "<td>&nbsp;</td>"; } print "</tr>"; } print "</table>"; mysql_close($d); ?>
Пример 2. Более читабельный код. <? #Переменные для входа на сервер БД $dbserver="localhost"; $dblogin="root"; $dbpass="gjekrlg"; $dbname="base"; #Создаем подключение к серверу и базе $dbase = mysql_connect("$dbserver", "$dblogin", "$dbpass"); if (!$dbase) die ("<b>Не могу подключиться к серверу баз данных.</b>"); mysql_select_db($dbname, $dbase) or die("<b>Не могу подключиться к базе данных!</b>"); #Считываем всю таблицу целиком $result=mysql_query("SELECT * FROM table"); if(!$result) die ("Произошла ошибка при запросе: <b>".mysql_error()."</b>"); $num_rows = mysql_num_rows($result); while ($a_row = mysql_fetch_row($result)) { $my_table[] = $a_row; } #Выводим данные в виде таблицы print "<table>"; for($i=0; $i<count($my_table); $i++) { $row=$my_table[$i]; print "<tr>"; for($k=0; $k<count($row); $k++) { #Если данные в ячейке есть, то выводим их, иначе выводим &nbsp; if($row[$k] == TRUE) { print "<td>$row[$k] </td>"; } else { print "<td>&nbsp;</td>"; } } print "</tr>"; } print "</table>"; mysql_close($dbase); ?>
Хотя код получается более "размашистым", но он становится более понятным для других. 10
PHP Inside №13
Свой проект свободно распространяемого программного обеспечения
Основная теория для создания своего PHP Open Source проекта изложена. Программа написана, документирована и расположена в сети. Что же дальше?
Продвижение своего проекта Привлечение новых разработчиков В нескольких программистских форумах стоит дать объявления, что есть такой-то проект, посмотрите, оцените, покритикуйте и т.п. Сторонние программисты посмотрят проект, сразу скажут плюсы и минусы с их точки зрения. После небольшой дискуссии и исправления первых ошибок стоит дать объявления, что требуется помощь программистов и всех желающих помочь. Откликнется мало людей. Но скорее всего откликнутся действительно заинтересованные. Помощь принимать лучше всего любую (не только программистскую, но и, например, дизайнерскую), т.к., отказывая людям, вы тем самым будете обижать их. Теперь, когда собралась небольшая команда, проект можно развивать и представлять миру. Стоит разработать небольшой план, по которому про проект узнает больше людей. Оставляйте сообщения на различных форумах, в том числе и англоязычных. Для этого стоит перевести документацию на английский язык, а также сделать английскую версию сайта (все-таки англоязычных разработчиков намного больше, чем русскоязычных, да и понятие об Open Source у них лучше развито, чем у нас).
Реклама Говоря о рекламе, я не имею в виду массовые спам-рассылки, флуд в форумах и т.п. Я говорю в основном о различных каталогах и программистских сайтах. Сначала о каталогах. Каждый из вас был на подобных сайтах, где есть множество различных скриптов. Таких каталогов в Интернете много, например, http://www.hotscripts.com. Обычно добавление в каталог своего проекта - услуга бесплатная. После добавления можно будет контролировать, сколько человек пришли через этот каталог. Кроме этого, во многих каталогах посетители могут оставлять комментарии и выставлять оценку программе. Чем выше балл, тем выше рейтинг, тем, скорее всего, на более высокой ступеньке будет стоять проект в этом каталоге. Есть еще один способ поведать миру о проекте. Существует огромное множество сайтов программистской тематики, где кроме каталогов и форумов существует раздел "Статьи". Чаще всего в этот раздел пишут различные программисты. Свяжитесь с администратором ресурса, предложите ему разместить на его сайте статью про такой-то проект. Обязательно укажите, что проект Open Source и что денег за него никто не получает. Если статья будет хорошо написана и если проект действительно хороший, то администратор разместит вашу статью.
11
PHP Inside №13
Свой проект свободно распространяемого программного обеспечения
Использование в других проектах Есть еще один вариант распространить информацию о вашем проекте. Предположим ваш проект - это полезная библиотека. Вы находите какой-либо проект, которому она может помочь. Предлагаете такой вариант администратору проекта, и если он согласится, это будет большой плюс: во-первых, вы поможете своему коллеге необходимыми наработками, во-вторых, вы обменяетесь постоянными аудиториями, и в-третьих, можно будет с гордостью заметить, что такой-то и такой-то проекты работают на базе вашего. Примеров такого симбиоза множество. Например: популярный комплекс для администрирования СУБД MySQL через веб-интерфейс phpMyAdmin включает в свой состав множество различных проектов, от OpenSource CMS до All-in-One веб-серверов (например, Denwer-2).
Проблемы существования и их решения Во время своего существования у любого проекта бывают не только хорошие времена. Бывают времена и смутные. Обусловлено это время проблемами, которые возникают. Вообще можно отделить два основных типа проблем. Первый тип - это когда нет разработчиков, которые хотели бы помогать в развитии проекта, а второй - это когда просто не хватает финансовых средств для каких-либо целей.
Отсутствие единомышленников Очень часто такая проблема возникает в самом начале жизненного пути проекта, когда никто не заинтересован в развитии проекта. Тогда стоит задуматься, а действительно ли нужен проект, или он является еще одним клоном? Потому как, если уже есть нечто готовое, то пользователям не понадобится что-то новое, но выполняющее те же функции. Если же все-таки вы решили, что проект действительно новаторский, то стоит еще раз попробовать поискать разработчиков, как описано выше. Если же разработчики-помощники были, но отказались от участия, то стоит призадуматься. Если проблема была не в отсутствии времени у разработчика, значит, ему либо надоело заниматься проектом, либо вы повели себя по отношению к нему некорректно. Все-таки не стоит забывать, что разрабатывать Open Source ПО дело добровольное. Если же человеку проект просто надоел, значит, ему перестало хватать воодушевления, новых идей, желания разрабатывать. В таких случаях стоит серьезно призадуматься о будущем проекта, обязательно составить небольшой TODO-list и поделиться идеями со всеми разработчиками. Если всех своих друзей-разработчиков постоянно воодушевлять (новые идеи, цифры скачанных копий проекта), то у них будет желание разрабатывать дальше. Но самое главное - чтобы желание было у вас.
Финансирование проекта После того, как проект просуществует некоторое время, захочется, чтобы он давал какую-либо отдачу (не только моральную, хотя письма с благодарностями тоже очень греют душу).
12
PHP Inside №13
Свой проект свободно распространяемого программного обеспечения
Это необходимо, если, например, вы хотите немного раскрутить проект с помощью баннерных сетей, купить новый домен для проекта, оплатить хостинг, либо же просто заплатить за Интернет. Здесь существует два пути: либо оставить сообщение на сайте проекта о том, что проект нуждается в финансовой поддержке, либо участвовать в баннерных сетях. Первый вариант в мире Open Source довольно распространен. На сайте Sourceforge.net для этого даже есть свой сервис, где каждый желающий может оставить свое пожертвование проекту через систему PayPal (т.н. Donations). На сайте можно оставить номера своих кошельков WebMoney и т.п. Второй вариант - размещение баннеров и другого типа рекламы на сайте проекта. Это менее прибыльный путь, но более стабильный + уже есть реклама для раскрутки. Реклама может быть в виде графических и текстовых баннеров, а также различных партнерских программ. Партнерские программы следует выбирать по тематике, которая будет интересна посетителям сайта, например, регистрация доменов или хостинг. Вообще, размещать сообщения о том, что проект не отказался бы от пожертвований, совсем не аморально. Ведь если проект популярный, реально помогает людям, экономя их время либо еще както, то его разработчик всегда заслуживает финансовой помощи. Источники: http://www.opensource.org/advocacy/faq.php http://www.libertarium.ru/libertarium/18586 http://www.sourceforge.net http://www.freshmeat.net http://ru.wikipedia.org
13
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
Две стороны документирования: PHPdocumentor и DocBook Документирование исходного кода: стандарт PHPdocumentor. Одной из тем для споров в программировании всегда был и остаётся вопрос документирования кода: существует множество методик замены комментариев (к примеру, изучение API по коду тестов в XP). Тема необходимости написания комментариев останется вне обсуждения. Затронуты будут лишь способы документирования, а вернее, один способ, являющийся де-факто стандартом: PHPdocumentor – утилита авто-документирования (в дальнейшем – PHPdoc).
Автор: Борис Безруков [lovchy] Технический директор DeltaInform, OSS-энтузиаст, член группы документирования PHP
Вкратце о JavaDoc Идея PHPdoc не нова: концепт берёт своё начало в реализации JavaDoc [1.1]. JavaDoc – аналогичный продукт для документирования исходного кода Java. Суть систем автодокументирования сводится к извлечению структурных данных (схемы API) из исходного кода и представления их в удобном для чтения виде. JavaDoc очень быстро обрёл популярность, исключая необходимость ведения раздельных документов, облегчая этим процесс описания программного обеспечения.
PEAR::PHPdoc Было сказано: подход автодокументирования всегда был достаточно популярен (особенно для open source ПО), и PHPdocumentor является не единственной попыткой адаптации JavaDoc. Одним из альтернативных решений был PEAR::PHPdoc, так и не получивший ни одного стабильного выпуска и завершённый в пользу PHPdocumentor, который в свою очередь сам стал стандартом для PEAR. Существуют и ныне живущие аналоги, которые, тем не менее, не представляют собой ничего концептуально нового.
Суть подхода Говоря о PHPdoc (или JavaDoc) используются два слова: утилита и стандарт. Средства автодокументирования всегда включают в себя совершенно необходимую возможность дополнения данных, которые они могут извлечь из пустого кода: теги. Теги заключаются в комментарии особого вида (docblocks): /** * Description goes here * @author Борис `lovchy` Безруков <boris@bezrukov.info> * @version 1.0 stable */
где foo – имя тега
14
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
Подобный способ написания также заимствован у JavaDoc. Именно набор тегов и является стандартом. Каждый тег начинается с символа “@” и может содержать как одну, так и несколько строк содержания. Стоит заметить, что все символы “@” в содержании тега рассматриваются как обычный символ. PHPdoc позволяет условно разделять документацию для внутреннего и внешнего пользования: здесь всегда стоит помнить, что в абсолютном большинстве случаев документацию обязательно надо писать для обоих случаев.
Обзор возможностей В случае PHP набор включает теги, специфичные как для процедурного кода, так и для ОО подхода. Кроме того, в него входят дополнительные теги, которые применяются к целому файлу, к целому разделу и т.д. Каждый блок содержит, кроме тегов, раздел описания. Он может рассматриваться как невидимый тег. Общее описание блока содержит общее описание элемента (класса, функции, метода, атрибута, …). Глобальные теги (описывающие файл) включают в себя следующие: •
@author – автор текущего файла. Тег может применяться как к файлу, так и к любому другому элементу.
•
@category, @package – используются для группировки и выделения отдельных пакетов. Широко используется PEAR, в целом – своеобразная замена пространств имён.
•
А также: @version, @see, @todo, …
/** * File docblock * @version 1.0 Prima * @todo Implement foobar */
Документирование процедурного кода Сущность функции PHP в PHPdoc, кроме двух описаний, включает в себя основные дочерние: параметры функции и возвращаемое значение функции. Несмотря на то, что PHP – слабо типизированный язык, теги как параметров, так и возвращаемого значения включают возможность определить тип (включая имя объявленного класса). Объявление параметров: @param тип $имя описание
Для объявления неопределенного количества [типовых] параметров используется синтаксис: @param тип $имя,… описание
15
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
Объявление возвращаемого значения @return тип описание
Безусловно, самой важной частью блока документации функции является её описание. PHPdoc позволяет давать функции два описания: соответственно короткое и длинное. Короткое описание всегда занимает одну строку и прерывается её окончанием, длинным описанием считается весь текст вплоть до первого тега. К длинному описанию в PHPdoc можно применять достаточно сложное форматирование – парсер умеет выстраивать списки из конструкций вида: -1 -2 - 3, подставляет параграфы в местах пропуска строки. Кроме того, PHPdoc понимает некоторые теги: <code />, <p />, <var />, … Длинное описание – очень важный элемент составления общей картины о функции. Часто в длинное описание функции стоит включать пример её использования (заключенный в <code>). /** * Foobar * * This function does foobar this way: * <code>$foo = foobar(true);</code> * * @param bool $baz Baz * @return integer Foobar value */ function foobar($baz) { return rand(); }
Константы и включения Константы и включения сторонних файлов могут (и, как правило, должны) быть документированы аналогично остальным элементам. Каждая подобная сущность может иметь описание, длинное описание и набор общих тегов (@author, @since, …).
Документирование ОО кода. PHPdoc и PHP5 Условно разделим описание классов на описание атрибутов и описание методов. Фактически описание методов можно рассматривать как расширенное описание функций: расширенное дополнительными метаданными. В PHP4, не имеющем стандартных возможностей объектно-ориентированной среды, PHPdoc позволяет условно определять элементы как финальные (final) или абстрактные: теги @final и @abstract. Кроме того, модификатор доступа также может быть указан с помощью тега @access.
16
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
Стоит заметить, что по умолчанию все элементы, имеющие @access private, не включаются в документацию (для этого есть специальный ключ при генерировании). Это позволяет внести то самое разделение на документацию для разработчиков кода и для людей сторонних – тех, кому может понадобиться API приложения. Фактически тег @access не имеет прямого отношения к модификатору доступа и может быть использован для любого элемента, дабы элемент был спрятан от “внешней документации”. Разумеется, его стоит использовать и в необъектном коде, и для описания внутренних констант. Атрибуты, в свою очередь, описываются с помощью тега @var, который, аналогично параметрам, может содержать тип переменной и описание. Если указано описание, но опущен тег @var, переменной присваивается тип mixed. Поддержка PHP5 в PHPdoc была включена в версию 1.3.0. С её выходом теги @final и @abstract считаются устаревшими: эти данные теперь также собираются из кода программы. На момент написания стабильной версии этой ветки ещё не было: последним выпуском был RC3. Говоря именно о документировании ОО кода, стоит рассказать о так называемых шаблонах блоков документации. Во избежание дублирования описания атрибутов или методов (как правило, дублируется тег @access и @var для атрибутов), в PHPdoc был введён механизм авто-дублирования по шаблону. Стоит просто обозначит начало блока как: /**#@+ * @access private */
и конец как: /**#@-*/
Теперь все элементы, находящиеся между приведёнными блоками, будут дополнены тегом @access со значением private. /** * Foobarer * @uses Bar */ class Foo { /** * @var bool Marks if life’s good */ public $LifesGood = true; }
Методы описываются аналогично функциям.
17
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
Технология и сообщество PHPdocumentor облегчает задачу восприятия кода. Восприятия авторами и потенциальными авторам, а также разработчиками расширений – людьми, которые будут использовать API. Нигде проблема восприятия кода большим количеством людей не стоит так остро, как в разработках с использованием идеологии Open Source. В частности, разработок, распространяющихся по идеологии GPL, в которых способность быстро понять как отрезок кода, так и отдалённо схему работы всей системы – одно из требований. Требований, которые часто незаслуженно остаются без внимания. Тем не менее, с развитием PHP PHPdoc стал стандартом дефакто. В настоящее время PHPdoc поддерживается как официальным IDE от Zend, так и большинством других редакторов. В большинстве крупных разработок (большинство из которых, к сожалению, типовые CMS) технология уже используется. Хорошим примером использования PHPdoc на передовой может служить TE Smarty. Говоря об Open Source, стоит упомянуть и PEAR. PHPdoc, будучи одним из его пакетов, стал официальным требованием к оформлению кода библиотек. Более того, PHPdoc включает в себя средства, разработанные специально для этого репозитория.
Элементы разработки пользовательской документации. Разбор полётов Стандарт PHPdoc включает в себя возможность написания так называемых учебных пособий (tutorials). Если исключая необходимость в сторонних документах, мы побеждаем лень и повышаем производительность, то мешая код (пусть и аккуратно) с документацией, не имеющей к нему самого прямого отношения, мы вносим путаницу. Для написания же полной документации стандартом является DocBook. Использовать стоит именно его. Решение существует. PHPdoc (с версии 1.2.0) умеет работать с DocBook, подставляя в цельную документацию мета-данные из кода приложения. Подобная тесная интеграция позволяет разделить документацию кода и собственно код с внутренней документацией, оставляя возможность первому черпать из последнего. Для реализации подобной схемы используются inline tags.
Пользовательская документация: DocBook. При написании электронных документов с целью распространения информации одна из проблем, с которой сталкиваются писатели, – совместимость. Совместимость форматов, в которых они пишут с потребительскими системами. Подобная проблема стоит при передаче любой информации, и выходы были давно найдены. Суть технологии; использование XML; стандарт де-юре Одним из них является формат XML. DocBook, в свою очередь, представляет собой DTD, укомплектованный для написания технического текста. Ничего более. Разумеется, существует готовый набор утилит для преобразования XML DocBook-кода в различные форматы, и это ещё более облегчает задачу.
18
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
Всё, что требуется для работы с DocBook – это понимание SGML. Существенная разница между DocBook и, к примеру, RTF или PDF заключается в том, что в XML мы не форматируем текст. Мы лишь указываем, как данный фрагмент должен быть выделен. В последствие для каждого формата, который будет сгенерирован из исходного кода, данный фрагмент может быть выделен по-разному. Стандарт DocBook существует очень долгое время и успел стать де-юре стандартом для написания пользовательской документации, а также статей и даже книг. Издательство O’Reilly, к примеру, принимает материал от авторов только в этом формате.
Краткий обзор DTD Работая с DocBook, всегда надо помнить, что перед вами просто подмножество XML. Все требования к XML предъявляются и к документу DocBook. Соответственно, все документы DocBook объявляются как XML-документы следующим образом: <?xml version='1.0'?> <!DOCTYPE book PUBLIC "-//Norman Walsh//DTD DocBk XML V3.1.4//EN" "http://nwalsh.com/docbook/xml/3.1.4/db3xml.dtd"> <book> </book>
где DTD – описание формата DocBook, а <book /> – один из возможных корневых элементов, который чаще всего используется в документации. Полный пример типовой книги (из документации): <book> <bookinfo> <title>My First Book</title> <author><firstname>Jane</firstname><surname>Doe</surname></author> <copyright><year>1998</year><holder>Jane Doe</holder></copyright> </bookinfo> <preface><title>Foreword</title> ... </preface> <chapter> ... </chapter> <chapter> ... </chapter> <chapter> ... </chapter> <appendix> ... </appendix> <appendix> ... </appendix> <index> ... </index> </book>
Другим корневыми элементом может быть <article />. Далее каждая глава делится на разделы: тег <sect [n] />. N может заменяться на число от 1 до 5, характеризуя уровень вложенности. Альтернативой подобному написанию может служить рекурсивный тег <section />, автоматически учитывающий уровень вложенности. После идут параграфы – тег <para>. DocBook, кроме уже описанных, содержит огромное количество тегов, имеющих отношение к технической литературе. Предусмотрено если не всё, то большая часть. Список тегов рассматриваться не будет. Для быстрого введения стоит обратиться к [2.2], а в процессе написания под рукой всегда стоит держать [2.1].
19
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
XML vs. SGML DocBook, представляющий собой стандарт написания, позволяет работать не только с XML, но и с более общей надстройкой – с SGML. Концептуальная разница заключается только в требованиях, которые накладывают оба эти языка. Впрочем, и она не столь велика. В действительности практически единственной разницей является процесс обработки документов. Здесь при необходимости написания своих утилит генерирования приходится выбирать между DSSSL и XSLT. В остальном, стоит повториться, разница крайне незначительна.
Entities: ввод терминов и разбиение на документы Кроме всех плюсов, которые уже были описаны выше, XML приносит с собой возможность объявлять entities (нечто вроде переменных). Содержание документации в абсолютном большинстве случаев не может быть размещено в одном файле уже из эстетических соображений, не говоря уже о практических. Здесь entities также находят применение. Сущности в XML являются константами, доступными по &foo;. За примером использования сущностей в качестве терминов стоит обратиться к следующему разделу. Ниже приведён пример использования сущностей для разбиения документа на составляющие файлы: <!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook V3.1//EN" [ <!ENTITY first_chapter SYSTEM "chapters/1.sgm"> <!ENTITY second_chapter SYSTEM "chapters/2.sgm"> <!ENTITY conclusion SYSTEM "conclusion.sgm"> ]> <book><title>My First Book</title> &second_chapter; &second_chapter; &conclusion; </book>
Примеры физического и гораздо более важного – логического разбиения – великолепно раскрыты во второй главе [2.1].
Формирование документов: DSSSL и XSLT Как уже было сказано выше, одной из серьёзных разниц между DocBook XML-DTD и SGML-версией является способ получения из исходного кода документов. Используются разные обработчики и технологии. DSSSL [2.4] (Document Style Semantics and Specification Language) представляет собой язык преобразования SGML, похожий на LISP. В большинстве дистрибутивов DSSSL-преобразователем является Jade. XSLT [2.5] (Extensible Stylesheet Language Transformation) является аналогичным преобразователем для XSLT.
20
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
Учитывая популярность DocBook, не удивительно, что существует готовое открытое решение. [2.3] содержит шаблоны как для DSSSL, так и для XSLT. Стоит заметить, что шаблоны для DSSSL вполне подходят и для XML-версий, что является хорошим аргументом в пользу выбора именно XML-DTD для работы.
Соавторство; Связка DocBook и CVS на примере документации PHP XML – текстовый формат, что, в частности, облегчает совместную работу над документами, давая возможность использовать CVS (и подобные системы).
Краткое введение в CVS CVS [2.6] представляет собой систему контроля версий для ведения истории файлов, которая, кроме того, содержит в себе средства предусмотрения параллельной работы. Суть подхода сводится к содержанию в отдельной копии файлов (репозитории) всех версий diff от начального. Каждый разработчик, получая отдельную копию, проводит операции коммита (записи на сервер), в процессе чего система предупреждает коллизии при параллельном доступе. Кроме этого, как правило, настраивают систему оповещения по e-mail, содержащую diff коммита.
Использование GNU-Autotools, организация мультиязычности Повторяющееся действие построения документов различных форматов из исходного XML можно сравнить с компиляцией. Для автоматизации построения ПО в UNIX стандартным средством является набор GNU-Autotools, которые находят применение в документации PHP. Документация крупного Open-Source проекта обязана быть доступной для большинства потенциальных технических писателей, причём необходимо минимизировать количество возможных проблем и упростить вопрос сборки и проверки правки исходного кода, позволяя авторам заняться делом: написанием документации. В случае инсталляции ПО, где мы встречаемся с аналогичными требованиями, GNU-Autotools используются давно и успешно. Три действия установки ПО совершенно аналогичны и в генерации документа по DocBook: •
configure – проверяющий доступность XSLTproc (либо Jade / других процессоров) и консистенцию поставки, а также позволяющий выбрать необходимый язык в случае мультиязычности;
•
make test – проверяющий исходный код по DTD, исключая “физические” ошибки;
•
make format – генерация запрошенного формата: format. Именно таким образом устроен репозиторий документации
PHP. При конфигурировании проверяется наличие всех необходимых утилит и собираются данные о несуществующих в выбранном переводе разделах: все они будут подставлены из исходной английской версии.
21
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
Каждый перевод находится в своей папке и фактически является своим репозиторием. Это позволяет переводчикам работать отдельно от авторов исходной документации: все проблемы согласования отсутствующих разделов, как было описано выше, возьмёт на себя Autoconf. По окончанию конфигурирования генерируется корневой файл manual.xml. Пример начала для русской версии: <!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.1.2//EN" "./dtds/dbxml-4.1.2/docbookx.dtd" [ <!—Сюда подставляются --> <!ENTITY % language-defs SYSTEM "./ru/language-defs.ent"> <!ENTITY % language-snippets SYSTEM "./ru/language-snippets.ent"> <!ENTITY % language-livedocs SYSTEM "./ru/livedocs.ent"> %language-defs; %language-snippets; <!-- Fallback to English definitions and snippets (in case of missing translation) --> <!ENTITY % language-defs.default SYSTEM "./en/language-defs.ent"> <!ENTITY % language-snippets.default SYSTEM "./en/language-snippets.ent" %language-defs.default; %language-snippets.default
Здесь наглядно видно, что подставляются не только недостающие файлы, но и, пользуясь возможностями XML, недопереведённые сущности. Каждый из переводов имеет свой лист и своего руководителя, что позволяет ещё больше абстрагироваться от документации в целом, позволяя каждому заниматься своим делом.
Общая организация документов Как было сказано выше, каждый перевод содержится в отдельной директории, дублирующей по структуре английский оригинал, откуда подставляются недостающие файлы. Исходный код документации PHP является прекрасным примером того, как стоит структурировать документацию как физически, так и логически. В репозитории, в соответствующих названиям глав директориях, лежат разбитые на файлы главы документации. В папке entities содержатся определения общих сущностей. Корневой xml-файл содержит определение частей: &bookinfo; &preface; <part id="getting-started"> <title>&GettingStarted;</title> &chapters.intro; &chapters.tutorial; </part> <part id="install"> <title>&InstallAndConfigure;</title> &install.intro; &install.unix.index;
22
PHP Inside №13
Две стороны документирования: PHPdocumentor и DocBook
&install.macos.index; &install.windows.index; &install.problems; &install.ini; </part>
Отдельные теги DocBook, вроде <function />, обрабатываются с подставлением ссылок на соответствующую функцию, если она существует, шаблоны документации PHP тоже заслуживают пристального внимания.
Livedocs С ростом размера документации и, соответственно, с увеличением времени компиляции её в HTML (любой другой формат) острее становится необходимость раздельной компиляции. Чаще в голову начинают приходить мысли о чём-то, аналогичном линковке. В процессе появления таких нужд у группы документирования PHP была создана утилита Livedocs, позволяющая просматривать части DocBook-документации, не генерируя полных документов на лету. При начальной установке создаётся индекс конкретного языка, после чего можно обращаться к любой части. Livedocs выпускается по лицензии PHP 3.0, а значит, может быть переписан и может использоваться в своих разработках.
Литература 1.1 JavaDoc Tool Homepage, Sun MS. http://java.sun.com/products/jdk/javadoc/ 1.2 PHPDocumentor http://phpdoc.org/
Manual,
http://manual.phpdoc.org/
1.3 PEAR::PHPdoc http://pear.php.net/package/PHPDoc 1.4 Документирование кода, или как сделать разработку удобнее? А. Сидоренко (PHPinside #8, 11.04) 2.1 DocBook: The definitive guide, N. Wellsh, L. Muellner (O’Reilly, 1999-05): http://www.docbook.org/tdg/en/html/docbook.html 2.2 DocBook PHPc Tutorial, A. Wormus, B. Bezrukov http://boris.bezrukov.info/Other/PHPconf.ru_4/DocBook.Tutorial. html 2.3 DocBook demystification HOW-TO, E. Raymond http://en.tldp.org/HOWTO/DocBook-Demystification-HOWTO/ 2.4 Complete XML+XSL DocBook Guide, B. Stayton (2002-05) http://www.sagehill.net/docbookxsl/index.html 2.5 Livedocs repository, http://cvs.php.net/livedocs/
23
PHP Inside №13
Разработка современной CMS.
Разработка современной CMS. Преимущества, которые дает PHP5 при разработке таких систем CMS можно условно разбить на две части: шаблонизатор низкоуровневая система, обеспечивающая единую систему обработки шаблонов и сборки страниц из отдельных фрагментов, и собственно CMS, которая пользуется шаблонизатором, снабжая его данными для формирования страниц, а также реализует интерфейс для управления сайтом.
Шаблонизатор
Авторы: Константин Погорелов, веб-программист, компания "AWWSOFT" Дмитрий Магунов, руководитель проекта компания "AWWSOFT" г.Одесса, Украина
На данный момент в мире имеется широкий спектр различных обработчиков шаблонов для PHP (далее шаблонизатор). Наиболее развитые из них довольно далеко развили язык шаблонов, открыв большие возможности для разработчиков. Интересно, что многие пришли к похожей схеме работы. Возможности вставки глобальных переменных или элементов массива по ключу, тривиальны и есть почти в каждом обработчике. Затем многие поддерживают вставку в шаблон вызова функции PHP. Следующий шаг - введение модификаторов и арифметических операций - упрощенный интерфейс вместо функций. И в конечном итоге, шаблонизатор дает доступ также к объектам, их полям и методам. В результате язык шаблонов превращается в дополнительный язык программирования, надстроенный над PHP. Примером такого средства может послужить Smarty. Здесь предлагается иная концепция шаблонизатора, не уступающая по возможностям описанной выше модели. В основу положена следующая идея: язык шаблонов - язык для доступа к данным и вывода их в некотором текстовом формате (HTML), а другие функции он выполнять не должен. Поэтому способ представления данных в программе должен быть скрыт от шаблонизатора, т.е. не имеет значения, хранится ли текст в поле объекта, в элементе массива или просто в глобальной переменной. Для этого создается виртуальное пространство переменных шаблонизатора, которое в реальности ссылается на необходимые источники данных. Само это пространство переменных представлено в виде дерева файловой системы системы UNIX. Вот примеры имен переменных: /var/group/petrov/math/mark /etc/tables/max_rows /usr/site1/templates/right_row
24
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
В рамках проекта можно самостоятельно настроить, какие ветки на что будут ссылаться. Можно сказать, что системы доступа к данным "монтируются" в дерево данных. (Причем, монтировать можно в любую точку системы, а не только в корневую). В процессе программирования можно даже изменить представление переменных, однако шаблоны при этом останутся прежними. Кроме того, данный механизм позволяет тестировать шаблоны и дизайн, когда еще нет самой программы, т.к. все данные легко имитировать, например, в виде файлов. Функции также представляют собой особые точки в этом дереве. Например: /etc/current_date может быть настроена на самом деле не как переменная, а как функция, которая отрабатывает только в случае, если шаблонизатор встретил ее вызов. Уйти от модификаторов, пожалуй, невозможно. Особенно это важно во фрагментах подобно следующему: <INPUT NAME=some_field VALUE="<?aww /var/field_value -html ?>">
Ясно, что в одних случаях следует преобразовывать строку, содержащую спец. символы HTML, а в других, наоборот, нельзя (например, вставка подшаблона в основной шаблон). AWW CMS включает в себя шаблонизатор, поддерживающий доступ ко многим видам представления данных - глобальные переменные, переменные с определенным префиксом (вопрос безопасности), массивы любой мерности в т.ч. ассоциативные, объекты, их методы и поля, функции, семейства функций с определенным префиксом и т.д. Таким образом, шаблонизатор превращается в более ответственную составляющую часть программы. Он полностью контролирует и управляет системой общих переменных в программе. Ведь, по сути, такое пространство переменных удобно использовать и в самом коде, т.к. через него можно обратиться к любым данным, не заботясь о видимости глобальных переменных. Кроме того, такая модель данных позволяет описать права доступа к точкам дерева в некотором виде и контролировать его в едином блоке. Так как весь вывод проходит через шаблонизатор, то конечный пользователь гарантированно получит только разрешенную для него информацию. Представление данных, как и в других шаблонизаторах сводится к организации элементарных циклов (для построения таблицы), вывода блоков по условию и автоматизированному формированию сложных блоков (например, отображение элементов управления select, radio на основе массива данных). Для обеспечения возможности реализации любого сложного проекта с помощью шаблонизатора AWW CMS, в языке предусмотрена возможность подключения модулей - добавление собственных модификаторов, тэгов, операторов шаблонизатора. AWW CMS уже имеет набор встроенных средств для работы с HTML. Ниже приведены примеры кода, которые, наверное, не нуждаются в особых комментариях:
25
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
<TABLE><TR> <TD> Номер </TD> <TD>ФИО</TD></TR> <AWW for i=1 to /var/students> <TR> <TD><?aww i ?></TD> <TD><?aww /var/students/$(i)/fio ?></TD> </TR> </AWW for> </TABLE> <SELECT NAME=some_select> <?aww auto_select_body assoc /var/pupils id fio ?> </SELECT> <AWW if /var/finished_ok><H1>Операция завершена успешно</H1></AWW if>
Как можно заметить, шаблонизатор свелся к дереву виртуальных переменных и обработчику языка с использованием этого дерева. Такая задача очень органично вписывается в концепцию объектно-ориентированного программирования. Совершенно очевидно, что в современных условиях данный модуль представляет собой не что иное, как определенный набор классов (он будет рассмотрен позже).
Предлагаемая модель CMS. Общие требования На базе шаблонизатора строится следующая ступень системы - собственно CMS. Основными требованиями, предъявляемыми к CMS, являются: •
модульность - иначе говоря, расширяемость, возможность кастомизации под потребности заказчика.
•
многоязычность - возможность представления интерфейса администрирования и возможность функционирования самого сайта на разных языках, и более сложное - возможность зеркалирования общей структуры на каждом из языков.
•
масштабируемость - возможность наращивать мощность и функциональность системы до следующего уровня без потерь существующих наработок. Грубо говоря, возможность преобразования сайта-визитки в интернет-магазин, а интернет-магазина - в портал и т.п.
•
удобство в использовании.
Составляющие CMS CMS, как система, управляющая множеством источников данных, обрабатывающая их и сводящая в единый целый сайт, должна обеспечивать максимальную гибкость в способах доступа к данным и их абстрактном представлении. Для удовлетворения вышеуказанных требований предлагается разделение на следующие основополагающие компоненты: •
Информационное наполнение сайта
26
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
Собственно данные, которые должны быть размещены на сайте. Это могут быть файлы в различных форматах: текстовом, CSV, XML и т.д.; также это могут быть данные, размещенные в некоторых БД. •
Ядро CMS
Система, оперирующая в пространстве виртуальных данных сайта. Субъектами для нее выступают такие понятия, как "сайт", "страница", "новость", "иллюстрация к новости" и т.д. •
модули CMS
Расширения CMS, реализующие такие компоненты как "форум", "опрос", "оглавление раздела" и т.д. Эти модули, как и ядро CMS работают на уровне виртуального пространства переменных. Они могут иметь свои собственные хранилища информации, но желательно, чтобы использовались абстрактные источники данных. Тут следует отметить, что модули, предоставляемые конечным пользователям - это нечто иное, чем описанные выше структурные единицы. "Модуль" для конечного пользователя имеет смысл поставлять как совокупность шаблонов, изображений, собственно программных модулей, возможно, нескольких совершенно разнородных, которые все вместе реализует некоторую функцию (например, "форум", "магазин", "галерея") •
Шаблонизатор
Центральный класс, в котором реализована трансляция виртуальных переменных, обработка шаблонов. Кроме того, он является интерфейсом, через который работает вся система в целом. •
Модули, обеспечивающие связь шаблонизатора с данными
Модули, обеспечивающие трансляцию определенного типа данных в пространство имен CMS. Упрощенно работу такого модуля можно описать так: модуль получает от ядра запрос в виде имени и возвращает значение для этого имени. •
Модули расширения шаблонизатора
Работу CMS по этой схеме можно показать на следующей иллюстрации.
27
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
Структура и иерархия классов определяется вышеуказанной схемой работы. Одним из проблемных вопросов в PHP всегда являлся доступ к глобальным переменным из сложных функций (или методов). Перечисление большого числа нужных в данной функции переменных довольно затруднительно, а обращение через массив $GLOBALS загромождает программный код. Так как CMS работающая по предложенной модели оперирует переменными виртуальными, то в общем случае глобальным достаточно сделать только сам класс, реализующий доступ к этим переменным. В то же время этот класс должен иметь доступ ко всем подключенным модулям, чтобы по необходимости через них получать, обрабатывать данные. Следует учесть общность в представлении различных модулей и структур в базе данных. Методы для получения данных, кеширования запросов на уровне шаблонизатора удобно зашить в абстрактные классы для CMS. При разработке CMS нужно учитывать, что программа должна работать в двух принципиально отличающихся режимах: панель управления сайтом для администраторов (админ-панель) и собственно выдача страниц готового сайта. В первом случае следует особое внимание уделить простоте и прозрачности программного кода, т.к. очевидно, что пользовательский интерфейс для управления сайтом неизбежно довольно сложный комплекс. Необходимо предусмотреть, что этот интерфейс может быть многократно изменен, модернизирован. Вероятно, эта задача потребует усилий нескольких программистов, потому здесь можно максимально полно использовать преимущества ООП. В случае формирования страниц приоритетом является производительность системы. С целью оптимизации можно частично пожертвовать чистотой и универсальностью кода. Однако в конечном итоге все функции желательно реализовать в общих классах, методах, т.к. вся CMS представляет собой единый комплекс. Таким образом, учтя все вышеперечисленное, можно предложить следующую иерархию классов. o Класс Шаблонизатора o Модуль Шаблонизатора о Модуль доступа к данным о Глобальные переменные PHP о Скрытые массивы о Доступ к файлам указанной директории о Ссылка на функцию PHP о Переадресация о Модуль расширения Шаблонизатора о Ввод/вывод дат 28
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
о Вывод ошибок и проверка введенных данных для формы о Обзор файлов о Контекстная справка о Построение таблицы по массиву о Построение SELECT-элемента по заданному массиву ... Компонент CMS o Модуль CMS o Модули данных CMS о Форма CMS о Стандартный список о Модуль информационного блока CMS о Документ о Форма о Иллюстрация о Простой текст o Встраиваемые модули о Вход на сайт о Форум о Вывод текущего раздела о Публикации о Постраничная навигация о Поиск по сайту о Оглавление раздела o Пользователь CMS o Группа пользователей CMS o Узел структуры CMS o Страница CMS o Шаблон CMS
Шаблонизатор (AWWRoot) Как уже отмечалось выше, шаблонизатор - основной класс в структуре CMS. С его помощью система объединяется в единый комплекс.
29
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
Модуль шаблонизатора (AWWModule) Все модули AWW наследуют общий абстрактный класс, реализующий связь с главным классом шаблонизатора.
Модуль доступа к данным (AWWProcessor) Модуль доступа определяет набор методов для получения и записи данных из определенного источника. Примером таких источников могут быть массивы, глобальные переменные, файлы, таблицы в БД и т.п. При вызове конструктора такого модуля ему ставится в соответствие определенный префикс (точка монтирования) виртуальных переменных. В системе может работать несколько экземпляров одного типа модуля для разных префиксов.
Расширение шаблонизатора (AWWExtencion) Расширение шаблонизатора в отличие от модулей доступа к данным могут описывать новые тэги aww и алгоритмы их обработки. Таким образом, с одним только шаблонизатором и его модулями можно получить полноценную систему с разделением кода и данных. Причем эта система позволяет автоматически рекурсивно обрабатывать шаблоны. Имеет развитый встроенный язык шаблонов с возможностью его расширения. Однако полноценная CMS требует возможность визуального редактирования страниц и ряд дополнительных сервисов, например, систему кеширования, резервного копирования и т.п. Поэтому над шаблонизатором возводится надстройка - собственно CMS-система. В ней данные, которые для шаблонизатора представляют собой просто переменные в древовидной иерархической системе, должны быть представлены в виде таких объектов, как "страница", "раздел сайта" и т.п., а шаблоны из набора равноправных деталей конструктора должны быть формализованы и приведены к виду, который был бы интуитивно понятен пользователю ("шаблон оформления внешнего вида сайта", "шаблон размещения информационных блоков" и т.д.).
Компонент СMS (CMSComponent) Для обеспечения максимальной гибкости системы любой компонент CMS кроме стандартного источника для своих данных (непосредственно оперативная память или таблица в БД), может иметь дополнительные данные в виде пар строковых пар "имя" "значение". Так, например, некоторые модули имеют до сотни различных свойств. Записи о пользователях также могут различаться в зависимости от группы и насчитывать по нескольку десятков полей. В этом случае единый родитель для всех элементов CMS, как "Компонент CMS" позволяет привести все эти операции к единому стандарту, упростить процессы получения и обновления данных для каждого из компонентов. Для обеспечения простоты отладки компоненты имеют встроенную систему логирования ошибок и сообщений. 30
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
Представление страницы в CMS. Страница - одно из базовых понятий в CMS. Соответственно, следует уделить особое внимание возможности настройки составляющих ее компонентов. Например, на одной странице может быть голосование, главное меню, статья, состоящая из текста, иллюстраций, сносок, комментариев специалистов, комментариев пользователей, форма для подписки на новости, форма логина. Одно из популярных решений - выделить в системе, например 2-3 колонки и выводить соответствующие блоки один за другим. Однако здесь предлагается иная система, позволяющая делать сайты без ограничений по размещению, и в то же время дающая возможность неподготовленным людям создавать и редактировать сайты даже без знания HTML. Любая страница строится на основе некоторого шаблона, который, в свою очередь, может включать другие шаблоны с неограниченной вложенностью. Такой подход характерен для многих CMF. Но среди всех шаблонов, составляющих страницу, можно выделить несколько базовых шаблонов, принципиально различающихся по функциональности (а не по расположению на странице). Здесь предлагается следующая концепция разделения. Любая страница состоит из 4х шаблонов: основной шаблон, шаблон внешнего вида (включаемый внутрь основного), шаблон размещения и шаблон главного меню (включаемые в шаблон внешнего вида). Функции каждого из них вполне очевидны: •
основной шаблон - фактически все то, что выходит за пределы тэга <BODY> в случае HTML представления страницы. Сюда входят МЕТА-данные, различные подключения скриптов и других сущностей, не связанные с самими данными, выводимыми на странице, и с дизайном, который используется для оформления.
•
шаблон внешнего вида - описывает дизайн сайта.
•
шаблон размещения - описывает блок с собственно основным информационным наполнением данной страницы.
•
шаблон меню формирует меню на основе структуры сайта.
Остановимся на шаблоне размещения, т.к. остальные три вряд ли нуждаются в каких-то дополнительных комментариях. В отличие от других шаблонов в CMS - этот шаблон кроме фрагментов HTML содержит специальное описание компонентов данных, из которых состоит страница. Примерами компонентов данных могут служить: понятия "документ", "иллюстрация", "сноска", "опросник" и прочие цельные элементарные понятия. Каждый из них имеет различное представление как на сайте, так и в админ-панели и оформлен в виде модуля данных (см. Модуль данных CMS CMSDataModule). Сам же шаблон размещения описывает, как эти компоненты соотносятся между собой.
31
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
Приведем пример шаблона размещения для некоторой новости: <DIV CLASS=header><?aww /comp/data/header/title ?></DIV> <TABLE WIDTH=100%> <TR> <TD VALIGN=TOP> <?aww /comp/data/pictures/text ?> </TD> <TD VALIGN=TOP> <?aww /comp/data/main/text ?> </TD> </TR> </TABLE>
Здесь можно видеть ссылку на поле компонента данных "header" (этот компонент включает в себя общие сведения о статье: заголовок, имя автора, краткий анонс для ленты новостей), ссылку на полосу изображений, которые следует вывести в колонке слева, и ссылку на основной текст статьи, который будет выведен в колонке слева. Таким образом, через шаблон размещения можно создать разнообразные комбинации из элементов данных и модулей под конкретные задачи.
Модуль CMS (CMSModule) Модуль, в отличие от остальных компонентов, может быть непосредственно привязан к странице. Кроме того, каждый модуль при подключении к системе регистрирует себя в админ-панели, что позволяет использовать данный тип встроенного элемента или информационного блока для страниц.
Модуль данных CMS (CMSDataModule) Данная разновидность модуля описывает определенный вид информационного блока, и имеют жесткий интерфейс работы в составе админ-панели и в режиме собственно формирования страниц. В админ-панели данные структуры однозначно представляются в виде некоторых форм. Эти же классы обеспечивают сохранение и получение данных для каждого элемента данных. При заполнении информации на форме для данной страницы сайта может выводиться один или несколько модулей данных (например: текст и выбор иллюстрации). Следует отметить, что данные об этих элементах актуальны для какой-то одной выбранной страницы. В режиме формирования страниц элементы представляются в запрашиваемом формате. Наиболее очевидный формат - это фрагменты HTML, однако возможны другие формы представления для вывода страниц в WAP, страниц оптимизированных для печати, XML и др.
Встраиваемый модуль CMS (CMSInlineModule) В отличие от модуля данных, встраиваемый модуль представляет собой элемент, который не обязательно привязан к одной выделенной странице.
32
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
Такой модуль обычно представляет собой динамически генерируемый фрагмент, зависящий от текущих настроек или страницы, на которой он появляется. Например: постраничная навигация. Очевидно, что данный модуль имеет смысл показывать, когда страниц больше чем одна и ясно, что он зависит от того, что именно сейчас отображается и разбивается на страницы. Встраиваемые модули, кроме всего прочего, имеют интерфейс для настройки и администрирования. Например, у модуля "форум" система администрации по сложности сопоставима с самим форумом, который видит рядовой пользователь.
Структура сайта. Отдельно следует отметить идею отображения структуры сайта. Это некоторая древовидная структура, каждый узел которой соответствует разделу сайта, странице или модулю. Во всех CMS она примерно одинакова, однако с использованием описанного выше шаблонизатора появляется возможность органично вписать эту структуру в общее дерево виртуальных переменных. Фактически оно монтируется просто как ветвь среди всех доступных данных в системе. Как и для переменных, для каждого из узлов структуры предусмотрены настройки доступа, но, кроме того, есть масса общих полей, таких как время создания, модификации, язык и т.д. Кроме того, в это же глобальное дерево CMS включены и отдельные компоненты данных, как потомки цельной страницы. Таким образом, вся система имеет единообразное, цельное объектное представление.
Производительность системы Одной из проблем многих существующих на данный момент CMS является довольно низкая производительность. Здесь, понятно, свою роль играет довольно низкая производительность PHP (в силу того, что он работает в режиме транслятора), задержки связанные с работой по сети, сложность структуры вызова программы (клиентброузер-сеть-веб-сервер-PHP). Тут следует отметить, что частично производительность подобной системы можно повысить за счет использования PHP5 вместо PHP4. (см. подраздел 3.3.) Однако основной причиной этому является сложность сведения многих данных в одну страницу. Зачастую на одной странице сайта одновременно находится опрос, фрагмент ленты новостей, собственно статья, иллюстрации, баннеры, элементы навигации.
33
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
Каждый из этих составляющих в CMS должен генерироваться динамически, и уйти от нагрузки на сервер при формировании такой страницы фактически невозможно, однако ее можно ощутимо снизить при грамотной оптимизации алгоритмов и структур данных, а кеширование даст возможность при последующих обращениях к странице формировать ее по упрощенному алгоритму с использованием заготовок.
Оптимизация Для CMS, как и для любых других программ, применимы стандартные методы оптимизации производительности. Такая оптимизация, в основном, осуществляется за счет увеличения программного кода либо введением избыточных данных в БД (денормализация БД). В случае систем, обслуживающих несколько сайтов, принадлежность тому или иному сайту можно вводить непосредственно в каждой таблице. Это позволяет делать выборки из таблиц, которые по логике связаны с сайтом через несколько посредников, значительно более эффективными. Некоторые задачи по выборке и сортировке данных можно реализовать более эффективно средствами PHP. Например, построение визуального дерева решается в PHP довольно просто из простой выборки всех узлов, тогда как последовательные рекурсивные выборки вида "SELECT * FROM node WHERE parent=..." значительно бы замедлили работу. Реализовать же такую схему с помощью хранимой процедуры еще более затруднительно.
Кеширование Гибкая система кеширования позволяет решить основную проблему, связанную с быстродействием - быстрый ответ сервера при большом количестве одновременных запросов. Одним из подходов является хранение полностью сформированной страницы для каждого адреса и обновление таких страниц при внесении изменений на сайте. С одной стороны данный подход имеет очевидное преимущество - простота алгоритма, однако если на странице нужно вывести хотя бы одно значение, которое часто меняется - число проголосовавших людей, ротация баннера, количество просмотров и т.п., то всю страницу нужно формировать заново. Этот недостаток можно устранить, если хранить не полностью сгенерированную страницу, а заготовку, специализированный шаблон, в котором сформировано все, кроме нескольких выделенных фрагментов. Алгоритм работы модуля кеширования в таком случае можно описать следующим образом: •
Если нет закешированной заготовки страницы, то построить ее заново, заменив все переменные, кроме отмеченных как динамические. Сохранить страницу в кеше.
34
PHP Inside №13 •
Преимущества, которые дает PHP5 при разработке таких систем
Заменить все динамические элементы на их значения и выдать пользователю.
При внесении изменений в нединамические составляющие страницы данная страница изымается из кеша.
Преимущества использования PHP5 при реализации данной модели Кроме повышения производительности при работе с объектами и улучшения формализации объектной модели комплекса, PHP5 дает еще ряд преимуществ. Эти преимущества, строго говоря, относятся ко всем крупным приложениям, использующим объекты. Ниже приведены некоторые решения, активно использующие особенности PHP, примененные при разработке AWW CMS.
Разыменование объектов Так как в CMS интенсивно используется объектный подход, то часто возникает необходимость использовать объекты, возвращаемые функциями (методами). Благодаря возможности разыменования удалось избежать использования промежуточных переменных и ненужного загромождения программного кода.
Использование __autoload() Одним из требований к CMS является модульность, причем зачастую одни модули зависят от других. В результате программа оказывается разнесенной на довольно большое число файлов с определением классом. Следует отметить, что в одном конкретном запросе обычно используется только часть модулей, и нет смысла включать все эти классы (ведь это увеличивает время выполнения). Наличие функции __autoload() дает возможность решить эту проблему. При подключении модуля в специальную директорию модулей вносится небольшой фрагмент, лишь регистрирующий модуль в системе, а все необходимые классы вынесены в отдельные файлы, которые подключаются по мере необходимости. Этот механизм позволяет, кроме того, вызывать классы одних модулей другими модулями, а также контролировать ситуации, когда такие файлы отсутствуют.
Обработка ошибок Так как CMS является довольно сложной системой, то появление сбоев в ее работе неизбежно. В том числе они могут происходить из-за неправильной эксплуатации (например, в случае затирания файлов, необходимых для работы системы). Благодаря введенной в PHP5 обработке исключений удалось создать удобную и полноценную систему отладки. В зависимости от режима, сообщения об ошибках сохраняются в нескольких логах, которые могут выводиться непосредственно на конечной странице после основного кода HTML. Исключения дают возможность вернуть обработку ошибки в тот блок, для которого она более актуальна.
35
PHP Inside №13
Преимущества, которые дает PHP5 при разработке таких систем
Очень кстати тут оказалась новая псевдо-константа __METHOD__, позволившая легко локализовать проблему. Не смотря на то, что переход с PHP4 на PHP5 является довольно простым, очевидно, что проектирование проекта сразу для PHP5 позволяет наиболее полно использовать преимущества новой версии и получить за счет этого максимальный выигрыш в производительности.
Заключение Как можно видеть, предложенная модель объектной CMS оптимально будет работать в случае ее реализации средствами PHP5. В ней учтены преимущества наиболее популярных на данный момент CMS и шаблонизаторов, выработана концепция построения цельной, системы ориентированной для определенной задачи. AWW CMS является воплощением этих идей и демонстрирует, как они работают в реальных условиях, предоставляя широкие возможности как для разработчиков, так и для пользователей, создателей контента.
36
PHP Inside №13
MySQL - просто о сложном
MySQL - просто о сложном Данный доклад рассматривает возможности повышения доАвтор: Александр пустимой нагрузки и отказоустойчивости одной из наиболее расВойцеховский пространенных на сегодня базы данных mySQL. В докладе рассмотрены репликация и mySQL-кластер как два возможных решения поставленных задач. При этом следует понимать, что это два совершенно разных продукта, каждый из которых решает свой круг задач.
Репликация В репликации различают основной (Master) и подчиненные (Slave) сервера. Master-сервер заносит все изменяющие данные SQLзапросы в бинарный лог. Один или несколько Slave-серверов соединяются с Master-сервером, читают все запросы из его бинарного лога и последовательно выполняют их над локальными данными. MySQL, начиная с версии 3.23.15, поддерживает одностороннюю репликацию: это означает, что все запросы на изменение данных должны выполняться на главном (Master) сервере, в то время как запросы на выборку данных можно выполнять на любом из задействованных в репликации серверов. Возможны различные комбинации, например, один и тот же сервер может быть основным сервером в одной паре и подчиненным в другой. Типичная конфигурация репликации показана на рисунке справа.
Задачи, которые решает репликация: Актуальная копия базы mySQL Например, вам необходимо иметь локальную копию базы данных в одном из удаленных регионов. Также возможен вариант, когда у вас нет постоянного соединения с Master-сервером, например, при Dialup-подключении. Еще один вариант применения - вам необходимо протестировать более новую версию mySQL, не используя дополнительных серверов и не прерывая работу текущей версии. В таком случае вы можете собрать новую версию mySQL на том же сервере и использовать репликацию для постоянного обновления данных. Используйте репликацию всегда, когда вам нужна актуальная копия базы данных.
Распределение нагрузки В случае, если вы используете запросы на выборку (SELECT) гораздо чаще, чем запросы на модификацию данных (INSERT/UPDATE/DELETE), вы можете использовать репликацию для распределения нагрузки между несколькими серверами. Возможность распределять нагрузку при запросах на выборку данных (как правило, наиболее частый и ресурсоемкий вид запросов) между несколькими серверами - наиболее распространенная причина использования репликации. 37
PHP Inside №13
MySQL - просто о сложном
Создание резервной копии БД При создании резервной копии базы данных возможны длительные блокировки таблиц при больших объемах данных. Поскольку репликация содержит актуальные данные, ее можно использовать для создания резервной копии.
Повышение отказоустойчивости Для повышения отказоустойчивости можно использовать несколько серверов с настроенной репликацией. В таком случае используется либо динамическое доменное имя вида db.example.com, либо скрипт, подменяющий IP-адреса. При отказе Master-сервера один из Slave-серверов автоматически становится Master-сервером и начинает выполнять его функции. В случае, если новый Master-сервер выходит из строя, ситуация повторяется. Сам же Master-сервер должен быть настроен таким образом, чтобы не пытаться автоматически восстанавливать свой первоначальный статус после сбоя, а становиться одним из Slave-серверов.
Двустороння репликация Начиная с версии 3.23.26, стало возможно настраивать циклическую репликацию (A->B->С->A), или же двустороннюю репликацию как ее "сокращенный" вариант (A->B->A). Опция log-slave-updates (в настоящее время используется по умолчанию) используется для избежания рекурсивного обновления. Двусторонняя репликация, в отличие от односторонней, позволяет наличие двух (или более) серверов, которые могут принимать и выполнять запросы, изменяющие данные. Обратите внимание: наличие такой возможности, не означает, что она безопасна - база данных и ваш код должны быть спроектированы с учетом потенциальных проблем, которые могут возникнуть в данном случае. Основная проблема при построении двусторонней репликации - ключи AUTO_INCREMENT, поскольку вполне возможна ситуация, когда оба Master-сервера попытаются вставить разные записи, с одинаковыми значениями первичного ключа. Например, у вас есть двусторонняя репликация (A->B->A) и некая пустая таблица, использующая первичный ключ my_primary_key. Что произойдет, если оба сервера получат одновременные запросы на вставку данных в эту таблицу? Если запросы не содержат ошибок, они будут успешно выполнены, и в каждой из баз данных появится по записи со значением ключа my_primary_key, равным единице. При попытке отреплицировать эти записи на каждом из серверов возникнет ошибка - дубликат первичного ключа. Итог: репликация остановлена, оба сервера содержат неактуальные данные. Как вариант решения сложившейся ситуации можно использовать составной уникальный ключ вместо первичного. Например: CREATE TABLE orders server_id record_id stuff
( INTEGER UNSIGNED NOT NULL, INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, VARCHAR(255) NOT NULL,
38
PHP Inside №13
);
MySQL - просто о сложном
UNIQUE ukey (server_id, record_id)
После серии запросов insert into orders values (1, NULL, 'testing'); insert into orders values (1, NULL, 'testing'); insert into orders values (2, NULL, 'testing');
результат будет следующим: mysql> select * from orders; +-----------+-----------+---------+ | server_id | record_id | stuff | +-----------+-----------+---------+ | 1 | 1 | testing | | 1 | 2 | testing | | 2 | 1 | testing | +-----------+-----------+---------+
Основные недостатки: •
Данная конструкция будет работать только с myISAM-таблицами.
•
Возможны другие ошибки, например, при модификации структуры таблиц (alter table).
•
Необходимо знать ID сервера, на котором выполняется первичный запрос. В случае, если вы подключаетесь через лоад-балансер, или же просто подключаетесь к разным серверам, вам необходимо использовать предварительный запрос, для получения актуального идентификатора: show variables like 'server_id';
Также следует заметить, что двусторонняя репликация не дает ощутимого выигрыша ни по скорости, ни по производительности, так как каждый из серверов должен выполнять все запросы, изменяющие данные. Какие тогда преимущества? Например, возможность иметь удаленные географически сервера, позволяющие запросы любого типа, или же возможность выстроить часть запросов в ждущую очередь, своевременно освобождая сетевые соединения.
Какое количество серверов мне необходимо задействовать? Данная оценка достаточно приблизительна и предполагает использование равноценных по мощности серверов. Вначале вам необходимо определить: •
Соотношение запросов на чтение/модификацию данных.
•
Какое максимальное количество запросов на модификацию данных может выполнять ваш сервер в секунду? На чтение данных? Как эти два значения связанны между собою?
39
PHP Inside №13
MySQL - просто о сложном
Предположим, что путем тестирования мы определили следующие показатели: •
Запросов на чтение данных - 90%, модификацию данных - 10%
•
Максимальное количество запросов на чтение - 1200 в секунду, на модификацию данных - 600 в секунду. Запросы на модификацию данных в среднем в два раза медленнее, чем на чтение. В таком случае мы получаем следующие зависимости.
Допустимое количество запросов на чтение данных можно оценить так: reads = 1200 - 2 * writes (отражено на графике 1)
Но с другой стороны, запросы на чтение могут быть разнесены по разным серверам: reads = 9 * writes / (N + 1) (поскольку запросов на модификацию данных в 90%, N - количество репликаций) Исходя из приведенных выше равенств, можно получить следующую зависимость:
Полученная зависимость отражена на графике 2. График 1, соотношение запросов на чтение/модификацию данных:
40
PHP Inside №13
MySQL - просто о сложном
График 2, Зависимость максимально-допустимой нагрузки от количества используемых серверов:
Дополнительные возможности повышения производительности Поскольку Master-сервер заносит транзакции в бинарный лог уже после ее окончания, то понятие "транзакции" для Slave-сервера теряет свою актуальность. Это дает возможность использовать опции --skip-innodb, --skip-bdb для принудительного использования myISAM таблиц. Также использование опций --low-priority-updates, -delay-keywrite=ALL и некоторых других, аналогичных им, могут повысить производительность Slave-сервера. Пример использования репликации для распределения нагрузки:
41
PHP Inside №13
MySQL - просто о сложном
Какие могут возникнуть проблемы: •
"Ручные" правки данных на Slave-сервере, которые могут привести к расхождению данных, что, в свою очередь, может привести к остановке репликации.
•
Некоторые команды, например, LOAD DATA INFILE, до версии 4.0. В версиях 3.x в бинарный лог заносился исключительно сам запрос, без содержимого файла. Таким образом, файл должен физически существовать на диске, пока все Slave-серверы не запросят его. Следует заметить, что с командой LOAD DATA LOCAL INFILE такие проблемы не возникают, поскольку явно указано, что файл локальный.
•
Транзакции. Первая особенность в том, что транзакция заносится в бинарный лог после своего завершения. И если основной сервер в момент физического отказа проводил множество длительных транзакций, то фактическая потеря данных может оказаться ощутимой. Или же, что хуже, если отказ сервера произошел в те микросекунды, когда транзакция была завершена, но еще не записана в бинарные логи, результатом будет расхождение основного и подчиненных серверов.
•
Нереплицируемые зависимости. В случае, если вы настроили выборочную репликацию базы данных production и выполните следующий запрос:
INSERT INTO production.sales SELECT * FROM staging.sales
Результат - ошибка при выполнении запроса и остановка репликации.
MySQL-кластер MySQL-кластер - решение, позволяющее гарантировать достаточно высокий уровень доступности (до 99,99%). Чем отличается кластер и репликация? В репликации основной сервер обновляет бинарный лог по мере выполнения транзакций, вследствие чего и сам журнал, и все реплицируемые сервера несколько запаздывают. В случае, если произойдет отказ основного сервера, то несколько последних транзакций будут отсутствовать на Salve-серверах. Другими словами, репликация не гарантирует идентичность данных на Master и Slave серверах. В случае использования кластера все задействованные элементы полностью синхронизированы. Если транзакция была завершена на одной из нод кластера, значит, она была завершена на всех нодах (двухфазный коммит). Говоря совсем просто, репликация асинхронна, в то время как кластер - синхронное решение. Для того чтобы запустить кластер, рекомендуется использовать минимум три сервера (хотя, если схитрить, его можно запустить и на двух, но в таком случае об отказоустойчивости речь не идет). Для использования кластера под нагрузкой, рекомендуется задействовать минимум четыре сервера. Давайте рассмотрим принципиальную схему кластера (показана на рисунке) и каждую из его составляющих.
42
PHP Inside №13 •
MGM (Management) Node. Руководящая нода, которая задает общую конфигурацию и используется для управления кластером. Выполняет такие функции, как запуск и остановка кластера, создание резервных копий и восстановление после сбоя. В типовой конфигурации кластера используется только одна нода данного типа, хотя допускается использование нескольких управляющих нод. Фактически после запуска кластера отказ или просто отключение управляющей ноды никак не влияет на остальные процессы внутри кластера. MGM-нода должна быть запущена раньше, чем все остальные ноды, для этого используйте команду ndb_mgmd.
•
DB Node (Network Data Base Node, Storage Node). Основная нода, которая обеспечивает хранение данных и выполнение всех транзакций. Количество необходимых нод данного типа зависит от объема ваших данных (количества фрагментов, на которые их необходимо разбить) и количества репликаций. Например, если вы хотите, чтобы данные обрабатывались двумя серверами, которые будут реплицироваться (дублироваться), вам необходимо использовать четыре ноды данного типа. Для запуска используйте команду ndbd.
•
MySQL Server (SQL Node). Стандартный MySQL-сервер, использующий NDB-ноды для хранения данных. Вы можете использовать любое количество нод данного типа. Выполняемые функции - предоставление стандартного SQL-интерфейса. Для запуска используйте команду mysqld -ndbcluster.
MySQL - просто о сложном
43
PHP Inside №13
MySQL - просто о сложном
Кластерное хранилище данных Давайте рассмотрим хранилище данных более детально. Основное отличие - все данные хранятся в оперативной памяти. Все действия с данными выполняются в оперативной памяти. Более того, используемый тип таблиц в кластере - NDBCLUSTER. Это означает, что при переносе данных в кластер с обычного mySQLсервера вы должны заменить в описании структуры все строки вида 'TYPE=MyISAM' на 'TYPE=NDBCLUSTER'. Чем это обусловлено? На самом деле вы можете использовать таблицы MyISAM/InnoDB и в кластере, но в таком случае эта таблица будет размещена локально на создавшем ее MySQL-сервере и не будет использовать кластерное хранилище данных. Соответственно, она будет видна только для тех клиентов, которые соединяются с соответствующей SQL-нодой. В противовес этому NDBCLUSTER-таблицы используют кластерное хранилище данных и доступны для любой SQL-ноды. Сколько необходимо задействовать серверов для хранения данных? Вы можете воспользоваться следующей формулой для ответа на этот вопрос:
Например, если вы хотите, чтобы ваши данные были продублированы один раз, и при этом их прогнозируемый размер - 6 гигабайт, вам необходимо использовать четыре сервера с 4 Гб оперативной памяти на каждом из них (в таком случае ваша база данных будет распределена между двумя серверами). В случае, если вы получили сообщение об ошибке вида "ERROR 1114: The table 'my_cluster_table' is full", вам необходимо увеличить задействованный объем оперативной памяти. Количество задействованных серверов для хранения данных не должно превышать 48. Общее количество задействованных серверов в кластере не должно превышать 63. Данные загружаются в оперативную память при старте кластера. Все дальнейшие операции также выполняются в оперативной памяти. Все проведенные транзакции пишутся на диск в виде логфайла. При нормальном (не аварийном) завершении работы кластера все данные записываются на жесткий диск. SQL-нода может использовать следующие обращения к кластерному хранилищу данных: •
Выборка по первичному ключу. Любая таблица, расположенная в кластерном хранилище данных, имеет первичный ключ, даже если вы не задали его явно при создании таблицы. Выборка по первичному ключу - наименее ресурсоемкий из возможных запросов, от NDB к SQL ноде передается только одна запись.
•
Выборка по уникальному ключу. Очень похоже на первый вариант и также использует первичный ключ для выборки данных. От NDB к SQL ноде передается только одна запись.
44
PHP Inside №13 •
Полный просмотр таблицы. В случае, если серверное хранилище данных получает запрос на полный просмотр одной из таблиц, на каждом задействованном для хранения данной таблицы сервере запускается соответствующий процесс. В текущей конфигурации по умолчанию все записи будут переданы на SQL-ноду для дальнейшей обработки запроса. В кластере на базе mySQL 5.0 данный вид обращений существенно оптимизирован: в случае выборки по неиндексированному полю проверка условия выполняется на стороне хранилища, и на SQL-ноду передаются только те записи, которые соответствуют заданному условию. Данная возможность находится в стадии тестирования и по умолчанию отключена. Для ее активации, используйте опцию --engine-conditionpushdown. В таком случае скорость выполнения таких запросов может возрасти в 5-10 раз.
•
Выборка по индексу. В таком случае также выполняется полный просмотр таблицы, но на SQL-ноду передаются только те записи, которые имеют соответствующие значения индексного поля.
MySQL - просто о сложном
Производительность кластера Высокие показания производительности кластера достигаются за счет распределения нагрузки между несколькими физическими серверами и использования оперативной памяти для хранения данных. В пресс-релизе mySQL AB задекларирована следующая производительность кластера: •
10 000 транзакций в секунду при использовании двух однопроцессорных серверов;
•
100 000 транзакций в секунду при использовании четырех двухпроцессорных серверов.
В блоге Jeremy Zawodny были опубликованы следующие показатели производительности при использовании 72-процессорного кластера (количество физических серверов не уточняется): •
380 000 операций записи в секунду
•
1,5 миллиона операций чтения в секунду
Отказоустойчивость Отказоустойчивость кластера обеспечивается следующими особенностями его архитектуры: •
MySQL-серверы соединены с каждой нодой кластерного хранилища данных. В случае отказа одной из NDB-нод выполнение транзакции продолжается на другой ноде.
•
Все NDB-ноды, используемые для хранения данных, могут быть продублированы. В случае отказа одной из нод всегда есть еще одна нода с теми же данными.
•
Управляющая нода также может быть продублирована. Отказ управляющей ноды (даже если она одна) никак не влияет на работу остальных нод кластера.
45
PHP Inside №13 •
Вы можете использовать несколько SQL-нод и равномерно распределять клиентов между ними. Все SQL-ноды оперируют одними и теми же данными (в случае использования кластерного хранилища данных).
•
Любая из нод, составляющих кластер, может быть отключена физически, не нарушив при этом работу самого кластера (конечно же, в том случае, если она была продублирована).
MySQL - просто о сложном
Что происходит в случае сбоя одной из нод? В случае, если обнаружен отказ одной из нод (недоступность ее по сети, отказ жесткого диска), она автоматически помечается как недоступная, и все остальные ноды кластера оповещаются об этом. После того, как восстановлена работоспособность "недоступной" ноды, она соединяется с кластером и инициализируется заново.
Логирование, резервное копирование и восстановление данных Для обеспечения целостности данных используется: •
Непрерывное ведение лога всех выполненных транзакций.
•
Локальные контрольные точки. По мере выполнения транзакций размер лог-файла достаточно быстро увеличивается в размерах. Мо мере достижения определенного количества проведенных транзакций все консистентные данные (контрольная точка) сохраняются на диск, и лог-файл очищается.
•
Контрольные точки. Поскольку все транзакции фиксируются в оперативной памяти, каждой выполняемой транзакции присваивается последовательный уникальный идентификатор (контрольная точка). Эти значения используются при восстановлении системы после глобального сбоя и для контроля записи транзакций на диск. Например, если транзакция с идентификатором 15 была сохранена на диск, это означает, что все транзакции с идентификатором меньше 15-ти также были сохранены на диск.
Восстановление кластера после глобального сбоя выглядит следующим образом: вначале с диска загружаются последние сохраненные данные (локальная контрольная точка), после чего последовательно выполняются все транзакции, занесенные в лог (последняя контрольная точка). Резервная копия кластера включает в себя три части: структура таблиц, данные таблиц, сохраненные на каждой их нод серверного хранилища данных, и логи транзакций. Команда на создание резервной копии задается на управляющей ноде. При получении такой команды каждая нода хранилища создает три соответствующие файла: * BACKUP-<BackupId>.<NodeId>.ctl * BACKUP-<BackupId>-0.<NodeId>.data * BACKUP-<BackupId>.<NodeId>.log
Ограничения То, чего кластер не делает: •
Не готовит кофе :) 46
PHP Inside №13
MySQL - просто о сложном
•
Не поддерживает FULLTEXT-индексы.
•
Не поддерживает составные ключи.
•
Не поддерживает работу с геометрическими типами данных.
•
Не поддерживает внешние ключи (foreign keys).
•
Не поддерживает кеширование запросов (в кластере на базе mySQL 5.0 такая возможность присутствует).
•
Все поля имеют фиксированную длину. Например, если вы объявили поле VARCHAR(255), то для хранения такого поля понадобится 256 байт, независимо от того, какова реальная длина хранящейся в нем строки.
•
Работа SQL-ноды ориентирована на кластерное хранилище данных, поэтому работа с таблицами myISAM / innoDB может не быть оптимальной.
•
Максимальное количество используемых NDB-нод ограничено числом 48.
•
Максимальное количество используемых нод ограничено числом 63.
•
При добавлении новой ноды необходимо перезапускать весь кластер.
Литература и ссылки * http://dev.mysql.com/doc/mysql/en/replication.html, Страница официальной документации * Книга "High Performance MySQL", авторы Derek J. Balling, Jeremy Zawodny, изталеьство O'Reilly * http://dev.mysql.com/doc/mysql/en/ndbcluster.html, Страница официальной документации * http://www.mysql.com/products/cluster/faq.html, Cluster FAQ
MySQL
* http://www.mysql.com/it-resources/white-papers/cluster.php, A Guide to High Availability Clustering * http://www.mysql.com/it-resources/white-papers/clustertechnical.php, MySQL Cluster Architecture Overview * http://jeremy.zawodny.com/blog/, Блог Jeremy Zawodny
47
PHP Inside №13
Оптимизация PostgreSQL
Оптимизация PostgreSQL Оптимизация работы СУБД - лишь малая часть задач, котоАвтор: Алексей Борзов рые требуется решить для повышения быстродействия веб-приложения. Важную роль также играет правильный выбор "железа", правильная настройка веб-сервера и аккуратное написание кода собственно веб-приложения. Тем не менее, очевидно, что без оптимизации базы данных добиться от приложения требуемой производительности невозможно.
1. Общие сведения об архитектуре PostgreSQL Рисунок 1: буферы PostgreSQL и операционной системы
PostgreSQL не читает данные напрямую с диска и не пишет их сразу на диск. Данные загружаются в общий буфер сервера, находящийся в разделяемой памяти, серверные процессы читают и пишут блоки в этом буфере, а затем уже изменения сбрасываются на диск. Если процессу нужен доступ к таблице, то он сначала ищет нужные блоки в общем буфере. Если блоки присутствуют, то он может продолжать работу, если нет - делается системный вызов для их загрузки. Загружаться блоки могут как из файлового кеша ОС, так и с диска, и эта операция может оказаться весьма "дорогой". Журнал транзакций PostgreSQL работает следующим образом: все изменения в файлах данных (в которых находятся таблицы и индексы) производятся только после того, как они были занесены в журнал транзакций, при этом записи в журнале должны быть гарантированно записаны на диск (для чего используется команда fsync или аналог). Журнал транзакций избавляет от необходимости сбрасывать на диск изменения в файлах данных при каждом успешном завершении транзакции: в случае сбоя БД может быть восстановлена по записям в журнале.
48
PHP Inside №13
Оптимизация PostgreSQL
Данные же из буфера сервера сбрасываются на диск при достижении так называемой контрольной точки, выражающемся либо в заполнении нескольких сегментов журнала транзакций, либо в прохождении определённого интервала времени с предыдущей контрольной точки (см. также пункт 2.2.1). В PostgreSQL используется версионная модель управления конкурентным доступом к данным. Это означает, что обращающаяся к данным транзакция видит свою версию данных, существовавшую на некоторый момент времени. При этом она не видит изменений, возможно внесённых в эти самые данные параллельно выполняющимися транзакциями, что обеспечивает изоляцию транзакций. Основными особенностями версионных движков БД (к которым относится и используемый в PostgreSQL) являются следующие: •
Транзакции, изменяющие данные в таблице, не блокируют транзакции, читающие из неё данные, и наоборот (это хорошо);
•
При изменении данных в таблице (командами UPDATE или DELETE) накапливается мусор (старые версии изменённых/удалённых записей), что плохо.
В каждой СУБД сборка мусора реализована особым образом, в PostgreSQL для этой цели применяется команда VACUUM (описана в пункте 3.1).
2 . Настройка сервера В этом разделе описаны рекомендуемые значения параметров, влияющих на производительность СУБД. Эти параметры обычно устанавливаются в конфигурационном файле postgresql.conf и влияют на все базы в текущей установке.
2.1 Используемая память 2.1.1 Общий буфер сервера: shared_buffers Объём буфера должен быть достаточен для хранения часто используемых рабочих данных, иначе они будут постоянно писаться и читаться из кеша ОС или с диска, что крайне отрицательно скажется на производительности. Объём задаётся параметром shared_buffers в файле postgresql.conf. Единица измерения параметра - блоки величиной 8 Кб. В актуальных версиях PostgreSQL (7.4 и 8.0) объём буфера по умолчанию подходит для работы, до этих версий значение было очень маленьким и требовало обязательного увеличения. При этом не следует устанавливать это значение слишком большим: PostgreSQL полагается на то, что операционная система кеширует файлы (см. пункт 2.1.4), и не старается дублировать эту работу. В качестве примерных значений предлагаются следующие: •
Рабочая станция: 4 МБ (512)
•
Средний объём данных и 256-512 МБ доступной памяти: 16-32 МБ (2048- 4096) 49
PHP Inside №13 •
Оптимизация PostgreSQL
Большой объём данных и 1-4 ГБ доступной памяти: 64-256 МБ (8192-32768). Буфер объёмом более 256 МБ создавать не рекомендуется.
2.1.2 Память для сортировки результата запроса: sort_mem1 Этот параметр определяет объём памяти, которую процесс может использовать для сортировки результата запроса. Учтите, что такой объём может быть использован каждым процессом для каждой сортировки (в сложных запросах их может быть несколько). Если объём памяти недостаточен для сортировки некоторого результата, то серверный процесс будет использовать временные файлы. Если же объём памяти слишком велик, то это может привести к своппингу. Объём памяти задаётся параметром sort_mem в файле postgresql.conf. Единица измерения параметра - 1 Кб. Значение по умолчанию - 1024. В качестве начального значения для параметра рекомендуется 2-4% доступной памяти.
2.1.3 Память для работы обслуживающих команд: vacuum_mem2 Этот параметр задаёт объём памяти, используемый командами: VACUUM, CREATE INDEX и т.п. Обычно такие команды больше нагружают диски, но увеличение vacuum_mem позволит ускорить процесс за счёт хранения в памяти больших объёмов информации. Объём памяти задаётся параметром vacuum_mem в файле postgresql.conf. Единица измерения параметра - 1 Кб. Значение по умолчанию - 8192.
2.1.4 Оценка объёма кеша ОС: effective_cache_size Этот параметр сообщает PostgreSQL примерный объём файлового кеша операционной системы, оптимизатор использует эту оценку для построения плана запроса. Если оптимизатор будет считать, что большая часть требуемых данных загружена в кеш операционной системы, то он выберет более агрессивный план выполнения запроса. Объём задаётся параметром effective_cache_size в postgresql.conf. Единица измерения - блоки величиной 8 Кб. По умолчанию значение параметра составляет 1000. В качестве начального значения можете использовать 25-50% доступной (т.е. не занятой операционной системой и приложениями) памяти.
2.2. Журнал транзакций и контрольные точки Изменение параметров журнала транзакций прямо не повлияет на скорость чтения, но может принести большую пользу, если данные в базе активно обновляются.
2.2.1 Уменьшение количества контрольных точек Если в базу заносятся большие объёмы данных, то контрольные точки могут происходить слишком часто3.
50
PHP Inside №13
Оптимизация PostgreSQL
При этом производительность упадёт из-за постоянного сбрасывания на диск данных из буфера. Для увеличения интервала между контрольными точками нужно увеличить количество сегментов журнала транзакций (checkpoint_segments). Каждый сегмент занимает 16 МБ, так что на диске будет занято дополнительное место. Обычно на диске будет не менее одного и не более 2*checkpoint_segments+1 сегментов журнала. Следует также отметить, что чем больше интервал между контрольными точками, тем дольше будут восстанавливаться данные по журналу транзакций после сбоя.
2.2.2 Прочие параметры wal_sync_method определяет метод, при помощи которого записи в журнале транзакций принудительно сбрасываются на диск. Значение по умолчанию зависит от платформы. Возможно, изменение этого параметра позволит увеличить производительность (а возможно, и не позволит). wal_buffers (в блоках по 8 Кб, 8 по умолчанию) определяет размер буфера журнала транзакций, в котором накапливаются записи перед сбросом их на диск. Данный буфер находится в разделяемой памяти и является общим для всех процессов. Стоит увеличить буфер до 256-512 Кб, что позволит лучше работать с большими транзакциями. commit_delay (в микросекундах, 0 по умолчанию) и commit_siblings (5 по умолчанию) определяют задержку между попаданием записи в буфер журнала транзакций и сбросом её на диск. Если при успешном завершении транзакции активно не менее commit_siblings транзакций, то запись будет задержана на время commit_delay. Если за это время завершится другая транзакция, то их изменения будут сброшены на диск вместе, при помощи одного системного вызова. Эти параметры позволят ускорить работу, если параллельно выполняется много "мелких" транзакций.
2.3. Free Space Map: как избавиться от VACUUM FULL Текущий вариант команды VACUUM (в отличие от варианта из версий 7.2 и старше) не пытается удалить все старые версии записей и, соответственно, уменьшить размер файла, содержащего таблицу, а лишь помечает занимаемое ими место как свободное. Для информации о свободном месте есть следующие настройки: max_fsm_relations - максимальное количество таблиц, для которых будет отслеживаться свободное место. max_fsm_pages - количество блоков, для которых будет хранится информация о свободном месте. Информация хранится в разделяемой памяти, для каждой записи требуется по 6 байт.
51
PHP Inside №13
Оптимизация PostgreSQL
Если информация обо всех изменениях помещается в FSM, то команды VACUUM будет достаточно для сборки мусора, если нет - понадобится VACUUM FULL, во время работы которой нормальное использование БД сильно затруднено. Параметр max_fsm_relations должен быть не меньше общего количества таблиц во всех базах данной установки. В качестве начального приближения для max_fsm_pages можно взять половину от среднего количества записей, изменяемых (UPDATE или DELETE) между запусками команды VACUUM.
2.4 Настройки статистики У PostgreSQL есть специальная подсистема - сборщик статистики, - которая в реальном времени собирает данные о выполнении запросов. Эта подсистема контролируется следующими параметрами, принимающими значения true/false: stats_start_collector - включать ли сбор статистики. По умолчанию включён, отключайте, только если статистика вас совершенно не интересует. stats_reset_on_server_start - обнулить ли статистику при перезапуске сервера. По умолчанию - обнулить. stats_command_string - передавать ли сборщику статистики информацию о текущей выполняемой команде и времени начала её выполнения. По умолчанию эта возможность отключена. Следует отметить, что эта информация будет доступна только привилегированным пользователям и пользователям, от лица которых запущены команды, так что проблем с безопасностью быть не должно. stats_row_level, stats_block_level - собирать ли информацию об активности на уровне записей и блоков соответственно. По умолчанию сбор отключён. Данные, полученные сборщиком статистики, доступны через специальные системные представления. При установках по умолчанию собирается очень мало информации, рекомендуется включить все возможности: дополнительная нагрузка будет невелика, в то время как полученные данные позволят оптимизировать использование индексов (см. пункт 4.2).
2.5 Диски и файловые системы 2.5.1 Перенос журнала транзакций на отдельный диск Если на вашем сервере есть несколько физических дисков, то вы можете разнести файлы базы данных и журнал транзакций по разным дискам. Данные в сегменты журнала пишутся последовательно, более того, записи в журнале транзакций сразу сбрасываются на диск, поэтому в случае нахождения его на отдельном диске магнитная головка не будет лишний раз двигаться, что позволит ускорить запись. Порядок действий: •
Остановите сервер (!).
52
PHP Inside №13 •
Перенесите каталог pg_xlog, находящийся в каталоге с базами данных, на другой диск.
•
Создайте на старом месте символическую ссылку.
•
Запустите сервер.
Оптимизация PostgreSQL
2.5.2 PostgreSQL 8.0: использование пространств таблиц (tablespace) Начиная с версии 8.0 в PostgreSQL появилась возможность задавать так называемые пространства таблиц, предназначенные для того, чтобы разносить части базы данных на разные диски. При создании таблицы, индекса и т.п. теперь можно указать, в каком именно табличном пространстве его требуется создать. Выгоды очевидны: если, например, создать таблицу и индексы по ней на разных физических дисках, то магнитной головке диска не придётся при просмотре по индексу всё время перескакивать с данных индекса на данные таблицы.
3. Поддержание базы в порядке Описанные в этом разделе действия должны периодически выполняться для каждой базы. От разработчика требуется только настроить их автоматическое выполнение (при помощи cron) и опытным путём подобрать его оптимальную частоту.
3.1 Команда VACUUM Используется для "сборки мусора" в базе данных. Начиная с версии 7.2, существует в двух вариантах: •
VACUUM FULL (VACUUM до 7.2) пытается удалить все старые версии записей и, соответственно, уменьшить размер файла, содержащего таблицу. Этот вариант команды полностью блокирует обрабатываемую таблицу.
•
VACUUM (начиная с 7.2) помечает место, занимаемое старыми версиями записей, как свободное (см. также пункт 2.3). Использование этого варианта команды, как правило, не уменьшает размер файла, содержащего таблицу, но позволяет не дать ему бесконтрольно расти, зафиксировав на некотором приемлемом уровне.
При работе VACUUM возможен параллельный доступ к обрабатываемой таблице. При использовании в форме VACUUM [FULL] ANALYZE после сборки мусора будет обновлена статистика по данной таблице, используемая оптимизатором. В абсолютном большинстве случаев имеет смысл использовать именно эту форму. Рекомендуется достаточно частое выполнение VACUUM ANALYZE для часто обновляемых баз (или отдельных таблиц). В обыкновенных случаях достаточно ежедневного (точнее, как правило, еженощного) выполнения этой команды.
53
PHP Inside №13
Оптимизация PostgreSQL
При этом обратите внимание: если "бутылочное горлышко" вашего сервера находится в районе дисковой подсистемы, то выполнение VACUUM параллельно с обычной работой может крайне отрицательно сказаться на производительности.
3.2 Команда ANALYZE Служит для обновления информации о распределении данных в таблице. Эта информация используется оптимизатором для выбора наиболее быстрого плана выполнения запроса. Обычно команда используется в связке VACUUM ANALYZE. Если в базе есть таблицы, данные в которых не изменяются и не удаляются, а лишь добавляются, то для таких таблиц можно использовать отдельную команду ANALYZE. Также стоит использовать эту команду для отдельной таблицы после загрузки в неё большого количества записей.
3.3 Утилита pg_autovacuum Начиная с версии 7.4, в дистрибутиве PostgreSQL поставляется программа pg_autovacuum, которая отслеживает изменения в таблицах и автоматически запускает команды VACUUM и/или ANALYZE для этих таблиц по достижении определённого предела. Для работы утилиты должен быть включён сборщик статистики (см. пункт 2.4) и включён параметр stats_row_level. Использование этой программы позволяет отказаться от настройки периодического выполнения команд VACUUM и ANALYZE. Более того, в случае использования pg_autovacuum ресурсы не тратятся впустую на обработку таблиц, которые практически не подвергались изменениям.
3.4 Команды REINDEX и VACUUM FULL Команда REINDEX используется для перестройки существующих индексов. Использовать её имеет смысл в случае •
порчи индекса;
•
постоянного увеличения его размера.
Второй случай требует пояснений. Индекс, как и таблица, содержит блоки со старыми версиями записей. До версии 7.4 PostgreSQL не всегда мог заново использовать эти блоки, и поэтому файл с индексом постепенно увеличивался в размерах. Если вы заметили подобное поведение какого-то индекса, то стоит настроить для него периодическое выполнение команды REINDEX. Учтите: команда REINDEX, как и VACUUM FULL, полностью блокирует таблицу, поэтому выполнять её надо тогда, когда загрузка сервера минимальна. Команду VACUUM FULL имеет смысл запускать вручную для конкретной таблицы после удаления или обновления большой части записей в ней.
54
PHP Inside №13
Оптимизация PostgreSQL
4. Оптимизация запросов
Рисунок 2: Обработка запроса в PostgreSQL Язык SQL является декларативным языком: вы описываете, что вы хотите получить в результате выполнения запроса, а не как вы это хотите получить. Достигать же этого результата СУБД может многими разными путями: •
таблицы могут соединяться в любом порядке;
•
условия могут накладываться в любом порядке;
•
подзапросы могут быть преобразованы в соединения.
Таким образом, для любого не совсем тривиального запроса возникает задача выбора из множества путей выполнения одного: оптимального по быстродействию. На рисунке 2 показан путь, который проходит в PostgreSQL запрос от пересылки его серверу до непосредственно выполнения. Оптимизатор PostgreSQL выбирает путь выполнения и строит план запроса на основе стоимости, под которой понимается количество обращений к диску. Все остальные действия выражаются через эту "условную единицу". Количество обращений к диску оценивается на основе статистики, собранной командой ANALYZE (именно поэтому регулярное выполнение этой команды так важно для производительности).
55
PHP Inside №13
Оптимизация PostgreSQL
4.1. Чтение планов запроса: команды EXPLAIN и EXPLAIN ANALYZE План запроса представляет из себя дерево, узлы которого обозначают действия (соединение таблиц, сортировка, просмотр таблицы). Строки передаются от листьев дерева к корню, для получения строк родительский узел обращается к дочерним узлам, которые, в свою очередь, обращаются к своим потомкам. Выполнение всего запроса заключается в том, что исполнитель обращается к корневому узлу. Команда EXPLAIN предназначена для вывода плана запроса в удобном для восприятия виде. Рядом с каждым узлом дерева, где обозначено действие, показываются оценки для ширины данных, количества строк и стоимости операции (в описанных выше "условных единицах"). Для стоимости показываются два значения: начальная стоимость - количество дисковых операций, нужных для того, чтобы узел смог начать отдавать строки, - и общая стоимость. Для некоторых узлов начальная стоимость нулевая, для некоторых (например, сортировки) - нет. Команда EXPLAIN ANALYZE выполняет запрос и выдаёт статистику его выполнения: время работы узла (в миллисекундах, не в "условных единицах"), количество строк и количество итераций. Следует обращать внимание на большие расхождения между предполагаемыми и реальными значениями. В следующих пунктах объясняются некоторые действия, которые могут осуществляться в узлах дерева.
4.1.1. Способы просмотра таблицы Seq Scan - полный просмотр таблицы. Наиболее простой (но не всегда эффективный) способ - блоки таблицы сканируются от начала к концу. Index Scan - просмотр по индексу. Следует отметить, что использование индекса не всегда предпочтительнее полного просмотра. Если требуется просмотреть большую часть блоков таблицы (а в случае таблицы, состоящей всего из нескольких блоков, это всегда так), то полный просмотр может быть значительно эффективней. Кроме того, современные диски гораздо быстрее читают данные последовательно, чем в произвольном порядке, поэтому полный просмотр может выбираться в случае, если оптимизатор будет считать, что данные скорее находятся на дисках, а не в буферах.
4.1.2. Способы подготовки данных Sort - сортировка данных. Это действие появляется, если явно задать ORDER BY в запросе, а также в случаях, когда требуется удаление из результата дублирующихся строк или при подготовке к некоторым типам соединений. В случае существования подходящего индекса можно обойтись без этого действия. Hash - строит хеш-таблицу по данным, используется при подготовке к Hash Join.
56
PHP Inside №13
Оптимизация PostgreSQL
4.1.3. Способы соединения таблиц Nested Loop наиболее простой способ соединения. Просматривается внешняя таблица, на каждую найденную запись просматривается внутренняя таблица. Merge Join одновременно проходит по двум заранее отсортированным наборам записей. Hash Join соединение по хеш-таблице. Похоже на Nested Loop тем, что просматривается внешняя таблица, но вместо просмотра внутренней таблицы на каждой итерации используется заранее построенная хеш-таблица. Merge Join и Hash Join, как правило, значительно эффективнее, чем Nested Loop, но требуют для своей работы подготовительных действий и потребляют значительно больше памяти.
4.1.4. Принудительный выбор плана Строго говоря, вы не можете заставить оптимизатор PostgreSQL выбрать желательный план выполнения запроса. Но существуют настройки, запрещающие оптимизатору использовать определённые действия (а точнее, добавляющие большие штрафы к их стоимости). Например, SET enable_seqscan=false;
запретит использование полного просмотра таблицы, и вы сможете выяснить, прав ли был оптимизатор, отказываясь от использования индекса. Используйте такие настройки только при тестировании запросов командой EXPLAIN [ANALYZE]. Ни в коем случае не следует прописывать подобные команды в postgresql.conf это может ускорить выполнение нескольких запросов, но сильно замедлит все остальные!
4.2 Использование статистики выполнения запросов Результаты работы сборщика статистики (см. пункт 2.4) доступны через специальные системные представления. Наиболее интересны для наших целей следующие: pg_stat_database содержит - для каждой базы данных - количество зафиксированных и откаченных транзакций, количество блоков, прочитанных с диска и из буфера. Большое количество блоков, прочитанных из буфера, - хороший показатель. Если число блоков, прочитанных с диска, больше количества прочитанных из буфера, то стоит задуматься об увеличении буфера (пункт 2.1.1). pg_stat_user_tables содержит - для каждой пользовательской таблицы в текущей базе данных - общее количество полных просмотров и просмотров с использованием индексов, общие количества записей, которые были возвращены в результате обоих типов просмотра, а также общие количества вставленных, изменённых и удалённых записей. Количество просмотров с использованием индексов должно быть больше количества полных просмотров для всех таблиц, кроме небольших.
57
PHP Inside №13
Оптимизация PostgreSQL
Информация об изменённых записях помогает определить необходимость использования команд VACUUM и ANALYZE, утилита pg_autovacuum использует статистику именно из этого представления для своей работы. pg_stat_user_indexes содержит - для каждого пользовательского индекса в текущей базе данных - общее количество просмотров, использовавших этот индекс, количество прочитанных записей, количество успешно прочитанных записей в таблице (может быть меньше предыдущего значения, если в индексе есть записи, указывающие на устаревшие записи в таблице, большая разница указывает на желательность перестройки индекса командой REINDEX). pg_statio_user_tables содержит - для каждой пользовательской таблицы в текущей базе данных - общее количество блоков, прочитанных из таблицы, количество блоков, оказавшихся при этом в буфере (см. пункт 2.1.1), а также аналогичную статистику для всех индексов по таблице и, возможно, по связанной с ней таблице TOAST. Из этих представлений можно узнать, в частности •
Для каких таблиц стоит создать новые индексы (индикатором служит большое количество полных просмотров и большое количество прочитанных блоков).
•
Какие индексы вообще не используются в запросах. Их имеет смысл удалить, если, конечно, речь не идёт об индексах, обеспечивающих выполнение ограничений PRIMARY KEY и UNIQUE.
•
Достаточен ли объём буфера сервера.
Также возможен "дедуктивный" подход, при котором сначала создаётся большое количество индексов, а затем неиспользуемые индексы удаляются.
5. Заключение В большинстве случаев PostgreSQL не требует слишком сложной настройки: вполне достаточно будет увеличить объём выделенной памяти, настроить периодическое поддержание базы в порядке и проверить наличие необходимых индексов. Стоит также отметить, что в интернете опубликовано достаточное количество материалов, посвящённых настройке производительности PostgreSQL. Кроме того, существует специализированный список рассылки postgresql-performance, в котором вы можете получить ответы на возникающие вопросы. Ссылки [1] PostgreSQL documentation, http://www.postgresql.org/docs/ [2] Momjian B., et. al. PostgreSQL FAQ, http://www.postgresql.org/docs/faqs.FAQ.html [3] Momjian B. PostgreSQL Hardware Performance Tuning, http://www.ca.postgresql.org/docs/momjian/hw\s\do6(p)erformance/ [4] Berkus J., Daithankar S. Tuning PostgreSQL for performance, http://www.varlena.com/varlena/GeneralBits/Tidbits/perf.html [5] Berkus J. Annotated postgresql.conf and Global User Configuration (GUC) Guide, http://www.varlena.com/varlena/GeneralBits/Tidbits/annotated\s\do6(c)onf\s\do6(e).html [6] Trout J. PostgreSQL: Features, Installation, Administration and Optimization, 58
PHP Inside №13
Оптимизация PostgreSQL
http://postgres.jefftrout.com/ [7] Elein Mustain A. Interpreting pg_stat Views, http://www.varlena.com/varlena/GeneralBits/107.php [8] Mullane G. S. Efficient SQL: OSCON 2003 presentation, http://www.gtsm.com/oscon2003/toc.html [9] Conway N. Inside the PostgreSQL Query Optimizer, http://neilc.treehou.se/optimizer.pdf 1.
В версии 8.0 параметр переименован в work_mem
2. В версии maintenance_work_mem
8.0
параметр
переименован
в
3. "слишком часто" можно определить как "чаще раза в минуту". Вы также можете задать параметр checkpoint_warning (в секундах): в журнал сервера будут писаться предупреждения, если контрольные точки происходят чаще заданного.
59
PHP Inside №13
Построение поисковых систем. Принципы и реализация
Построение поисковых систем. Принципы и реализация Поиск не роскошь, а средство передвижения... по сайту.
Автор: Юрий Ефимович Логвинов, технический директор ЗАО "МЕТА" http://meta.ua Харьков, Украина
Взаимодействие внешних и внутренних поисковых систем с сайтом Существует множество поисковиков, построенных по различным технологиям. Все они для отдельного сайта выглядят как веб-клиенты, роботы, также известные под именами crawler, spider или по именам собственным, Googlebot, например. Поисковые роботы чаще всего имитируют поведение пользователя, сканируя все ссылки на одной странице, затем переходя по ним к следующим. Поскольку поисковые системы (ПС) - это мощный инструмент привлечения пользователей, понимание особенностей их работы необходимо веб-мастеру для эффективного взаимодействия. К примеру, если на первой странице кроме флеш-заставки нет даже других ссылок, "улов" робота будет невелик - пустая страница и, если повезло, заголовок из тега <title>. На тему оптимизации сайтов для поисковиков можно прочесть немало статей и сообщений в форумах. Можно обратить внимание на следующие основные моменты.
Навигация по ссылкам. Сайт должен позволять открывать все страницы, переходя по ссылкам. Даже если используются javascript-меню, нужно выбирать реализацию, при которой возможна навигация с отключенным javascript, либо дублировать ссылки обычным образом.
Файл robots.txt Это простой инструмент для управления ПС, подробно о нем рассказывает сайт http://www.robotstxt.org. Файл robots.txt просто необходим, если индексирование каких-либо страниц сайта нежелательно или может нагружать сервер при постоянном обращении.
Кодировки Заголовок HTTP, возвращаемый сайтом, должен правильно указывать кодировку в строке Content-Type: text/html; Charset=windows-1251
И хотя многие ПС могут автоматически определять кодовую страницу документа, явно указывая ее, можно уменьшить количество ошибок.
60
PHP Inside №13
Построение поисковых систем. Принципы и реализация
Дата документа Сейчас, пожалуй, 95% сайтов построены на различных технологиях, использующих динамические страницы. Заголовок HTTP возвращает при этом текущую дату: Date: Thu, 12 May 2005 12:55:44 GMT
Если внешним ПС постоянное переиндексирование одних и тех же документов только лишь добавляет лишнюю работу, то внутренним ПС для поиска по сайту правильная дата документа полезна и позволяет правильно сортировать результаты по времени модификации. Внутренние ПС или системы поиска по сайту пользуются одним из способов интеграции с сайтом. Подход 1: ПС загружает и индексирует страницы в том же виде, как их получает пользователь. Преимущество - не требуется переделка существующего сайта. ПС при этом действует аналогично поисковым роботам внешних ПС. Подход 2: Сайт, построенный динамически, анализирует вебклиента и выдает своей ПС страницы, оптимизированные для поиска, например, без сложных меню, блоков новостей и другого контента, который есть на страницах для пользователей. Этот подход позволяет более точно находить нужную информацию, поскольку исключен "информационный шум". Подход 3: ПС индексирует записи БД, из которой скрипты генерируют страницы сайта. Это удобно когда число однотипных страниц очень большое, как в случае интернет-магазина, содержащего 100-150 тысяч наименований продукции.
Использование метаданных и структурной информации Вам, наверное, уже известны распространенные приемы поисковой оптимизации, такие, как использование необходимых ключевых слов в заголовке страницы (тег <TITLE>) и в мета-теге <meta name="keywords" ...>, <meta name="description" ...>. Кроме того, многие ПС анализируют такую структурную разметку документа, как заголовки (<H1>,<H2>,..), выделение слов (<B>,<STRONG>, <I>, <FONT>). Правильно разметив документ, мы даем возможность ПС использовать эту информацию для расчета релевантности, например, если слова из поискового запроса встречались в TITLE, такой документ получит больший вес. С помощью мета-тега <META NAME="ROBOTS"> можно управлять поведением ПС. Если на странице поместить <META NAME="ROBOTS" CONTENT="NOINDEX, FOLLOW">, то содержимое страницы не будет проиндексировано, она будет использована как оглавление для поискового робота. Например, нет смысла добавлять в поисковый индекс страницу с перечнем всех разделов/рубрик сайта, ведь при поиске пользователю обычно нужно найти страницу раздела по конкретному запросу. 61
PHP Inside №13
Построение поисковых систем. Принципы и реализация
Поместив на странице тег <META NAME="ROBOTS" CONTENT="NOINDEX, NOFOLLOW">, мы полностью запрещаем обработку страницы и ее ссылок.
Проблемы эффективности поисковых систем по сайту Рано или поздно в жизни интернет-ресурса наступает момент, когда без поиска по растущему количеству документов уже не обойтись. Фактически сайту, состоящему из сотни страниц, уже необходимы поисковые функции. Для небольших сайтов можно обойтись даже поисковыми скриптами, по каждому запросу сканирующими все документы или записи БД, но это совершенно не подходит для крупных и нагруженных интернет-ресурсов. Пользователи поиска по сайту проявляют то же поведение, что и при использовании больших ПС, - длина запроса в среднем равна 1.5 слова, к тому же пользователь не готов долго ждать результата и даже не готов искать его в предоставленной системой выборке. Так, по статистике только 50% пользователей открывают следующую порцию результатов. Привычка использования больших ПС также формирует и требования к представлению результатов поиска. Заголовок документа, цитата наиболее релевантного фрагмента с "подсветкой" ключевых слов запроса - это уже стандартные элементы страницы результатов ПС, но не все продукты для поиска по сайту их предоставляют. Например, в Microsoft Indexing Service цитируется только начало документа. Релевантность является еще одной важной характеристикой ПС сайта. Для страниц, содержащих более 1-2KБ информации, расчет релевантности становится нетривиальной задачей. Необходимо учитывать количество ключевых слов запроса в документе, близость слов, выделение текста форматированием или структурой (заголовки, абзацы, предложения). Никто не обучает пользователей ПС формулировать запросы. Интуитивно понятный язык запросов серьезно упрощает пользование поиском по сайту. Использование в ПС модуля морфологии позволяет находить документы, даже если слово в запросе находится не в такой форме, как в тексте. Кроме того, учет вхождения слова в разных его формах позволяет лучше рассчитывать релевантность. Сайты большого объема, 30-100 тыс. документов и более, вмещающие 2-3ГБ информации, предъявляют повышенные требования и к собственной поисковой системе. Она должна устойчиво работать при обработке запросов многих пользователей, обеспечивать достаточную скорость индексирования при больших объемах новой информации или обновлении страниц.
62
PHP Inside №13
Построение поисковых систем. Принципы и реализация
Установка и настройка поиска по сайту (на примере siteMETA www.sitemeta.com ) siteMETA - удобный инструмент для организации качественного поиска по веб-сайту. siteMETA состоит из трех основных модулей: собственно поискового сервиса, веб-интерфейса и административной консоли. Наличие версий под Linux, FreeBSD и Win32 дает гибкость в выборе платформы веб-сервера. Для серверов на других платформах siteMETA можно использовать на отдельном сервере с одной из вышеперечисленных ОС. Поисковый сервис - это самостоятельная программа, которая параллельно выполняет индексирование и поиск документов на вебсервере. После установки siteMETA поисковый сервис с заданной частотой опрашивает страницы сайта, начиная с заданного стартового адреса (адресов). Полученная страница добавляется в поисковый индекс, после чего сразу становится доступной для поиска. Следующие страницы для индексирования поисковый сервис получает, анализируя ссылки на обработанной странице. Поисковый сервис работает в фоновом режиме. Новые или изменившиеся страницы автоматически добавляются в индекс. Модуль веб-интерфейса связывает веб-сервер с поисковым сервером siteMETA. Он передает поисковый запрос поисковому сервису и после его обработки формирует страницу выдачи результатов поиска. Найденные документы упорядочиваются по степени соответствия запросу; при этом учитывается порядок слов в документе, расстояние между ними, форма слова и форматирование документа. Также предусмотрены дополнительные возможности сортировки результатов, например, по дате или по разделу сервера. Административная консоль предназначена для управления поисковым сервисом. Она позволяет задать список стартовых адресов и интенсивность индексации сайта, указать, какие каталоги и типы файлов подлежат обработке, наложить запрет на индексирование отдельных документов или целых групп. siteMETA в зависимости от комплекта поставки обеспечивает лингвистическую поддержку русского, украинского и английского языков. Поиск с использованием модулей лингвистической поддержки позволяет находить по запросу документы, содержащие не только точное вхождение искомого слова, но и все его грамматические формы, а также обеспечивает корректный поиск по несловарным словам: неологизмам, аббревиатурам, фамилиям и т.п. Установка siteMETA отличается простотой по сравнению с аналогами. Важно наличие прав administrator/root, как и при установке системного ПО. Для Win32 и IIS 5.0-6.0 вся установка автоматизирована и выполняется по подсказкам программы-установщика.
63
PHP Inside №13
Построение поисковых систем. Принципы и реализация
Дистрибутив siteMETA для Linux и FreeBSD поставляется в виде архива. Для установки siteMETA требуется распаковать архив и переместить дерево каталогов в заранее выбранный каталог (например, по адресу /usr/local/). Остается скопировать файлы из каталога /usr/local/siteMETA/www/ в каталог веб-сервера, используемый для запуска CGI-скриптов (обычно этому каталогу соответствует виртуальная директория cgi-bin), и запустить поисковый демон (usr/local/siteMETA/bin/mssSvrs -pid:{имя pid файла} -start). После этого все административные функции доступны через веб-интерфейс. Настройка siteMETA выполняется с помощью административной консоли. Для начала работы нужны следующие действия: •
задать правила формирования коллекции документов для индексирования (стартовые url и шаблоны url документов, подлежащих обработке);
•
задать интервал обновления коллекции документов;
•
указать интенсивность загрузки документов (интервал обращений к серверу, тайм-аут на обработку одного документа).
Модуль веб-интерфейса mssCgi после обработки запроса выдает страницу результатов по одному из заданных шаблонов. По умолчанию используется html-шаблон, его легко можно отредактировать, чтобы он соответствовал дизайну сайта. Также можно использовать готовый xml-шаблон, тогда полученный список результатов поиска можно обрабатывать стандартными средствами работы с XML любого из языков веб-программирования.
Подходы по интеграции siteMETA с сайтами, построенными на PHP Использование XML в кроссплатформенных решениях взаимодействия с поисковой системой В этом примере поисковая система работает на сервере Windows, а сайт работает на Linux с Apache. Для решения задачи кроссплатформенного взаимодействия поисковой системы и сайта был выбран стандарт XML, позволяющий четко структурировать обмен информации между поисковой системой и сайтом. PHPскрипт обращается к cgi-приложению веб-интерфейса siteMETA, которому передаются в качестве параметров поисковый запрос, номер первого документа в выдаче и формат выдачи результатов (в данном случае XML). Рассмотрим взаимодействие сайта и поисковой системы (на примере обработки запроса на сайте-магазине мобильных телефонов).
Формирование запроса PHP-скрипт, получающий запрос от пользователя, формирует строку запроса, которую он перенаправляет cgi-приложению в виде обычного GET-запроса.
64
PHP Inside №13
Построение поисковых систем. Принципы и реализация
Запрос имеет вид: mssCgi.exe?q=query&site=xml
mssCgi.exe - cgi-интерфейс взаимодействия с поисковой системой siteMETA где q - запрос пользователя, предварительно обработанный urlencode() site - шаблон для выдачи результатов.
Обработка запроса Осуществляется при помощи подачи соответствующего GETзапроса cgi-приложению, которое в ответ формирует документ в формате xml и выдает его вызывающему php-скрипту. Механизм взаимодействия php и cgi выглядит следующим образом: $answer=""; $fp = fsockopen ("searchserver.dom", 80, $errno, $errstr, 30); if (!$fp) { echo "$errstr ($errno)<br>\n"; } else { fputs ($fp, "GET /cgi-bin/mssCgi.exe?q=nokia&site=xml HTTP/1.0\r\n Host: mysite.com\r\n\r\n"); while (!feof($fp)) { $answer.= fgets ($fp,128); } fclose ($fp); }
В результате переменная $answer будет содержать полученные результаты поиска в xml. <?xml version="1.0" encoding="windows-1251"?> <report><count>1</count> <grpcount>166</grpcount> <docfound>166</docfound> <firstid>1</firstid> <group> <id>1313235170</id> <name>item 2031553</name> <rank>0.004752</rank> <foundcount>1</foundcount> <title><![CDATA[]]></title> <docs><doc><id>2689</id> <title><![CDATA[]]></title> <language>Russian</language> <modifytime>21695</modifytime> <quote> <name><![CDATA[Аксессуары мобильная связь - Телефонная гарнитура с FM-радио <b>Nokia</b> HS-2R]]></name> </quote> <urls> <url> <link><![CDATA[http://mysite.dom/itemlist/~2031553/2688]]></link> <size>551</size> <cp>WIN</cp> <time>0</time> </url> </urls>
65
PHP Inside №13
Построение поисковых систем. Принципы и реализация
</group> </report>
Данная схема обмена позволяет выносить cgi-интерфейс siteMETA и саму поисковую систему (вместе с индексной базой) на другой сервер. C тем же успехом рассмотренный пример можно использовать для взаимодействия веб-сервера под Windows и поисковой системы, установленной на Linux.
Обработка ответа поисковой системы В связи с тем, что возвращаемый xml имеет не сильно разветвленную структуру и небольшой размер, для его обработки мы строим DOM-дерево при помощи библиотеки php domxml. Это позволяет нам произвести валидацию полученного xml, до начала выдачи результатов пользователю. Построение дерева производится при помощи domxml_open_mem($xmldump), где $xmldump - ответ, полученный от поисковой системы. Выделение элементов в результатах поиска проходит при помощи прямого обхода дерева, начиная от некоторой искомой ветви и выдачей результатов на вывод. Так, для получения количества найденных элементов необходимо сделать следующее: $elements = $dom->get_elements_by_tagname("grpcount"); $element = $elements[0]; $count=$element->get_content(); для получения каждого результата поиска необходимо перебрать все элементы, включенные в quote : $elements = $dom->get_elements_by_tagname("quote"); for($i=0;$i<count($elements);$i++) { $child = $elements[$i]->first_child(); while($child) { //Создаем переменные с именами тегов if($child->node_type() == XML_ELEMENT_NODE) { $varname=str_replace(".","_",$child->node_name()); unset($$varname); $$varname=iconv("UTF-8","windows-1251",$child->get_content()); } $child = $child->next_sibling(); } echo $category_id,$item_modid,$name,$description; }
В результате получаем полную выдачу всех найденных результатов. Подобная схема взаимодействия позволяет стандартизировать обмен данными между сайтом и поисковой системой, упростив тем самым составление запроса и выдачу результатов.
66
PHP Inside №13
Построение поисковых систем. Принципы и реализация
Использование COM при работе под Windows Рассмотрим другой вариант взаимодействия php-скрипта и поисковой системы. Он предназначен лишь для работы с php под ОС Windows, так как предполагает работу с COM-объектами. Для использования соответствующий COM-объект, установленный с siteMETA, должен быть зарегистрирован при помощи команды regsvr32. /* создаем экземпляр объекта */ $factory = new COM("MIISInt.SFactory.1") or die("Unable to initialize search"); /* создаем класс отчета, предназначенный для передачи данных поискового запроса от клиента к серверу и доставки результатов от сервера клиенту */ $rep=$factory->CreateReport; /* задаем значение количества $rep->clipSize=20;
возвращаемых документов */
/* задаем поисковый запрос */ $rep->strQuery=$q; /* задаем номер первого элемента в порции */ $rep->first=$first;
После определения свойств класса вызываем метод обращения к поисковому серверу и подачу запроса $rep->ProcessQueryAt("localhost:18181);
После того, как этот метод завершит свою работу, в переменных класса будут содержаться результаты поиска. Так $rep->docFoundCount - количество найденных документов $rep->groupCount - количество документов в порции выдачи $doc=$rep->Group($i) - метод, позволяющий получить результаты запроса и создать элемент класса, содержащий все данные найденного документа. Обращение к элементам происходит при помощи методов класса $doc-> quote // цитата релевантного фрагмента $doc(0)->title //название документа $doc(0)->lang //язык документа $doc(0)->size //размер $url = $doc(0)->Url(0) //ссылка на документ
Более подробно свойства и методы СОМ-объекта для работы с siteMETA описаны в руководстве пользователя.
67
PHP Inside №13
Доклад по целесообразности тестирования
Доклад по целесообразности тестирования В рамках данного доклада мы постараемся убедить новичков Автор: Сергей Юдин и скептиков в целесообразности внедрения автоматизированного тестирования, более того - разработки через тестирование. Мы постараемся доказать, что это того стоит. Автоматизированное тестирование - не просто помощь в отлове ошибок, как многие думают. Оно меняет весь процесс разработки программного обеспечения, снижает многие риски и делает его более контролируемым. Тесты в значительной мере способствуют повышению квалификации разработчиков, а также помогают формировать классный дизайн кода и выпускать в срок качественное ПО. Мы рассмотрим причины, почему нужно писать тесты, какие преимущества они дают, а также разберем, откуда появляются отговорки разработчиков.
Уровни тестирования Для начала определимся насчет предмета разговора. Необходимо прояснить ситуацию о том, какие тесты бывают и каково соотношение между ними. Несмотря на то, что тема доклада - модульное тестирования, необходимо указать, что модульное тестирование будет полезным только при наличии функциональных тестов. Это обязательное условие. Итак, условно выделяют 2 уровня тестирования: Модульное тестирование (unit-тестирование), объектом которого является (чаще всего) один класс, иногда группа классов. Данные тесты должны выполняться максимально быстро, они обладают широкими знаниями о деталях реализации тестируемого кода (в плане внешних проявлений, естественно). Часто их делают изолированными от внешних ресурсов (базы данных, почтовая система) в целях увеличения скорости и гарантированности исполнения в любых условиях. Функциональное тестирование, которое иногда еще называют приемочным тестированием или тестированием "черного ящика". Функциональное тестирование проверяет систему или приложение в целом. Функциональные тесты не должны (в идеале) знать ничего про архитектуру системы, из каких компонентов она состоит. Тестирование проводится так, как если бы реальный пользователь работал с системой. Приемочное тестирование можно использовать для любых проектов, вне зависимости от уровня внутренней реализации. Это может быть как гостевая, написанная "на лапше", так и сложные b2b системы, которые требуют рефакторинга. Большим достоинством приемочных тестов является то, что на создание комплекта таких тестов не уходит много времени, и они не требуют серьезных знаний технологий тестирования. Функциональные тесты являются первыми кандидатами при введении автоматизированного тестирования в существующие проекты. Они могут значительно снизить риски чтолибо сломать при рефакторинге приложений.
68
PHP Inside №13
Доклад по целесообразности тестирования
С модульными, напротив, все не так однозначно, поэтому-то и возникает столько вопросов о целесообразности их внедрения. Ведь модульным можно назвать как тест на класс-фасад, который через небольшой интерфейс управляет работой десятка классов, так и тест на простейшее правило валидации, которое проверяет, пришел ли параметр в запрос или нет. У себя в компании мы обычно выделяем 2 группы модульных тестов - это тесты в стиле TDD, а также модульные тесты высших уровней для проверки работы целых групп классов. При тестировании в стиле TDD, тест пишется до тестируемого кода, а сами тесты диктуют детали реализации тестируемого кода. Развитие и теста, и тестируемого кода ведется мелкими шагами, что предупреждает появление ошибок. Тестов пишется много, они в деталях проверяют всевозможные ситуации работы класса. В итоге такие тесты очень сильно зависят от тестируемого кода. Другая группа тестов менее детальна, их цель - проверка классов в реальных условиях. Здесь тестируется значительно меньше комбинаций взаимодействий, чем в первой группе. Из-за "реальности" эти тесты выполняются значительно медленнее, зато они почти не подвержены изменениями при рефакторинге. Обычно мы выделяем эти тесты в отдельные группы и запускаем их только после значительных изменений в коде или же в конце рабочего дня, чтобы убедиться, что ничего не сломалось. Хотелось бы заострить внимание на том, что автоматизированное тестирование будет полезно только при внедрении всех уровней тестирования - от нижнего до высшего. Только в этом случае это позволит избежать всех проблем, которые возникают при введении тестов. Только приемочный тест покажет, как ведет себя приложение в реальных условиях, только тест на целую подсистему поможет отловить непонятный баг, только тест в стиле TDD позволит сформировать качественный дизайн, который легко поддается рефакторингу и расширению.
Почему нужно тестировать код Разберем, какие преимущества дает использование автоматизированных тестов. Основная суть всего, что будет сказано ниже это то, что тесты позволяют писать более качественный код, а также снижают риск появления ошибок в коде. Некоторые вещи могут показаться спорными, неочевидными, или же очень знакомыми.
Тесты предотвращают появление ошибок в новом коде •
Тесты являются сами ранними клиентами кода и позволяют выявлять многие ошибки сразу.
•
Тесты заставляют разработчика идти мелкими шагами и более пристально уделять внимание коду, который он пишет. Если разработчик чрезмерно ускоряется - то шанс нарваться на красную полоску значительно повышается.
•
Тесты позволяют убедиться в работоспособности кода на самых ранних этапах разработки, когда другие части системы еще не готовы. 69
PHP Inside №13
Доклад по целесообразности тестирования
В результате ошибки выявляются и исправляются в самом начале разработки, а количество ненайденных багов в новом коде сокращается.
Тесты позволяют рефакторить код без риска его сломать •
При внесении изменений в хорошо протестированный код, риск появления новых ошибок значительно ниже. Если новая функциональность приводит к ошибкам, тесты, если они конечно есть, сразу же это покажут. При работе с кодом, на который нет тестов, ошибку можно обнаружить спустя значительное время, когда с кодом работать будет намного сложнее.
•
Хорошо протестированный код легко переносит рефакторинг. Уверенность в том, что изменения не сломают существующую функциональность, придает уверенность разработчикам и увеличивает эффективность их работы. Если существующий код хорошо покрыт тестами, разработчики будут чувствовать себя намного свободнее при внесении архитектурных решений, которые призваны улучшить дизайн кода.
Тесты могут использоваться в качестве документации •
Хороший код расскажет о том, как он работает, лучше любой документации. Документация и комментарии в коде могут устаревать. Это может сбивать с толку разработчиков, изучающих код. А так как документация, в отличие от тестов, не может сказать, что она устарела, такие ситуации, когда документация не соответствует действительности - не редкость.
•
Когда программисты изучают API, они обычно ищут примеры кода. Тесты являются хорошими примерами в этом смысле, так как они показывают как можно использовать классы, через какой API, какие у классов есть зависимости и т.д. Конечно, для того, чтобы быть хорошими примерами, тесты должны быть небольшими и понятными.
•
Стоит также отметить, что тест может играть роль ТЗ. Вы пишете функциональный тест на компонент и отдаете этот тест человеку, который должен будет сделать реализацию. Когда тест сработает - задание может считаться выполненным, и его дополнительно проверять не нужно.
Тесты улучшают дизайн кода •
Тесты заставляет вас делать свой код более приспособленным для тестирования. Например, отказываться от глобальных переменных, одиночек (singletons), делать классы менее связанными и легкими для использования. Сильно связанный код или код, который требует сложной инициализации, будет значительно труднее протестировать.
•
При добавлении новой функциональности или рефактиринге, тесты заставляют подумать о том, что именно должен делать код. Начиная с теста, вы сразу же видите клиентский код. В результате получается чистый и простой дизайн, который делает именно то, что нужно и ничего лишнего.
70
PHP Inside №13 •
Доклад по целесообразности тестирования
Модульное тестирование способствует формированию четких и небольших интерфейсов. Каждый класс будет выполнять определенную роль, как правило небольшую. Как следствие, зависимости между классами будут снижаться, а зацепление повышаться.
Тесты способствуют повышению квалификации разработчиков •
Тесты заставляют разработчика по-другому посмотреть на наследование и делегирование, чаще применять шаблоны проектирования, искать оптимальные пути организации кода. Это будет сокращать размеры ваших классов и упрощать их. С другой стороны, количество классов резко возрастет, но гибкость системы повысится. Как следствие, вы будете более интенсивно использовать шаблоны проектирования. Декомпозиция классов приведет к необходимости их организации новыми способами, и тогда на выручку придут шаблоны проектирования. Такие шаблоны, как Стратегия, Композиция, Декоратор, Посетитель, станут вашими постоянными спутниками.
•
По мере роста опыта в TDD вы самостоятельно придете к пониманию сути таких базовых принципов ООП как "инверсия зависимостей", "отделение интерфейсов", "открытие-закрытие".
•
Автоматизированное тестирование, в отличие от ручного, заставляет разработчика глубже "копать" код. Ему становятся понятными многие решения, примененные к коду. При этом разработка через тестирование обычно ведется в парах, поэтому такие знания распространяются быстрее.
Тесты ускоряют процесс разработки •
Тесты защищают от ошибок. Поэтому время, затрачиваемое на отладку, снижается многократно.
•
Тесты позволяют различным группам разработчиков легко действовать параллельно, не ожидая результатов труда друг друга.
•
Тесты сокращают количество кода, необходимого для удовлетворения требованиям клиента. После внедрения разработки через тестирование, вы начинаете приложение с приемочных тестов, постепенно углубляясь в модульное тестирование. Затем пишете код, который позволит этим тестам срабатывать. Таким образом, на начальных итерациях вы пишете только самое необходимое, а не придумываете универсальное решение. Компоненты можно легко расширить позже, если набор тестов достаточно хорош. При этом качество и дизайн кода будет значительно лучше.
•
Уверенность в том, что все работает как надо после изменений, позволяет выпускать версии продукта горазда раньше и чаще. Немаловажен и тот факт, что автоматизированное тестирование сокращает по продолжительности бета-тестирование.
71
PHP Inside №13
Доклад по целесообразности тестирования
Как убедить разработчика начать тестировать? Разберем некоторые причины, почему программисты не пишут автоматизированные тесты. Конечно, это будет далеко не полный список. Хотелось лишь указать, что большинство отговорок связаны как правило с непониманием сути автоматизированного тестирования, незнания эффективных технологий тестирования, плохим дизайном тестируемого кода и т.д.
Код, над которым работает разработчик, слишком простой для тестирования Наш опыт подсказывает, что это только так кажется во время написания кода. Через некоторое время (скажем через полгода, а может быть даже через пару недель) связи данного кода с другим могут показаться не такими очевидными, а сам код - не таким уж и простым. Конечно, нужно быть благоразумным. Ведь тесты - это вовсе не самоцель, а средство для формирования дизайна и проверки работоспособности кода. Вовсе не обязательно все досконально тестировать. Я лично часто не пишу тесты на нестандартные ситуации, хотя сам код, который эти ситуации проверяет, пишу. Кого-то возрастающий объем кода, который нужно перерабатывать в случае изменения тестируемого класса, вовсе не смущает. Возможно, что все дело в уровне тестирования. Модульное тестирование - прежде всего деятельность, направленная на формирование дизайна кода. Если разработчик пишет код, который только конфигурирует родительский класс или занимается организацией нескольких классов в один, то нужен не модульный тест, а функциональный. Хорошо чувствовать эту грань можно, только получив определенный опыт в разработке и тестировании.
Разработчики ссылаются, что тесты писать куда сложнее, чем сам тестируемый код Этому могут быть 2 причины: •
Низкая квалификация разработчика.
•
Признак плохого дизайна тестируемого кода.
Первый пункт мы разберем чуть позже, а пока уделим внимание плохому дизайну. На начальном этапе введения тестирования в нашу практику нам приходилось постоянно сталкиваться с ситуацией, когда написать тест на какой-либо класс практически невозможно. Необходимо было зарегистрировать пользователя, добавить кучу записей в базу данных, иметь в наличие с десяток конфигурационных файлов и т.д. Конечно, опыта тестирования было немного, однако плохой дизайн системы был главной причиной. Из-за этого новые тесты писать было сложно, они очень часто ломались. О том, чтобы писать тест до реализации вообще можно было не говорить.
72
PHP Inside №13
Доклад по целесообразности тестирования
Причина крылась только в одном - классы очень сильно зависели друг от друга, иногда весьма хитрым образом. В итоге система быстро костенела, сопротивлялась изменениям и постепенно покрывалась "плесенью". Тогда мы попробовали сменить тактику и попробовать тестировать самые простые классы с минимальным количеством зависимостей. Такой подход очень хорошо подошел, например, для правил валидации, компонентов шаблонной системы, утилитарных классов. Каждый тест и тестируемый класс был небольшим, и даже по прошествии большого количества времени можно было легко понять, что и как тестируется. Стабильность системы повышалась. Постепенно тесты проникали из нижних уровней системы в высшие. Мы стали хорошо представлять, что такое изоляция тестов, как правильно использовать заглушки и моки, в чем беда статических методов, что такое роли классов и т.д. Написание тестов уже не казалось таким сложным процессом, а уверенность в коде росла. Теперь на код, который нельзя изолировать на время тестов, мы смотрим совсем по-другому. Итак, в следующий раз, отказавшись от тестов по причине сложности их написания, задумайтесь, а не загнивает ли ваш проект?
Для написания тестов недостаточно времени Конечно, если сравнивать скорость написания класса с тестами и без них, то быстрее писать код без тестов. Однако сам процесс написания кода значительно изменяется - вы пишете только то, что нужно для того, чтобы тесты выполнялись. Ваши классы становятся меньше по размеру и выполняют четко определенный набор функций. То есть вы пишете меньше кода. Это во-первых. Освоившись в модульном тестировании, вы заметите, что скорость написания кода, напротив, возросла. Причины этого кроются в постоянном ритме работы, который становится нормой для мастера TDD: тест, код, рефакторинг, тест, код, рефакторинг и т.д. Делая мелкие шаги, вы всегда знаете, что вас ждет дальше, поэтому двигаетесь без промедлений. В-третьих, разработка через тестирование значительно сокращает время, проводимое в отладчике. Опять же причины в ритме работы. Добавляя небольшую порцию кода, мы тут же убеждаемся, что эта порция кода работает. Изменив какие-либо классы, мы проверяем, что в системе все в порядке. Конечно, всегда есть соблазн написать сразу большой кусок кода, однако в этом случае почти наверняка получаешь красную линию, и приходится откатываться назад. В итоге мы уже давно забыли, что такое многочасовые сессии в отладчике для поиска непонятной ошибки. Сборка нескольких частей в единое целое легче, если отдельные части уже хорошо протестированы. То есть налицо экономия времени.
73
PHP Inside №13
Доклад по целесообразности тестирования
Количество багов в коде уменьшается, а их правка производится быстрее. Плюс сокращается вероятность каскадных ошибок то есть ситуаций, когда при исправлении ошибки в одном месте, мы добавляем их в другом. Поэтому фаза бета-тестирования и окончальной доводки становится короче по времени. Особенно, если проект протестирован при помощи приемочных тестов, когда количество ручного труда на тестирование приложения вообще будет минимальным. И, наконец, более качественный дизайн, полученный в ходе разработки через тестирование, помогает в дальнейшем улучшать код, делая это очень быстро. Если предполагается, что ваш проект доживет до второй версии, то от тестов вы только выиграете.
Разработчики ссылаются, что они не умеют писать тесты Это крайне неумелая отговорка, т.к. начать писать тесты можно с самых простых вещей, не используя всех возможностей xUnit, моков (mock objects) и проч. Многие программные продукты используют очень простые скрипты для самотестирования. Можно попытаться для начала записать с помощью кода те действия, которые разработчик совершает для тестирования вручную. Пусть это даже будет набор разрозненных и неуклюжих тестовых скриптов, однако это лучше, чем ничего. Попробуйте начать с простых приемочных тестов и тестов на классы низших уровней. Продвинутые техники тестирования приходят постепенно с более глубоким осознанием предмета тестирования. Самое главное - не стоит спешить. Как только тестирование перейдет в приятную привычку из разряда обременительного процесса, вы вдруг обнаружите, что сами изобретаете приемы тестирования, а от дикого "хакинга" вас уже воротит. Каждый разработчик стремится быть профессионалом. Профессионалы пишут тесты. Точка. Да, написание тестов требует многих более продвинутых навыков, но....ведь мы же профессионалы, не так ли?
Тесты - это двукратное увеличение количества кода По мнению многих разработчиков, которые выступают против, модульные тесты - это двукратное увеличение количества кода. Могу сказать, что это не так. На самом деле оно ... троекратное. Однако почему это должно вас волновать? Ведь это код, который досконально проверяет работоспособность вашего рабочего кода. А объем этого самого рабочего кода с введением тестов кстати, становится меньше, это уж поверьте. Но ведь при изменении тестируемого кода, приходится корректировать и тестируемый код, а это двойная или даже тройная работа, заметят критики. Ответ и да, и нет. Во-первых, при разработке через тестирование классы становятся очень узкоспециализированными с четко определенными интерфейсами. Они стремятся взять на себя как можно меньше обязанностей, но те, что берут, выполняют хорошо. Плюс к этому зависимости между классами снижаются.
74
PHP Inside №13
Доклад по целесообразности тестирования
В итоге любые изменения, которые вносятся в систему, носят локальный характер и отражаются на минимальном количестве классов. Тесты, постоянный рефакторинг и богатый опыт единственные средства позволяющие создавать качественные, а значит, и коммерчески успешные программные продукты. Конечно, кто-то возразит, что можно с самого начала спроектировать систему таким образом, чтобы обязанности между классами были распределены оптимальным образом, например, при помощи UML. Я просто уверен в том, что весь опыт разработки ПО говорит о том, что это просто невозможно, потому что никто не может целиком увидеть всей картины, в то время как требования к программным продуктам постоянно меняются. Поэтому - тесты и только тесты. Во-вторых, подумайте, а правильно ли вы тестируете свой код. Как-то на форуме был вопрос о целесообразности теста на класс, который лишь заносит в таблицу базы данных определенные данные и считывает их. Если такой класс один, то это еще полбеды. Что делать, если таких классов много? Конечно, в данном случае гораздо правильнее было создавать не отдельные тесты на каждый класс, а автоматизированный тест на все подобные классы, который бы генерировал случайные данные для сохранения, а затем - соответствие записанных данных считанным. Для особых случаев можно было бы создавать отдельные тесты. Вообще к тестам нужно применять точно такие же правила, как и для тестируемого кода, например, избегать дублирования. Старайтесь прислушиваться к своему кода, как к рабочему, так и к тестовому. Тогда вы скоро поменяете свои количественные оценки размера кода на качественные.
В итоге В заключение доклада хочется акцентировать внимание вот на чем. TDD - это лучший способ стать профессионалом и выучить все тонкости ООП. Я не удивлюсь, что многие даже опытные программисты, задают себе вопрос, зачем нужны интерфейсы, что такое роли объектов, когда уместнее отказаться от наследования и предпочесть делегирование, какой шаблон проектирования выбрать в том или ином случае, что мешает развитию проекта и т.д. По-моему мнению нет более эффективного и быстрого способа заставить себя слушать и понимать свой код, кроме как начать писать для него тесты. Научившись "вынюхивать" запахи тестового кода вы получите мощнейший стимул к рефакторингу своих проектов. Не успеете оглянуться, как увидите, что ваша система состоит из набора четкоопределенных, невзаимосвязанных компонентов, каждый из которых легко модифицировать и расширять. А главное - исчезнет страх что-либо сломать, не сделать что-то в срок, просидеть несколько часов в отладчике. Конечно, профессионализм в TDD приходит не сразу. Самое сложное - это сломать стереотипы, заставить себя думать по-другому. Уйти от дикого "хакинга" сложно, особенно в режиме острой нехватки времени. На это уйдет довольно много времени. После этого вы почувствуете, что стали "инфицированным тестами", и теперь смотрите на процесс разработки ПО совсем другими глазами.
75
PHP Inside №13
Доклад по целесообразности тестирования
За рамками доклада данного доклада остались запахи тестового кода, которые можно связать с темой целесообразности. Для этого нужно иметь достаточный опыт, чтобы понимать, о чем идет речь. Поэтому мы решили оставить их на мастер-класс "Влияние тестирования на дизайн кода".
76
PHP Inside №13
XML Sapiens
XML Sapiens как универсальная концепция сайтостроения в разрезе XML/PHP Любой веб-проект начинается с технического задания (ТЗ). Что должно содержать ТЗ? Открываем «библию проект-менеджера» («Управления проектом по созданию интернет-сайта», Альпина паблишер) и находим, что, кроме прочего, ТЗ устанавливает принципы взаимодействия программных модулей, принципы «человеко-машинного диалога», описывает задачи функциональных элементов. Т.е. ТЗ описывает пользовательские интерфейсы сайта. Неудивительно, что проект-менеджеры «спят и видят», как новый проект автоматически собирается на основе их UMLсхем. Бизнес-аналитики негодуют: «Мы дали вам логику интерфейсов, почему же на их сборку уходит столько времени?». В Windows, все формы GUI уже «отрисованы», и для создания нового интерфейса достаточно лишь инициализировать класс .Net Frame Work Class Library или же вызвать функцию Win32 API. Каждый новый веб-проект требует уникальных информационной архитектуры и графического дизайна. Соответственно, следует каждый раз создавать все интерфейсы заново. Или все же, как говорят в Microsoft, однажды написанное приложение мы можем развертывать многократно (build-once, deploy n-times)?
Автор: Дмитрий Шейко (www.cmsdevelopment.com), ведущий программист Red Graphic Systems (www.redgraphic.com)
Почему в веб-разработках неприменимы модели Windows Forms, Win32 API, MFC? Для того чтобы выделить в приложении абстрактный слой интерфейса и управлять им самостоятельно, логично воспользоваться языком разметки UIML (www.uiml.org). В результате мы получили бы четкое дерево структуры пользовательского интерфейса приложения с динамической базой контента элементов и определенной моделью событий. Для воссоздания многофункциональных интерфейсов можно было бы воспользоваться комплектом XAML + .Net языки для платформы Longhorn и XUL + Java-script/Python/C++ для приложений Gecko-базированных браузеров. По части эффектных интерфейсов по-прежнему вне конкуренции решения от Macromedia. Ныне речь о Flex и обогащенных приложениях (Rich Internet Applications). Впрочем, программисты PHP могут воспользоваться богатым инструментарием GTK (http://gtk.php.net) для рендеринга GUI-приложений, но это будут уже не совсем веб-приложения.
XML Sapiens
77
PHP Inside №13
XML Sapiens
Очевидно, что библиотека готовых компонентов для реализации сложных форм пользовательского интерфейса (интерактивные деревья, управляемые таблицы, drag&drop и т.д.) существенно сократило бы время разработки проектов. Схематика декларации UIML повысила бы качество сложных веб-интерфейсов. Однако еще до этапа формирования пользовательского интерфейса имеется серьезная и актуальная задача. Веб-документ, помимо интерфейсных форм, содержит также контент и его оформление. PHP располагает таким замечательным инструментом, как SMARTY (http://smarty.php.net), что позволяет программистам эффективно разделять программный код приложения и его оформление. Но наиболее концептуальный подход предлагает стандарт W3C XSL – компиляция «чистых» данных в XML и их оформление «на лету». Изображение 1. Данные, представление, пользовательский интерфейс
Данные в формате XML и графический дизайн в формате XSLT поступают на преобразователь XSL. Последний возвращает пользователю документ, готовый к просмотру. Казалось бы, вот и решение. Однако XSL разрабатывался как язык декларации оформления документов, но не разметки пользовательских интерфейсов. Т.е. для полноценного разделения данных, их оформления и форм пользовательских интерфейсов требуется дополнительная технология. XAML, XUL и MXML (Flex) хоть и не отвергают XSL напрямую, но по большей части ориентированы на самостоятельный рендеринг приложений.
78
PHP Inside №13
XML Sapiens
Пример 1. XSLT-описание пользовательского интерфейса простейшего навигационного меню <xsl:template name="menu"> <xsl:for-each select="child::*"> <div> <xsl:if test="not(./@state='selected')"> <a class="active" > <xsl:attribute name="href"> <xsl:value-of select="./variable"/> </xsl:attribute> <xsl:value-of select="./label"/> </a> </xsl:if> <xsl:if test="./@state='selected'"> <a class="inactive" > <xsl:attribute name="href"> <xsl:value-of select="./variable"/> </xsl:attribute> <xsl:value-of select="./label"/> </a> </xsl:if> </div> </xsl:for-each> </xsl:template>
UIML, напротив, полагается на XSL. Однако UIML представляет собой общий подход к описанию пользовательских интерфейсов, и необязательно веб-приложений. Что касается веб-приложений, он будет не всегда эффективен. Давайте конкретизируем задачу. Требуется методика разделения данных, их оформления и форм пользовательского интерфейса для веб-проектов. Веб-проектов, содержащих интерфейсные слои для различных групп пользователей, один из которых – административная область. Приложение, реализующие административную область зачастую именуется CMS, хоть и решает куда большие задачи, нежели управление содержанием проекта. Методика разделения данных, их оформления и форм пользовательских интерфейсов в CMS детально разобрана в спецификации XML Sapiens. И далее я предлагаю ознакомиться с этой концепцией.
Динамический сайт в объектах Как реализуется интеграция пользовательских интерфейсов в открытых популярных проектах CMS XOOPS2, TYPO3, Mambo? Там применяются либо собственные конструкции PHP в HTML, либо шаблонизация на базе SMARTY. А к чему стремимся мы? •
Унифицированная концепция;
•
Независимость от платформы;
•
Независимость от форматов представления данных;
•
Простота описания логики интерфейсов.
79
PHP Inside №13
XML Sapiens
Как этого достичь? Давайте пробежимся по теории динамических сайтов. Итак, информационное пространство представляет собой множество сайтов и их языковых версий. Каждый сайт является множеством разрозненных документов, где документ – это веб-ресурс с уникальным интернет-адресом. Документ определяет данные и их представление. Структура информационного пространства определяет отношение сайтов между собой. Структура сайта определяет отношение документов между собой. Структура документа определяет наличие, специфику и расположение данных в коде представления. Задача разработчика организовать: •
Управление структурой информационного пространства, сайта;
•
Управление структурой документа;
•
Управление данными;
•
Управление представлением;
•
Управление функциональностью пользовательских интерфейсов.
Разделяем функциональность, данные и представление по «рецепту» XML Sapiens Теория теорией, но стоит вспомнить и о практике. А на практике часто бывает так, что программисту проще представить документ как код, переданный браузеру. Фактически речь о шаблоне представления данных, в котором сразу же можно выделить следующие логические блоки (см. рис. 2): •
Редактируемые в административном режиме фрагменты содержания, относящиеся к данному документу;
•
Статический код, общий для группы документов;
•
Динамический код, формируемый на основе сценария.
Другими словами, мы располагаем шаблоном, содержащим контейнеры данных. В спецификации XML Sapiens они определены следующим образом: Контейнеры запросов (QC) подразумевают: •
в режиме доставки содержания – данные текущего документа или стороннего (атрибут SRC);
•
в режиме администрирования – пользовательский интерфейс запросов данных, соответствующий их типу.
Контейнеры статических данных (SDC) представляют собой код, общий для группы документов. Контейнеры динамических данных (DDC) отображают код, в зависимости от состояния среды (действий пользователя, внешних факторов и т.д.), согласно определенному сценарию. Эти контейнеры, по сути, и есть основа интерфейсов динамических сайтов с позиции XML Sapiens. Как выглядят DDC?
80
PHP Inside №13
XML Sapiens
Изображение 2. Объекты динамического сайта
Пример 2. DDC пользовательского интерфейса навигационного меню <sapi:for-each select="get_tree()" name="enum"> <sapi:choose> <sapi:when exp="eq(this.this.currentpage.value,1)"> <sapi:code> <a class="active" sapi:href="this.this.HREF.value"><sapi:apply name="this.this.TITLE.value" /></a><br /> </sapi:code> </sapi:when> <sapi:when exp="neq(this.this.currentpage.value,1)"> <sapi:code> <a class="inactive" sapi:href="this.this.HREF.value"><sapi:apply name="this.this.TITLE.value" /></a><br /> </sapi:code> </sapi:when> </sapi:choose> </sapi:for-each>
81
PHP Inside №13
XML Sapiens
Изображение 3. UML DDC пользовательского интерфейса навигационного меню
Как видно из примеров, сценарии пользовательских интерфейсов в XML Sapiens имеют линейную логику, основанную на иерархии условий. Особенностью подхода является возможность разбора рядов данных, запрашиваемых прямо в сценарии непосредственно у системной функции CMS согласно заданным параметрам. В результате мы всегда получаем алгоритм, легко транслируемый в UML.
82
PHP Inside №13
XML Sapiens
Переносимость динамических сайтов Жесткая зависимость документов динамических сайтов от его движка противоречит принципам семантического веба (W3C RDF, http://www.w3.org/2001/sw/). Если мы возьмем типовой движок динамического сайта, то, скорее всего, мы сможем заставить его формировать XML-документы с данными. Если мы возьмем движок, основанный на XSL, то мы будем располагать и данными, и их представлением, независимыми от платформы. XSL позволяет создавать сценарии для динамически формируемых интерфейсов. Однако в XSL алгоритм интерфейса не отделен от кода представления. Может ли XML Sapiens отделить сценарий интерфейса и его представление? В настоящее время на SourceForge открыт проект процессора сценариев XML Sapiens (http://sapiprocessor.sourgeforge.net). Классическое разделение данных и представления подразумевает XML-документ с данными, ссылающийся на XSLT-документ с оформлением. Было бы логично добавить в него ссылку на документ с описанием интерфейсов. Процессор сценариев XML Sapiens в случае обнаружения подобной ссылки или ссылок на документ со сценариями пользовательских интерфейсов в формате XML Sapiens, анализирует его сценарии и на их основе расширяет документ с данными. После этого обычным образом происходит XSLT-преобразование документа. Пример 3. Исходный файл данных для процессора сценариев XML Sapiens <?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type='text/xsl' href='template.xsl'?> <?xml-sapi type='text/xml' href='interface.sapi'?> <content xmlns:sapi="http://www.xmlsapiens.org/spec/sapi.dtd" xmlns:xlink="http://www.w3.org/1999/xlink"> <data1>data1</data1> <data2>data2</data2> <menu><sapi:apply name="ddc.menu.value" /></menu> <title><sapi:apply name="qc.title.value"></title> <publication><sapi:apply name="qc.publication.value"></publication> </content>
Пример 4. Сценарий XML Sapiens в файле interface.sapi <?xml version="1.0" encoding="UTF-8"?> <sapi version="1.0" xmlns:sapi="http://www.xmlsapiens.org/spec/sapi.dtd"> <sapi:ddc name="menu"> <sapi:choose> <sapi:when exp="TRUE"> <sapi:for-each select="get_tree()"> <sapi:choose> <sapi:when exp="TRUE"> <sapi:code> <row sapi:id="this.this.id.value" sapi:activity="this.this.currentpage.value"> <link><sapi:apply name="this.this.href.value" /></link> <item><sapi:apply name="this.this.title.value" /></item> </row> </sapi:code> </sapi:when>
83
PHP Inside №13
XML Sapiens
</sapi:choose> </sapi:for-each> </sapi:when> </sapi:choose> </sapi:ddc> </sapi>
Пример 5. Сформированный XML <?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type='text/xsl' href='template.xsl'?> <content xmlns:sapi="http://www.xmlsapiens.org/spec/sapi.dtd" xmlns:xlink="http://www.w3.org/1999/xlink"> <data1>data1</data1> <data2>data2</data2> <menu> <row id="01" activity="1"> <link>/intro/</link> <item>Introduction</item> </row> <row id="02" activity="0"> <link>/chapter1/</link> <item>Chapter 1</item> </row> <row id="03" activity="0"> <link>/chapter2/</link> <item>Chapter 2</item> </row> </menu> <title><![CDATA[Introduction]]></title> <publication><![CDATA[<p>Content</p>]]></publication> </content>
Изображение 4. Схема формирования отображаемого документа
Как XML Sapiens практически реализуется в PHP Возникает закономерный вопрос: как на практике использовать технологию XML Sapiens в собственных проектах?. Потребуется ли рекомпиляция сервера или же специализированный браузер? На самом деле, для использования концепции XML Sapiens в проекте потребуется добавить к движку его веб-сайта программный процессор XML Sapiens. Как это сделать?
84
PHP Inside №13
XML Sapiens
Изображение 5. Модель процессора XML Sapiens
85
PHP Inside №13
XML Sapiens
Как вы можете видеть, алгоритм процессора XML Sapiens достаточно прост. На вход программы подается шаблон формируемого документа. Программа перебирает все вхождения указателей XML Sapiens в шаблоне и подставляет на их место код возврата, соответствующего им объекта в файле сценариев XML Sapiens. В PHP подобный алгоритм реализуется особенно легко, благодаря регулярным выражениям PRCE. На выходе программы мы получаем готовый к просмотру документ.
Парадигма MVC для CMS на базе XML Sapiens Общую модель CMS, базированной на XML Sapiens, можно представить следующим образом. Изображение 6. Парадигма MVC для CMS на базе XML Sapiens
Запрос из браузера поступает на программный контроллер CMS, который в свою очередь отражает его системной среде окружения. Теперь этот запрос определяет поведение форм пользовательского интерфейса (DDC/QC) при компиляции документов процессором XML Sapiens. Сформированный документ направляется в браузер.
Итоги •
XML Sapiens содержит интуитивно понятную модель описания пользовательских интерфейсов сайтов;
•
XML Sapiens определяет инфраструктуру динамического сайта, наиболее близкую CMS;
•
XML Sapiens разделяет логику пользовательских интерфейсов сайтов, данные и их представление; 86
PHP Inside №13 •
XML Sapiens не противоречит принципам семантического веба;
•
XML Sapiens легко реализуется программно.
XML Sapiens
Выводы XML Sapiens – открытый проект, созданный веб-разработчиками для веб-разработчиков. Проект несет в себе концепцию сайтостроения, удобную для применения в CMS. Проект динамично развивается и каждый из вас свободно может принять в нем участие. Ссылки по теме: * Адрес сайта проекта: http://xmlsapiens.org * Публичная библиотека интерфейсных решений: http://xmlsapiens.org/lib/ * Англоязычный форум: http://xmlsapiens.org/board/ * Дискуссионный лист: http://cms-forum.ru/index.php?showforum=29 * Список рассылки: http://groups.yahoo.com/group/xmlsapiens/ * Открытый проект CMS на базе XML Sapiens: http://sapid.sourgeforge.net/ * Процессор сценариев XML Sapiens: http://sapiprocessor.sourgeforge.net/
Краткая информация об авторе: Занят разработкой программного обеспечения с 1987 года. Начиная с 1998 года опубликовано более 50 авторских технических статей в специализированных изданиях. С 2001 года разрабатывает архитектурные решения и инструментальные средства для управления содержанием. За прошедшее время спроектировал ряд успешных коммерческих продуктов, включая систему электронных публикаций MyPRESS, систему управления информационным пространством MySITE (www.cmsmysite.com), среду разработки вебприложений Site Sapiens (www.sitesapiens.ru). Является автором спецификации универсального языка для разработчиков CMS XML Sapiens (www.xmlsapiens.org).
87
PHP Inside №13
XML в PHP 5
XML в PHP 5 История интеграции языка программирования PHP и языка разметки XML началась с выходом PHP третьей версии, в котором была реализована возможность обработки XML—документов на основе SAX—модели. В четвертой версии PHP была реализована возможность работы с XML—документами как с DOM—объектами, хотя программный интерфейс модуля DOMXML отличался от рекомендации W3C по DOM [1]. Также появилась возможность производить над XML—документами XSL трансформацию [2], используя различные библиотеки и, соответственно, методы.
Автор: Жолудов Денис [DAN], программист, ООО "Астелнет". Ведущий раздела "PHP & XML-технологии" на PHPClub.ru
XML в PHP5 С выходом PHP пятой версии возможности работы с XML— документами существенно обогатились и унифицировались. Это связано с тем, что все модули для работы с XML—документами были полностью переписаны и стали базироваться на одной библиотеке Libxml2 [3]. Выбор данной библиотеки не случаен. Во-первых, эта библиотека стабильно работает в различных операционных системах, таких как Linux, Unix, Windows, MacOS X, OS/2 и других. Во-вторых, она реализует множество рекомендаций консорциума W3C, в частности стандарт XML, пространство имен XML, XPath, Xpointer, Xinclude, Relax NG, XML Schemas, DOM2, SAX. В третьих, это одна из самых быстрых библиотек для обработки XML—документов. Стабильное развитие этой библиотеки позволяет расширять и возможности PHP—модулей, а поддержка рекомендаций W3C полностью реализуется в PHP. Все эти факторы убедили разработчиков взять библиотеку Libxml2 как базисную. На ее основе в PHP5 были реализованы следующие модули: 1. DOM – модуль, позволяющий обрабатывать с XML—документ как DOM—объект в соответствии с API, описанным в рекомендации W3C. 2. SimpleXML – модуль, реализующий простой доступ к XML —документу посредством обхода древовидной структуры документа и итерации соседних узлов документа. 3. XMLReader – модуль, позволяющий делать прямой не кэширующий обход дерева XML—документа. В некотором смысле работа этого модуля сходна с работой SAX—парсера, поэтому модуль XMLReader не требователен к памяти и вполне подойдет для обработки большого по размеру XML—документа. Реализует в себе XMLReader API библиотеки Libxml2. 4. XMLWriter – модуль, предоставляющий простой интерфейс для потоковой записи XML—документа. Реализует в себе XMLWriter API библиотеки Libxml2. 5. SOAP – модуль, позволяющий реализовывать SOAP—клиента и SOAP—сервер на PHP.
88
PHP Inside №13
XML в PHP 5
6. XSL – модуль, позволяющий проводить XSL—трансформации над XML—документами. Остановимся на некоторых особенностях реализации вышеперечисленных модулей.
DOM Данный модуль можно по праву считать самым функциональным из всех имеющихся модулей для обработки XML—документов. Наличие полнофункционального и расширяющегося API, причем, практически полностью совместимого с [1], позволяет строить гибкие приложения, а также перенимать опыт программистов, пишущих на других языках с использованием DOM—объектов. Основные классы DOM—модели реализованы в PHP5 по умолчанию. Это означает, что больше нет необходимости компилировать и/или подключать модуль дополнительно. Теперь он устанавливается вместе с PHP. А обновленная объектная модель в PHP позволяет наследовать от базовых DOM—классов, создавая производные классы, моделирующие реальные объекты, но наследующие всю мощь методов DOM. Возможность применения XPath—запросов к DOM—документу добавляет еще большей функциональности в его обработке. С выходом PHP5.1 у класса DOMXPath появится новый метод evaluate (). Данный метод позволит вычислять XPath выражения, результатом которых может быть не только список узлов типа DOMNodeList. И соответственно, будет возвращать результат, отличный по типу метода query(). Весьма существенным дополнением стала возможность подключать сторонние XML—документы с помощью метода xinclude(), что дает более гибкие возможности построения DOM—документа. Возможность переопределения потоков ввода—вывода позволяет использовать различные источники для получения XML—документа, а также для его экспорта. Наличие методов валидации документа на основе XML Schema, RelaxNG, и DTD позволяют контролировать входные данные, а функциональность Libxml2 к тому же позволяет читать HTML—документ (возможно даже не wellformed) и применять к нему все методы DOM—модели. Еще одной существенной особенностью является возможность обмена данными между модулем DOM и другими модулями, такими как SimpleXML и XSLT.
SimpleXML Данный модуль появился вместе с выходом в свет пятой версии PHP. В предыдущих версиях аналогов этому модулю не было. Простота этого модуля (как следует из его названия) заключается в исключительно понятном объектно—ориентированном подходе к обработке XML—документов [4], когда имена узлов представляются как свойства объектов.
89
PHP Inside №13
XML в PHP 5
Эти объекты, в свою очередь, также могут быть представлены объектами со своими свойствами, если XML—документ имеет вложенные узлы. Этот процесс происходит рекурсивно до тех пор, пока не попадется узел, не имеющий потомков. В этом случае объект выступает в роли итератора. Данная возможность обхода XML—дерева документа появилась благодаря свойству Zend Engine2 перегрузки объектов. Также имеется возможность использования XPath— запроса к документу, который возвращает массив объектов класса SimpleXMLElement, удовлетворяющих запросу.
XMLReader Можно сказать, что данный модуль приходит на смену хорошо зарекомендовавшей себя SAX—модели обработки XML—документов. Данный модуль наследует интерфейс XmlTextReader, включенный в библиотеку Libxml2 и представляет из себя так называемый pull—парсер. То есть, в отличие от SAX—модели, которая выдает данные пользователю при каждом событии обработки (push), XMLReader выдает данные лишь при наступлении заявленного события (pull) таким образом фактически “вытаскивая” те данные, которые ожидает от него пользователь. Так же, как и SAX—парсер, XMLReader не загружает в память весь документ, но быстрее обрабатывает его за счет чтения лишь затребованных узлов документа. К тому же модуль поддерживает валидацию, пространство имен, и другие стандартные возможности XML. Начиная с версии PHP5.1 данный модуль “переезжает” из PECL [5] в основной дистрибутив расширений PHP, что существенно упростит разработку и программирование обработчиков XML— данных.
XMLWriter Модуль позволяет записывать потоковый XML непосредственно в память или на диск, что существенно снижает затраты машинных ресурсов. Модуль не имеет объектно—ориентированного интерфейса, что несколько увеличивает трудозатраты программиста. Зато по сравнению с прямой записью даных в память или на диск с использованием PHP—функции fwrite(), данный модуль работает быстрее, требует меньшего написания кода и позволяет сохранять данные в объявленной кодировке без их предварительной конвертации. К сожалению, в отличие от вышеупомянутых модулей, данный модуль находится в репозитории PECL, что несколько усложняет его повсеместное использование.
Обработка ошибок XML С выходом PHP5.1 существенно улучшится обработка ошибок, возникающих в процессе работы библиотеки Libxml2. Появятся функции libxml_* которые будут позволять перехватывать ошибки библиотеки, получать информацию об ошибках и обрабатывать их на усмотрение разработчика. Данная возможность существенно повысит качество кода и предсказуемость его работы.
90
PHP Inside №13
XML в PHP 5
XSL Данный модуль реализует в себе возможности библиотеки Libxslt, которая является логическим продолжением Libxml2, основана на этой библиотеке и позволяет производить XSL—трансформацию XML—документа на основе XSL—шаблона. Принципиальных отличий от модуля DOMXML у данного модуля нет. Добавилась лишь возможность добавлять в шаблон свои функции а также вызывать PHP—функции из XSL—шаблона в процессе трансформации. Но эти возможности существенно расширяют область применения XSL—преобразований в рамках программирования на языке PHP.
XSLT как язык описания шаблонов Изначально язык XSL создавался как язык стилевой разметки XML—документов. В процессе создания спецификации языка XSL были выработаны правила преобразования одного XML—документа в другой. Данная спецификация получила название XSLT [2] и на сегодняшний день является основным документом, регламентирующим правила преобразований XML—документов. В PHP5 появилось несколько дополнительных возможностей для подготовки XML—документов к XSL—трансформации. Одним из нововведений является возможность вызова функций, определенных пользователем. Модуль XSL дает возможность вызывать как XSL—функции, так и любые, не запрещенные пользователем, PHP —функции внутри XSL—шаблона. Если вызов XSL—функций внутри шаблона лишь расширяет его функциональность, то вызов PHP —функций имеет как положительные, так и отрицательные моменты. С одной стороны вызов таких функций существенно расширяет возможности по обработке данных, привнося недостающую функциональность в шаблон. С другой стороны теряется смысл самой XSL —трансформации, так как в ее ход вмешивается PHP—функция. К тому же такой вызов может быть небезопасным, если шаблон был получен от третьих лиц и содержит в себе вызов недопустимой PHP —функции. С последним недостатком справляются ограничения на вызываемые функции, введенные с версией PHP5.1. Но тем не менее данный шаблон перестает быть pure—XSL и будет работать лишь при трансформации с использованием PHP. Поэтому к внедрению в шаблон таких функций нужно подходить осмысленно. При XSL—трансформации также появилась возможность включать сторонние документы в результирующий с помощью метода xinclude() класса DOMDocument. Еще одним немаловажным фактором стала возможность передавать значения параметров шаблона внутрь самого шаблона с помощью метода setParameter() класса XSLTProcessor. Вышеперечисленные нововведения позволяют реализовывать различные интересные идеи. В частности, появляется возможность реализовать мультиязычные шаблоны на основе XSLT и gettext.
91
PHP Inside №13
XML в PHP 5
PEAR и PECL пакеты для работы с XML XML—технологии широко представлены различными пакетами в репозиториях PEAR и PECL. В частности, выше уже описывался пакет XMLWriter из PECL. Среди представленных в PEAR пакетов наиболее интересными, на мой взгляд, представляются следующие: XML_XUL – класс, позволяющий создавать пользовательские интерфейсы на основе спецификации XUL от Mozilla Organization. Данное направление разработки пользовательского интерфейса представляется весьма перспективным в силу все большей популяризации Gecko—браузеров. К тому же в некоторых intranet—приложениях представляется рациональным ограничить пользователей системы выбором браузера в пользу улучшения интерфейса и увеличения скорости и качества разработки. XML_MXML – система разработки, позволяющая создавать приложения на основе технологии Macromedia Flex. Данная технология является конкурирующей с технологией XUL и предоставляет пользователю большие интерактивные возможности. Также в репозитории присутствует множество пакетов, позволяющих работать с данными определенной структуры (XML_SVG, XML_RSS и т.д.) и позволяющих преобразовывать данные из одного формата в другой (XML_fo2pdf, XML_sql2xml, XML_svg2image). В целом, представленные в PEAR пакеты для работы с данными в XML—формате, довольно множественны и разнообразны, хотя некоторые из них дублируют функционал, представленный в новых модулях PHP для работы с XML.
SOAP Появившийся в PHP5 модуль SOAP является первой реализацией семейства протоколов SOAP, написаной на языке С. Несомненное преимущество этого модуля перед другими реализациями протокола – скорость обработки информации. Модуль позволяет не только делать запросы к существующим web—сервисам посредством вызова удаленной процедуры (RPC), но и создавать собственные web—сервисы, в том числе с использованием WDSL. Этот модуль позволяет отдавать и получать как простые данные (числа, строки), так и комплексные (массивы, объекты). С включением этого модуля в стандартную поставку PHP становится проще разрабатывать собственные web—сервисы. В частности, появляется возможность предоставлять безопасный доступ к БД, расположенной на одном сервере с web—сервисом. Простота создания запросов web —клиентом и разбора запроса web—сервисом позволяет разрабатывать комплексные решения на базе протокола SOAP. Подводными камнями при этом могут стать структура WDSL—документа или сложность интеграции SOAP интерфейса с существующим Framework в следствии слабой типизации в php, но это решаемые трудности.
92
PHP Inside №13
XML в PHP 5
XML и СУБД Традиционный интерес в обсуждении данной темы представляет возможность хранения и извлечения из СУБД данных в формате XML. Если с хранением XML—данных особых проблем не возникает, то выборка таких данных представляется нетривиальной. В общем случае, для того, чтобы выбрать среди множества XML—документов один, удовлетворяющий определенному условию (например, один из узлов документа имеет определенное значение), придется либо производить полнотекстовый поиск по всем документам в БД, либо выносить значения ключевого элемента в отдельное поле таблицы и производить поиск по этому полю. Если таких полей несколько, то рентабельность хранения XML—данных пропадает. С другой стороны, было бы удобно одним запросом сразу выбирать нужные структуры данных. В некоторых современных СУБД такая возможность вполне реализуема путем установки дополнительной надстройки к СУБД. К примеру, СУБД PostgreSQL-7.4.2 имеет в дистрибутиве дополнительно подключаемый модуль XML, позволяющий выполнять XPath —запросы к XML—документу, хранящемуся в БД. Также недавно появился патч [6] для СУБД MySQL-5.0, позволяющий выполнять XPath—запросы к XML—документам, хранящимся в БД. В частности, вводятся новые SQL—функции ExtractValue(xml, xpath) и UpdateXML(xml, xpath, value). В дальнейшем планируется включить этот патч в официальный релиз MySQL. Существует множество СУБД, позволяющих напрямую извлекать данные из реляционных таблиц в виде XML—документа (MSSQL, Oracle). Также есть возможность формирования результирующего XML -- документа с помощью хранимой процедуры. В любом случае, при необходимости хранения и извлечечения данных в формате XML, можно поместить между приложением и самой СУБД дополнительный уровень абстракции данных и в нем реализовать возможность конвертации данных из древовидной структуры XML—документа в реляционную структуру таблиц БД.
Чего ожидать в будущем? Динамика развития PHP—модулей, реализующих различные XML—технологии, показывает, что интеграция таких широко распространенных языков как XML и PHP находится на высоком уровне и будет продолжаться в дальнейшем. Среди актуальных направлений на сегодняшний день можно выделить такие как обмен данными между web—системами, развитие веб-сервисов на основе технологии RPC и протокола SOAP, повышения уровня взаимодействия в клиент—серверных приложениях с применением технологий XUL, XForms.
93
PHP Inside №13
XML в PHP 5
Литература 1. Document Object Model Level 1 ( http://www.w3.org/TR/1998/REC-DOM-Level-1-19981001/ ) 2. XSL Transformations (XSLT)
( http://www.w3.org/TR/xslt )
3. The XML C parser and toolkit of Gnome http://xmlsoft.org/ )
(
4. SimpleXML, Sterling Hughes http://www.zend.com/php5/articles/php5-simplexml.php )
(
5. PECL (http://pecl.php.net/ ) 6. XML/XPath Support In MySQL5.x, Alexander Barkov. April, 2005 ( http://mysql.r18.ru/~bar/myxml/XMLXpathSupportInMySQL. pdf )
94
PHP Inside №13
Поддержка нескольких СУБД в проекте
Поддержка нескольких СУБД в проекте Существующие подходы, их анализ. Методика перевода суАвтор: Соловьёв Денис ществующего проекта на поддержку нескольких СУБД. [ForJest], вольнонаёмный разработчик программного обеспечения.
Исходные условия
г. Днепропетровск, Украина
Таблица 1. Полная и частичная поддержка СУБД СУБД
ADODB
PEAR::DB
MySQL
+
+
PostgreSQL
+
+
Microsoft SQL Server
+
+
Oracle
+
+
DB2
+
+
Sybase
+
+
Interbase/Firebird
+
+
SQLite
+
+
Основные возможности и преимущества по сравнению с использованием «родных» функций определённой СУБД: •
ОО подход к доступу СУБД – удобство обращений, использования;
•
Единый интерфейс для общения со всеми СУБД ;
•
Централизованная обработка ошибок, отладка;
•
Комфортный доступ к результатам запроса – отдельные классы результаты;
•
Построители запросов - для INSERT/UPDATE и выбора данных;
•
Рутинные операции по извлечению данных;
•
Библиотека готовых решений для основных задач во всех поддерживаемых СУБД;
•
Основные задачи востребованы временем и проверены практикой.
Таблица 2. Функциональность для поддержки нескольких СУБД, попытка учесть различия Функциональность
ADODB
PEAR::DB
Частичный выбор записей (LIMIT, TOP…)
+
+
Fetch Modes
+
+
Эмуляция COUNT
+
-
Блокировка и транзакции
+
+
OUTER JOIN-ы (LEFT, RIGHT…)
+
+ 95
PHP Inside №13
Поддержка нескольких СУБД в проекте
Функциональность
ADODB
PEAR::DB
Последовательные ID
+
+
Подготовленные (prepared) запросы
+
+
Вызовы Stored Procedures
+
-
Placeholders
+
+
Конкатенация строк
+
-
Системное время
+
-
Системная дата
+
-
Дата для INSERT/UPDATE
+
-
Время для INSERT/UPDATE
+
-
Форматирование даты
+
-
+/- к датам/времени
+
-
Строки: длина
+
-
Строки: подстрока
+
-
Строки: верхний/нижний регистр
+
-
Строки: случайное число
+
-
96
PHP Inside №13
Поддержка нескольких СУБД в проекте
Таблица 3. Простой пример – получение интервала записей СУБД
SQL
MySQL
SELECT * FROM my_table ORDER BY id LIMIT 10, 20;
MS SQL Server
SELECT TOP 30 * from my_table ORDER BY id; пропустить первые 10 записей при выборке. Или страшная конструкция типа SELECT * FROM ( SELECT TOP 20 * FROM ( SELECT TOP 30 * FROM my_table ORDER BY id ASC ) ORDER BY id DESC) ORDER BY id ASC;
ADODB
$db-> SelectLimit('SELECT * FROM my_table ORDER BY id’, 20, 10);
97
PHP Inside №13
Поддержка нескольких СУБД в проекте
Таблица 4. Более сложный пример – получение записей, для которых нет соответствия в другой таблице СУБД MySQL
SQL $mysql_query = ‘SELECT t1.* FROM my_table t1 LEFT JOIN another_table ON t1.id = t2.id_my_table WHERE t2.id IS NULL’;
MS SQL Server
$mssql_query = ‘SELECT t1.* FROM my_table t1 WHERE t1.id NOT IN (SELECT id_my_table FROM another_table)’;
ADODB
Предоставляет $db->leftOuter для оператора LEFT JOIN и проверку IfNull, но, как мы видим, удовлетворительного решения, учитывающего особенности каждой СУБД, найти нельзя. В зависимости от начальной СУБД выбирается либо 1. Сделать LEFT JOIN, как в MySQL, хотя решение для MSSQL не лучшее. 2. Эмулировать подзапрос через IN(), пользуясь схемой MSSQL, хотя для MySQL есть своё эффективное решение.
Обычно
switch ($db->databaseType) { case 'mysql': $sql =$mysql_query; break; case 'mssql’: $sql = $mssql_query; break; default: die(‘Unsupported DB type’); } $db-> Execute($sql) $mysql_query и $mssql_query см. выше
Библиотеки абстрактного доступа не решают проблем поддержки нескольких СУБД. Неизбежно приходится сталкиваться с задачами, которые нельзя решить путём транслирования запроса Мета-СУБД в запрос конкретной СУБД. Тогда появляются “решения” через switch, case. Добавив ещё одну СУБД, мы получим блок switch … case, состоящий уже из 3-х пунктов. Это придётся делать в каждом месте, где пасует библиотека абстрактного доступа. Код приобретает множество конструкций вида: switch ($db->databaseType) { case '<тип СУБД>': {
98
PHP Inside №13
Поддержка нескольких СУБД в проекте
<предварительное задание параметров>; $sql = <здесь СУБД зависимый SQL>; <обработка результатов>; } … default: die(‘Unsupported DB type’);
} $db-> Execute($sql); <обработка результатов>; return $results;
Вынесем все разные операции в разные классы и дадим им одно имя switch ($db->databaseType) { case 'mysql': { $Handler = &new MySQLHandler($db); $results = $Handler -> do_operation($param1, $param2, $param3) } case 'mssql': { $Handler = &new MSSQLHandler($db); $results = $Handler -> do_operation($param1, $param2, $param3) } … default: die(‘Unsupported DB type’); } return ($results);
Вынесем повторяющиеся строки из switch case: switch ($db->databaseType) { case 'mysql': { $Handler = &new MySQLHandler($db); } case 'mssql': { $Handler = &new MSSQLHandler($db); }… default: die(‘Unsupported DB type’); } $results = $Hanlder-> do_operation($param1, $params2, $param3);
Т.к. данная конструкция встречается в нескольких местах, вынесем её в метод отдельного класса. function &create_handler($type) { switch ($type) { case 'mysql': { return new MySQLHandler($this-> db); } case 'mssql': { return new MSSQLHandler($this-> db); } …
99
PHP Inside №13
Поддержка нескольких СУБД в проекте
default: die(‘Unsupported DB type’);
} }
$Factory = new DBHandlerFactory($db); … $Handler = &$Factory-> create_handler($db-> databaseType); $results = $Hanlder-> do_operation($param1, $params2, $param3);
Так как $db у нас уже имеется и так в классе Factory, то function &create_handler() { switch ($db->databaseType) { … } $Factory = new DBHandlerFactory($db); $Handler = &$Factory-> create_handler(); $results = $Hanlder-> do_operation($param1, $params2, $param3); $Factory = new DBHandlerFactory($db); $Handler = &$Factory-> get_db_handler(); $results = $Hanlder-> do_operation($param1, $params2, $param3);
Дизайн кода существенно упростился, разница в реализации инкапсулирована в классы «Операции с СУБД». Недостатки: при изменении требований к какой-либо функциональности можно изменить реализацию для одной СУБД и забыть сделать изменения для другой. При наличии 20-30 подобных конструкций/операций это выливается в (20-30)*N проверок и изменений. Но, по крайней мере, мы имеем решение, которое выглядит достаточно удобным для того, чтобы базировать на нём методику перевода проекта на поддержку нескольких СУБД. №
Шаг
Описание
1 Выделение операции
Прикидываем, какие выходными данными
параметры
являются
входными
и
2 Написание теста
Тестов не бывает много. Лучше иметь мало тестов, чем не иметь их вовсе
3 Вынесение операции
Переносим участок кода в метод класса и заменяем его (участок кода) вызовом метода
4 Тестирование
В процессе вынесения операции мы тестируем создаваемый метод. После замены тестируем клиентскую функциональность
Пункты 1-4 повторяются, пока все запросы и/или вызовы конструкторов запросов ADODB или PEAR::DB не будут вынесены в методы класса. 5 Рефакторинг
После того, как все операции вынесены в отдельный класс, производим рефакторинг. Устраняем дублирующийся код, разделяем классы, и т.д.
6 Создание копии
По технологии Cut&Paste создаём копию всех полученных в результате шагов 1-5 классов и меняем их названия. Создаём параллельный вызов тестов
100
PHP Inside №13
№
Поддержка нескольких СУБД в проекте
Шаг
Описание
7 Коррекция
Тесты показывают места, где необходимо внести изменения. Корректируем классы, всю подсистему общения с СУБД
8 Вынесение общих операций
Избавляемся от дублированного кода, выносим все общие операции в классы-предки
9 Рефакторинг тестов
Учитываем вновь появившиеся абстрактные классы
Выделение операции Под операцией подразумеваем логичную и обособленную последовательность действий. •
Выборка для формирования отчёта.
•
Внесение записи.
•
Изменение баланса на счету.
•
…
Если в проекте использованы транзакции, то можно ориентироваться на них. Или другой подход – ориентироваться на SQL-запросы и вызовы методов, которые конструируют запросы в библиотеке абстрактного доступа. Поиск мест, содержащий операции по работе с СУБД, может быть просто автоматизирован.
Написание теста Тест пишется для проверки клиентской функциональности, с одной стороны, и для создаваемой операции – с другой. Клиентской функциональностью является участок кода, содержащий выделенную операцию. Обычно достаточно написать один-два теста, которые доказывают, что всё работает хорошо. Это нужно сделать, чтобы не делать этого впоследствии вручную. Для вновь создаваемого метода тоже должны быть разработаны тесты. Это, с одной стороны, позволит разработать интерфейс обмена клиентской части с вновь создаваемым методом – параметры, ожидаемые значения, а с другой, позволит в дальнейшем проверить работу метода для другой СУБД. Написание теста экономит время на проверках, т.к. автоматизирует их. Также существенно уменьшает вероятность возникновения ошибок.
Вынесение операции Тут всё просто – копируем уже имеющуюся реализацию в метод класса. После копирования добиваемся срабатывания тестов. Тесты на метод можно написать и после переноса, но на клиентскую часть должны быть написаны до этого. После этого участок кода заменяем на вызов метода с параметрами. Все тесты должны после этого срабатывать. 101
PHP Inside №13
Поддержка нескольких СУБД в проекте
Рефакторинг После вынесения всех операций в отдельный класс можно заняться поиском дублирующихся частей. Весь код для работы с БД теперь находится в одном классе, а не разнесён по всему проекту. На данном этапе основная задача – убрать основные видимые дублирования и разнести операции по отдельным классам, которые отвечают за определённый набор функциональности. К примеру, управление записями клиентов – в один класс, а управление каталогом товаров – в другой. Также придётся изменить тесты классов для операций с БД. Заметим, что тесты клиентской функциональности менять скорее всего не придётся.
Создание копии Создаём копии всех получившихся классов. Меняем имена. К примеру, DBMySQLClients -> DBMSSQLClients Также нужно создать удобный путь для запуска тестов. Т.е., чтобы тестируемые классы можно было легко подменить. Для этого мы используем класс-фабрику: $DBFactory-> set_type(‘mysql’); $DBFactory-> get_clients(); Таким образом, мы легко получаем тесты для новой СУБД. В то же время получаем объект-фабрику, которую затем используем в проекте.
Коррекция Запускаем тесты и сразу же видим, где и что перестаёт срабатывать. Внесение изменений занимает минимум времени. Корректируем, смотрим на увеличивающуюся зелёную полоску тестов, радуемся. Во время этой стадии необходимо дописать все недостающие тесты клиентской функциональности. Т.е. на всё то, что начинает проверяться вручную «по-быстрому», нужно создать тесты. Тесты мы создадим один раз, а пользоваться потом будем очень долго.
Вынесение общих операций Теперь, после того, как весь код работает, можно действительно увидеть общие части, одинаковые для обеих СУБД. К серьёзным изменениям в коде это не приведет, т.к. нас защищает класс-фабрика. В целом достаточно будет: •
создать класс предок;
•
сделать имеющиеся 2 класса его потомками;
•
вынести все полностью одинаковые методы в класс-предок.
102
PHP Inside №13
Поддержка нескольких СУБД в проекте
Их мы выносим в класс-предок. Класс-фабрика будет возвращать во всех случаях экземпляр одного класса.
Рефакторинг тестов Создаём тесты для вновь появившихся классов. Переносим тесты методов и тестов для потомков в тесты для предков. Трудозатраты на добавление ещё одной СУБД будут минимальными, т.к. мы имеем •
Тесты для клиентской части;
•
Систему доступа к СУБД, учитывающую особенности каждой из СУБД;
•
Тесты на общие части и на особенности.
Чтобы добавить ещё одну СУБД, нужно будет произвести операции 5-9, в то время как объёмная, рутинная и «опасная» работа 1-4 в повторении не нуждается. В целом применение методики повлияет благотворно на дизайн кода проекта, позволит получить выгоды от наличия тестов (то, что вы никогда не решались сделать, но всегда хотели иметь) и увидеть новые перспективы улучшения кода.
103
PHP Inside №13
PHP::SOAP и Xforms
PHP::SOAP и Xforms Внедрение современных открытых стандартов на ОАО «АВАвтор: Анохин Александр, ТОВАЗ» начальник бюро экспертизы интернет-проектов ОАО «АВТОВАЗ» Соавторы: Булов Владимир Данный доклад подготовлен на основе опыта разработки Геннадьевич, начальник ряда модулей информационных систем ОАО «АВТОВАЗ» с исполь- центра развития зованием веб-служб (в реализации PHP5) и XForms (x-port информационных технологий FormsPlayer) и представляет собой краткое введение в данную тех- ОАО «АВТОВАЗ», Литвинов нологию. Кирилл Анатольевич, Предполагается, что аудитория знакома с базовыми XML- инженер-программист ОАО технологиями, такими как XML Namespaces, XPath. Акцент доволь- «АВТОВАЗ»
Введение
но сильно смещен автором в сторону XForms, поскольку литературы и онлайн-публикаций, посвященных SOA и веб-службам, накопилось уже достаточное количество, а в отношении XForms еще наблюдается некоторый дефицит информации на русском языке.
Предпосылки перехода на веб-службы и SOA Исторически ОАО “АВТОВАЗ” разрабатывает и внедряет информационные системы (ИС) для автоматизации своих бизнеспроцессов самостоятельно силами IT-подразделений, входящих в структуру предприятия. При постепенном развитии существующих и внедрении новых систем, ориентированных на веб, неизбежно возникает потребность в организации взаимодействия. Вопрос осложняется тем, что, хотя на данный момент основной связкой при построении систем является Intel-Linux-Apache-PHP-Oracle, все же стек языков и технологий, используемых (или использовавшихся) для построения приложений на предприятии чрезвычайно широк (ASP, Perl, Cobol, Delphi, C++, FoxPro,...). До недавнего времени решение было в организации обмена на уровне данных. Поскольку в качестве RDBMS преимущественно применялись продукты Oracle, то при необходимости доступа к данным соседней ИС требовалась организация db_link'а к удаленной БД, либо периодическая синхронизация хранилищ. Но, естественно, что доступность хранилища системы и доступность результатов ее работы – это совершенно разные вещи, и зачастую разработчикам одной ИС при такой “интеграции” требовалось заново реализовывать функциональность сторонней системы по работе с данными (вникать в структуру таблиц, повторять алгоритмы обработки данных), т.е., фактически делать частичный реинжиниринг. При появлении все большего количества систем, нуждающихся в одних и тех же данных (в основном нормативно-справочного характера – НСИ), решение с копированием порождало проблему поддержания актуальности в реальном времени, а организация большого количества db_link'ов отрицательно сказывалось на производительности сервера-хранилища НСИ.
104
PHP Inside №13
PHP::SOAP и Xforms
Проанализировав методы решения подобных проблем другими производителями ПО, подразделение ОАО “АВТОВАЗ”, отвечающее за стандартизацию веб-разработок, обращает внимание на стек спецификаций Web Services Activity от W3C. К тому же данные технологии уже довольно активно используются такими крупными вендорами, как, например, IBM, Oracle и Microsoft, и, следовательно, последующую интеграцию собственных разработок с готовыми покупными решениями, возможно, будет провести с минимальными финансовыми затратами. Таким образом, было принято решение о развитии направления развертывания веб-служб как основы для внутрикорпоративной информационной архитектуры. Появление в PHP5 расширения для обработки SOAP-сообщений кажется нам существенным шагом в развитии языка. К тому же появилась возможность, не переучивая разработчиков, разрабатывать интерфейсы систем в новом формате. Веб-службы, изначально являющиеся надстройками над основной бизнес-логикой систем, существенно облегчают машинное взаимодействие. Протокол SOAP (с помощью которого происходит общение) позволяет осуществлять взаимодействие с высоким уровнем абстракции в гетерогенных системах. Но, в отличие от «стандартных» веб-приложений, понимающих обычные POST- и GET-запросы, приходящие прямо от браузеров пользователей (в виде данных веб-форм, либо гиперссылок), бизнес-функция информационной системы оформленная в виде вебслужбы лишена такой возможности, поскольку в качестве входных и выходных сообщений в SOAP-запросах используются XML-документы. Поэтому общение конечного пользователя с таким приложением может осуществляться только через промежуточный модуль, осуществляющий трансформацию данных, пришедших, например, из формы в SOAP-сообщение и обратно. Технология XForms, предлагаемая в качестве рекомендации W3C, является новым поколением веб-форм и является одним из модулей XHTML – более строгого и гибкого приемника HTML. Одно из основных преимуществ Xorms – в том, что Xforms-клиент отправляет и получает данные как XML-документы. Эта функциональность исключительно полезна в связке с вебслужбами, где, как было отмечено выше, в качестве сообщений также используется XML. Таким образом можно исключить промежуточный модуль преобразования запросов, поскольку XForms-браузер уже является клиентом, способным общаться с веб-службой напрямую.
105
PHP Inside №13
PHP::SOAP и Xforms
Рис. Применение технологии XForms в схеме взаимодействия с веб-службами. Поставщик веб-службы (ИС1), предоставляя веб-сервис, опубликовывает его описание в виде WSDL-файла. Все потребители веб-службы могут получить данное описание, отправив GET-запрос. Целевой URL для подачи запроса может быть получен потребителем либо из реестра сервисов UDDI (для этого поставщик должен сначала зарегистрировать свой сервис в реестре), либо по прямой ссылке, предоставленной, скажем, службой поддержки системы-поставщика. SOAP-процессор способен преобразовывать сообщения из формата XML(SOAP) в объекты языка, поэтому разработчикам системы-потребителя (ИС2) нет необходимости знать правила преобразования данных в SOAP-сообщения и обратно и даже термин «XML» в целом (но, как правило, это не так). Основная особенность при взаимодействии в веб-службами с помощью XForms – это то, что в этом случае XML-документы отправляются «as is». XForms-процессор не умеет читать WSDLописание и формировать SOAP-конверты автоматически, поэтому автору формы необходимо самостоятельно составлять запросы и разбирать ответы веб-служб, что на самом деле не такая сложная задача, как будет показано далее на примере. Единственное, что XForms может «позаимствовать» у WSDL-описания, это типы данных входных и выходных сообщений веб-службы для валидации, а это не так мало.
106
PHP Inside №13
PHP::SOAP и Xforms
Но рассмотрим все по порядку. Формы – это важный механизм сбора информации. Поэтому онлайновые формы - основная часть, составляющая электронный бизнес и предоставление онлайн сервисов. Наиболее широко используемая технология веб-форм на сегодня – HTML-формы. Однако HTML-формы очень просты и не удовлетворяют всех потребностей, возникающих у авторов онлайн форм.
HTML-формы Преимущества HTML-форм: •
Малое время доставки данных и ответа через Интернет;
•
Легкость внесения изменений в содержание или макет формы;
•
Отсутствие необходимости повторно набирать данные формы;
•
Не требуется установка проприетарного ПО для поддержки форм, поскольку стандарт HTML очень широко поддерживается;
Легкость конструирования элементов формы внутри HTMLдокумента, используя стандартный набор тегов элементов управления.
Ограничения HTML-форм: •
Элемент управления формы может иметь лишь заранее заданное или введенное значение, из чего вытекает следующее:
- Отсутствие доступа к значениям элементов формы (такой, как чтение и запись, например), кроме отправки всей формы по назначенному адресу. - Проверка данных невозможна. - Вычисляемые результаты не могу быть сгенерированы формой. - Заранее заданные значения должны быть жестко прописаны в HTML-документе. •
Форма строго привязана к HTML-странице.
- Сложно повторно использовать форму на различных устройствах (иногда даже на различных веб-браузерах). - Содержание формы зависит от представления и макета страницы. •
Очень простая модель данных (параметр-значение). - Нет поддержки структурированных данных. - Маппинг из и в XML и системы может быть достаточно сло-
жен. Для преодоления этих ограничений в HTML-формах, применяются различные техники. Большинство подходов делятся на две группы: скриптинг на стороне клиента и серверные решения.
107
PHP Inside №13
PHP::SOAP и Xforms
Скриптинг на стороне клиента Со временем веб-браузеры обзавелись поддержкой скриптинга, используя такие языки, как JavaScript или VBScript. При таком подходе возможно включать функции языка непосредственно в HTML-страницу. Внедренный код будет работать в веб-браузере и с помощью интегрированной системы событий взаимодействовать с элементами управления форм. Это решение «в лоб» позволяет, однако, изменять значения, вычислять математические функции или режим представления элементов формы. Применение скриптинга не ограничивается только формами – можно менять структуру всего HTML-документа через его объектную модель (DOM: Document Object Model). Скриптинг расширяет функциональность и увеличивает способность документа взаимодействовать с пользователем, что делает его одним из самых часто используемых решений.
Преимущества скриптинга на стороне клиента: •
Повышает уровень взаимодействия между пользователем, формой и веб-браузером;
•
Пользователь и браузер порождают события, которые могут быть отслежены и обработаны;
•
Дает возможность проверки данных «на-лету»;
•
Возможность менять значения элементов формы извне.
Ограничения: •
Сложность HTML-форм многократно увеличивается;
•
Поддерживать HTML-документ становится сложнее;
•
Ограниченная переносимость, обусловленная различиями браузеров в части поддержки скриптовых языков;
•
Возможность выполнения встроенных программ может быть отключена пользователем;
•
Все-таки ограниченная функциональность (отсутствует, например, возможность сохранения данных на диск).
Решения на стороне сервера Другой путь преодоления ограничений HTML-форм – в использовании серверных приложений. Существует большое количество технологий, которые могут быть использованы: PHP, ASP, JSP, CGI… Эти приложения конструируют стандартные HTML-формы «на-лету». Они могут динамически формировать содержание, исходя из данных, полученных от пользователя (например, из предыдущей формы). Преимущества server-side решений: •
Способность динамической генерации HTML-документов.
•
Валидация пользовательских данных на сервере.
108
PHP Inside №13 •
PHP::SOAP и Xforms
Обработчиком данных формы может быть система управления или СУБД.
Ограничения: •
Пользователь должен быть онлайн. Реализация локальной формы невозможна.
•
Избыточное количество запросов на сервер, например, для валидации, данные сначала должны быть переданы на сервер.
•
Большие задержки, т.е. время задержки, зависящее от соединения и способностей сервера.
•
Повышенные требования к аппаратному обеспечению сервера.
•
Удар по производительности сервера.
Комбинация обоих решений Третий путь расширения способностей HTML-форм – в использовании комбинации серверных и клиентских техник. Хотя это не исключает вышеописанных помех серверных решений, но позволяет дополнительно расширить функциональность за счет скриптинга.
Проприетарные технологии и средства HTML-формы – не единственный способ представления формы в веб. Существует минимум еще две основных компании, которые предлагают свои средства разработки форм: •
Семейство продуктов Acrobat от Adobe – это широко используемая технология создания и обработки форм. Acrobat-форма сохраняется в файле формата PDF (Portable Document Format). Пользователь может загрузить такой файл, заполнить форму и отправить назад на сервер. В последних версиях Acrobat позволяет формировать данные формы как XML-структуры.
•
Майкрософт (Microsoft) выпустила новый продукт, названный InfoPath, который поставляется с новой версией офисного пакета Microsoft Office 2003. Он содержит средства для визуального создания форм, основанных на XML, которые могут быть опубликованы для просмотра веб-браузером.
Обзор преимущества Xforms Стандарт XForms адресован устранить недостатки серверных и клиентских решений, описанных выше. Он позволяет создавать динамичные и функциональные формы с использованием открытых стандартов, которые легко реализовать и поддерживать. XForms базируются на XML, что обеспечивает бесшовную интеграцию в другие XML-стандарты. Этот раздел описывает ключевые преимущества этого стандарта. •
Открытый и непроприетарный стандарт (стандартизован W3C):
109
PHP Inside №13
PHP::SOAP и Xforms
- Интероперабельности способствует тот факт, что основные вендоры участвуют в процессе на базе взаимного согласия. •
Полностью описывается с помощью XML:
- Природа Xforms позволяет использование расширяемого и ясного синтаксиса XML. Слияние со смежными технологиями, такими как XHTML, XPath или XML-Events, совершенно прозрачно; •
Разделяет содержание, называемое Моделью (XForms Model), пользовательские (входные и выходные) данные, называемые Экземплярами данных (Instanse data) и уровень представления, называемый Интерфейс пользователя (Xforms UI). - Модель содержит:
* XML-описание структуры, содержащей данные, управляемые элементами управления формы; * Инструкции отправки данных для взаимодействия с приложениями; * Функциональные компоненты, которые могут проверять данные, манипулировать ими и влиять на поведение формы в целом; - Элементы управления (XForms Controls) предназначены для отображения конечному пользователю; •
Модель XForms может быть многократно использована с различными вариантами пользовательских интерфейсов и устройств.
- Благодаря слабой связанности Модели и Пользовательского интерфейса, можно получить одинаковое поведение формы в различных средах отображения (например, в XHTML или SVG); •
Встроенная возможность валидации данных, использующая типизацию WXS и дополнительные условия пользователя (включая условные зависимости), сокращает потребность в скриптинге и дополнительных серверных компонентах;
- XForms может использовать Xpath для манипуляции булевыми, строковыми или числовыми значениями; •
Расширенный обмен информацией между клиентом (веб-браузером) и сервером.
- XForms определяет Протоколы передачи (Submission Protocols), которые позволяют клиенту отправлять и принимать экземпляры данных в фоновом режиме без перезагрузки всей формы. Для примера, возможна смена элементов выпадающего списка без перезагрузки всей страницы. •
Поддержка режима взаимодействия оффлайн;
•
Данные хранятся в XML-структуре: - Легко интегрируются с приложениями, поддерживающими
XML; - Данными можно обмениваться с другими non-XForms системами, например, Acrobat forms; •
Повышает доступность документов: - Доступны горячие клавиши; 110
PHP Inside №13
PHP::SOAP и Xforms
- Элементы управления формы содержат встроенные метаданные, которые можно использовать для устройств с ограниченными характеристиками (голосовые и брайль-браузеры);
Техническое описание Xforms Как говорится в спецификации XForms: «XForms – это XML-приложение, которое представляет следующее поколение форм для веб. Разделяя традиционные формы HTML на три части: Модель, Экземпляры данных и Пользовательский интерфейс, – позволяет отделять контент от представления, повышает повторную используемость кода, дает строгую типизацию, сокращает количество обращений к серверу, снижает надобность в скриптинге и делает формы независимыми от устройств отображения».
Общие положения Для создания формы в HTML странице, стандарт HTML описывает синтаксис элементов формы, одновременно определяя внешний вид, т.е. в форме HTML это одновременно и структура данных, и слой пользовательского интерфейса с такими элементами, как таблицы, параграфы или переносы строк. В отличие от HTML, XForms – не одиночная технология, а базирующаяся на других (базовых) технологиях разметки, например, XHTML или SVG. Эти базовые технологии (или любая другая XML-технология) формируют базу для документа XForms. Благодаря XML-природе, они могут реализовать и использовать синтаксис, определенный в стандарте XForms. Для того, чтобы создать форму XForms, можно использовать в качестве базового, например, XHTML-документ, и определить пользовательский интерфейс путем встраивания элементов управления XForms в XHTML <body>. Каждый используемый элемент управления может ссылаться на т.н. экземпляр данных, который является аккумулятором XMLэлементов. Элементы в экземпляре данных могут быть использованы для хранения как значений элементов управления, так и предопределенных заранее значений, например, для выпадающих списков либо результатов вычислений. Эта ссылка на элементе управления – всего лишь связь между пользовательским интерфейсом и контентом формы. В зависимости от конкретного назначения элемента управления он может отображать значение по ссылке и/или изменять его. Внешний вид элементов формы зависит от базовой (хост) разметки и XForms-процессора, который используется для отображения XForms-документа. XForms-процессор – это программа, понимающая синтаксис XForms и базовой разметки, которую он использует для отображения формы пользователю, т.е. соответственно клиентское ПО, веб-браузер, например.
111
PHP Inside №13
PHP::SOAP и Xforms
Экземпляр данных (instance elements) – это часть контента и функционального слоя Xforms-документа, также называемая Модель. Помимо экземпляров данных, Модель определяет характеристики и требования к элементам этих данных, т.е. типы, необходимость присутствия или диапазон значений. Все это описывается с помощью т.н. «привязок» (bind elements). Они также могут содержать инструкции вычисления выражений или определения отношений между двумя или более элементами экземпляра данных, т.е. назначения зависимостей. Для примера, элемент, хранящий номер кредитной карты только тогда требует значение, если другой элемент, определяющий тип оплаты, установлен в «credit card». Другая часть Модели XForms – элементы отправки данных (submission elements), которые определяют, данные какого экземпляра и куда отправляются, например, локальный файл на диске, веб-сервер или приложение. В зависимости от выбранного протокола, элемент отправки данных также может быть настроен на прием данных для экземпляра. Отправка может активироваться либо действиями пользователя, либо другими событиями, происходящими в фоном режиме (в момент, когда вводятся данные, например). Элементы отправки очень гибки и определяют взаимодействие между самой формой и ее окружением. XForms-модель также может также содержать элементы действий (action elements), которые способны отслеживать события, порождаемые пользователем, элементами управления, XForms-процессором или агентом, и обрабатывать их определенным образом. Например, если пользователь закрывает приложение, которое обслуживает форму, возникнет событие, которое может породить процесс сохранения введенных данных на диск перед закрытием, и в следующий раз, когда пользователь снова откроет документ с формой, другой обработчик событий загрузит сохраненные перед этим данные, тем самым восстановив состояние на момент закрытия.
Разделение контента и представления Для разделения содержания и представления стандарт XForms описывает 3 логические части: Модель, Экзепляр данных и Элементы управления. В следующих разделах поясняется, что они собой представляют и как взаимодействуют. Для описания всех частей используется синтаксис XML.
Модель Xforms Модель определяет функциональную часть формы, т.е. как форма будет себя вести и взаимодействовать с пользователем, описывается контейнером <model>, который в свою очередь содержит различные вложенные элементы, такие как:
Instance Этот элемент ссылается на документ Экземпляра данных, как показано на листинге 2, или непосредственно содержит его XMLструктуру. Модель может содержать несколько элементов <instance>, каждому из которых присваивается уникальный ID.
112
PHP Inside №13
PHP::SOAP и Xforms
Bind Элементы <bind> создают связь между элементами экземпляров данных и функциями или событиями. Они также могут контролировать поведение формы путем определения требований для значений. Выражения привязки могут использовать предопределенные атрибуты и функции XForms, а также и другие XML-технологии, например, XPath или XML-events. XPath, кроме всего прочего, содержит функции, позволяющие манипулировать со строками, булевыми и числовыми значениями. События XML (XML-events) используются для доступа к информации о воздействиях пользователя на объектную модель документа (DOM), например, события мыши или клавиатуры.
Submission Элемент <submission> определяет, как форма взаимодействует с окружением. Целью может быть удаленная система, локальный файл или что-либо другое. В XForms описываются протоколы коммуникаций, которые позволяют обмениваться XML-данными; только экземпляры данных XForms могут использоваться для обмена. Элемент <submission> уточняет, какая часть экземпляра данных будет отправлена или получена. Модель XForms может содержать несколько элементов отправки данных, которые могут быть активированы множеством событий, например, когда пользователь нажал кнопку «отправить», или если форма уничтожается закрытием веббраузера.
Action. Элементы <action> используются для перехвата событий и определения действий по их обработке. Они размещаются внутри элементов управления формы, отправки данных и элементов модели XForms. В последнем случае они применяются для обработки событий модели (примеры таких событий: "xforms-model-initialize", "xforms-model-destruct" и "xforms-reset"). Пример. Вышеописанные элементы могут быть оценены в листинге 1 с комментариями: •
элемент <model> содержит атрибут, указывающий на XML Schema для экземпляра данных.
•
элемент <instance> ссылается на XML-документ, содержащий данные (приведен в листинге 2).
•
элемент <bind> назначен на элемент экземпляра данных "/data/sum/". Он (bind) вычисляет произведение значений элементов "/data/amount" и "/data/price/" и помещает результат в элемент "/data/sum".
•
элемент <submission> определяет, какой экземпляр данных будет отправлен на приложение "order.php", используя протокол POST.
113
PHP Inside №13 •
PHP::SOAP и Xforms
элемент <action> перехватывает событие "xforms-model-destruct", которое возникает, если модель уничтожается, закрытием веббраузера, например. Вложенный элемент <send> активизирует отправку данных, описанную в элементе <submission> с id="submit_1". <xforms:model schema="instanceDataSchema.xsd"> <xforms:instance src="instanceData.xml"/> <xforms:bind nodeset="/data/sum" calculate="/data/amount/ * /data/price" /> <xforms:submission id="submit_1" action="http://example.com/order.asp" method="post" /> <xforms:action ev:event="xforms-model-destruct"> <xforms:send submission="submit_1" /> </xforms:action> </xforms:model>
Пример Модели Xforms
Экземпляр данных Т.н. Экземпляр данных – это XML-структура, содержащая текущие значения элементов управления формы. Она также может описывать элементы, которые будут содержать вычисляемые или временные значения в процессе работы с формой. Как видно из листинга 2, экземпляр данных – это обыкновенный XML-документ. Для придания данным большей осмысленности можно использовать описание XML Schema, которое определяет природу отдельных элементов экземпляра данных, т.е. какой тип значения элемент может содержать (integer, string, date, и т.п.). Эта информация далее может использоваться для валидации данных, введенных пользователем. <?xml version="1.0" encoding="UTF-8"?> <data xmlns="myNamespace"> <amount/> <quantity/> <sum/> </data>
XML-документ экземпляра данных Для использования экземпляра данных как хранилища значений элементов управления последние должны содержать ссылку на соответствующий элемент экземпляра данных. В листинге 3 элементы <input> и <output> содержат атрибут “ref”, предназначенный именно для этой цели, и в данном случае целью служит элемент "/data/amount". Значения, вводимые в поле ввода (<input>), сохраняются в этом элементе и одновременно отображаются в поле вывода (<output>).
114
PHP Inside №13
PHP::SOAP и Xforms
Элементы управления формы Спецификация XForms описывает набор элементов управления для построения пользовательского интерфейса формы. Примерами таких элементов служат поля ввода (input fields), поля вывода (output fields), кнопки (buttons) и опции (options). Для отображения элементов управления поддержка XForms может быть реализована в других XML-технологиях, которые нацелены на отображение документов (примерами такой базовой разметки могут служить XHTML или SVG). Следующий код показывает использование элементов XForms в XHTML-документе. Веб-браузер, поддерживающий XForms, отобразить элемент "<xforms:input>" как поле для ввода, а на месте "<xforms:output>" будет лишь значение. <table> <tr><td> <xforms:input ref="/data/amount"> <xforms:label>Enter a number:</xforms:label> </xforms:input> </td></tr> <tr><td> <h3>You entered: <xforms:output ref="/data/amount"/> </h3> </td></tr> </table>
Элементы управления XForms внутри XHTML-документа.
Полноценный XForms-документ. Три логические части: Модель, Экземпляр данных, Пользовательский интерфейс отделяют содержание от представления формы. Этот раздел объясняет, как эти три части работают вместе и образуют функциональную форму.
Изображение отношений в XForms-документе.
115
PHP Inside №13
PHP::SOAP и Xforms
Существующие средства Приложения, которые поддерживают XForms, становятся все более доступными. С тех пор, как спецификация стала официальной рекомендацией, это развитие заметно ускорилось. На данный момент можно разделить все их множество на процессоры и редакторы.
XForms процессоры Приложения, которые могут обрабатывать модель Xforms. Элементы управления представляются, используя соответствующую технологию отображения. Далее пользователь способен вводить информацию и отправить форму. В свою очередь процессоры могут быть нескольких различных типов: •
Клиентские приложения – полностью автономные, устанавливаются на стороне клиента. Фактически это веб-браузеры, обладающие полноценной поддержкой XForms на уровне ядра. К примеру, XSmiles или Nowell Technology Preview.
•
Плагины веб-браузеров. С минимальным процессом установки на клиенте плагин может обрабатывать Xforms посредством обычного веб-браузера, такого как Internet Explorer6.0. FormsPlayer, Xero или плагин IBM’s XML Forms Package – процессоры, работающие только на IE6.0. Более общий подход используется DENG-процессор, реализованный на Flash-технологии, и следовательно, может быть использован везде, где установлен Flash-плеер.
•
Серверные решения. Эти приложения обрабатывают XForms-документы на стороне сервера и предоставляют стандартный HTML+Javascript код, который может быть обработан любым обычным веб-браузером. Примерами могут служить IBM XML Forms Package, FormFaces.
Все три способа обработки Xforms-документов изображены ниже. Единственное, что хочется отметить, что сам документ составляется однажды независимо от выбранного подхода в обработке.
Различные типы приложений, поддерживающие XForms.
116
PHP Inside №13
PHP::SOAP и Xforms
Средства разработки Несмотря на то, что формат XForms-документа подчиняется синтаксису XML, создание полноценной формы может быть неэффективно в текстовом редакторе. На данный момент представлено не так много средств разработки, ниже перечислены основные из них: •
Xero – плагин к веб-браузеру, является частью «Web form development framework», где имеется также встроенный редактор.
•
Еще одно средство от компании «HoT Training and Consultancy Ltd».
•
В OpenOffice2.0 имеется возможность составлять документы XMLForm, используются стандартные теги XForms для разметки модели, но собственные для элементов интерфейса.
•
XFormation – коммерческая IDE от создателей FormsPlayer.
•
XML-редакторы общего назначения, но достаточно мощные, чтобы облегчить создание – XMLSpy и StylusStudio. Пример поисковой формы.
После освещения материальной части рассмотрим конкретный пример построения простейшей формы для работы с веб-службой поиска данных пользователя по имени.
Веб-служба Основной задачей нашей службы является трансляция сообщений клиента в DML-запросы хранилища. Бессмысленно описывать алгоритмы методов самого php-класса на который будут проецироваться операции веб-службы – они элементарны, поэтому ограничимся только описанием входного и выходного сообщений, которые будут приведены далее в примерах.
Форма Начнем с создания простейшей формы. Форма состоит из Модели и Пользовательского интерфейса. В простейшем случае Модель содержит Экземпляры данных <instance> и элементы для их отправки <submission>. <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:ns1="urn: example.com:users/binding" xmlns:xsd1="urn:example.com:users.xsd" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xml:lang="ru" lang="ru"> <head> <title>Пример 1</title> <!-- подключение плагина -->
117
PHP Inside №13
PHP::SOAP и Xforms
<object id="FormsPlayer" classid="CLSID:4D0ABA11-C5F0-4478-991A-375C4B648F58"> <b>плагин FormsPlayer (http://www.formsplayer.com) не загружен (работает только с I E6.0_SP1+) </b> </object> <?import namespace="xforms" implementation="#FormsPlayer" ?> <xforms:model id="users_ model"> <xforms:instance id="message"> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns2="urn: example.com:users /binding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd1="urn:example.com:users.xsd" xmlns="" > <SOAP-ENV:Body> <ns2:proceed> <actionsRequest> <Search> <uname/> </Search> </actionsRequest> </ns2:proceed> </SOAP-ENV:Body> </SOAP-ENV:Envelope> </xforms:instance> <xforms:submission id="real_ws" method="post" action="http://example.com/ws.php" replace="instance" ref="instance('message')" /> </xforms:model> </head> <body/> </html>
Встраивание Модели XForms в хост-документ.
В данном случае в качестве базового используется XHTMLдокумент. Декларируем пространства имен словарей, используемых в данном файле. Вторым этапом сообщаем Internet Explorer о необходимости загрузить объект FormsPlayer, в случае неудачи пользователю будет выведено содержимое элемента <object>. Инструкция обработки (PI) также необходима для указания префикса, используемого элементами XForms. Далее мы описываем элементы <instance> и <submission> в Модели нашей формы. Здесь экземпляр данных “message” – это не только данные для отображения пользователю, но и для отправки на сервер, а так как принимающей стороной является веб-служба, то в этом экземпляре содержится точная копия ее входного SOAP-сообщения (которое после передачи данных заменится на ответ службы). Сам элемент submission мы рассмотрим позже, когда дойдем до отправки данных. Пользовательский интерфейс формы располагается в секции <body>:
118
PHP Inside №13
PHP::SOAP и Xforms
... <body> <xforms:input ref="instance('message')//uname"> <xforms:label>Логин: </xforms:label> <xforms:hint>Имя пользователя системы, регистр не имеет значения</xforms:hint> </xforms:input> </body> ...
Стандартный элемент пользовательского интерфейса формы - поле ввода.
Элемент <xforms:input> предоставляет поле для ввода параметра поискового запроса (имя пользователя). Введенное в это поле значение будет автоматически изменять содержимое элемента экземпляра данных, к которому оно привязано (в данном случае uname). Поэтому, если мы введем имя пользователя, к примеру, “vpupkin”, то содержащийся в памяти инстанс будет выглядеть следующим образом: <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns2="urn:example.com:users/binding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd1="urn:example.com:users.xsd" xmlns=""> <SOAP-ENV:Body> <ns2:proceed> <actionsRequest> <Search> <uname>vpupkin</uname> </Search> </actionsRequest> </ns2:proceed> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
SOAP-запрос с данными пользователя И именно такой документ будет отправлен в качестве запроса на веб-службу.
Смена состояния Когда пользователь отправляет данные, отправляемый экземпляр замещается ответом веб-службы, имеющим совершенно другую структуру, таким образом, чтобы отобразить эту новую информацию, нам нужен элемент управления, базирующийся на новой структуре ответа. Но размещение нового элемента на одном экране с уже существующим приведет к сообщениям об ошибках, потому что один и них будет постоянно ссылаться на несуществующий элемент в экземпляре данных. Для того чтобы избежать этого, нужно создать условия, в которых только один набор элементов управления присутствовал бы на странице, или/и чтобы элементы привязывались к различным экземплярам данных.
119
PHP Inside №13
PHP::SOAP и Xforms
Воспользуемся конструкцией switch/case для разделения пользовательского интерфейса на экраны, переключая их в момент отправки запроса. Заключим каждый набор элементов управления в отдельный <case> и добавим триггеры для переключения: ... <body> <xforms:switch id=”mysearchform”> <xforms:case id=”search_UI”> <xforms:input ref="instance('message')//uname"> <xforms:label>Логин: </xforms:label> <xforms:hint>Регистр не имеет значения</xforms:hint> </xforms:input> <xforms:trigger> <xforms:label>К результатам</xforms:label> <xforms:action ev:event="DOMActivate"> <xforms:toggle case="result_UI"/> </xforms:action> </xforms:trigger> </xforms:case> <xforms:case id=”result_UI”> Здесь будут результаты <xforms:trigger> <xforms:label>К форме поиска</xforms:label> <xforms:action ev:event="DOMActivate"> <xforms:toggle case="search_UI"/> </xforms:action> </xforms:trigger> </xforms:case> </xforms:switch> </body> ...
Организация многоэкранной формы Выше представлена двухэкранная форма. Только один элемент <xforms:case> может быть активным в любой момент времени (при загрузке активным делается первый). Для переключения между экранами добавим кнопки <xforms:trigger>. Когда происходит нажатие кнопки, выполняются все действия, находящиеся в элементе <xforms:action> (в нашем случае это переключение на другой экран с помощью <xforms:toggle>). Но этого еще не достаточно, потому что до переключения к экрану с результатом нужно этот результат получить, т.е. инициировать отправку данных и получить ответ. ... <xforms:trigger> <xforms:label>К результатам</xforms:label> <xforms:action ev:event="DOMActivate"> <xforms:send submission="real_ws" /> <xforms:toggle case="result_GUI" /> </xforms:action> </xforms:trigger> ...
120
PHP Inside №13
PHP::SOAP и Xforms
Действие активизации отправки данных Эта инструкция говорит активировать отправку данных с @id=”real_ws” (который мы ранее уже описали). Рассмотрим некоторые свойства <xforms:submission> подробнее: ... <xforms:submission id="real_ws" method="post" action="http://example.com/ws.php" replace="instance" ref="instance('message')" /> ...
Элемент описания отправки данных Кроме описаний, какие данные (@ref), куда (@action) и каким методом (@method) отправлять, есть еще уточняющие атрибуты, как в нашем случае @replace=”instance”, говорящий, что нужно перегрузить лишь экземпляр данных, а не весь документ. Ответ веб-службы (замещающий данные запроса) в нашем случае будет выглядеть примерно так: <?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:example.com:users/binding" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns2="urn:example.com:users.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Body> <ns1:proceedUSERSResponse> <actionsResponse xsi:type="ns2:USERSResponseType"> <USERSResult xsi:type="ns2:USERSResultType"> <rowset xsi:type="ns2:USERSRowsetType"> <rows xsi:type="ns2:USERSrowsetRowsType"> <row xsi:type="ns2:USERSRowType"> <uid xsi:type="ns2:uidType">10090</uid> <uname xsi:type="xsd:string">vpupkin</uname> <gid xsi:type=”xsd:gidType”>2000</gid> </row> <row xsi:type="ns2:USERSRowType"> <uid xsi:type="ns2:uidType">10091</uid> <uname xsi:type="xsd:string">vpupkin2</uname> <gid xsi:type=”xsd:gidType”>2010</gid> </row> <row xsi:type="ns2:USERSRowType"> <uid xsi:type="ns2:uidType">10092</uid> <uname xsi:type="xsd:string">vpupkin_ftp</uname> <gid xsi:type=”xsd:gidType”>2020</gid> </row> </rows> <meta xsi:type="ns2:rowsetMetaType"> <id xsi:type="xsd:string">resID1</id> <current_page xsi:type=”xsd:integer”>1</current_page> <total_count xsi:type=”xsd:integer”>3</total_count> <rows_per_page xsi:type=”xsd:integer”>25</rows_per_page> </meta>
121
PHP Inside №13
PHP::SOAP и Xforms
</rowset> </USERSResult> </actionsResponse> </ns1:proceedUSERSResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
SOAP-ответ веб-службы В отличие от обычных форм, в спецификации XForms существует элемент управления <xforms:repeat>, специально предназначенный для вывода повторяющихся фрагментов разметки. Так будет выглядеть разметка для вывода нашего ответа веб-службы в виде таблицы: <xforms:case id=”result_UI”> <xforms:repeat id=”users_table” nodeset=”instance('message')//row” appearance=”compact”> <xforms:output ref=”uid”> <xforms:label>Идентификатор</xforms:label> </xforms:output> <xforms:output ref=”uname”> <xforms:label>Имя пользователя</xforms:label> </xforms:output> <xforms:output ref=”gid”> <xforms:label>Группа</xforms:label> </xforms:output> </xforms:repeat> <p>Текущая позиция <xforms:output value="index ('users_table')"/></p> </xforms:case>
Вывод повторяющихся данных в виде таблицы Весь код, содержащийся в <xforms:repeat>, будет выведен столько раз, сколько узлов выберет Xpath-выражение в атрибуте @nodeset (в данном примере выбираются все элементы row внутри экземпляра “message”). Причем каждый раз контекстным узлом для вложенных элементов является очередной элемент из набора, т.е. при каждой итерации элемент <xforms:output> будет отображать содержимое вложенного элемента <uname> обрабатываемого узла <row>. Хотелось бы обратить внимание, что информации о том, что результатам вывода будет именно таблица, нигде не наблюдается, обусловлено это тем, что одной из основных целей XForms является высокая доступность документа, поэтому внешний вид элементов интерфейса определяется исключительно реализацией стандарта на конкретном устройстве. Разработчику документа дозволено лишь рекомендовать внешний вид элемента управления назначением атрибуту @appearance одного из значений “minimal”, “compact” или “full” (хотя, естественно, даже это не предполагает четкого определения внешнего вида).
122
PHP Inside №13
PHP::SOAP и Xforms
Таким образом, в используемой нами имплементации (FormsPlayer) указание @appearance=”compact” в <xforms:repeat> дает желаемый эффект – данные выводятся в виде таблицы. Причем кликом на какую-либо строку списка можно сделать ее контекстной, и специальная функция расширения index() вернет ее позицию, и все это без единой строчки скрипта!
. Форма уже вполне работоспособна и может сама отправить параметры поиска веб-службе, получить ответ и отобразить его на экране. Но что произойдет, если пользователь захочет осуществить повторный поиск? Переключение на экран с формой поиска приведет к ошибке, поскольку исходного экземпляра данных уже нет (помните? он замещен ответом веб-службы). Единственный выход – перезагрузка всего документа в первоначально состояние. Хотя этого можно избежать, используя привязку элементов управления поиска и отображения к различным экземплярам данных. Причем один из экземпляров («message») по-прежнему будет служить «контейнером-обменником» с веб-службой и хранилищем результата, а элементы формы поиска, содержащие данные, введенные пользователем, будут привязаны к другому экземпляру (назовем его «to_search»). Исходя из этого, нам потребуется “сконструировать” экземпляр данных «message» перед отправкой запроса для того, чтобы он содержал параметры поиска из экземпляра «to_search». Итак, начнем переделывать модель: ... <xforms:model id="users_proceed_model"> <xforms:instance id="message"> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns2="urn:example.com:users/binding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd1="urn:example.com:users.xsd" xmlns="" > <SOAP-ENV:Body> <ns2:proceed> <actionsRequest> <Search/> </actionsRequest> </ns2:proceed> </SOAP-ENV:Body> </SOAP-ENV:Envelope> </xforms:instance> <!-добавляем новый экземпляр to_search для хранения данных поискового запроса -->
123
PHP Inside №13
PHP::SOAP и Xforms
<xforms:instance id=”to_search”> <item xmlns=””> <uname/> </item> </xforms:instance>
<!-и перепривязываем поле поиска --> <xforms:input ref="instance('to_search')//uname"> <xforms:label>Логин: </xforms:label> <xforms:hint>Регистр не имеет значения</xforms:hint> </xforms:input>
Декомпозиция экземпляров данных Заметьте, что, поскольку экземпляры данных больше не конфликтуют между собой, то необходимость в применении конструкции switch/case/toggle именно для устранения этой проблемы отпадает, и все элементы могут быть активны одновременно (вы, естественно, можете реализовывать многоэкранность в сложных формах, с целью повышения удобства использования). Теперь приступим к «конструированию». Модифицируем кнопку отправки данных, добавляя дополнительно необходимые действия: <xforms:trigger> <!-новая кнопка теперь будет называться по-новому --> <xforms:label>Искать</xforms:label> <xforms:action ev:event="DOMActivate"> <xforms:setvalue ref=”instance('message')//Search” value=”instance('to_search')//item” /> <xforms:send submission="real_ws" /> </xforms:action> </xforms:trigger>
Конструирование SOAP-запроса Добавив действие <xforms:setvalue> перед отправкой данных, мы производим операцию присваивания: элементу из атрибута @ref присваивается значение атрибута @value (не сами значения атрибутов, а элементы, адресуемые выражениями XPath, причем адресовать можно не только скалярное значение, но и любой узел документа). Итак, имеем второй вариант поисковой формы:
124
PHP Inside №13
PHP::SOAP и Xforms
Но проблема повторного поиска без перезагрузки формы еще не решена, т.к. после первого же поиска в экземпляре «message» элемент <Search/> будет отсутствовать как таковой (см. структуру ответа веб-службы), поэтому повторного копирования данных не произойдет. Для этого добавим в модель «эталонный» экземпляр данных для отправки (назовем его «USERS_soap_template»), который будем использовать в качестве “рыбы” при конструировании запроса к веб-службе и который является точной копией экземпляра «message» в первоначальном состоянии: ... <xforms:instance id=”to_search”> <item xmlns=””> <uname/> </item> </xforms:instance> <xforms:instance id="USERS_soap_template"> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns2="urn:example.com:users/binding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd1="urn:example.com:users.xsd" xmlns="" > <SOAP-ENV:Body> <ns2:proceed> <actionsRequest> <Search/> </actionsRequest> </ns2:proceed> </SOAP-ENV:Body> </SOAP-ENV:Envelope> </xforms:instance> ... <xforms:trigger> <xforms:label>Искать</xforms:label> <xforms:action ev:event="DOMActivate"> <!-приведение экземпляра message в исходное состояние --> <xforms:setvalue ref="instance('message')" value="instance('USERS_soap_template')" /> <xforms:setvalue ref="instance('message')//Search" value="instance('to_search')//item" /> <xforms:send submission="real_ws" /> </xforms:action> </xforms:trigger> ...
Восстановление исходного SOAP-сообщения из шаблона Таким образом получаем сборку SOAP-запроса при каждом нажатии пользователем кнопки «Искать».
125
PHP Inside №13
PHP::SOAP и Xforms
В дополнение хочется отметить расширяемость такого решения – можно «заготовить» несколько шаблонов сообщений к различным веб-службам и дать возможность пользователю производить поиск данных по введенным параметрам одновременно в нескольких ресурсах.
Типизация Одним из недостатков веб-форм является отсутствие какойлибо типизации вводимых данных и невозможность накладывать дополнительные сценарии поведения формы (даже тривиальная потребность автора пометить одно из полей как «обязательное для заполнения» сразу же приведет к скриптингу). Спецификация XForms решает и эту задачу. Типы для элементов в экземплярах данных можно задавать, используя словарь W3C XML Schema (WXS). XForms поддерживает большинство основных WXS-типов и добавляет несколько собственных. Причем именно типизация влияет, прежде всего, на то, как будет выглядеть «виджет», связанный с данным элементом, так, например, <xforms:input>, связанный с элементом типа xsd:boolean выглядит как «checkbox», с xsd:string – простое поле ввода, а на xsd:date многие имплементации выводят т.н. «datepicker». Более того, WXS позволяет автору схемы создавать собственные типы данных на основе уже существующих. Давайте попытаемся связать элемент <uname> нашего экземпляра “to_search” с неким типом данных (назовем его “myUnameType”). Итак, составляем схему: <?xml version="1.0" encoding="UTF-8" ?> <xsd:schema targetNamespace="urn:example.com:users.xsd" xmlns:xsd1="urn:example.com:users.xsd" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" elementFormDefault="qualified"> ... <xsd:simpleType name="myUnameType"> <xsd:restriction base="xsd:string"> <xsd:pattern value="[A-Za-z]{4,8}"/> </xsd:restriction> </xsd:simpleType>
Определение пользовательских типов данных Как видим, нашему типу соответствуют все строки, включающие только латинские буквы длиной от 4 до 8 символов. Вообще, WXS имеет мощный механизм работы с типами (это ее предназначение), но подробное описание этой технологии выходит за рамки данной статьи. Далее необходимо связать нашу схему с Xforms-документом – делается это указанием местоположения схемы в атрибуте @schema модели Xforms: <!-схема во внешнем файле. --> <xforms:model id="users_proceed_model" schema="http://example.com/wxs/USERTypes.xsd">
126
PHP Inside №13
PHP::SOAP и Xforms
Ассоциация модели XForms со схемой данных Связывание После того, как схема подключена, типы, определенные в ней, доступны для назначения элементам экземпляров данных. Существует несколько способов назначения типа, опишу наиболее, на мой взгляд, оптимальный – путем добавления элементов связывания (binding) в модель XForms: ... <xforms:model id="users_proceed_model" schema="USERTypes.xsd"> ... <xforms:bind id="uname_bind" type="xsd1:myUnameType" nodeset="instance('to_search')//uname" /> ... </xforms:model> ... <body> ... <!-элемент управления ссылается на bind, а тот, в свою очередь, уже на данные, попутно налагая какие-либо условия и/или типы. --> <xforms:input bind="uname_bind"> <xforms:label>Логин: </xforms:label> <xforms:hint>Регистр не имеет значения</xforms:hint> <xforms:alert>имя должно состоять только из латинских букв и иметь длину от 4 до 8 символов</xforms:alert> </xforms:input> ... </body>
Использование xforms:bind Введение связок позволяет создать промежуточный слой между элементами управления и экземплярами данных с добавлением дополнительных условий и типизацией. Как видим, при введении <xforms:bind> можно изменить адресацию поля ввода логина с абсолютной (непосредственно до элемента в экземпляре данных с помощью @ref или @nodeset) на относительную (по имени связки c помощью @bind). Сразу же появляется очевидная выгода – теряется зависимость интерфейса формы от структуры ее модели, потому что все элементы интерфейса не связываются с данными напрямую, и изменения структуры экземпляров данных повлечет лишь изменение адресации привязок и не затронет сам пользовательский интерфейс. Форма может реагировать на определенные действия пользователя порождением событий XML (спецификация XMLevents) и динамическим назначением CSS-псевдоклассов.
127
PHP Inside №13
PHP::SOAP и Xforms
После того, как Xforms-процессор обнаружит несоответствие вводимых данных заявленному типу, возникнет событие “xformsinvalid” (отловив которое, можно предпринять какие-либо действия, вывести сообщение, например), и элементу управления назначается псевдокласс :invalid (используя который, можно с помощью CSS оформлять “неправильные” элементы): /* подсвечиваем неправильные элементы */ *:invalid, *.invalid { background-color: red; } /* и управляем выводом сообщений alert в случае ошибки */ *:invalid>xforms|alert, *.invalid xforms\:alert { display:inline; } *:valid>xforms|alert, *.valid xforms\:alert { display:none; } /* раскраска repeat в реализации formsplayer */ .repeat-grid-header {background-color:black;color:white;text-align:center;} .repeat-item-odd {background-color:#EEEEEE;} .repeat-item-even {background-color:white;} .repeat-index {background-color:darkblue; color: white;} .repeat-grid-cell {font-style:normal;} .repeat-grid-header-cell {font-weight:bold;} .repeat-value {border:1px black solid;}
Визуальное оформление событий с помощью CSS-псевдоклассов
Сообщение об ошибке В приведенном выше фрагменте CSS на каждую группу определено по 2 селектора, что обусловлено нюансами реализации этой функциональности именно плагином FormsPlayer.
Автозапрос Введем небольшое дополнение, необходимое для того, чтобы пользователь при загрузке формы сразу увидел первую страницу результатов (иногда это удобно), а не пустую форму. Для этого нужно использовать механизм событий Xforms и конкретнее отследить и обработать событие “xforms-ready”, которое возникает после полной загрузки модели формы и отрисовки пользовательского интерфейса (ближайший аналог <body onLoad=”...”/>). Добавляем обработчик события в модель формы (как дочерний элемент <xforms:model>, поскольку именно она является целью данного события): ... </xforms:instance> <xforms:action ev:event="xforms-ready"> <xforms:setvalue ref="instance('message')" value="instance('USERS_soap_template')" /> <xforms:setvalue ref="instance('message')//Search" value="instance('to_search')//item" /> <xforms:send submission="real_ws"/> </xforms:action>
128
PHP Inside №13
PHP::SOAP и Xforms
<xforms:submission> ...
Обработчик события xforms-ready Таким образом, после внедрения этого фрагмента в модель и перезагрузки формы произойдет выполнение всех действий из контейнера <xforms:action>, связанных с событием «xforms-ready» (суть – эмуляция нажатия кнопки «отправить»).
Передача данных в «обычное» веб-приложение Хотя в этой статье технология XForms рассматривается именно в контексте взаимодействия с веб-службами, это не ограничивает ее применения. Данные можно посылать на уже существующие ресурсы, работающие, например, с методом GET. Для этого нужно лишь определить, какие параметры принимает внешнее приложение, и сформировать соответствующий запрос. Допустим «простой» скрипт «card.php» может быть вызван методом GET с параметром user, равным идентификатору пользователя, тогда дополним форму следующими элементами: … <!-- в модель --> <xforms:instance id="card"> <data xmlns=""> <user/> </data> </xforms:instance> <xforms:submission id="get_card" method="get" action="http://example.com/card.php" ref="instance('card')" separator="&amp;" /> … <!-- в пользовательский интерфейс --> <xforms:trigger> <xforms:label>Карточка пользователя</xforms:label> <xforms:action ev:event="DOMActivate"> <xforms:setvalue ref="instance('card')//user" value="instance('message')//row[position() = index('r1')]/ uid" /> <xforms:send submission="toget" /> </xforms:action> </xforms:trigger> …
Пример взаимодействия с веб-приложением Здесь идентификатор пользователя выбирается из контекстной записи в результате и копируется в макет запроса к card.php.
129
PHP Inside №13
PHP::SOAP и Xforms
Замечания по отладке При работе с XForms возникают некоторые специфические сложности, связанные с невозможностью без специального ПО просмотреть содержание того или иного экземпляра данных в любой момент времени, что несколько замедляет разработку. Поэтому далее приведены некоторые простейшие приемы, используемые авторами. Просмотр отправляемых данных может быть осуществлен путем отправки (тестовый элемент submission и триггер «тест») на скрипт, подобный приведенному ниже: <?php header('Content-type: application/xml; charset=UTF-8'); $doc = DOMDocument::loadXML($HTTP_RAW_POST_DATA); $doc->encoding="UTF-8"; echo $doc->saveXML(); ?>
Скрипт, отображающий данные, пришедшие с XForms-формы методом POST
Просмотр ответа, который попадает в экземпляр, затруднен только в том случае, когда используется передача данных без перезагрузки всей страницы, поэтому для отладки бывает полезно временно убрать атрибут @replace=”instance” из того элемента submission, возвращенное содержимое которого мы хотим посмотреть. При этом, когда отправка данных будет инициирована, форма перезагрузится и результаты, посылаемые в экземпляр, отобразятся в браузере.
Ошибка веб-службы Если веб-служба не в состоянии обработать входной запрос по каким-либо причинам, то спецификация обязывает выдать SOAPFault, причем для передачи такого сообщения назначается код 500 (Internal Server Error). В этом случае браузер с поддержкой XForms реагирует стандартным образом, даже не принимая документ, следующий за заголовком на обработку. В веб-службах собственной разработки можно определять потребителя веб-службы путем чтения заголовков (HTTP_USER_AGENT, например) и не выдавать SOAPFault с кодом 500, если потребителем является браузер. Сам же SOAP-конверт с ошибкой может быть обработан формой стандартным образом, и если к элементу <faultstring> привязан какой-либо элемент управления, то сообщение об ошибке отобразится на экране пользователя.
Типы данных SOAP XForms не поддерживает типы данных, определяемые в спецификации SOAP, такие, например, как Struct и Array, поэтому, если в экземпляр данных попадет SOAP-сообщение с такими типами, XForms-процессор сообщит об ошибке данных. Выходом может служить отказ от использования SOAP-кодирования сообщений либо описание сложных типов всех структур запросов и ответов вебслужбы.
130
PHP Inside №13
PHP::SOAP и Xforms
Заключение Веб-службы предлагают хоть и не новую, но удачно выведенную на более высокий уровень идею за счет применения интернет-технологий для реализации и транспорта, а также практически неограниченную расширяемость благодаря использованию открытых стандартов из XML-стека в качестве базы. Утверждение вебслужб и SOA как основного направления развития информационной инфраструктуры такого предприятия, как ОАО «АВТОВАЗ», с полным циклом производства и множеством сложнейших бизнес процессов, обещает упростить связывание отдельных бизнес-функций в цепочки процессов; предоставить прозрачный доступ к любой корпоративной ИС для интеграции либо агрегации, например, в портальном решении. Хочется отметить также высокую повторную используемость разработок за счет очень слабой связанности компонентов (сервисов) SOA и их высокой интероперабельности. При использовании веб-сервисов у разработчиков не будет необходимости в непосредственном обращении к хранилищам данных, что снизит требования к уровню знаний предметной области разработчиками сторонних приложений и снизит зависимость логики программы от структуры данных хранилища. Обладая достаточно большим количеством php-разработчиков, с выходом php5 информационные службы завода не потеряли возможность развиваться в направлении мейнстрима и утвердились в мнении о стабильном и правильном курсе развития языка. XForms является наиболее влиятельной спецификацией на мир электронных форм и предназначена для того, чтобы устранить множество недостатков HTML-форм, а также предоставить возможности, далеко превосходящие их. Архитектура XForms позволяет с легкостью интегрироваться с веб-документами и системами управления на основе XML. Спецификация является открытым стандартом, обещает независимость от платформы либо вендора и уже набирает популярность у разработчиков и пользователей. Доказательством широкой заинтересованности разработчиков служит внушительный список уже существующих имплементаций (Oracle, например, на одном из «воркшопов» уже продемонстрировал пример переносимости формы, когда одна и та же форма обслуживалась на ПК, мобильном телефоне и войс-сервере посредством телефона без каких-либо изменений). Со своей способностью обмениваться XML-документами XForms удачно находит свое применение в связке с веб-службами, избавляя разработчиков от написания промежуточного ПО. XForms спроектирован с разделением процессов отображения данных и манипуляции ими, что может применяться для организации нескольких последовательных обращений на сервер по одному запросу пользователя.
131
PHP Inside №13
PHP::SOAP и Xforms
В этот процесс включается создание отдельного экземпляра данных для каждой веб-службы, содержащего необходимый SOAPзапрос. И когда пользователь порождает событие (например, нажатием кнопки), вы можете выполнить любое количество действий, и, так как каждый экземпляр может иметь свой элемент < submission>, возможно просто активировать их по очереди, перемещая данные между экземплярами с помощью элемента < setvalue>. В целом технология XForms представляется очень удобной для разработки веб-клиентов (или уже целых приложений) с расширенной функциональностью, которые могут работать вообще без перезагрузки. На фоне того, что ОАО «АВТОВАЗ» проводит активное внедрение бездисковых станций для рабочих мест с фиксированной функциональностью и с выходом полноценной поддержки XForms, в очередном релизе Gecko открываются широкие перспективы в построении функциональных пользовательских рабочих сред с минимальными требованиями – SOA-клиентов.
Полезные ссылки: 1. домашняя страница Xforms http://www.w3.org/MarkUp/Forms/ 2. FAQ - http://www.w3.org/MarkUp/Forms/2003/xforms-faq.html 3. домашняя страница Web Services Activity http://www.w3.org/2002/ws/ 4. XForms for HTML Authors - http://www.w3.org/MarkUp/Forms/2003/xforms-for-html-authors.html 5. XForms conversion guideline - http://xforms.dstc.edu.au/information/xforms-conversion-guideline.pdf 6. XForms Essentials - http://xformsinstitute.com/essentials/browse/ 7. XForms: XML Powered Web Forms http://safariexamples.informit.com/0321154991/book.html#bookpa2.html 8. сайт «технологии веб-сервисов» http://www.ubs.ru/ws/ 9. Ten Favorite XForms Engines (http://xml.com/pub/a/2003/09/10/xforms.html) 10. FormsPlayer - http://www.formsplayer.com/ XSmiles - http://www.x-smiles.org/ 11. DENG - http://claus.packts.net/ 12. xslt2Xforms - http://xforms.zeninteractif.com/xhtml/index.html 13. Oracle mobile browser - http://www.oracle.com/technology/tech/wireless/mobilebrowser.htm Novell - http://developer.novell.com/xforms/ Mozilla Firefox - http://www.mozilla.org/projects/xforms/ 14. Chiba - http://chiba.sourceforge.net/ Orbeon PresentationServer - http://www.orbeon.com/software/ IBM XML Forms Package - http://www.alphaworks.ibm.com/tech/xmlforms 15. OpenOffice 2.0beta - http://www.openoffice.org/ XFormation - http://www.xformation.com/ Stylus Studio XML Professional Edition - http://www.stylusstudio.com/ OnForm xPress - http://www.blackdog.co.uk/pages/onform_xpress.htm 16. Validator - http://xformsinstitute.com/validator/ XForms Buddy - http://www.beaufour.dk/index.php?sec=misc&pagename=xforms 17. XForms Wiki-1 - http://en.wikipedia.org/wiki/Xforms 18. XForms Wiki-2 - http://www.worldwidewiki.net/wiki/XForms
132
PHP Inside №13
Платежные системы - это не страшно
Платежные системы - это не страшно Поскольку речь идет о мастер-классе, а не об обзорном Автор: Евгений Бондарев докладе, то много внимания общим вопросам исторического характера уделяться не будет. Где, как, когда появились первые способы оплаты в сети, и какими путями продвигались в своем развитии эти направления электронной коммерции, можно прочесть в материалах прошлой конференции (или в интернете – Yandex по-прежнему не отменили). Данный материал ставит перед собой цель, в первую очередь, объяснить основные подходы к проведениям платежей и продемонстрировать их на конкретных показательных примерах. Например, несколько систем с абсолютно идентичным подходом, но несколько отличающимися (названиями или количеством передаваемых параметром) API подробно рассматривать нет смысла – это переливание из пустого в порожнее. Зато некоторые экзотические примеры, с которыми, возможно, и не придется сталкиваться в реальной жизни, смогут наглядно продемонстрировать то, что даже самое неожиданное на первый взгляд решение – это всего лишь одна из вариаций стандартных решений.
1. Основополагающие принципы При всем кажущемся многообразии платежных систем и, соответственно, API, существует 2 принципиально различных способа взаимодействия с платежной системой: •
передача управления серверу ПС;
•
прямое обращение к шлюзу ПС. Большего пока не дано.
Первый способ считается наиболее безопасным и корректным для проведения платежей. Второй обычно дает больше возможностей для взаимодействия с платежной системой, например, позволяет производить автоматическую оплату, контролировать статус оплаты определенного счета, проверять баланс и т.п. Хотя в большинстве случаев это не является необходимым для приема оплаты функционалом. Тут же следует оговориться, что данные функции автоматизации, предоставляемые ПС, применимы, в основном, для ЦПС, хотя не исключено, что некоторые ПС поддерживают вышеуказанный функционал. Кроме того, при процессинге кредитных карт серверу ПС передается критично важная информация типа номера карты, CVV, данных о кард-холдере. Соответственно, чем меньше вероятность того, что эта информация попадет в руки третьей стороне (например, магазину), тем охотнее будут пользоваться сервисом как веб-мастера, так и покупатели. Поэтому предоставление прямого доступа к шлюзу ПС для процессинга кредитных карт – достаточно редкое явление.
133
PHP Inside №13
Платежные системы - это не страшно
С процессингом КК вообще ситуация весьма щекотливая – современные средства антифродовых защит развиваются достаточно быстро, и реализация приема оплаты через merchant-panel гарантирует разработчику, что любые изменения регламента оплаты (например, введение дополнительных проверок по принципу 3D-secure от Visa) не потребуют доработки/переделки уже существующего и стабильного кода. Некоторые ПС предоставляют поддержку сразу двух способов, иногда все же распределяя между ними функционал, например, прием оплаты через merchant-panel, а управление аккаунтом – через прямое обращение (eGold). То же самое можно сказать и о способах контроля целостности (а также аутентификации) данных. Принципиально различных способов 2: использование механизмов хеширования и использование цифровой подписи. Наиболее простой вариант, используемый в некоторых системах, – ПС при возвращении результата транзакции серверу продавца передает также хеш-код, построенный из основных параметров, которые переданы при запросе на проведение транзакции, например, MD5 (номер заказа + сумма платежа + внутренний ключ продавца в ПС). Скрипт-получатель результата транзакции проверяет целостность данных совпадением вернувшегося хеша. Часто в ответ также добавляется «секретная фраза», известная только ПС и продавцу, что позволяет интерпретировать результат как однозначно полученный от ПС. Кстати, раз уж зашел разговор о «секретной фразе», стоит упомянуть, что многие системы, используют 2 пароля. Один из них на вход в систему (возможно, и на проведение элементарных операций типа проверки баланса, состояния оплаты счетов и т.п.), а второй – на финансовые операции (оплаты счетов, перевода денег и т.п.). Это относится как к онлайновым системам (e-Gold, RuPay), так и к оффлайновым (Privat24, например). Обычно в качестве «секретной фразы» используется или этот «пароль платежа», или же, что более удобно и надежно, данную фразу можно задать отдельно (для каждого сайта у RuPay или для каждого кошелька при приеме оплаты через WM Merchant системы WebMoney). Однако более надежным способом проверки целостности и, что важно, аутентификации, являются механизмы цифровой подписи. Чаще всего для создания используется тот же OpenSSL или PGP, с помощью которых формируется подпись при помощи одного из алгоритмов RSA (Ривест, Шамир и Адлеман). Некоторые ЦПС разрабатывают свои средства создания цифровой подписи (к примеру, WMSigner от WebMoney). Снабженные такой подписью данные однозначно позволяют как идентифицировать отправителя, так и быть уверенным в их целостности. Справедлива и обратная зависимость – поступившие в ПС данные о транзакции гарантированно не изменены при передаче процессинговому центру и поступили от владельца ключа (что при соблюдении мер предосторожности следует расценивать как «от магазина, от которого оно и должно было прийти»).
134
PHP Inside №13
Платежные системы - это не страшно
Число «2» для платежных систем можно считать кармическим. Достаточно вспомнить, что сами по себе ПС можно разделить на Цифровые (работающие со своими «виртуальными» денежными единицами) и Реальные (работающие с реальными счетами и вполне материальными дензнаками). И сами ЦПС отлично разделяются по способу доступа и управления счетом на два вида: через программу типа «Цифрового кошелька» или через интернет на сайте ЦПС. От себя добавлю еще одно наблюдение: если ваш сервер (сайт), который работает с ПС, сломали (а в некоторых случаях, например, обменного пункта, казино, и т.п. случаях, когда есть функционал по выводу средств, это будут делать с настойчивостью и упорством), его обязательно сломают и второй раз. Как говорят украинские буддисты: «Цэ карма».
2. WebMoney Из всех ЦПС (характерных как для наших широт, так и в мировом масштабе) WM являются одной из самых развитых (с технической точки зрения), распространенных и доступных. Хотя сразу же говорюсь, что для западного рынка WM не самое массовое решение – «буржуи» отдают предпочтение PayPal или e-Gold, но... Тут вступает в силу весьма существенное «но», которое позволяет говорить о WM, как о лидере по совокупности вышеупомянутых показателей. Распространенность и развитость PayPal (во многом благодаря тесному сотрудничеству и уникальному в своем роде симбиозу с интернет-аукционом eBay) перечеркивается показателем доступности – поддержка данной ЦПС ограничена набором из четырех десятков стран, автоматически причисляя остальных к странам «третьего сорта мира». E-Gold (о нем будет отдельный материал) доступен и не менее распространен, но остановился в развитии уже давно и, видимо, надолго (особых изменений, кроме появления новых металлов, за последние 4 года я не наблюдал). Кроме того, и PayPal, и e-Gold – это чисто «серверные» системы, в то время как WM управляется при помощи «Цифрового кошелька», что несколько разделяет их. Правда, в этой бочке меду есть изрядная доля дегтя: в последнее время WM постепенно превращается в такого себе «монстра» с приличной бюрократической машиной и атрофированной службой поддержки. Если бюрократию можно хоть как-то оправдать – получение сертификата весьма важный процесс, поскольку в системе цифровой наличности идентификация участника (особенно получающего деньги) не столь явна, как в реальных деньгах, где по реальному счету всегда можно вычислить владельца, то оправдать службу поддержки, которая официально «не поддерживает не-ASP технологии», весьма сложно. Особенно учитывая, что основная масса решений все же базируется на unix-платформах и проблемы в основном возникают именно у PHP-программистов (во всяком случае, об этом можно судить по темам в форуме поддержки).
135
PHP Inside №13
Платежные системы - это не страшно
WebMoney предоставляют 2 способа для взаимодействия с ЦПС: это прием оплаты через WM Merchant панель, расположенную на сервере платежной системы (самый простой, быстрый и надежный способ получить оплату), а также два интерфейса (HTTPS и XML) для «плотного» взаимодействия с системой (для того, чтобы с этими интерфейсами можно было работать, необходимо 2 вещи: аттестат уровня «Персональный» (или выше) и письмо в службу поддержки с просьбой активации интерфейсов для данного WMID.
2.1. WM Merchant Сервис позволяет задавать отдельные настройки панели приема платежа для каждого кошелька, на который будет производиться платеж. Эти настройки можно сконфигурировать на странице «Настройка» на сайте https://merchant.webmoney.ru. Основными параметрами являются адреса для оповещения и возвращения клиента на сайт, метод передачи результата (GET, POST, URL), режим работы (тестовый/реальный), метод формирования подписи (это может быть или MD5, или подпись, сформированная программой WMSigner). Реальный режим проведения платежа можно включить, только имея аттестат уровня не ниже «Персональный». Для обладателей «Начального» аттестата есть возможность работать только в тестовом режиме. Нас интересует в первую очередь Result URL, на который будет передано оповещение об успешном платеже. Интересно, что по данному адресу совершается 2 обращения: первое для проверки работоспособности сайта торговца, а второе – для передачи уже, собственно, всех реквизитов проведенного платежа. Также важны Success URL и Fail URL – адреса, на которые будет переброшен клиент в случае успешного или неуспешного платежей соответственно. Есть еще два маленьких, но существенных параметра, которые позволяют понизить риск мошенничества во время проведения платежа. Первый из них отвечает за то, можно ли изменять параметры Success, Fail и Result URL в форме перевода клиента на сайт Webmoney для оплаты (то есть, игнорировать ли приходящие из формы эти параметры). Второй отвечает за то, будут ли передаваться параметры готовящегося платежа при предварительном запросе (первом обращении на Result URL). Если в настройках Merchant-панели включена данная опция, то будет передан набор параметров, характеризующих происходящий платеж: кошелек торговца, сумма платежа, номер заказа, WM-идентификатор покупателя, флаг тестового режима и дополнительные поля, передаваемые торговцем в форме запроса платежа. В таком случае скрипт должен проверить все полученные данные и разрешить проведение платежа (вернуть строку YES) или отказать покупателю в платеже (вернуть любую другую строку, которая и будет показана покупателю системой). Эта проверка может быть полезна не только для контроля суммы готовящегося платежа, но и для того, чтобы исключить повторную оплату со стороны клиента. Пример формы для проведения платежа через WM Merchant: <form method="POST" action="https://merchant.webmoney.ru/lmi/payment.asp">
136
PHP Inside №13
Платежные системы - это не страшно
<!--Сумма платежа --> <input type="hidden" name="LMI_PAYMENT_AMOUNT" value="18.29"> <!--Описание платежа --> <input type="hidden" name="LMI_PAYMENT_DESC" value="платеж по счету №18"> <!--Номер платежа --> <input type="hidden" name="LMI_PAYMENT_NO" value="18"> <!--Кошелек торговца, на который совершается платеж --> <input type="hidden" name="LMI_PAYEE_PURSE" value="Z123456789012"> <!--Режим симуляции платежа --> <input type="hidden" name="LMI_SIM_MODE" value="0"> <!--Любые другие поля --> <input type="hidden" name="FIELD_1" value="VALUE_1"> <input type="hidden" name="FIELD_2" value="VALUE_2"> ... <input type="hidden" name="FIELD_N" value="VALUE_N"> ... </form>
«Любые другие поля» должны быть без префикса LMI_, и сюда хорошо «раскладывать» всевозможные хеши (которые удобнее использовать в качестве идентификатора, чем просто цифру, а именно цифра и должна передаваться в форме проведения платежа), а также сюда попадают идентификаторы сессий, viewstate и прочие удобства. Все эти поля никак не участвуют в подписи, которая формируется ПС. Типичная функция проверки целостности платежа выглядит примерно таким образом:
function check_crc($responce_data) { $crc_data = array( "LMI_PAYEE_PURSE" => $responce_data["LMI_PAYEE_PURSE"], "LMI_PAYMENT_AMOUNT" => $responce_data["LMI_PAYMENT_AMOUNT"], "LMI_PAYMENT_NO" => $responce_data["LMI_PAYMENT_NO"], "LMI_MODE" => $responce_data["LMI_MODE"], "LMI_SYS_INVS_NO" => $responce_data["LMI_SYS_INVS_NO"], "LMI_SYS_TRANS_NO" => $responce_data["LMI_SYS_TRANS_NO"], "LMI_SYS_TRANS_DATE" => $responce_data["LMI_SYS_TRANS_DATE"], "LMI_SECRET_KEY" => "СЕКРЕТНАЯ_ФРАЗА", "LMI_PAYER_PURSE" => $responce_data["LMI_PAYER_PURSE"], "LMI_PAYER_WM" => $responce_data["LMI_PAYER_WM"], ); $crc_str = strtoupper(md5(implode("", $crc_data))); return $crc_str==$responce_data["LMI_HASH"]; }
В случае, когда подпись формируется не при помощи MD5, а при помощи WMSigner, хеш будет так же формироваться при помощи данного программного обеспечения, о котором более подробно речь пойдет немного ниже.
Настоятельно рекомендуется тщательно проверять данные, пришедшие на Result URL, с оповещением о платеже. Проверять целостность полученных данных, сумму платежа, кошелек торговца и режим проведения платежа (тестовый или реальный). Списки и описания всех полей, а также подробные описания всех требуемых для проведения платежа действий (документировано все на высочайшем уровне) можно найти по адресу https://merchant.webmoney.ru/conf/guide.asp.
137
PHP Inside №13
Платежные системы - это не страшно
Парадоксальный в своем роде глюк был замечен при одновременной работе с двумя запущенными киперами и оплатой через WM Merchant одним из них на счет второго. Деньги зачислялись, но вызов Result URL с оповещением об оплате был пустым, хотя предварительный запрос при включении соответствующей опции передавал все положенные данные…
2.2. HTTPS и XML интерфейсы Данные интерфейсы позволяют полноценно управлять своими кошельками. Эти интерфейсы практически дублируют друг друга с точки зрения функциональности. Принципиальное отличие состоит в способе формирования и отправки запроса. Также некоторые функции, предоставляемые XML-интерфейсом, отсутствуют в HTTPS-интерфейсе (в частности, проведение оплаты). Любой запрос, вне зависимости от выбранного программистом интерфейса, должен быть подписан программой WMSigner. WebMoney предоставляют как ЕХЕ файл для Windows-платформ, так и исходники для сборки программы самостоятельно под управлением любой ОС. Установленная программа снабжается конфигурационным INI файлом, в котором указываются WMID, пароль и путь к файлу ключей. Формирование подписи из PHP сводится к вызову подобной функции: function _GetSign($inStr){ $fp = popen("./WMSigner", "r+"); $PlanStr = "$inStr\004\r\n"; fwrite($fp,$PlanStr); $s = fgets($fp, 133); pclose($fp); }
return $s;
Обычно подпись формируется из «склеенных» между собой ключевых переменных запроса. NB! Абсолютно неочевидная, недокументированная, но при этом проблемная особенность: для стабильной и корректной работы механизма подписи необходимо использовать файл ключей минимального размера (что противоречит самим WM, которые рекомендуют использовать файл как можно большего размера). Работа с неминимальным файлом ключей очень часто невозможна, и WMSigner возвращает ошибку. Зависимость от платформы требуемого размера файла и каких-либо других параметров обнаружить не удалось Это же касается и стандартных библиотек-примеров, которые с некоторого момента стали работать нестабильно. Что характерно, один и тот же код может работать или не работать на разных платформах. Особенно это касается XML-интерфейса, когда сервер ЦПС возвращает ответ о «невалидном XML» на ровном месте. Поскольку использование XML-интерфейса является более предпочтительным, это весьма досадно. Из общих рекомендаций могу посоветовать форматировать уровни xml при помощи табов.
138
PHP Inside №13
Платежные системы - это не страшно
Основные и наиболее используемые функции – это выставление счета, проверка оплаты счета, перевод средств на кошелек клиента и проверка баланса. Это обеспечивают интерфейсы Х1, Х4, Х2 и Х9 соответственно. Рассматривать каждый из них по отдельности смысла нет – принцип действия один и тот же. В качестве примера рассмотрим выписывание счета покупателю. Интерфейс Х1 выполняется путем отправки сформированного XML методом POST на URL https://w3s.webmoney.ru/asp/XMLInvoice.asp Формат отправляемого XML следующий: <w3s.request> <reqn>НОМЕР_ЗАПРОСА</reqn> <wmid>WMID_ПОДПИСАВШЕГО_ЗАПРОС</wmid> <sign>ПОДПИСЬ_СФОРМИРОВАННАЯ_ИЗ_ПАРАМЕТРОВ_INVOICE+reqn</sign> <invoice> <orderid>НОМЕР_СЧЕТА_В_СИСТЕМЕ_ПРОДАВЦА</orderid> <customerwmid>WMID_ПОКУПАТЕЛЯ</customerwmid> <storepurse>КОШЕЛЕК_ПРОДАВЦА</storepurse> <amount>СУММА</amount> <desc>ОПИСАНИЕ</desc> <address>АДРЕС_ДОСТАВКИ</address> <period>МАКСИМАЛЬНЫЙ_СРОК_ПРОТЕКЦИИ</period> <expiration>МАКСИМАЛЬНЫЙ_СРОК_ОПЛАТЫ</expiration> </invoice> </w3s>
Обратно вернется XML следующего формата:
<w3s.response> <reqn>НОМЕР_ЗАПРОСА</reqn> <retval>КОД_ВЫПОЛНЕНИЯ_ЗАПРОСА</retval> <retdesc>РАСШИФРОВКА_КОДА_ВЫПОЛНЕНИЯ</retdesc> <invoice id="НОМЕР_СЧЕТА_В_СИСТЕМЕ_WEBMONEY" ts=" СЛУЖЕБНЫЙ_НОМЕР_СЧЕТА_В_СИСТЕМЕ_WEBMONEY "> <orderid>НОМЕР_СЧЕТА_В_СИСТЕМЕ_ПРОДАВЦА</orderid> <customerwmid>WMID_ПОКУПАТЕЛЯ</customerwmid> <storepurse>КОШЕЛЕК_ПРОДАВЦА</storepurse> <amount>СУММА</amount> <desc>ОПИСАНИЕ</desc> <address>АДРЕС_ДОСТАВКИ</address> <period>МАКСИМАЛЬНЫЙ_СРОК_ПРОТЕКЦИИ</period> <expiration>МАКСИМАЛЬНЫЙ_СРОК_ОПЛАТЫ</expiration> <state>СОСТОЯНИЕ_СЧЕТА</state> <datecrt>ДАТА_И_ВРЕМЯ_СОЗДАНИЯ СЧЕТА</datecrt> <dateupd>ДАТА_И_ВРЕМЯ_ИЗМЕНЕНИЯ_СОСТОЯНИЯ</dateupd> </invoice> </w3s>
Переменная НОМЕР_ЗАПРОСА – это целое число без знака, максимум 15 цифр (по спецификации, хотя на практике рекомендуется ограничиться 14-ю), которое при каждом новом запросе должно быть больше, чем при предыдущем. Такой себе автоинкремент. Очевидно, что для формирования данного числа больше всего подходит зависимость от текущей даты. В таком случае функция формирования данного номера приобретает примерно следующий вид:
function _GetReqN() { list($usec, $sec) = explode(" ",microtime()); $reqn = sprintf("%010u%04u", (float)$sec, round((float)$usec*100000)); return $reqn; }
139
PHP Inside №13
Платежные системы - это не страшно
Полный же цикл формирования и отправки запроса с использованием модуля CURL приобретет следующий вид: // номер счета в системе магазина $orderid = 18; // WMID клиента $customerwmid = 1818181818; // кошелек магазина на который будет производиться платеж $storepurse = "Z1869284749"; // сумма к оплате. разделитель точка, незначимые нули убраны $amount = "58.5"; // описание платежа $desc = "пример счета № 18"; // адрес доставки $address = "на деревню к дедушке"; // оплату принимаем без протекции $period = 0; // счет действителен 4 дня $expiration = 4; // получаем номер запроса $reqn = _GetReqN(); // формируем подпись $sign = _GetSign($orderid . $customerwmid . $storepurse . $amount . $desc . $address . $period . $expiration . $reqn); // формируем запрос $request = "<w3s.request> <reqn>$reqn</reqn> <wmid>1231234321</wmid> <sign>$sign</sign> <invoice> <orderid>$orderid</orderid> <customerwmid>$customerwmid</customerwmid> <storepurse>$storepurse</storepurse> <amount>$amount</amount> <desc>$desc</desc> <address>$address</address> <period>$period</period> <expiration>$expiration</expiration> </invoice> </w3s>"; // инициализируем CURL $cu = curl_init("https://w3s.webmoney.ru/asp/XMLInvoice.asp"); curl_setopt($cu, CURLOPT_HEADER, 0); $fp = tmpfile(); curl_setopt($cu, CURLOPT_SSL_VERIFYPEER, 0); // настраиваем передачу методом POST curl_setopt ($cu, CURLOPT_POST, 1); curl_setopt ($cu, CURLOPT_POSTFIELDS, $request); // перенаправляем результат во временный файл curl_setopt ($cu, CURLOPT_FILE, $fp); // выполняем запрос curl_exec($cu); curl_close($cu); // получаем результат из временного файла fseek($fp,0,SEEK_SET); $result = ''; while ($str = fgets($fp,1024)) { $result .= $str; } fclose($fp);
140
PHP Inside №13
Платежные системы - это не страшно
Полученный в переменную $result XML можно разобрать или при помощи регулярных выражений, или воспользовавшись своим любимым парсером. В данном случае нам необходимо проверить, что поле retval хранит значение 0 (что означает «запрос выполнен»), а также получить значение атрибута id из invoice.
2.3. Merchant VS XML В каких случаях выписывание счета предпочтительней, чем оплата через WM Merchant. и наоборот? Какой из способов выбрать для приема оплаты? На самом деле наиболее удобным для покупателя будет предоставление обоих вариантов. Оплата через WM Merchant удобна и возможна только в том случае, когда клиент пользуется обозревателем Internet Explorer (хотя и это еще не гарантирует возможности оплаты – ActiveX ПС, используемый для аутентификации пользователей, может вешать браузер, если последний «обвешан» тулбарами, скинами, коллекциями смайлов и прочими троянами) и готов заплатить сразу и на месте. Если же покупатель использует для серфинга Мозиллу или Оперу, хочет заказать товар, но готов заплатить несколько позже или знает, что за товар заплатит его друг (подруга, брат, начальник, должник и т.п. – нужное подчеркнуть), и для этого достаточно, чтобы счет пришел другу, единственный способ – это выписка счета. Размещение на странице оплаты двух кнопок с вариантами решает данную проблему. Или можно предложить покупателю указать свой WMID для выставления счета, или оставить поле незаполненным для оплаты через WM Merchant. Или… В общем, количество вариантов ограничено только полетом фантазии разработчика…
3. RuPay Основным преимуществом ЦПС «RuPay» является то, что, кроме своей внутренней системы денежных единиц, сервис позволяет принимать платежи с десятков внешних источников, в которые включены как ЦПС (WebMoney, Яndex-деньги, Internet-money, EGold), так и денежные переводы (Western Union, банковские переводы, оплата наличными и по чеку). Также система сотрудничает с рядом банковских сервисов (например, «Приват-24» от ПриватБанка). Нацеливание на аудиторию стран бывшего союза для приема платежей из этих стран и из дальнего зарубежья весьма приятно. И что особенно радует, это темпы развития системы. Для уменьшения риска мошенничества и «отмывки денег» система ввела аттестацию (менее бюрократическую, чем у WebMoney). Без аттестата ограничиваются способы ввода денег на счет. Хотя вывод денег не ограничивается отсутствием аттестата. Особо интересными являются новые функции системы, такие как инвестирование (с ежедневным начислением процента и возможностью пополнить счет или снять с него деньги в любое время), а также совсем новая возможность «кредитование». Для приема платежей с сайта торговцу необходимо зарегистрировать сайт в системе и настроить его параметры: 141
PHP Inside №13
Платежные системы - это не страшно
Параметр
Формат
Описание
Название сайта
255 символов
Название осуществляющего платежей
сайта, прием
Адрес сайта
255 символов
URL сайта, осуществляющего прием платежей
Описание сайта
-
Описание осуществляющего платежей
URL оповещения о платеже
255 символов
URL (на веб-сайте продавца), на который система RuPay посылает HTTP POSTоповещение о совершении платежа с его реквизитами. Если продавец не определил этот URL, он не будет оповещаться системой о совершенных платежах. URL должен начинаться с префикса “http://” или “https://”
Секретный ключ
32 символа
Строка символов, добавляемая к реквизитам платежа, высылаемым продавцу вместе с оповещением. Эта строка используется для повышения надежности идентификации высылаемого оповещения. Содержание строки известно только системе RuPay и продавцу!
Способы оплаты
-
сайта, прием
Способы оплаты, которые будет использовать сайт продавца Сам платеж проходит в несколько этапов. Торговец формирует форму запроса проведения платежа с action=http://rupay.ru/rupay/pay/index.php, и покупатель переходит к оплате на сервере ЦПС. Эта форма будет выглядеть примерно так: <form action="http://rupay.ru/rupay/pay/index.php" name="pay" method="POST"> <!--Идентификатор магазина --> <input type="hidden" name="pay_id" value="18"> <!--Сумма платежа --> <input type="hidden" name="sum_pol" value="18.69"> <!--Описание платежа --> <input type="hidden" name="name_service" value="платеж по счету #1823"> <!--Номер заказа --> <input type="hidden" name="order_id" value="1823"> <!--Любые другие поля --> <input type="hidden" name="user_field_1" value="value_1"> <input type="hidden" name="user_field_2" value="value_2"> ... <input type="hidden" name="user_field_3" value="value_3"> ... <input type="submit" name="button" value="оплатить"> </form>
142
PHP Inside №13
Платежные системы - это не страшно
Далее система пытается авторизировать покупателя по его электронному адресу и предоставляет ему возможность выбрать способ оплаты товара из разрешенных торговцем при настройке ЦПС для сайта. Как и WebMoney, RuPay формирует предварительный запрос на проведение платежа, а также запрос оповещения о платеже. Для аутентификации источника запросов используется секретный ключ, указанный в настройках сайта. Если сервер торговца поддерживает защищенное соединение, то этот ключ будет просто передан в явном виде. Если HTTPS-соединение не поддерживается, ключ не передается, но он является неотъемлемой частью формируемой ЦПС цифровой подписи, которую как раз надо в таких случаях проверять для уверенности в целостности полученных сервером торговца данных. Для формирования контрольной подписи ЦПС «склеивает» параметры запроса в одну строку, используя разделитель “::”. Порядок следования параметров при формировании строки подписи следующий. Для предварительного запроса: rupay_action, rupay_site_id, rupay_order_id, rupay_name_service, rupay_id, rupay_sum, rupay_user, rupay_email, rupay_data, rupay_secret_key; для оповещения о платеже: rupay_action, rupay_site_id, rupay_order_id, rupay_sum, rupay_id, rupay_data, rupay_status, rupay_secret_key. После этого строка подписи обрабатывается MD5, и полученное значение передается параметром rupay_hash. Таким образом, примерный код проверки цифровой подписи оповещения о платеже будет следующим: function check_crc($responce_data) { $crc_data = array( "rupay_action" => $responce_data["rupay_action"], "rupay_site_id" => $responce_data["rupay_site_id"], "rupay_order_id" => $responce_data["rupay_order_id"], "rupay_sum" => $responce_data["rupay_sum"], "rupay_id" => $responce_data["rupay_id"], "rupay_data" => $responce_data["rupay_data"], "rupay_status" => $responce_data["rupay_status"], "rupay_secret_key" => "СЕКРЕТНАЯ_ФРАЗА", ); $crc_str = md5(implode("::", $crc_data)); return $crc_str==$responce_data["rupay_hash"]; }
4. E-gold Отличительной особенностью данной системы есть то, что средствами расчетов в ней являются ценные металлы (золото, серебро, платина, палладий), и средства хранятся не в долларах-евро, а в граммах-унциях. При расчете (переводе средств) сумма указывается или в массе цветного металла, или в денежном эквиваленте, хотя на счет будет все равно переведен металл. Например, можно перевести 18 долларов в эквиваленте золота. Будет произведен расчет массы, необходимой для перевода данной суммы (курсы всех металлов всегда самые актуальные), и масса, эквивалентная по стоимости 18-ти долларам, уйдет на счет адресата. Комиссия за переводы также берется из расчета массы и снимается с получателя.
143
PHP Inside №13
Платежные системы - это не страшно
В системе существуют 2 пароля: пароль на вход (доступ к аккаунту) и пароль на выполнение операций с содержимым счета.
4.1. Прием оплаты Прием оплаты производится на сайте ЦПС, куда покупатель переправляется при помощи подобной формы: <form action="https://www.e-gold.com/sci_asp/payments.asp" method="POST"> <!-- Номер счета продавца --> <input type="hidden" name="PAYEE_ACCOUNT" value="900123"> <!-- Название продавца --> <input type="hidden" name="PAYEE_NAME" value="Магазын рускава йазыка"> <!-- Сумма к оплате --> <input type="hidden" name="PAYMENT_AMOUNT" value="18.99"> <!—Номер счета в системе торговца --> <input type="hidden" name="PAYMENT_ID" value="100A-12287"> <!-- Валюта --> <input type="hidden" name="PAYMENT_UNITS" value="1"> <!-- Металл, эквивалент которого в указанной валюте будет переведен --> <input type="hidden" name="PAYMENT_METAL_ID" value="1"> <!-- URL на который придет уведомление о платеже --> <input type="hidden" name="STATUS_URL" value="https://server.com/e-gold-gateway.php"> <!-- URL на который будет переправлен покупатель в случае успешной транзакции --> <input type="hidden" name="PAYMENT_URL" value="https://server.com/paymentok.php"> <!-- URL на который будет переправлен покупатель в случае не успешной транзакции --> <input type="hidden" name="NOPAYMENT_URL" value="https://server.com/paymentfail.php"> <!-- Список дополнительных «custom» полей --> <input type="hidden" name="BAGGAGE_FIELDS" value="ORDER_NUM CUST_NUM"> <!-- Набор «custom» полей --> <input type="hidden" name="ORDER_NUM" value="9801121"> <input type="hidden" name="CUST_NUM" value="2067609"> <input type="submit" value="Оплатить"> </form>
На сайте E-gold клиенту будет предложено ввести свой номер аккаунта, пароль, код, показанный на «картинке, защищающей от роботов», далее клиенту будет еще раз показана форма платежа со всеми реквизитами и предложено оплатить или отказаться. После оплаты будет выведен соответствующий текст и предложение вернуться на сайт торговца. Общая схема процесса оплаты приведена на картинке (взято из спецификации Shopping Cart интерфейса Egold):
144
PHP Inside №13
Платежные системы - это не страшно
Уведомление о платеже передает торговцу следующие поля: Название поля
Описание
PAYEE_ACCOUNT
Номер счета торговца в системе E-gold. То же значение, что и в форме на оплату
PAYMENT_ID
Номер оплаченного счета в системе учета на сайте торговца (то же значение, которое передавалось в форме на оплату, или текст NULL, если поле не было передано)
PAYMENT_AMOUNT
Сумма. То же значение, что и в форме на оплату
PAYMENT_UNITS
Валюта. То же значение, что и в форме на оплату
PAYMENT_METAL_ID
Вид металла. То же значение, что и в форме на оплату. Принимает значение от 1 до 4 для определения соответствующего металла (соответственно золота, серебра, платины и палладия)
PAYMENT_BATCH_NUM
Номер транзакции в системе E-gold
PAYER_ACCOUNT
Номер счета плательщика
HANDSHAKE_HASH
MD5 подпись
ACTUAL_PAYMENT_OUNCES
Реальная масса перевода средств в тройских унциях переведенного металла
USD_PER_OUNCE
Текущая стоимость одной унции данного металла в долларах
FEEWEIGHT
Масса металла, которая была взята в качестве комиссии
TIMESTAMPGMT
Дата и время транзакции
V2_HASH
MD5 подпись
145
PHP Inside №13
Платежные системы - это не страшно
Посчитать реально полученную сумму можно, высчитав полученный вес металла ( NET_WEIGHT = ACTUAL_PAYMENT_OUNCES – FEEWEIGHT) и умножив на стоимость металла в долларах (ACTUAL_AMOUNT = USD_PER_OUNCE * NET_WEIGHT). 2 хеша возвращается из соображений совместимости. До 2000 года возвращалось только значение HANDSHAKE_HASH, в котором фигурировал пароль платежа в явном виде. В октябре 2000 года был введен V2_HASH, в котором вместо пароля платежа используется его хеш. Таким образом, проверка целостности будет выглядеть примерно таким образом: function check_crc($responce_data) { $crc_data = array( "PAYMENT_ID" => $responce_data["PAYMENT_ID"], "PAYEE_ACCOUNT" => $responce_data["PAYEE_ACCOUNT"], "PAYMENT_AMOUNT" => $responce_data["PAYMENT_AMOUNT"], "PAYMENT_UNITS" => $responce_data["PAYMENT_UNITS"], "PAYMENT_METAL_ID" => $responce_data["PAYMENT_METAL_ID"], "PAYMENT_BATCH_NUM" => $responce_data["PAYMENT_BATCH_NUM"], "PAYER_ACCOUNT" => $responce_data["PAYER_ACCOUNT"], "ALT_PASS_HASH" => "РАСЧИТАННЫЙ_MD5_ПАРОЛЯ_ПЛАТЕЖА", "ACTUAL_PAYMENT_OUNCES" => $responce_data["ACTUAL_PAYMENT_OUNCES"], "USD_PER_OUNCE" => $responce_data["USD_PER_OUNCE"], "FEEWEIGHT" => $responce_data["FEEWEIGHT"], "TIMESTAMPGMT" => $responce_data["TIMESTAMPGMT"], ); $crc_str = strtoupper(md5(implode(":", $crc_data))); return $crc_str==$responce_data["V2_HASH"]; }
Есть как минимум один отрицательный момент в форме приема: STATUS_URL должен обязательно находиться на стандартных портах (80 для HTTP и 443 для HTTPS). Это не всегда удобно, поскольку часто бывает, что доступ к сайту по https-протоколу возможен только через нестандартный порт. Приходится жертвовать защищенным соединением.
4.2. Функции автоматизации Данный функционал представлен в виде HTTPS-интерфейса. Параметры могут передаваться как GET, так и POST методом. Выполнение функций сводится к обращению к определенному URL и разбору ответа. Для вызова используется CURL. Например, для получения текущего баланса необходимо сделать обращение по адресу https://www.e-gold.com/acct/balance.asp и передать туда параметры AccountID (с номером счета) и Passphrase (с паролем). // Задаем необходимые параметры $params = "AccountID=18181818&Passphrase=cheburashka"; // Собираем адрес $url = "https://www.e-gold.com/acct/balance.asp?".$params; // Инициализируем CURL $cu = curl_init($url); curl_setopt ($cu, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt ($cu, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($cu, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)"); curl_setopt($cu, CURLOPT_RETURNTRANSFER, 1); curl_setopt($cu, CURLOPT_HEADER, 0);
146
PHP Inside №13
Платежные системы - это не страшно
curl_setopt($cu, CURLOPT_TIMEOUT, 100); // Выполняем запрос и получаем результат $res = curl_exec($cu); // Закрываем ресурс curl_close($cu);
Полученный ответ необходимо разобрать, воспользовавшись немудреным регулярным выражением: $acc = preg_match("/<input type=hidden name=Gold_Ounces value=\"(.*?)\"><input [^>]+>\s+<td align=right><font[^>]+>(.*?)<\/font><\/td>/", $result[1], $res); if ($acc) { // Первое значение в унциях, второе – в граммах $gold_ballance = array($res[1], $res[2]); }
Таким же образом можно получить стоимость любого из представленных в системе металлов по отношению к любой из двух десятков представленных валют на любую дату.
К примеру, запрос вида http://www.egold.com/unsecure/metaldata.asp?LATEST=1&GOLD=1 вернет актуальную стоимость одной унции золота в системе в следующем виде: 4/18/05 18:18:18 PM, 476.000
Сам же перевод денег может производиться в 2 этапа. Первый (не обязательный, но рекомендуемый) позволяет проверить, будет ли возможна транзакция, и получить конкретные данные об актуальной стоимости перевода в массе металла. Второй этап уже совершает окончательный перевод средств. В целом параметры платежа одинаковы для обоих этапов. Различны адреса, по которым происходит обращение, и ответы системы. Кстати, результатом является HTML, в котором есть набор hidden-полей с результирующими значениями (как и в случае с балансом). Для проверки готовящейся транзакции вызывается URL https://www.e-gold.com/acct/verify.asp, в который передаются следующие поля: Поле
Описание
ACCOUNTID
Номер аккаунта плательщика
PASSPHRASE
Пароль
PAYEE_ACCOUNT
Номер аккаунта получателя
AMOUNT
Сумма перевода. Не менее 0.000010 металла унции «по курсу»
PAY_IN
Код единиц перевода. Это могут быть как валюты, так и граммы или унции
WORTH_OF
Имя переводимого металла. Одно из “Gold”, “Silver”, “Platinum”, “Palladium”.
MEMO
Примечание до 50 символов. Будет доступно как плательщику, так и получателю
PAYMENT_ID
Номер перевода в системе пользователя. Опциональное поле.
147
PHP Inside №13
Платежные системы - это не страшно
Система вернет HTML, в котором будут поля Payee_Account, ACTUAL_PAYMENT_OUNCES, PAYMENT_AMOUNT, PAYMENT_UNITS, PAYMENT_METAL_ID, PAYER_ACCOUNT, USD_PER_OUNCE, PAYMENT_ID и ERROR. Все значения соответствуют входным данным, добавляется только стоимость унции металла в долларах США, количество переводимых унций металла и поле ERROR, которое содержит в себе текст ошибки в случае возникновения таковой (например, если указан несуществующий код металла или неверный аккаунт, или суммы на счету недостаточно для проведения перевода и т.п.). Повторный вызов с этими же параметрами, но по адресу https://www.e-gold.com/acct/confirm.asp инициирует перевод средств. Результат будет выражен такими же полями, как и при проверке, но к ним еще добавятся поля PAYMENT_FEE_OUNCES (размер комиссии в унциях) и PAYMENT_BATCH_NUM (внутренний номер транзакции в системе E-gold). Как мы можем увидеть из данных примеров, автоматизация в данной системе сделана весьма суррогатным образом. Фактически это просто отправка форм (или переходы по ссылкам), и так расположенных на сайте. Остается поблагодарить разработчиков за то, что они хотя бы документировали это (облачив в форму официальной спецификации).
5. Int-Commerce Данная система является одной из немногих Украинских систем, предоставляющих процессинг кредитных карт. Есть некоторые несущественные ограничения в выборе банка эквайера (банка, обслуживающего торговца) – это должен быть АККБ «Аваль». Этот же банк является и расчетным банком платежной системы. Для проведения оплаты с помощью системы интернет-коммерции (СИК) «Int-Commerce» клиент перенаправляется торговцем на адрес https://secure.intcommerce.com/servlets/SoftPOS_alias/SoftPOS.Pay (где alias - идентификатор торговца, согласованный при регистрации), куда методом GET передаются следующие параметры: Параметр
Описание
psum
Сумма оплаты в копейках
148
PHP Inside №13
Платежные системы - это не страшно
Параметр
Описание
pmode
Режим проведения транзакции. Может принимать значения: 1 – автоматический (после поступления данных авторизация происходит автоматически. В случае успешной авторизации автоматически происходит расчет по транзакции); 0 – ручной (то же, что предыдущее, но без автоматического расчета. То есть администратор сайта сам проводит расчет по транзакции либо ее откат); -1 - отложенный (после поступления данных от клиента администратору сайта высылается оповещение. Администратор сам принимает решение, проводить ли авторизацию карточки или нет).
poterm
Срок истинности заказа в часах
porder
Номер заказа
Plang
Язык интерфейса пользователя. Может принимать значения: 1 – украинский; 2 – русский;
3 – английский Результат транзакции торговец получает или на email, или вызовом ИПС-скрипта на сервере торговца. Во втором случае скрипту передаются следующие параметры методом GET: oderdID - номер заказа summ - сумма оплаты ordStat - статус заказа Код
Состояние
Описание
-20
Оплата отменена
После успешной авторизации транзакции администратором магазина была проведена операция отмены платежа
0
Заказ поступил, данных для авторизации недостаточно
На сервер были переданы данные заказа, но клиент еще не ввел данные карточки
10
Ожидание авторизации
Данные карточки были введены, транзакция ожидает авторизации администратором магазина
11
Авторизация отложена
Зарезервировано. Сейчас не используется
16
В авторизации отказано
В авторизации отказано
149
PHP Inside №13
Платежные системы - это не страшно
Код
Состояние
Описание
20
Заказ авторизован, ожидание расчета
Транзакция успешно авторизована и ожидает проведения расчета администратором магазина
30
Заказ оплачен Заказ оплачен Данные о результате транзакции поступают сразу же после изменения статуса заказа. Проверить, что данные поступили от ИПС, можно, только проверив IP-адрес вызывающей стороны. Подробности можно узнать на сайте www.int-commerce.com Я намеренно воздерживаюсь от своих комментариев в адрес данной системы. Достаточно сравнить ее с любой другой системой (показательным примером выступит процессинговый центр банка «Менатеп»), но, как писал бывший президент Л. Кучма: «Украина не Россия», так что «маемо тэ, що маемо».
6. Менатеп Для меня процессинговый центр банка «Менатеп» пока остается классическим примером грамотного и верного подхода к приему платежей. Речь идет в первую очередь о формировании корзины и передаче ее ПС. Во-первых, сама корзина представляет собой XML-файл, в котором есть место и для описания платежа, и для перечня покупок, и для служебной информации. Во-вторых, корзина подписывается магазином (для цифровой подписи используется алгоритм RSA-MD5), а файл с ключами можно получить только лично в банке. Подпись позволяет аутентифицировать продавца и гарантирует целостность переданных на сервер ПС данных, а то, что за ключами надо «тащиться» в банк лично, значительно снижает риск утечки ключей. В-третьих, хорош подход с передачей результата транзакции – в подписанном ПС XML-ответе (подпись, опять-таки, гарантирует целостность и источник данных) возвращается, кроме результата транзакции, еще и исходная корзина. Такое себе «быстрое решение» для обратной связи. Но этот маленький нюанс позволяет не спешить с хранением лишних данных в базе или где-то еще, а фиксировать, например, только завершенные транзакции. Схема проведения транзакции следующая: •
Сервер торговца формирует XML-файл корзины.
•
Клиент перебрасывается торговцем на адрес: https://www.menatepspb.com/ib/eps3/enter/?basket_url=http://www..., где параметр basket_url задает адрес, с которого сервис ИПС сможет считать файл корзины, закодированный BASE64.
•
ИПС раскодирует корзину и проверяет цифровую подпись торговца.
•
Если все в порядке, ИПС получает от покупателя данные по кредитной карте или реквизиты для банковского перевода и пытается провести транзакцию.
•
Результат транзакции возвращается торговцу на cmdAckUrl или cmdCancelUrl в зависимости от успешности.
•
Пользователь перенаправляется на returnUrl, указанный в корзине. 150
PHP Inside №13 •
Платежные системы - это не страшно
В зависимости от результатов, полученных торговцем на 5-м шаге, торговец выводит результат транзакции покупателю.
Особенно удобно то, что для механизмов подписи не нужно устанавливать стороннее ПО. Для работы можно использовать любые программные решения (большинство из которых есть на каждом сервере), поддерживающие RSA-MD5 (обычно это OpenSSL или GPG).
8. PayCash Семейство платежных систем PayCash достаточно велико. На самом деле это замечательная попытка объединить под одной крышей обработку платежей в разных странах и валютах. Создать универсальную ЦПС для всех. В России PayCash знакомы под маркой Yandex.Деньги, в Украине это Интернет.Деньги. Хотя принципиальное различие только в доменном имени (ну и еще кое в чем, iMoney опережают Yandex.Деньги, о чем я упомяну ниже). Данная ЦПС базируется на программном обеспечении типа «Интернет.Кошелек» (упрощенный аналог WM Keeper от WebMoney), который прост в установке, настройке и использовании, поскольку обладает минимально необходимым функционалом. Прием оплаты производится путем размещения на сайте формы такого вида: <form action="http://127.0.0.1:8129/wallet" method="post"> <input type="hidden" name="wbp_Version" value="2"> <input type="hidden" name="wbp_MessageType" value="DirectPaymentIntoAccountRequest"> <input type="hidden" name="wbp_ShopAddress" value="billing@shop.com"> <input type="hidden" name="wbp_accountid" value="40056565489"> <input type="hidden" name="wbp_currencyamount" value="643;18.02"> <input type="hidden" name="wbp_shortdescription" value="ОПИСАНИЕ"> <input type="hidden" name="wbp_template_1" value="ТЕКСТ_ВЫВОДИМЫЙ ПРИ ОПЛАТЕ"> <input type="hidden" name="wbp_template_2" value=""> <input type="hidden" name="wbp_ShopErrorInfo" value=""> </form>
При этом у покупателя ПО кошелька должно быть запущенно. Запрос попадает покупателю прямо в кошелек, покупатель оплачивает счет, и результат уходит на wbp_ShopAddress, указанный в форме. В качестве данного адреса могут выступать или email продавца, куда придут данные о результате транзакции, или IP-адрес компьютера продавца, на котором запущен кошелек. С автоматизацией дело обстоит очень плохо, поскольку обработать платеж «на лету» несколько проблематично (можно разбирать пришедший email, в котором для автоматизации все результирующие поля вынесены в отдельный раздел, но это полумера). Хотя в будущей версии ПО iWallet обещали это исправить и позволить передавать данные непосредственно серверному скрипту.
151
PHP Inside №13
Платежные системы - это не страшно
Долгие поиски возможности автоматизации процесса (читай – поиски документации) подарили весьма удивительные результаты: на сайте http://www.imoney.com.ua удалось обнаружить раздел, посвященный способам автоматизации (ни на сайте http://money.yandex.ru, ни на сайте http://www.paycash.com ничего подобного найдено не было), в котором описывается некий «HTTPудлинитель», с помощью которого можно добиться определенного прогресса в данной области. Данный «PayCash ShopAPI HTTP-wrapper» (AHW) по сути есть вспомогательное Windows-приложение (сервис), реализующее набор функций платежной системы PayCash ShopAPI: •
GetMoneyRequest,
•
GetIncomingPaymentApproval,
•
PaymentAuthorizationResult
посредством упаковки параметров вызываемой функции и передачи зашифрованного пакета на заданный сервер по протоколу HTTP, извлечения зашифрованного пакета с результатами работы функции из ответа сервера, распаковки и возврата их вызывающему. AHW предназначен для работы в качестве посредника между Кошельком-кассой и Магазином, функциональность которого реализована на заданном HTTP-сервере. Реализуются три основных функции, есть куча ASP-примеров, написанных на VB и JS, но становится очень грустно от того, что все это завязано под одну вполне определенную платформу. В комплект поставки входят примеры (точнее заготовки) ASP, CGI и COM серверных скриптов и объектов. Учитывая то, что в мире больше, чем одна операционная система, замечательную идею «единой ЦПС для всех» можно считать успешно проваленной… Примечание: на момент подготовки данного материала автор не имел возможности провести эксперименты со сборкой CGIпримеров под другие ОС, кроме Той Самой. Вероятно, что возможность таковой сборки все же есть. К моменту проведения конференции автор будет обладать более подробной информацией на этот счет.
9. ScanNet Данная датская ПС попала в обзор по причине несколько специфического способа взаимодействия с сайтом торговца. Это касается в равной степени как подготовки формы приема платежа, так и передачи результата транзакции. Характерной особенностью данной системы является фактическое отсутствие документации, хотя все основные вопросы подробно рассмотрены в FAQ. Вроде бы, это полбеды, но основным языком является датский, а с английским служба поддержки знакома только понаслышке (читали в газете, что есть такой язык, но не более).
152
PHP Inside №13
Платежные системы - это не страшно
Тем не менее, система весьма хороша с точки зрения торговца – оплата принимается во всех основных валютах мира, поддерживаются основные международные и датские карточные системы. Достаточно удобно контролировать поступления оплаты через панель управления и статистики. Там же, в панели управления, можно загрузить шаблоны для страниц формы оплаты и результата платежа. Сама загрузка выполнена не в виде UPLOAD файлов, а в форме указания URL, с которого данные шаблоны будут загружены. Относительный путь к шаблонам так же важен в силу того, что на сервере ПС создается как бы «зеркало» данных файлов и обращение к ним будет происходить именно с использованием данного относительного пути. К примеру, если для формы оплаты был загружен шаблон с адреса http://shop.com/payment/order.html, то оплата будет производиться по адресу https://pay.scannet.dk/ИДЕНТИФИКАТОР_МАГАЗИНА/payment/ord er.html. И вот тут-то и начинается красота простого и удобного решения. Шаблоны страниц формы оплаты и результата оплаты работают на шаблонном движке, что позволяет реализовывать практически все, что угодно душе. Во-первых, это позволяет даже в простой HTML-версии магазина получать выписки на email и сохранять все необходимые переменные при переходе с сервера на сервер. Пример шаблона результата платежа для HTML версии магазина: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html><head><title>HTML DemoShop</title></head><body> <h2>HTML DemoShop</h2> <!-- ЕСЛИ ПРОИЗОШЛА ОШИБКА --> <AML:ifdef name="error"> <!-- ОТПРАВЛЯЕМ EMAIL --> <AML:mailto from="${forhandler_Email}" to="${email}" subject="ошибка оплаты ${forhandler_Navn}"> Во время проведения платежа произошли следующие ошибки: <AML:list name="error"> ${error}<br> </AML:list> </AML:mailto> <!-- ТЕКСТ ДЛЯ ВЫВОДА РЕЗУЛЬТАТА --> <font size="3"><b>Результат оплаты:</b></font><br> <b>Во время оплаты обнаружены следующие ошибки:</b> <ul> <AML:list name="error"> <li>${error}</li> </AML:list> </ul> <a href="javascript:history.back()">Вернуться назад</a><br> <br>&nbsp; </AML:ifdef> <!-- ЕСЛИ ОШИБКИ НЕ ПРОИЗОШЛО И ТРАНЗАКЦИЯ ПРОШЛА УСПЕШНО --> <AML:ifndef name="error"> <!-- ТЕСТ EMAIL --> <AML:mailto from="${forhandler_Email}" to="${email}" subject="Оплата сервиса ${forhandler_Navn}"> Уважаемый ${kunde_navn}, Вы оплатили заказ:<br> <br>
153
PHP Inside №13
Платежные системы - это не страшно
Данные заказа:<br> -----------------------------------------------------------------<br> [Покупатель]:<br> ${kunde_navn}<br> ${kunde_adresse}<br> ${kunde_postnr} ${kunde_by}<br> ${kunde_email}<br> <br> [Продавец]:<br> ${forhandler_Navn}<br> ${forhandler_Attention}<br> ${forhandler_Vej}<br> ${forhandler_Land}-${forhandler_Postnummer} ${forhandler_By}<br> <br> Дата заказа: ${dato}<br> Номер заказа: ${ordrenr}<br> <br> <AML:list name="varer"> ${antal} шт. ${navn} по ${valutatxt.get(${valuta})} ${pris.talformat(',')} итого: ${valutatxt.get(${valuta})} ${liniepris.talformat(',')}<br> </AML:list> <br> Сумма без учета НДС: ${valutatxt.get(${valuta})} ${prisumoms.talformat(',')}<br> Сумма с учетом НДС: ${valutatxt.get(${valuta})} ${prisialt.talformat(',')}<br> -----------------------------------------------------------------<br> <br> С Уважением,<br> ${forhandler_Navn}<br> </AML:mailto> <!-- ТЕКСТ С ЕЗУЛЬТАТОМ --> <h3>Результат:</h3> Уважаемый ${kunde_navn}! На Ваш email (<b>${email}</b>) был выслан оплаченный Вами счет!<br> С Уважением,<br> ${forhandler_Navn}<br> </AML:ifndef> <center> <br> <img src="https://pay.scannet.dk/images/dan-xs.gif" width="32" height="20" alt="" border="0">&nbsp; <img src="https://pay.scannet.dk/images/visa-xs.gif" width="32" height="20" alt="" border="0">&nbsp; <img src="https://pay.scannet.dk/images/ec-mc-xs.gif" width="32" height="24" alt="" border="0">&nbsp; <img src="https://pay.scannet.dk/images/elec-xs.gif" width="32" height="20" alt="" border="0">&nbsp; <img src="https://pay.scannet.dk/images/jcb-xs.gif" width="19" height="24" alt="" border="0"> </center> </body> </html>
Но всего лишь одна маленькая конструкция в шаблоне потребуется программисту на самом деле. Это конструкция <AML:include. Несложно догадаться, что она позволяет включить нужный код. Таким образом, результат обрабатывается подобным шаблоном: <!-- Transaction not OK --><AML:ifdef name="error"> <AML:include src="${shopurl}/payment/result.php?id=${id}&state=error&trans=$ {ordrenr}&checkcode=${checkcode}"/>
154
PHP Inside №13
Платежные системы - это не страшно
</AML:ifdef> <!-- Transaction OK --> <AML:ifndef name="error"> <AML:include src="${shopurl}/payment/result. php?id=${id}&state=ok&trans=$ {ordrenr}&checkcode=${checkcode}"/> </AML:ifndef>
Следует отметить, что есть предопределенный набор обязательных переменных, передаваемых в форме оплаты. Сама форма будет передаваться скрипту /cgi-bin/auth3.pl, расположенному на сервере ЦПС. Примерный вид скрипта, вызываемого из шаблона формы оплаты заказа и формирующий форму, будет таким: <?php // // // //
инициализация всех необходимых библиотек опущена. подразумевается, что данные о заказе уже извлечены из БД в переменную $order. а данные о заказанных товарах также извлечены в переменную $order_items
?> <form action="/cgi-bin/auth3.pl" method="post" autocomplete="off"> <?php $i = 1; foreach ($order_items AS $oi) { ?> <input type="hidden" name="vare_<?= $i ?>_navn" value="<?=$oi["name"]?>"> <input type="hidden" name="vare_<?= $i ?>_antal" value="<?=$oi["count"]? >"> <input type="hidden" name="vare_<?= $i ?>_pris" value="<?=$oi["price"]? >"> <input type="hidden" name="vare_<?= $i ?>_exmoms" value="<?= number_format($oi["price"]/6, 2, ".", "")?>"> <? $i++; } ?> <input type="hidden" name="id" value="<?= $order["id"] ?>"> <input type="hidden" name="navn" value="<?= $order["customer_name"] ?>"> <input type="hidden" name="adresse" value="<?= $order["customer_address"] ? >"> <input type="hidden" name="postnr" value="<?= $order["customer_zip"] ?>"> <input type="hidden" name="by" value="<?= $order["customer_city"] ?>"> <input type="hidden" name="tlf" value="<?= $order["customer_phone"] ?>"> <input type="hidden" name="email" value="<?= $order["customer_email"] ?>"> <input type="hidden" name="butiksnummer" value="НОМЕР_МАГАЗИНА"> <input type="hidden" name="shopurl" value="http://shop.com/"> <input type="hidden" name="checkcode" value="<?= $order["hash"] ?>"> <input type="hidden" name="tekst1" value=""> <input type="hidden" name="valuta" value="208"> <table> <tr> <td>Cart Number: &nbsp;&nbsp;</td> <td><input type="text" name="kortnr" value="" maxlength="16"></td> </tr> <tr> <td>Expire Date: &nbsp;&nbsp;</td> <td> <select name="udloebsmaaned"><option>01<option>02<option>03<option>04<option>05<option>0 6<option>07<option>08<option>09<option>10<option>11<option>12</select>&nbsp;/&nb sp; <select
155
PHP Inside №13
Платежные системы - это не страшно
name="udloebsaar"><option>02<option>03<option>04<option>05<option>06<option>07<o ption>08<option>09<option>10<option>11<option>12<option>13<option>14</select> mm/yy<br> </td> </tr> <tr> <td>CVC Number: &nbsp;&nbsp;</td> <td><input type="text" name="kontrol" value="" maxlength="4" size="4"></td> </tr> <tr> <td>&nbsp;</td> <td><input type="submit" value="Submit Order"></td> </tr> </table> </form>
На сайте есть пример магазина, реализованного на ASP, что позволяет, изучив данный пример, написать вполне аккуратный и красивый код. У автора материала абсолютно замечательно данные формы формировались из шаблонов, что вызывало улыбку, поскольку для стороннего человека схема, когда шаблон вызывает скрипт, использующий шаблоны для генерации контента, звучит устрашающе.
Тем не менее, датское решение мне понравилось. Выбор такого варианта был мотивирован явно не удобством пользователей, а ленью разработчиков. Так что можно смело утверждать, что именно лень, а не что-либо другое, двигатель прогресса!
Заключение Завершить хотелось бы напутствием: страшных платежных систем нет. Есть только простые. Пусть некоторые из них глюкавые, пусть встречаются недоделанные. Но ни одна из них не страшная и ни коим образом не сложная. Самое главное – это запомнить две вещи: 1. Всегда проверять все данные, которые пришли в результате транзакции. Поскольку абсолютное большинство программистов – люди весьма ленивые и не любят лишний раз нажать на кнопку, это важное замечание. Проверяйте все. Взяв за основу номер заказа, сравните оплаченную сумму, проверьте все хеши и получателя платежа. 2. Читайте спецификации (если их нет в явном виде, ищите их). Старайтесь быть в курсе всех изменений и доработок систем, с которыми вы работаете. И прислушивайтесь к советам разработчиков системы по отношению к безопасности. Они плохого советовать не будут.
156
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
Мастер-класс по влиянию TDD на дизайн кода Влияние тестирования на дизайн кода Использование в тестах реальных классов связано со значиАвтор: Сергей Юдин тельными наглядными расходами на формирование среды, в которой эти классу будут работать именно так, как это требуется в тесте. Эти расходы могут быть в виде процессорного времени или и в виде большого объема кода. Это приводит к следующим последствиям: •
Тесты работают медленно
•
Тесты часто ломаются, так как имеют зависимости от большого количества классов
•
Тесты громоздки и сложны для понимания.
Суть проблемы заключается в том, что тестам необходима изоляции тестов от максимального количества внешних деталей, которые не касаются тестируемого кода.
Возможность изоляции Итак, необходимость изоляции очевидна. Однако каким образом ее можно достигнуть. Для изолирования программисты используют такие технологии как заглушки и моки (MockObjects). Изучение технологии моков большого труда не составляет, но вот способы внедрения моков в тестируемый код, а также создание интерфейсов, на базе которых создаются моки - требуют особого разбирательства. Для того чтобы мок можно было использовать, его необходимо как-то передать в тестируемый код, а это зачастую нетривиальная задача.
Внедрение моков в тестируемый код При написании тестов изоляцию приходится обеспечивать постоянно, что имеет большие последствия для дизайна кода. Среди методов внедрения моков можно выделить передачу делегируемого класса в качестве аргумента конструктора, set/get-методы, фабричные методы, использование Registry. Общая суть изменений сводится к следующему - классы как бы отказываются от знаний, кому именно они делегируют обязанности. Для них становится важным только то, чтобы делегируемый класс “умел” делать, что от него требуется, а кто он именно такой неважно. В коде перестают напрямую использоваться конструкторы и статические вызовы, в том числе и родительские вызовы, так как в этом случае имя класса указывается явно. Как результат резко возрастает количество интерфейсов в системе. Она как бы распадается на множество независимых подсистем, каждая из которых выполняет свою задачу. Остальная часть доклада будет посвящена тому, как код изменяется для обеспечения возможности изолирования тестов.
157
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
Тестирование методов, использующих статические методы Избегайте использования статических методов - они снижают гибкость кода и затрудняют тестирование.
Нарушение принципа инверсии зависимостей при использовании статических методов При использовании статических методов мы явно указываем название класса со статическим методом. Таким образом возникает зависимость нашего класса от класса со статическим методом. При наличии такой зависимости изолировать тест нашего кода от деталей класса со статическим методом уже невозможно. Такая зависимость нашего кода от класса со статическим методов является нарушением принципа инверсии зависимостей. Рассмотрим небольшой абстрактный пример. У нас есть класс A, который состоит из одного статичного метода, совершающего некоторые действия с базой данных. Также есть класс B, который зависит от результатов, полученных от класса A: <?php class A { static public function doSomethingComplex() { // Implements complex logic } } class B { public function doSomethingUsefull(){ $result = A :: doSomethingComplex(); if ($result) return $this->_method1(); else return $this->_method2(); } protected function _method1(){} protected function _method2(){} } Тест будет схематично выглядеть таким образом: class ClassBTest extends TestCase { function setUp(){ $this->_fixtureForClassA(); $this->_fixtureForClassB(); } function testMethod1(){ $this->_provideAResult1(); $b = new B(); $this->assert($b->doSomethingUsefull(), $as_method1); } function testMethod1(){ $this->_provideAResult2(); $b = new B(); $this->assert($b->doSomethingUsefull(), $as_method2); } } В тоже время тест на класс А будет выглядеть очень похоже: class ClassATest extends TestCase{ function setUp(){ $this->_fixtureForClassA(); } function testResult1(){ $this->_provideAResult1(); $this->assert(A :: doSomethingComplex(), $as_result1); } function testResult1(){ $this->_provideAResult2(); $this->assert(A :: doSomethingComplex(), $as_result2);
158
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
}
} ?>
Для тестирования методов _method1() и method2() класса B нам нужно будет контролировать среду выполнения метода класса А. Не исключено, что для данного контроля нужно будет затратить множество усилий. То есть изоляцию тестируемого кода провести не удается. В итоге тест будет большим по размеру, чем он мог бы быть. Налицо дублирование тестового кода на класс B и на класс А.
Решение проблемы Для того чтобы приведенный пример следовал принципу инверсии зависимостей необходимо: •
Ввести интерфейс, например, ComplexDoer.
•
Сделать так, чтобы класс A реализовывал этот интерфейс.
•
Создать в классе B метод setComplexDoer($doer) и передавать туда объект класса, поддерживающего интерфейс ComplexDoer.
В нашем случае это может быть класс A, а в тесте мок на интефейс ComplexDoer. Также объект с интерфейсом ComplexDoer можно передавать в конструкторе класса B. •
Изменить поведение метода doSomethingUsefull() класса B так, чтобы в нем использовался объект, реализующий интерфейс ComplexDoer вместо вызова статичного метода класса A.
Таким образом, класс B больше не будет зависеть от класса A. Вместо этого класс A будет зависеть от интерфейса ComplexDoer (в PHP4 можно наследоваться от базового класса, если класс реализует только 1 интерфейс). Вот как должен измениться код: class ComplexDoer(){ function doSomethingComplex(){die('implement me!');} } class A extends ComplexDoer { function doSomethingComplex() { // Implements some logic} class B{ var a; function B(&$a){$this->a =& $a;} public function doSomethingUsefull(){ $result = $this->a->doSomethingComplex();
}
}
if ($result) return $this->_method1(); else return $this->_method2();
function _method1(){} function _method2(){}
Теперь тесты: Mock :: generate('ComplexDoer');
159
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
class ClassBTest extends TestCase{ var $a; function setUp(){ $this->a = new MockComplexDoer($this); $this->_fixtureForClassB(); } function tearDown(){$this->a->tally();} function testMethod1(){ $this->a->expectOnce('doSomethingComplex'); $this->a->setReturnValue('doSomethingComplex', $result1);
}
}
}
$b = new B($this->a); $this->assert($b->doSomethingUsefull(), $as_method1); function testMethod1() { $this->a->expectOnce('doSomethingComplex'); $this->a->setReturnValue('doSomethingComplex', $result1); $b = new B($this->a); $this->assert($b->doSomethingUsefull(), $as_method1);
В тоже время тест на класс А будет выглядеть по-старому, только тестироваться будут уже методы объекта, а не статические методы.
Сокрытие вызова статического метода для изоляции теста Разработчик не всегда имеет возможность изменять класс со статическими методами, например, в случае, если это внешняя библиотека. Для упрощения тестирования класса, который вызывает статические методы другого класса, можно ввести метод, который будет скрывать вызов статического метода. В некоторых случаях этот метод - единственно доступный для упрощения тестов: class B { function doSomethingUsefull(){ $result = $this->_doSomethingComplex();
}
}
if ($result)return $this->_method1(); else return $this->_method2();
function _doSomethingComplex(){ return A :: doSomethingComplex(); }
Теперь в тесте можно создать частичный мок на класс B и иметь возможность контролировать результат метода _doSomethingComplex(). Тесты таким образом можно значительно упростить. Mock :: generatePartial('B', 'BTestVersion', array('_doSomethingComplex')); class ClassBTest extends TestCase{ function setUp(){ $this->_fixtureForClassB(); }
160
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
function testMethod1(){ $b = new BTestVersion($this); $b->expectOnce('_doSomethingComplex'); $b->setReturnValue('_doSomethingComplex', $result1); $this->assert($b->doSomethingUsefull(), $as_method1); $b->tally(); }
}
function testMethod1(){ $b = new BTestVersion($this); $b->expectOnce('_doSomethingComplex'); $b->setReturnValue('_doSomethingComplex', $result2); $this->assert($b->doSomethingUsefull(), $as_method2); $b->tally(); }
Стоит обратить внимание, что теперь в системе есть один непротестированный метод, а именно _doSomethingComplex класса B. Использование частичных моков может быть очень опасным, поэтому без тестов более высокого уровня все равно не обойтись.
Тестирование и паттерн Singleton. Отказываемся от одиночек. Использование паттерна Registry Избегайте использования одиночек, где это возможно. Они значительно ограничивают гибкость кода и затрудняют тестирование. Одиночки можно использовать только для делегирования обязанностей другим классам. Используйте паттерн Registry вместо множества одиночек. Причина в том, что одиночкам свойственны все недостатки присущие статическим методам: по причине явного указания имени класса одиночки затрудняется тестирование, а проект развивать становится труднее.
Использование одиночек в качестве диспетчеров Основная проблема с одиночками - это явное указание класса одиночки в коде. Так как мы не можем изменить эту зависимость, значит нужно иметь возможность подменять реализацию интерфейса одиночки. То есть одиночки нужно использовать для делегирования выполнения методов другим объектам. Например, вместо: class A { function method1() { $b =& B :: instance(); $b->doSomething(); } } class B { function & instance(){...} function doSomething(){// method implementation} }
Гораздо лучше будет сделать так:
class A { function method1() { $b =& B :: instance(); $b->doSomething(); } }
161
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
class B // implements Server { var $server; function & instance(){...} function doSomething(){$this->server->doSomething(); } }
function setServer(&$server){$this->server =& $server;}
class C // implements Server { function doSomething() { // method implementation } }
Теперь можно передать в одиночку B объект другого класса в качестве $server. То есть мы будем иметь возможность расширить поведение класса A, в случае необходимости. Теперь тест не обязан знать детали реализации интерфейса Server. Mock :: generate('Server'); class ClassATest extends TestCase { var $mock_server; function setUp() { $this->mock_server = new MockServer($this); $this->_fixtureForClassA(); } function tearDown(){ $this->mock_server->tally(); } function test1(){ $b =& B :: intance(); $b->setServer($this->mock_server); $this->mock_server->expectOnce('doSomething'); $this->mock_server->setReturnValue('doSomething', $for_result1);
}
$a = new A(); $this->assert($a->method1(), $result1);
}
Данный способ хорошо зарекомендовал себя в случае, когда одиночек мало, а их интерфейсы не слишком раздуты.
Использование паттерна Registry Мы предпочитаем не разводить множество классов-одиночек, а использовать один класс-одиночку с широким интерфейсом для получения других объектов. В конце концов, мы пришли к следующему решению насчет одиночек: Вводится класс Toolkit, который содержит методы для получения большинства одиночек, а также многие фабричные методы. Toolkit-ы обычно создаются для какого-либо пакета. •
Toolkit-ы хранятся в специальном хранилище, который мы называем Limb.
•
Limb может содержать несколько toolkit-тов одного или различных классов одновременно.
•
Методы Limb-а статические, а сам он выполнен в виде одиночки.
162
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
По сути, это видоизмененный паттерн Registry (у нас toolkit обладает четким интерфейсом). class Limb{ var $_toolkits = array(array()); function & instance(); function registerToolkit(&$toolkit, $name = 'default'){ $instance =& Limb :: instance(); array_push($instance->toolkits[$name], $toolkit); } function restoreToolkit($name = 'default') { $instance =& Limb :: instance(); if (isset($instance->toolkits[$name])) return array_pop($instance->toolkits[$name]); } function toolkit($name = 'default') { $instance =& Limb :: instance(); if (isset($instance->toolkits[$name])) return end($instance->toolkits[$name]); }
}
function saveToolkit($name = 'default') { //no &, we simply make a copy $toolkit = clone(Limb :: toolkit($name)); $toolkit->reset(); Limb :: registerToolkit($toolkit, $name); }
class BaseToolkit { function & getDB() { return DB :: instance();} function & getUser(){ return User :: instance(); } function & createDBTable($table_name){ return $this->db_table_factory->createDBTable($table_name); }
} BaseToolkit нужно регистрировать в Limb: $toolkit = new BaseToolkit(); Limb :: registerToolkit($toolkit, 'base');
Для получения объекта-подключения к базе данных будет использоваться следующий код: $tookit =& Limb :: toolkit('base'); $db = $tookit->getDB();
Теперь в тестах мы легко можем проводить любую необходимую изоляцию, а зависимости между классами – минимальны: class A { function method1(){ $toolkit =& Limb :: toolkit('base'); $server =& $toolkit->getServer(); $server->doSomething(); } } class BaseToolkit { var $server; function BaseToolkit() { $this->reset(); }
163
PHP Inside №13
}
Мастер-класс по влиянию TDD на дизайн кода
function &getServer(){ return $server; } function reset() { $this->server = new Server();}
И тест:
class ClassATest extends TestCase{ var $mock_server; var $mock_toolkit; function setUp() { $this->mock_server = new MockServer($this); $this->toolkit = new MockBaseToolkit($this); $this->toolkit->setReturnReference('getServer', $this->mock_server); Limb :: registerToolkit($this->toolkit, 'base'); $this->_fixtureForClassA();
}
function tearDown() { $this->mock_server->tally(); $this->toolkit->tally(); Limb :: restoreToolkit(); }
}
function test1(){ $this->mock_server->expectOnce('doSomething'); $this->mock_server->setReturnValue('doSomething', $for_result1); $a = new A(); $this->assert($a->method1(), $result1); }
Отказ от одиночек При использовании Registry можно отказаться от использования одиночек, в случаях: •
если их внедрение связано именно с экономией на инициализации объекта,
•
если класс часто используется клиентами, однако нет строгого правила того, чтобы он был синглтоном.
Если все клиенты используют Registry для получения часто используемых объектов, то достаточно внедрить LazyLoading для каждого такого объекта. Например: class AnyToolkit{ var $_some_object; function & getSomeObject(){ if ($this->_some_object) return $this->_some_object;
}
}
$this->_some_object = new SomeObject(); return $this->_some_object;
В случае если требование наличие только одного экземпляра объекта какого-либо класса сохраняется, то его конструктор можно закрыть и все также использовать одиночку. Условие только одно клиенты должны получать этот экземпляр только через Registry, чтобы возможность изоляции сохранялась.
164
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
Тестирование и наследование. Отдаем предпочтение делегированию Избегайте наследования. Предпочитайте делегирование. Дочерние классы не должны расширять функциональность родителей, если эта функциональность родителей связана с внешними ресурсами. Родительские классы по возможности должны быть абстрактными. Написание тестов на дочерние классы - простейший способ поиска узких мест в дизайне кода. Модульное тестирование приводит к появлению небольших классов с четкими интерфейсами.
Сложность тестирования дочерних классов Тестирование дочерних классов в иерархии наследования может создать множество проблем. Это особенно актуально, если родительский класс имеет сложное поведение и его поведение зависит от внешних ресурсов. При тестировании дочерних классов нужно будет контролировать среду работы родительского класса. А это уже дублирование и откровенно «попахивает». Из-за этого часто тестированием дочерних классов пренебрегают, особенно, если изменения в поведении, по сравнению с родительским классом небольшие. Часто причиной сложностей тестирования дочерних классов является чрезмерное использование паттерна Template Method для расширения поведения родительского класса. То есть в родительском классе выделяют методы, которые дочерние классы могут перекрывать. Рассмотрим небольшой пример: class A{ function perform(){ $this->_doSomethingA();
}
}
if ($this->_someConditionA()) $this->_extentionMethod1(); else $this->_extentionMethod2();
function function function function
_extentionMethod1(){} _extentionMethod2(){} _someConditionA(){// Some useful functionality} _doSomethingA(){// Some useful functionality}
class B extends A { function _extentionMethod1(){ // Some useful functionality } function _extentionMethod2(){ // Some useful functionality } }
Попробуем представить тестирование классов примера. Для тестирования метода _someConditionA() класса А через публичный метод perform() нужно будет создать частичный мок с методами _extentionMethod1() и _extentionMethod2(). В тесте нам нужно будет таким образом настраивать среду исполнения метода perform(), чтобы в результате работы _someConditionA() был частичным моком был зафиксирован вызов того или иного метода:
Mock :: generatePartial('A', 'ATestVersion', array('_extentionMethod1', '_extentionMethod2')); class ClassATest extends TestCase{
165
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
function testSomeCondition1() { $this->_provideConditionResult1(); $a = new ATestVersion($this); $a->expectOnce('_extentionMethod1'); $a->perform(); $a->tally(); }
}
function testSomeCondition2(){ $this->_provideConditionResult2(); $a = new ATestVersion($this); $a->expectOnce('_extentionMethod2'); $a->perform(); $a->tally(); }
Проблема в том, что практически те же самые действия по настройке среды исполнения метода _someConditionA() нужно будет сделать при тестировании методов _extentionMethod1() и _extentionMethod2() класса B. А это уже дублирование кода - ничего хорошего в нем нет.
Наш выбор – делегирование Для того чтобы сделать тестирование легким нам необходимо сделать следующее: Класс B разбить на 2 класса: Method1 и Method2 с методами perform(). •
Выделить из класса A класс ConditionA с методом perform(), который в конструкторе получает экземпляры Method1 и Method2.
•
Каждый из таких мелких классов протестировать намного легче. При тестировании ConditionA нам необходимо будет создать моки на интерфейс, содержащий метод perform(), и проверять, что был вызов к одному из делегируемых классов.
Вероятность повторного использования какого-либо из новых классов значительно повышается. class A{function perform(){}} class ConditionA{ var $cons1; var $cons2; function ConditionA(&$cons1, &$cons2){ $this->cons1 =& $cons1; $this->cons2 =& $cons2; } function perform(){ if ($this->_someConditionA()) $this->cons1->perform(); else $this->cons2->perform(); } function _someConditionA(){} } class Method1 {function perform(){}} class Method2 {function perform(){}}
В результате мы получаем несколько четко-определенных классов, которые легко можно использовать повторно. Для опытных разработчиков такой рефакторинг может показаться очевидным, однако его острую (или раннюю) необходимость диктуют именно тесты.
166
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
Тестирование методов, связанных с операциями с базами данных Методы, которые имеют SQL код - необходимо тестировать с реальным подключением к базе данных. Метод, в котором есть SQL-код, должен быть, по возможности, как можно более коротким, для того, чтобы тест был также коротким и быстрым. В идеале тест на класс, содержащий SQL-код, содержит только 1 тестовый метод. Нельзя писать код, который совмещает доступ к базе данных и логику по обработке данных из базы данных. Это делает тесты громоздкими, сложными и медленными. Поэтому бизнес-логику и доступ к базе данных нужно разделать на отдельные классы тестировать отдельно. Код, который делегирует классам DBAL и который не содержит SQL-кода, можно смело изолировать от базы данных.
Среда тестирования Для тестирования подключения к базе данных необходима отдельная база данных, отличная от рабочей. Использование транзакций не подходит, так как нам нужно будет четко контролировать содержимое для получения ожидаемых ответов, поэтому приходится использовать еще одну тестовую базу данных. Она должна содержать те же таблицы, что будут использоваться в продукционной базе данных при условии, что у вас есть комплект приемочных тестов или только те таблицы, которые необходимы для тестирования. Таблицы тестовой базы данных должны быть пустыми перед началом и после выполнения теста. Все данные, которые необходимы в том или ином тестовой случае, должны будут записаны в таблицы в методе setUp() и удалены в tearDown(). Так как в php нет отдельной фазы компиляции, а php4 нет обработки исключительных ситуаций, это значит, что скрипт может закончить свою работу в любой момент, поэтому у нас сложилась практика вызывать метод для очищения тестовой базы данных также и в setUp() теста. Метод для очистки базы данных можно, правда, включать только в первый тестовый метод вместо setUp(). Вот пример типового теста на код, связанный с работой с базой данных. class DBTableTest extends LimbTestCase{ var $db = null; function setUp(){ $toolkit =& Limb :: toolkit(); $this->db =& new SimpleDB($toolkit->getDbConnection); $this->_cleanUp(); }
}
function tearDown(){$this->_cleanUp();} function _cleanUp(){$this->db->delete('test1');} //...several test methods for sql-containing code
167
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
Фикстура и изоляция тестовых методов друг от друга Вопрос делать ли отдельные тестовые методы полностью независимыми друг от друга является очень важным. Некоторые разработчики предпочитают добавлять данные в базу только один раз, а удалять в самом конце. Это якобы положительно сказывается на скорости выполнения тестов. Наш опыт подсказывает, что если разделение бизнес логики и работа с базой данных в тестируемом коде проведены грамотно, тесты, не изолированные от базы данных, имеют весьма небольшие размеры и содержат минимальное количество отдельных методов. К тому же мы стараемся выделять тесты DAO-классов в отдельные группы, которые можно не выполнять каждый раз. Поэтому мы предпочитаем полностью изолировать тестовые методы друг от друга.
Использование моков при тестировании баз данных Старайтесь не использовать моки во время тестирования SQLкода. Протестировать процедуру формирования правильного SQLкода обычно недостаточно. Вам нужно протестировать именно то, как реальный драйвер базы данных отреагирует на реальный запрос. Учитывайте также, что различные базы данных могут интерпретировать один и тот же запрос по-разному. Поэтому смиритесь, что тесты будут чуть медленнее, - это снизит количество головной боли в будущем.
Разделение бизнес-логики и получение данных Начальное состояние Рассмотрим некий класс FeaturedOrdersDAO, который выбирает только те заказы, которые удовлетворяют некоторому критерию. Причем этот критерий почему-то не может быть применен на этапе выборки из базы данных, так как является элементом сложной бизнес-логики. Сам критерий зависит еще от каких-либо внешних параметров, например, времени года. Для заполнения объектов класса Order из массива данных используется OrderMapper. class FeaturedOrdersDAO{ function & fetch(){ $toolkit =& Limb :: toolkit(); $db =& new SimpleDB($toolkit->getDbConnection()); $sql = 'SELECT * FROM orders'; $stmt =& $db->createStatement($sql); $orders_rs =& stmt->getRecordSet(); $order_mapper = new OrderMapper(); $result = array(); for($orders_rs->rewind(); $orders_rs->valid(); $orders_rs->next()){ $order_record =& orders_rs->current(); $order =& $order_mapper->map($order_record); if($this->_passCriteria($order)) $result[] =& $order_record;
}
} return new PagedArrayDataset($result);
168
PHP Inside №13
}
Мастер-класс по влиянию TDD на дизайн кода
function _passCriteria($order){ //some complex logic }
Представим, каким будет выглядеть тест на данный класс. Необходимо будет занести несколько записей в базу данных, проверить, как работает метод _passCriteria() для различных заказов, при этом OrderMapper должен работать как часы. Тестирование бизнес-логики, как правило, связано с различными условиями (помним про время года), поэтому тестовых методов может быть много. Схематично, тест будет выглядеть так: class FeaturedOrdersDAOTest extends TestCase{ function setUp() { $this->_cleanUp(); $this->_loadOrders(); } function tearDown(){$this->_cleanUp(); }
}
function testManyDifferentOrdersForWinter(){} function testManyDifferentOrdersForSummer(){} function testManyDifferentOrdersForSpring(){}
Его явные недостатки - он медленный, так как каждый тестовый метод требует записи заказов в базу данных. Каждый тестовый метод содержит проверку различных типов заказов, что делает тест большим и малопонятным. А при изменении бизнес-правил нужно будет поправить большой объем кода. Итак, тест показывает, что классу просто необходим рефакторинг.
Выносим SQL-код в отдельный класс Первым делом мы инкапсулируем работу с DB в отдельный класс OrdersDAO, чтобы эту часть кода можно было бы использовать повторно. Введем класс OrdersDAO и будем получать объекты этого класса через Registry. class FeaturedOrdersDAO { function & fetch() { $toolkit =& Limb :: toolkit(); $orders_dao =& $toolkit->createDAO('OrdersDAO'); $orders_rs =& $orders_dao->fetch(); $order_mapper = new OrderMapper(); $result = array(); for($orders_rs->rewind(); $orders_rs->valid(); $orders_rs->next()){ $order_record =& orders_rs->current(); $order =& $order_mapper->map($order_record); if($this->_passCriteria($order)) $result[] =& $order;
} ... }
} return new PagedArrayDataset($result);
Теперь выборка заказов инкапсулирована в класс OrdersDAO (тело класса мы показывать не будем). OrdersDAO можно тестировать отдельно. Так как мы легко можем менять поведение объекта $toolkit, чтобы он возвращал мок на OrdersDAO, это позволит изолировать тест на FeaturedOrderDAO от базы данных.
169
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
class FeaturedOrdersDAOTest extends TestCase { function setUp() { $mock_orders_dao = new MockDAO($this); $mock_orders_dao->expectOnce('fetch'); $mock_orders_dao->setReturnReference('fetch', $orders_rs); $mock_toolkit = MockToolkit($this); $mock_toolkit->setReturnReference('OrdersDAO', $this->_mock_orders_dao); }
Limb :: registerToolkit($mock_toolkit);
function tearDown(){ $this->mock_orders_dao->tally(); Limb :: restoreToolkit($mock_toolkit) }
}
function testManyDifferentOrdersForWinter(){} function testManyDifferentOrdersForSummer(){} function testManyDifferentOrdersForSpring(){}
Теперь тест будет намного быстрее. Однако он все также громоздок и сложен.
Выносим бизнес логику в отдельный класс Следующим шагом может стать вынос метода _passCriteria() в отдельный класс, например, FeaturedOrdersCriteria, у которого будет метод pass(&$order). По сути, мы применяем здесь паттерн Specification, в краткой форме. class FeaturedOrderCriteria{ function pass(&$order){ // some complex logic that return true or false} }
Этот класс гораздо легче тестировать в одиночку, более того, теперь этот элемент бизнес-логики можно легко использовать повторно с другими подобными классами. class FeaturedOrderCriteriaTest extends TestCase{ function testOrder1ForWinter(){} function testOrder2ForWinter(){} function testOrder3ForWinter(){} ... }
Конечное состояние FeaturedOrdersDAO теперь можно модифицировать таким образом, чтобы он мог работать с любым критерием: class FeaturedOrdersDAO { var $criteria; function FeaturedOrdersDAO(&$criteria){$this->criteria =& $criteria;} function & fetch() { $toolkit =& Limb :: toolkit(); $orders_dao =& $toolkit->createDAO('OrdersDAO'); $orders_rs =& $orders_dao->fetch(); $order_mapper = new OrderMapper(); $result = array(); for($orders_rs->rewind(); $orders_rs->valid(); $orders_rs->next()) { $order_record =& orders_rs->current();
170
PHP Inside №13
Мастер-класс по влиянию TDD на дизайн кода
$order =& $order_mapper->map($order_record);
}
if($this->criteria->pass($order)) $result[] =& $order;
return new PagedArrayDataset($result);
} ... }
Теперь в тест можно будет также изолировать от деталей бизнес-логики, что позволит сделать его очень кратким. По сути, мы теперь имеет класс-диспетчер, который лишь делегирует обязанности других классам. class FeaturedOrdersDAOTest extends TestCase{ function setUp(){ $mock_orders_dao = new MockDAO($this); $mock_toolkit = MockToolkit($this); $mock_toolkit->setReturnReference('OrdersDAO', $this->_mock_orders_dao); Limb :: registerToolkit($mock_toolkit);
}
function tearDown(){ $this->mock_orders_dao->tally(); Limb :: restoreToolkit($mock_toolkit)
}
}
}
function testFetch(){ $orders_rs = new .... (2 orders in rs initially); $expected_rs = new .... (1 order in rs - only 1 should pass); $mock_orders_dao->expectOnce('fetch'); $mock_orders_dao->setReturnReference('fetch', $orders_rs); $mock_criteria = new MockOrderCriteria($this); $mock_criteria->expectCallCount('pass', 2); $mock_criteria->setReturnValueAt(0, 'pass', true); $mock_criteria->setReturnValueAt(1, 'pass', false); $dao = new FeaturedOrdersDAO($mock_criteria); $this->assertEqual($dao->fetch(), $expected_rs); $mock_criteria->tally();
171
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
Мастер-класс по введению TDD в существующий проект Самым сложным шагом в освоении TDD является, что не Автор: Павел Щеваев удивительно, самый первый. Многие разработчики с радостью мечтают перейти от хаосной разработки к TDD, однако проблема первого шага (а точнее вопрос, “А с чего же собственно начать?”) не позволяет им сделать этого. Еще более непреодолимым препятствием является уже существующий код, который в принципе трудно протестировать, т.к. он разрабатывался не в рамках TDD. Но…так ли это на самом деле? Мы попытаемся показать на примере, что практически любое существующее приложение можно перевести на рельсы TDD, используя определенные техники тестирования. Быть может, этот пример и поможет вам сделать этот самый “первый шаг”.
Приложение по формированию обратной связи с посетителями веб-узла Нас все устраивает Итак, представим себе ситуацию, что у нас есть некоторое приложение, которое позволяет посетителям веб-узла, связываться с владельцем ресурса, оставляя сообщения различного характера. Вот список того, что именно наше приложение умеет: •
принимать сообщения от посетителей и сохранять их в MySQL БД
•
выводить сообщения на фронтовой части, используя пейджер
Допустим, не мы сами писали это приложение, и оно предоставляет собой кандидата на звание “Лучшая итальянская лапша 2005 года”. Однако приложение работает вроде бы исправно, и у нас даже и мысли не появляется, чтобы заняться его рефакторингом, не говоря уже о тестах, т.к .это настоящее сумасшествие. К тому же лозунг “не трогайте то, что уже работает”, нас вполне устраивает в данной ситуации. Файловая структура приложения: +--styles | +--js | +--index.php связи | +--db.php
-- центральный скрипт по добавлению, выводу сообщений обратной -- конфигурационные данные для БД
Схема базы данных приложения:
feedback
CREATE TABLE `feedback` ( `id` int(11) NOT NULL auto_increment, `name` varchar(255) default NULL, `time` int(11) default NULL, `message` text,
172
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
`email` varchar(255) default NULL, PRIMARY KEY (`id`) ) TYPE=MyISAM
Содержимое db.php: <?php $db_host = $db_name = $db_user = $db_password = ?>
'localhost'; 'feedback'; 'root'; 'test';
Содержимое index.php:
<?php ob_start(); ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=windows-1251"> <link rel=stylesheet type='text/css' href='styles/main.css'> <script language="JavaScript" type="text/javascript" src="js/form.js"></script> </head> <body> <table width="100%" style="height:100%" border="0" cellpadding="0" cellspacing="0"> <tr> <td width="10%" style="background-color:#7F1A22"> </td> <td width="90%" style="padding:5px 10px 5px 10px" valign="top"> <h1>Обратная связь</h1> <form action="index.php" method="post" onsubmit="return submit_form(this);"> <table> <tr> <td align="right">Ваше имя:</td> <td><input name="name" value="" type="text"></td> </tr> <tr> <td align="right">Email:</td> <td><input name="email" value="" type="text"></td> </tr> <tr> <td align="right">Текст вопроса:</td> <td><textarea name="message" cols="50" rows="4"></textarea></td> </tr> <tr> <td></td> <td><input value="Отправить" name="submit" type="submit"></td> </tr> </table> </form> <?php include_once('db.php'); $conn = mysql_connect($db_host, $db_user, $db_password); if($conn === FALSE) die('db connect error: ' . mysql_error()); if(!mysql_select_db($db_name, $conn)) die('can not use db: ' . mysql_error()); if(isset($_POST['submit'])) { $name = mysql_escape_string($_POST['name']); $email = mysql_escape_string($_POST['email']); $message = mysql_escape_string($_POST['message']);
173
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
$time = time(); $sql = "INSERT INTO feedback (name, email, message, time) VALUES ('$name', '$email', '$message', '$time')"; $result = mysql_query($sql, $conn); if(!$result) die('invalid query: ' . mysql_error());
}
$limit = 3; $offset = isset($_GET['offset']) ? $_GET['offset'] : 0; $sql = "SELECT * FROM feedback ORDER BY time DESC LIMIT " . ($offset * $limit) . ", {$limit}"; $fetch_result = mysql_query($sql, $conn); if(!$fetch_result) die('invalid query: ' . mysql_error()); $sql = "SELECT COUNT(*) as counter FROM feedback"; $count_result = mysql_query($sql, $conn); if(!$count_result) die('invalid query: ' . mysql_error()); $row = mysql_fetch_assoc($count_result); $total = (int)$row['counter']; ?> <?php if($offset > 0) :?><b><a href="?offset=<?=($offset-1)?>">&lt;</a></b><?php endif; ?> <?php if($total > 0) :?><?=($offset*$limit)+1?> - <?=(($offset+1)*$limit > $total)? $total : ($offset+1)*$limit ?><?php endif; ?> <?php if(($offset+1)*$limit < $total) :?><b><a href="?offset=<?=($offset+1)? >">&gt;</a></b><?php endif; ?> <?php while ($row = mysql_fetch_assoc($fetch_result)) { ?> <hr> <b>Автор:</b> <a href="mailto:<?=htmlspecialchars($row['email']);? >"><?=htmlspecialchars($row['name']);?></a><br> <b>Сообщение:</b> <?=htmlspecialchars($row['message']);?><br> <b>Время:</b> <?=strftime("%m/%d/%y %H:%M:%S", $row['time'])?></br> <?php } ?> </td> </tr> </table> </body> </html> <?php ob_end_flush(); ?>
Нет, только не это! Однако совершенно неожиданно от новоизбранного начальника отдела инноваций вдруг поступает требование расширить функциональность приложения и сделать его более гибким, а именно: •
заложить возможность перехода к отличной от MySQL БД для хранения сообщений
•
использовать шаблоны для вывода фронтовой части сообщений, т.к. дизайнеры не в силах разобраться в существующем коде
•
сделать уведомление администратора сайта по электронной почте при поступлении нового 174
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
сообщения План действий Изменения носят глобальный характер и без практически полного переписывания кода нам не обойтись. Двигаться следует небольшими, но решительными шагами. План таков: •
используем WACT(версия 0.02, http://wact.sourceforge.net) в качестве шаблонной системы и в качестве DBAL(Database Abstraction Layer)
•
используем phpmailer(версия 1.73, http://phpmailer.sourceforge.net) для формирования уведомлений по электронной почте Все внешние библиотеки будем хранить в директории external.
План нас устраивает, но остается вопрос, как же максимальным образом себя обезопасить, от возможных ошибок на каждой итерации разработки? Меньше всего хотелось бы сломать уже существующее приложение… А что если мы попытаться вначале каким-то образом “зафиксировать” правильность работы уже существующей функциональности? Идея заманчивая, но как это сделать?
Функциональное тестирование Прежде чем мы начнем глобальные изменения, мы попробуем “зафиксировать” приложение при помощи функциональных тестов, используя замечательную библиотеку SimpleTest (http://www.lastcraft.com/simple_test.php) и входящий в нее web tester пакет (http://www.lastcraft.com/web_tester_documentation.php) для проведения функционального веб-тестирования.
Веб-тестирование при помощи SimpleTest Работа с библиотекой SimpleTest при веб-тестировании определенным образом напоминает работу непосредственно с браузером. В самом деле, WebTestCase предоставляет удобные методы, эмулирующие браузер, а именно: •
получение страниц по адресу
•
навигация по ссылкам и кнопкам
•
заполнение и отправление форм
•
организация непосредственных GET, POST, HEAD запросов
•
эмуляция фреймов
•
формирование HTTP заголовков
•
установка и модификация cookies
Кроме этого WebTestCase позволяет посмотреть на запрос “изнутри”: •
вывести на экран дамп данных запроса
•
отобразить HTTP заголовки
•
показать исходный код полученной страницы
175
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
И, конечно же, самой главной особенностью WebTestCase является способность проверить полученные в процессе браузинга результаты, вот лишь некоторые возможности: •
сравнить контент страницы на предмет совпадения с некоторым регулярным выражением
•
проверить содержимое <title> тега
•
проверить наличие ссылок и их содержимого
•
удостовериться в правильности содержимого полей формы
•
проверить cookie на содержимое
•
проверить HTTP заголовки
Для всего вышеописанного WebTestCase предоставляет исключительно чистые и понятные интерфейсы, превращающие работу с ним в удовольствие, и скоро в этом убедитесь.
Знакомимся с SimpleTest ближе Библиотека SimpleTest является одним из самых популярных xUnit аналогов для PHP, и по сути, основывается на формировании некоторого количества утверждений, которым должна соответствовать тестируемая сущность. Фокус в том, что утверждения записываются тестирующим программным кодом, что позволяет им быть синхронизированными с кодом приложения, в отличие от словесных описаний, которые имеют тенденцию устаревать через определенное время разработки. Рассмотрим наипростейший пример, который тестирует тот факт, что на странице по адресу http://www.lastcraft.com/simple_test.php присутствует надпись “Simple Test for PHP”. Создайте следующий небольшой скрипт (simpletest должен находиться в php.ini inlude_path): <?php require_once('simpletest/web_tester.php'); require_once('simpletest/reporter.php'); class TestOfSimpleTestHomePage extends WebTestCase { function testSimpleTestHome() { $this->get('http://www.lastcraft.com/simple_test.php'); $this->assertWantedPattern('/Simple Test for PHP/'); } } $test = new TestOfSimpleTestHomePage('Web tests'); exit ($test->run(new TextReporter()) ? 0 : 1); ?>
Если все прошло нормально, то после запуска данного скрипта в консоли, вы должны увидеть нечто похожее: Web site tests OK
Test cases run: 1/1, Failures: 0, Exceptions: 0
176
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
Поздравляю, наш первый функциональный веб-тест сработал! Как вы успели заметить, тестовые классы являются наследниками от служебного класса библиотеки SimpleTest (WebTestCase в нашем случае) и организуются при помощи методов, которые начинаются со слова test (testSimpleTestHome() в нашем примере). Каждый такой метод должен иметь название, вкратце отражающее суть тестового прецедента. Таких методов может быть бесконечное множество и все вместе они формируют тестовый набор.
Вводим функциональные веб-тесты для нашего приложения Подготавливаем тестовую среду Для начала создадим директорию tests, в которой будут располагаться все тесты для нашего приложения. В этой директории создадим файл runtests.php следующего содержания: <?php require_once(dirname(__FILE__) . '/setup.php'); class AllTests extends GroupTest { function AllTests() { $this->GroupTest('All tests for feedback project'); $this->addTestFile('acceptance_tests.php'); } } $test =& new AllTests(); if (SimpleReporter::inCli()) exit ($test->run(new TextReporter()) ? 0 : 1); $test->run(new HtmlReporter()); ?>
Этот скрипт будет точкой входа для всех тестов, причем его можно запускать как из консоли, так и из браузера. Для работы этого скрипта нам также потребуются файлы setup.php и acceptance_tests.php. В setup.php мы будем хранить глобальные настройки для всех тестов. Пока мы в нем только подключаем библиотеку SimpleTest и определяем адрес веб-хоста с приложением:
<?php define('SIMPLE_TEST', dirname(__FILE__) . '/../external/simpletest/'); define('FEEDBACK_PROJECT_HOST', 'http://localhost/feedback/'); if (!file_exists(SIMPLE_TEST . '/browser.php')) die ('Make sure the SIMPLE_TEST constant is set correctly in this file(' . SIMPLE_TEST . ')'); require_once(SIMPLE_TEST require_once(SIMPLE_TEST require_once(SIMPLE_TEST require_once(SIMPLE_TEST ?>
. . . .
'/web_tester.php'); '/reporter.php'); '/unit_tester.php'); '/mock_objects.php');
Первые тесты В acceptance_tests.php будут располагаться все функциональные тесты для приложения. Не долго раздумывая, поместим в него самый первый тест: class AcceptanceTestOfFeedbackProject extends WebTestCase {
177
PHP Inside №13
}
Мастер-класс по введению TDD в существующий проект
function testOfIndexPage() { $this->get(FEEDBACK_PROJECT_HOST); $this->assertWantedPattern('/Обратная связь/'); }
Удостоверившись в том, что он сработал, переходим к более сложному тесту, целью которого будет проверка правильности отправки формы: class AcceptanceTestOfFeedbackProject extends WebTestCase { [...] function testOfSimpleSubmitFeedback() { $this->_addFeedback($name = 'Bobby', $email = 'email@dot.com', $message = "This a message with `non-escaped characters`"); $this->assertWantedPattern('/' . preg_quote($email) . '.*' . $name . '.*' . $message . '/s');
}
} function _addFeedback($name, $email, $message) { $this->get(FEEDBACK_PROJECT_HOST); $this->setField('name', $name); $this->setField('email', $email); $this->setField('message', $message); $this->clickSubmitByName('submit'); sleep(1); }
Как вы успели заметить, мы также добавили внутренний метод, _addFeedback, который заполняет поля формы и отсылает ее. Этот метод окажется весьма кстати в последующих тестах. Постоянный рефакторинг тестов - не менее важная задача, чем рефакторинг тестируемого кода. Чтобы избежать ситуации, когда у нас может быть несколько сообщений, пришедших в одно и то же время, мы принуждаем PHP “засыпать” на 1 секунду после добавления каждого сообщения. Этот тест также успешно срабатывает, но мы работаем с продукционной базой данных, что крайне опасно!!! Нам необходимо некоторым образом заставить приложение работать с другими настройками БД. К счастью, оригинальные разработчики решили хранить конфигурационные данные в отдельном файле db.php, который подключается в index.php. Мы можем на время тестов заменять db.php другим файлом, в котором находятся тестовые настройки. Но как это сделать лучше всего?
Устанавливаем фикстуру Каждый тестовый прецедент должен в идеале быть независимым, атомарным и выполняться в “чистой” среде, в которой не осталось мусора от выполнения предыдущих прецедентов. SimpleTest позволяет подготовить некоторую окружающую среду для каждого тестового прецедента. Такая окружающая среда называется фикстурой(fixture). Сделать это можно при помощи методов setUp() и tearDown(). Эти методы вызываются соответственно до и после каждого тестового метода, что дает возможность разработчику произвести определенные подготавливающие мероприятия (очистка/заполнение БД, удаление временных файлов и проч.). 178
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
Как упоминалось ранее, сделаем так, чтобы на время тестов настройки базы данных подменивались тестовыми значениями. Для этого в директории tests создадим файл db.php - аналог того, который находится в корне приложения. <?php $db_host = $db_name = $db_user = $db_password = ?>
'localhost'; 'feedback-web-tests'; 'root'; 'test';
Теперь напишем фикстуру, подменяющую эти файлы перед каждым тестовым прецедентом. Также заставим фикстуру полностью очищать таблицу feedback, чтобы каждый тестовый прецедент имел “чистую” окружающую среду. class AcceptanceTestOfFeedbackProject extends WebTestCase { [...] function setUp() { $this->_switchToWebTestingDb(); } function tearDown() { $this->_switchToProductionDb(); } function _switchToWebTestingDb() { $project_dir = dirname(__FILE__) . '/../'; $tests_dir = dirname(__FILE__) . '/'; include($tests_dir . 'db.php'); $conn = mysql_connect($db_host, $db_user, $db_password); mysql_select_db($db_name, $conn); mysql_query('DELETE FROM feedback', $conn); rename($project_dir . 'db.php', $project_dir . 'db.php~'); copy($tests_dir . 'db.php', $project_dir . 'db.php'); } function _switchToProductionDb() { $project_dir = dirname(__FILE__) . '/../'; unlink($project_dir . 'db.php'); rename($project_dir . 'db.php~', $project_dir . 'db.php'); } }
Более сложные тесты Добавим метод, проверяющий, что при выводе сообщения обрабатываются на предмет небезопасных символов и тегов. class AcceptanceTestOfFeedbackProject extends WebTestCase { [...] function testOfEscapingUserInput() { $this->_addFeedback('<script>', '<br>', '"\'');
}
}
$this->assertWantedPattern("/&lt;br&gt;.* " . "&lt;script&gt;.*\\\&quot;\\\&#039;/s");
Теперь напишем тест, проверяющий правильность работы пейджера при добавлении нескольких сообщений: class AcceptanceTestOfFeedbackProject extends WebTestCase { [...] function testOfPager() { $this->get(FEEDBACK_PROJECT_HOST); $this->assertNoLink("<"); $this->assertNoLink(">");
179
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
for($i=1; $i<8; $i++) { $this->_addFeedback('Robot' . $i, 'robot' . $i . '@usrobotics.com', 'Hello i am Robot' . $i); } $this->get(FEEDBACK_PROJECT_HOST); $this->assertWantedPattern('/Robot7.*Robot6.*Robot5/s'); $this->assertNoLink("<"); $this->assertLink(">"); $this->clickLink(">"); $this->assertWantedPattern('/Robot4.*Robot3.*Robot2/s'); $this->assertLink("<"); $this->assertLink(">"); $this->clickLink(">"); $this->assertWantedPattern('/Robot1/'); $this->assertLink("<"); $this->assertNoLink(">"); $this->clickLink("<"); $this->assertWantedPattern('/Robot4.*Robot3.*Robot2/s'); $this->assertLink("<"); $this->assertLink(">");
}
}
$this->clickLink("<"); $this->assertWantedPattern('/Robot7.*Robot6.*Robot5/s'); $this->assertNoLink("<"); $this->assertLink(">");
Пейджер выводит по 3 сообщения, поэтому мы добавляем в тесте 8 сообщений, чтобы проверить граничные ситуации. В этом тесте мы также воспользовались методом clickLink класса WebTestCase, который позволяет проэмулировать навигацию пользователя по ссылкам. Эти тесты покрывают весь функционал приложения, поэтому, убедившись в том, что все работает, мы приступаем к долгожданному рефакторингу приложения.
Рефакторинг приложения Отделяем бизнес логику от презентационной Пожалуй, это самый важный первый шаг, который стоит сделать. Для этого несколько модифицируем index.php, выделив из него разметку в отдельный файл templates/feedback.html. index.php: <?php ob_start(); include_once('db.php'); $conn = mysql_connect($db_host, $db_user, $db_password); if($conn === FALSE) die('db connect error: ' . mysql_error());
180
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
if(!mysql_select_db($db_name, $conn)) die('can not use db: ' . mysql_error()); if(isset($_POST['submit'])) { $name = mysql_escape_string($_POST['name']); $email = mysql_escape_string($_POST['email']); $message = mysql_escape_string($_POST['message']); $time = time(); $sql = "INSERT INTO feedback (name, email, message, time) VALUES ('$name', '$email', '$message', '$time')";
}
$result = mysql_query($sql, $conn); if(!$result) die('invalid query: ' . mysql_error());
$limit = 3; $offset = isset($_GET['offset']) ? $_GET['offset'] : 0; $sql = "SELECT * FROM feedback ORDER BY time DESC LIMIT " . ($offset * $limit) . ", {$limit}"; $fetch_result = mysql_query($sql, $conn); if(!$fetch_result) die('invalid query: ' . mysql_error()); $sql = "SELECT COUNT(*) as counter FROM feedback"; $count_result = mysql_query($sql, $conn); if(!$count_result) die('invalid query: ' . mysql_error()); $row = mysql_fetch_assoc($count_result); $total = (int)$row['counter']; include_once('templates/feedback.html'); ob_end_flush(); ?>
templates/feedback.html :
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=windows-1251"> <link rel=stylesheet type='text/css' href='styles/main.css'> <script language="JavaScript" type="text/javascript" src="js/form.js"></script> </head> <body> <table width="100%" style="height:100%" border="0" cellpadding="0" cellspacing="0"> <tr> <td width="10%" style="background-color:#7F1A22"> </td> <td width="90%" style="padding:5px 10px 5px 10px" valign="top"> <h1>Обратная связь</h1> <form action="index.php" method="post" onsubmit="return submit_form(this);"> <table> <tr> <td align="right">Ваше имя:</td> <td><input name="name" value="" type="text"></td> </tr> <tr> <td align="right">Email:</td> <td><input name="email" value="" type="text"></td> </tr> <tr>
181
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
<td align="right">Текст вопроса:</td> <td><textarea name="message" cols="50" rows="4"></textarea></td> </tr> <tr> <td></td> <td><input value="Отправить" name="submit" type="submit"></td> </tr> </table> </form> <?php if($offset > 0) :?><b><a href="?offset=<?=($offset-1)?>">&lt;</a></b><?php endif; ?> <?php if($total > 0) :?><?=($offset*$limit)+1?> - <?=(($offset+1)*$limit > $total)? $total : ($offset+1)*$limit ?><?php endif; ?> <?php if(($offset+1)*$limit < $total) :?><b><a href="?offset=<?=($offset+1)? >">&gt;</a></b><?php endif; ?> <?php while ($row = mysql_fetch_assoc($fetch_result)) { ?> <hr> <b>Автор:</b> <a href="mailto:<?=htmlspecialchars($row['email']);? >"><?=htmlspecialchars($row['name']);?></a><br> <b>Сообщение:</b> <?=htmlspecialchars($row['message']);?><br> <b>Время:</b> <?=strftime("%m/%d/%y %H:%M:%S", $row['time'])?></br> <?php } ?> </td> </tr> </table> </body> </html>
До идеала еще далеко, но читается уже намного лучше. Удостоверившись в том, что все тесты срабатывают, продолжаем дальше.
Используем WACT WACT предоставляет мощные средства для отделения презентационной и бизнес логики. Помимо гибкой шаблонной системы в WACT присутствуют также инструменты для абстрагирования работы с БД. Т.к. эти инструменты очень гибко интегрируются с шаблонной системой, имеет смысл остановить на них выбор. WACT требует наличие config.ini файла, в котором описываются глобальные конфигурационные настройки приложения. Перенесем данные из db.php в config.ini. [templates] forcecompile = TRUE [database] driver = mysql mysql.database = "feedback" mysql.user = "root" mysql.password = "test" mysql.host = "localhost"
Создадим также файл tests/config.ini, в котором мы будем хранить настройки для тестов. [templates] forcecompile = TRUE [database] driver = mysql mysql.database = "feedback-web-tests" mysql.user = "root"
182
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
mysql.password = "test" mysql.host = "localhost"
Заставим WACT также пользоваться этим конфигурационным файлом при вызове из тестов. Для этого добавим следующую строку в tests/setup.php: define('WACT_CONFIG_DIRECTORY', dirname(__FILE__) . '/');
Нам необходимо заменять config.ini на tests/config.ini на время выполнения тестов, как мы это делали с db.php, для этого несколько изменим фикстуру:
class AcceptanceTestOfFeedbackProject extends WebTestCase { function setUp() { DBC :: execute('DELETE FROM feedback'); $this->_switchToWebTestingConfig(); } function tearDown() { $this->_switchToProductionConfig(); } function _switchToWebTestingConfig() { $project_dir = dirname(__FILE__) . '/../'; $tests_dir = dirname(__FILE__) . '/'; if(!file_exists($project_dir . 'config.ini~')) rename($project_dir . 'config.ini', $project_dir . 'config.ini~'); copy($tests_dir . 'config.ini', $project_dir . 'config.ini'); } function _switchToProductionConfig() { $project_dir = dirname(__FILE__) . '/../'; if(file_exists($project_dir . 'config.ini~')) { unlink($project_dir . 'config.ini'); rename($project_dir . 'config.ini~', $project_dir . 'config.ini'); } } [...]
Имеет смысл также пользоваться в тесте средствами WACT для работы с БД. Как можно видеть, вызов DBC :: execute(’DELETE FROM feedback’) - пришел на смену жесткой привязке к mysql_ функциям. Все готово к тому, чтобы полностью отделить бизнес логику от презентационной. Начинаем с index.php:
<?php ob_start(); require_once(dirname(__FILE__) . '/external/wact/framework/common.inc.php'); require_once(WACT_ROOT . '/db/db.inc.php'); require_once(WACT_ROOT . '/template/template.inc.php'); function &getList(&$pager) { return DBC::NewPagedRecordSet('SELECT * FROM feedback ORDER BY time DESC', $pager); } function insertFeedback($arr) { $dataspace = new DataSpace(); $dataspace->import($arr); $record =& DBC::NewRecord($dataspace); return $record->insert('feedback', array('name', 'email', 'message', 'time')); } if(isset($_POST['submit'])) { insertFeedback(array('name' => $_POST['name'], 'email' => $_POST['email'],
183
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
'message' => $_POST['message'], 'time' => time()));
} $page = new Template('/feedback.html'); $pager =& $page->getChild('pager');
$feedback =& $page->findChild('feedback'); $feedback->registerDataSet(getList($pager)); $page->display(); ob_end_flush(); ?>
WACT ищет по-умолчанию шаблоны в директории templates/source, модифицируем и перенесем feedback.html. <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=windows-1251"> <link rel=stylesheet type='text/css' href='styles/main.css'> <script language="JavaScript" type="text/javascript" src="js/form.js"></script> </head> <body> <table width="100%" style="height:100%" border="0" cellpadding="0" cellspacing="0"> <tr> <td width="10%" style="background-color:#7F1A22"> </td> <td width="90%" style="padding:5px 10px 5px 10px" valign="top"> <h1>Обратная связь</h1> <form action="index.php" method="post" onsubmit="return submit_form(this);"> <table> <tr> <td align="right">Ваше имя:</td> <td><input name="name" value="" type="text"></td> </tr> <tr> <td align="right">Email:</td> <td><input name="email" value="" type="text"></td> </tr> <tr> <td align="right">Текст вопроса:</td> <td><textarea name="message" cols="50" rows="4"></textarea></td> </tr> <tr> <td></td> <td><input value="Отправить" name="submit" type="submit"></td> </tr> </table> </form> <list:LIST id="feedback"> <page:navigator id="pager" items="3"> Страница: {$PageNumber} из {$TotalPages} <page:first>&lt;&lt;</page:first> <page:prev>&lt;</page:prev> <page:list> <page:number> <page:elipses>...</page:elipses> <page:separator> </page:separator> </page:list> <page:next>&gt;</page:next> <page:last>&gt;&gt;</page:last> </page:navigator> <list:ITEM> <hr> <b>Автор:</b> <a href="mailto:{$email}">{$name}</a><br> <b>Сообщение:</b> {$message}<br>
184
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
<b>Время:</b> {$time|date:"H:i:s m/d/Y"}<br> </list:ITEM> </list:LIST> </td> </tr> </table> </body> </html>
WACT предоставляет набор компонентов, позволяющих заметно облегчить жизнь верстальщика. При помощи <list:LIST> компонента организуется итерация по сообщениям в шаблоне. <page:navigator> компонент берет полностью на себя всю рутину по организации и выводу пейджера. При этом содержимое index.php и feedback.html заметно упростилось и приобрело более логический вид. Конечно, мы сделали очень много изменений, но выполняющиеся функциональные тесты позволили нам неустанно держать руку на пульсе разработки.
Дальнейший рефакторинг и модульные тесты Если приглядеться к index.php, то можно заметить, что хотя нам и получилось сделать его более читабельным, однако в коде есть определенные “нехорошие” связи с базой данных, явно требующие инкапсуляции. Неплохо было бы всю функциональность по работе с БД, выделить в отдельную сущность. Прежде чем, мы начнем это делать, добавим модульные тесты в приложение. SimpleTest предоставляет средства для модульного тестирования при помощи класса UnitTestCase. Как и WebTestCase, UnitTestCase позволяет иметь набор тестовых методов/прецедентов, начинающихся с ключевого слова test. Как видно из названия, UnitTestCase не предоставляет средств для функционального веб-тестирования, его основной целью является предоставление инструментария для организации модульных тестов. Именно модульные тесты, в отличие от функциональных, углубляются в детали реализации. Добавим модульные tests/runtests.php:
тесты,
для
этого
изменим
[...] class AllTests extends GroupTest { function AllTests() { $this->GroupTest('All tests for feedback project'); //$this->addTestFile('acceptance_tests.php'); $this->addTestFile('unit_tests.php'); } }
На время закомментируем выполнение функциональных тестов, чтобы сделать выполнение модульных тестов мгновенным. Создадим также файл tests/unit_tests.php:
<?php class TestOfFeedbackActiveRecord extends UnitTestCase { function setUp() { DBC :: execute('DELETE FROM feedback'); } }
185
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
?>
Первое что пришло в голову - создание самой простой фикстуры, а именно, очистка таблицы feedback перед каждым тестовым прецедентом. Почему именно TestOfFeedbackActiveRecord? Потому что в данной ситуации вполне уместно воспользоваться паттерном ActiveRecord для инкапсуляции отображения сообщения в БД.
Пока не совсем понятно, что именно из себя будет представлять FeedbackActiveRecord, можно сделать очень простые тесты на сеттеры/геттеры. Во время написания тестов зачастую бывает так, что совершенно неясно, что именно требуется сделать, в такой ситуации написание тестов на, казалось бы, простейшую функциональность на самом деле развивает ход мысли разработчика на подсознательном уровне. require_once(dirname(__FILE__) . '/../feedback.inc.php'); class TestOfFeedbackActiveRecord extends UnitTestCase { [...] function testOfSettersGetters() { $feedback = new Feedback($name1 = 'Bobby', $email1 = 'email@dot.com', $message1 = "This a message", $time1 = time(), $id = 10); $this->_compareWithFeedback($feedback , $id, $name1, $email1, $message1,
$time1);
$feedback->setName($name2 = 'Bobby2'); $feedback->setEmail($email2 = 'email2@dot.com'); $feedback->setMessage($message2 = "This a message2"); $feedback->setTime($time2 = time() + 10); $this->_compareWithFeedback($feedback , $id, $name2, $email2, $message2, $time2); } function _compareWithFeedback($feedback, $id, $name, $email, $message, $time){ $this->assertEqual($feedback->getId(), $id); $this->assertEqual($feedback->getName(), $name); $this->assertEqual($feedback->getEmail(), $email); $this->assertEqual($feedback->getMessage(), $message); $this->assertEqual($feedback->getTime(), $time); } }
Локально мы также применили небольшой рефакторинг, выделив метод compareWithFeedback, тем самым сделав тело теста более читабельным.
После попытки выполнить тест мы, естественно, получили parse error, т.к. класса Feedback еще даже и не существует. Самое время сделать его простейшую реализацию в файле feedback.inc.php: class Feedback { var $id; var $name; var $email; var $message; var $time; function Feedback($name, $email, $message, $time, $id=NULL) {
186
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
$this->name = $name; $this->email = $email; $this->message = $message; $this->time = $time; $this->id = $id;
} function getId() {return $this->id;} function getName() {return $this->name;} function setName($name) {$this->name = $name;} function getEmail() {return $this->email;} function setEmail($email) {$this->email = $email;} function getMessage() {return $this->message;} function setMessage($message) {$this->message = $message;}
}
function getTime() {return $this->time;} function setTime($time) {$this->time = $time;}
Убедившись в положительном результате тестирования, перейдем к реализации сохранения объекта Feedback в БД. Очень удобно иметь в интерфейсе данного объекта единый метод save, который бы в зависимости от внутреннего состояния Feedback, либо его вставлял, либо обновлял в БД. Для начала протестируем ситуацию, когда объект у нас является новым: class TestOfFeedbackActiveRecord extends UnitTestCase { [...] function testOfSaveInsertNew() { $feedback = new Feedback('Bobby', 'email@dot.com', 'This a message', time()); $this->assertNull($feedback->getId()); $feedback->save(); $id = $feedback->getId(); $rs = DBC :: NewRecordSet('SELECT * FROM feedback'); $this->assertEqual($rs->getTotalRowCount(), 1); $rs->reset(); $rs->next(); $this->_compareWithRS($rs, $feedback);
}
} function _compareWithRS($rs, $feedback){ $this->assertEqual($rs->get('id'), $feedback->getId()); $this->assertEqual($rs->get('name'), $feedback->getName()); $this->assertEqual($rs->get('email'), $feedback->getEmail()); $this->assertEqual($rs->get('message'), $feedback->getMessage()); $this->assertEqual($rs->get('time'), $feedback->getTime()); }
Опять же в целях читабельности мы ввели метод _compareWithRS, проверяющий некоторый объект Feedback с непосредственной выборкой из базы данных. Убедившись в “неисполнимости” данного тестового случая, приступаем к реализации метода save. class Feedback { [...] function save() { if(is_null($this->id))
187
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
$this->id = $this->_insert();
else
$this->_update(); } function _insert() { $record =& DBC::NewRecord($this->_makeDataSpace()); return $record->insertId('feedback', array('name', 'email', 'message', 'time'), 'id'); } function _update(){} function & _makeDataSpace() { $dataspace = new DataSpace(); $dataspace->import(array('name' => $this->name, 'email' => $this->email, 'message' => $this->message, 'time' => $this->time)); return $dataspace; } }
Как видно из кода, мы исходим из простого предположения о том, что если у объекта id === NULL, значит, он является новым, а, следовательно, при вызове save его надо поместить в БД. WACT DBAL работает с данными, которые располагают в контейнере класса DataSpace, поэтому нам также пришлось ввести внутренний метод _makeDataSpace(). Стоит заметить, что на месте метода _update пока располагается пустая заглушка, однако позволяющая тестам выполняться. Одной из центральных идей TDD является как можно более быстрое срабатывание тестов даже при самой слабой реализации. В дальнейшем слабая реализация будет постепенно отрефакторена на последующих итерациях. Попробуем выразить ожидаемую работу метода save уже для существующего объекта при помощи тестов:
class TestOfFeedbackActiveRecord extends UnitTestCase { [...] function testOfSaveUpdate() { $feedback1 = new Feedback('Bobby1', 'email1@dot.com', 'This a message1', time()); $feedback1->save(); $feedback2 = new Feedback('Bobby2', 'email2@dot.com', 'This a message2', time() - 10); $feedback2->save(); $feedback2->setName('Bobby3'); $feedback2->save(); $rs = DBC :: NewRecordSet('SELECT * FROM feedback'); $this->assertEqual($rs->getTotalRowCount(), 2);
}
$rs->reset(); $rs->next(); $this->_compareWithRS($rs, $feedback1); $rs->next(); $this->_compareWithRS($rs, $feedback2);
188
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
Заметьте, мы использовали одновременно 2 объекта класса Feedback, сделано это для того, чтобы удостовериться, что второй объект никаким образом не был затронут после вызова метода save. Дело в том, что фикстура полностью удаляет содержимое таблицы feedback, и если бы мы проводили тесты, работая только с одним объектом, тесты бы полностью не покрывали ожидаемой функциональности. Тест не сработал, пора браться за реализацию метода _update: class Feedback { [...] function _update() { $record =& DBC::NewRecord($this->_makeDataSpace()); $record->update('feedback', array('name','email','message','time'), "id=" . DBC::makeLiteral($this->id)); } }
Интерфейс Feedback получается чистым, но для полноты в нем не хватает метода delete.
class TestOfFeedbackActiveRecord extends UnitTestCase { [...] function testOfDelete() { $feedback1 = new Feedback('Bobby1', 'email1@dot.com', 'This a message1', time()); $feedback1->save(); $feedback2 = new Feedback('Bobby2', 'email2@dot.com', 'This a message2', time() + 10); $feedback2->save(); $feedback2->delete(); $rs1 = DBC :: NewRecordSet('SELECT * FROM feedback'); $this->assertEqual($rs1->getTotalRowCount(), 1); $rs1->reset(); $rs1->next(); $this->_compareWithRS($rs1, $feedback1); $feedback2->save(); $rs2 = DBC :: NewRecordSet('SELECT * FROM feedback'); $this->assertEqual($rs2->getTotalRowCount(), 2);
}
}
$rs2->reset(); $rs2->next(); $this->_compareWithRS($rs2, $feedback1); $rs2->next(); $this->_compareWithRS($rs2, $feedback2);
Как и в предыдущем примере, мы проверяем работу метода delete(), при работе с несколькими объектами Feedback, тем самым проверяя его на безопасность. Тест также проверяет тот факт, что после того, как у объекта вызвали метод delete(), а затем метод save (), объект будет вновь помещен в БД.
189
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
Привычным образом получив “красную полосу”, приступаем к реализации: class Feedback { [...] function delete() { if(is_null($this->id)) return; DBC::execute("DELETE FROM feedback WHERE id=". DBC::makeLiteral($this>id)); $this->id = null; } }
Мок-объекты Остается только вопрос, куда поместить методы поиска Feedback записей. Самое простое решение - пока поместить их непосредственно Feedback, сделав статическими. Начнем с теста, который бы нам возвращал список всех записей из таблицы, используя ограничивающий пейджер. class Pager{ function getStartingItem(){} function getItemsPerPage(){} function setPagedDataSet(){} } Mock :: generate('Pager'); class TestOfFeedbackActiveRecord extends UnitTestCase { [...] function testOfGetList() { $feedback1 = new Feedback('Bobby1', 'email1@dot.com', 'This a message1', time()); $feedback1->save(); $id1 = $feedback1->getId(); $feedback2 = new Feedback('Bobby2', 'email2@dot.com', 'This a message2', time() - 10); $feedback2->save(); $id2 = $feedback2->getId(); $pager = new MockPager($this); $pager->expectOnce('setPagedDataSet'); $pager->setReturnValue('getStartingItem', 1); $pager->setReturnValue('getItemsPerPage', 1); $rs =& Feedback :: getList($pager); $rs->reset(); $rs->next(); $this->_compareWithRS($rs, $feedback2); $this->assertEqual($rs->getRowCount(), 1); $this->assertEqual($rs->getTotalRowCount(), 2);
}
}
$pager->tally();
190
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
Здесь мы воспользовались очень удобным инструментом, который предоставляет SimpleTest - мок-объекты. В нескольких словах, мок-объекты позволяют имитировать некоторую функциональность и управлять ею из теста. Для формирования мок-объекта требуется только интерфейс реального объекта. Здесь в качестве такого интерфейса выступает класс Pager. Вызов Mock :: generate(’Pager’); волшебным образом формирует на лету PHP код для мок-объекта. Тест как бы “учит” мок-объект тому, как он должен вести себя в зависимости от проявлений окружающей среды. Причем мокобъекты могут не только возвращать определенные значения, но также и выступать в роли “критиков”, предоставляя отчет по количеству вызовов методов, переданных аргументах и проч. Преимущество разработки с мок-объектами заключается в том, что можно на ранних этапах независимо разрабатывать программное обеспечение, имея на руках только интерфейсы. Также моки позволяют заметно ускорить исполнение тестов, изолируя ресурсоемкие участки кода. Однако не стоит забывать, что нельзя слишком увлекаться моками, т.к. это может привести к неадекватности тестов и реализации. Теперь можно попробовать реализовать метод getList, который, благодаря WACT оказался простым до безобразия: class Feedback{ [...] function &getList(&$pager) { return DBC::NewPagedRecordSet('SELECT * FROM feedback ORDER BY time DESC', $pager); } }
Финальные штрихи Еще раз изучив интерфейс Feedback, можно сказать, что логично также иметь статический фабричный метод load($rs), который бы нам позволял конструировать объекты Feedback на основе непосредственной выборки из БД. class TestOfFeedbackActiveRecord extends UnitTestCase { [...] function testOfLoad() { $feedback = new Feedback('Bobby1', 'email1@dot.com', 'This a message1', time()); $feedback->save(); $rs = DBC :: NewRecordSet('SELECT * FROM feedback'); $rs->reset(); $rs->next();
}
}
$this->assertEqual($feedback, Feedback :: load($rs));
Ничуть не огорчившись из-за невыполняющегося теста, приступим к реализации: class Feedback{ [...] function &load(&$rs) {
PHP Inside №13
}
}
Мастер-класс по введению TDD в существующий проект
return new Feedback($rs->get('name'), $rs->get('email'), $rs->get('message'), $rs->get('time'), $rs->get('id'));
А вот теперь самое интересное, давайте попробуем использовать Feedback в нашем приложении. Для этого мы опять изменим index.php: <?php ob_start(); require_once(dirname(__FILE__) . '/external/wact/framework/common.inc.php'); require_once(dirname(__FILE__) . '/feedback.inc.php'); require_once(WACT_ROOT . '/template/template.inc.php'); if(isset($_POST['submit'])) { $feedback = new Feedback($_POST['name'], $_POST['email'], $_POST['message'], time()); $feedback->save(); } $page = new Template('/feedback.html'); $pager =& $page->getChild('pager'); $feedback =& $page->findChild('feedback'); $feedback->registerDataSet(Feedback :: getList($pager)); $page->display(); ob_end_flush(); ?>
Также раскомментируем строку, включающую функциональные тесты в файле tests/runtests.php: [...] class AllTests extends GroupTest { function AllTests() { $this->GroupTest('All tests for feedback project'); $this->addTestFile('acceptance_tests.php'); $this->addTestFile('unit_tests.php'); } } [...]
Вуаля! Наше приложение было полностью отрефакторено и переведено на рельсы TDD!
Добавляем функциональность Как было указано в новых требованиях, приложение должно оповещать администратора при появлении нового сообщения. Сделать это несложно, вопрос лишь в том, как можно это правильно протестировать? Дело в том, проверить обычными средствами отсылку письма довольно-таки сложная задача. На помощь нам придет пакет FakeMail(http://sourceforge.net/projects/fakemail), находящийся в альфа разработке.
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
FakeMail эмулирует работу почтового сервера и складывает всю приходящую почту в заданное пользователем место. Таким образом, можно проконтролировать из теста все обращения к почтовому серверу. Центральной частью FakeMail является perl скрипт, стартующий в режиме демона на определенном порту. Также в состав пакета входит класс FakeMailDaemon, удобным образом инкапсулирующий работу с perl сервером. Чтобы запустить FakeMailDaemon в Windows, требуется наличие библиотеки cygwin, а именно интерпретатора perl, входящего в состав cygwin. Внимание: ActiveState Perl не работает с Fakemail. Для отправки писем воспользуемся замечательным пакетом phpmailer. Создадим небольшую глобально доступную фабричную функцию CreateMail() в файле mail.inc.php, которая будет скрывать детали инициализации phpmailer: <?php @define('USE_PHPMAIL', false); @define('SMTP_PORT', '25'); @define('SMTP_HOST', 'localhost'); @define('SMTP_AUTH', false); @define('SMTP_USER', ''); @define('SMTP_PASSWORD', ''); function & CreateMail() { include_once(dirname(__FILE__) . /external/phpmailer/class.phpmailer.php'); $mail = new PHPMailer(); $mail->LE = "\r\n"; if(USE_PHPMAIL) return $mail; $mail->IsSMTP(); $mail->Host = SMTP_HOST; $mail->Port = SMTP_PORT; if(SMTP_AUTH == true) { $mail->SMTPAuth = true; $mail->Username = SMTP_USER; $mail->Password = SMTP_PASSWORD; } return $mail;
} ?>
Предположим для простоты, что вся уведомляющая почта доставляется на ящик admin@feedback.com. Изменим наши функциональные тесты немного, заменим метод testOfSimpleSubmitFeedback на testOfSimpleSubmitFeedbackWithEmailNotification: class AcceptanceTestOfFeedbackProject extends WebTestCase { function setUp() { DBC :: execute('DELETE FROM feedback'); $this->_switchToWebTestingConfig(); $this->fakemail = new FakeMailDaemon(); $this->fakemail->start(); } function tearDown() { $this->_switchToProductionConfig(); $this->fakemail->stop(); $this->fakemail->removeRecipientMail('admin@feedback.com'); } function testOfSimpleSubmitFeedbackWithEmailNotification() { $this->_addFeedback($name = 'Bobby', $email = 'email@dot.com',
PHP Inside №13
Мастер-класс по введению TDD в существующий проект
$message = "This a message"); $this->assertWantedPattern('/' . preg_quote($email) . '.*' . $name . '.*' . $message . '/s'); $mails = $this->fakemail->getRecipientMailContents ('admin@feedback.com'); $this->assertTrue(sizeof($mails) == 1); $this->assertTrue(preg_match('~' . preg_quote($email) . '.*' . $name . '.*' . $message . '~s', $mails[0])); } [...] }
Отметим, что нам также пришлось изменить фикстуру, теперь в ее обязанности также входит запуск/остановка FakeMailDaemon и очистка пришедшей почты. Нам также tests/setup.php:
пришлось
добавить
следующую
строку
в
define('FAKE_MAIL_DUMP_PATH', dirname(__FILE__) . '/mail/');
С помощью нее мы указали FakeMailDaemon сохранять всю временную почту в директории tests/mail. Как всегда, тест не сработал, и мы переходим к реализации в index.php: <?php ob_start(); require_once(dirname(__FILE__) . '/external/wact/framework/common.inc.php'); require_once(WACT_ROOT . '/template/template.inc.php'); require_once(dirname(__FILE__) . '/feedback.inc.php'); require_once(dirname(__FILE__) . '/mail.inc.php'); if(isset($_POST['submit'])) { $feedback = new Feedback($_POST['name'], $_POST['email'], $_POST['message'], $time = time()); $feedback->save(); $mail =& CreateMail(); $mail->IsHTML(false); $mail->CharSet = 'windows-1251'; $mail->AddAddress('admin@feedback.com'); $mail->From = $_SERVER['SERVER_ADMIN']; $mail->FromName = $_SERVER['HTTP_HOST']; $mail->Subject = 'New feedback!!!'; $mail->Body = $_POST['email'] . "\n" . $_POST['name'] . "\n" . $_POST ['message']; $mail->Send(); } $page = new Template('/feedback.html'); $pager =& $page->getChild('pager'); $feedback =& $page->findChild('feedback'); $feedback->registerDataSet(Feedback :: getList($pager)); $page->display(); ob_end_flush(); ?>
Вот теперь, пожалуй, все. Не так уж и плохо для первого раза.