#18, Июль'2006 :: Инструментарий разработчика:Zend Framework

Page 1

#18' 2006

Инструментарий разработчика: Zend Framework

Осторожно, данные!

Продвинутая репликация в MySQL

«Тонкости» PHP


Семинар системы управления сайтом ABO.CMS Приглашаем Вас посетить семинар, посвященный разработке интернетпроектов на базе системы управления сайтом - ABO.CMS. На семинаре будут рассмотрены новые возможности системы управления сайтом ABO.CMS, вопросы кастомизации системы и создания крупных интернет-проектов на основе ABO.CMS. В семинаре примут участие сотрудники компании ARMEX, будут также приглашены ведущие специалисты компании Креарс Системс и представители фирмы А-Айсберг. На семинар приглашаются сотрудники компаний, перед которыми стоит задача создания сайта, веб-разработчики, маркетологи, специалисты, которые заинтересованы в получении информации о продукте «ABO.CMS». Семинар состоится 20 июля в конференц-зале гостиницы «Измайлово-Вега» Программа семинара: Время 11:00

Наименование выступления Вступительное слово, представление участников семинара.

Докладчик Кащеев С.А., компания «Армекс»

11:05

Современные тенденции развития Интернет рынка в России.

Кащеев С.А., компания «Армекс»

11:25

Современная технологичная платформа «ABO.CMS» оптимальный инструмент для разработки Интернет-проектов. Новые возможности в системе управления сайтом.

Леонидов С.И., компания «Армекс»

12:00 12:20 12:45

Ответы на вопросы Кофе-брейк Нестандартные решения на ABO.CMS от Креарс Системс

Сотрудники компании «Армекс»

13:15

Кастомизация системы управления АВО.CMS на примерах реальных проектов.

Клейменов Е. Б., компания «Армекс»

13:45

Опыт использование системы управления сайтом ABO.CMS в течение 2 лет. Эффективность и

Степанов Ю.А., компания «A-айсберг»

13:55

Ответы на вопросы.

Сотрудники «Армекс», «Креарс Системс», «A- айсберг» Сотрудники «Армекс»

14:05 - 15:00 Вернисаж: демонстрация продукта и решений участникам семинара.

Бобров Д.В., компания «Креарс Системс»

Семинар состоится по адресу: г. Москва, м. Партизанская, Отель «Измайлово-Вега», Измайловское шоссе, 71, корп. 3В Начало регистрации в 10:30 Начало выступлений в 11:00 Участие – бесплатное. Количество мест ограничено. Для участия в семинаре необходимо зарегистрироваться на сайте http://www.abocms.ru Компания ARMEX: г. Москва, ул. Рабочая 33, п. 1 Cайт компании: http://www.armexdesign.ru/ Сайт системы: http://www.abocms.ru Демоверсия: http://demo.abocms.ru/ Телефон: 8(495) 585-0659 (многоканальный) E-mail: abocms@abocms.ru


PHP Inside'18 >> Readme

Readme Следуя традициям, сложившимся в сфере веб-программирования, мы не могли назвать вступительное слово к читателям иначе как readme. Между readme-файлами и вступительными словами очень много общего – кто-то сначала внимательно прочтет небольшой файл, а лишь затем приступит к установке системы, а кто-то наоборот – минуя все детали ринется покорять новый дистрибутив. В этом номере мы немного обновили концепцию журнала. Новшества коснулись как внешнюю обертку – обложку и верстку, так и внутренних составляющих. В качестве эксперимента мы добавили небольшую (пока!) рубрику новостей и обзоров, сдобрили практически каждую статью мини-словарями терминов, а в некоторых местах снабдили материалы мнениями экспертов. В дальнейшем планируем расширять рубрику новостей и создать еще один раздел под названием «Лаборатория PHP Inside», где по косточкам будем разбирать популярные PHP-приложения с точки зрения разработчика, исследовать GUI, залезать в API и исходный код. Приятной новостью стал выпуск первого бумажного журнала PHP Inside с материалами прошедшей в мае сего года PHP-конференции в Москве. Было очень интересно подержать все то что мы делали несколько лет (since 2004) в руках в виде глянцевого издания. Кстати, и Вы можете прикоснуться к истории и заказать номер журнала, написав по адресу nw@phpinside.ru. Чтобы узнать, какие доклады вошли в бумажную версию, нужно посетить сайт нашей редакции http://phpinside.ru/?q=node/484. Выпуск бумажной версии показал, что в целом, интерес к «глянцу» у отечественных разработчиков существует (журналы уходили и в Эстонию, Белоруссию, Украину и Армению, разлетались по всем уголкам России от Владивостока и Якутска до Липецка и Санкт-Петербурга). При удачном стечении обстоятельств, мы попробуем повторить опыт издания печатных экземпляров и наладить их регулярные выпуски. Как Вы видите – планов огромное количество! Однако не стоит забывать, что журнал делаем мы с Вами вместе и поэтому, редакция всегда нуждается в помощи в переводах, написании статей и обзоров ПО. Если есть желание помочь в чем либо из вышеперечисленного – пишите на все тот же адрес nw@phpinside.ru и мы вместе с Вами создадим очередной номер журнала! Андрей Олищук [nw] nw@phpinside.ru http://phpinside.ru


PHP Inside'18 >> Содержание

Содержание Стр. 5. Новости и обзоры

Стр. 6. Zend Framework. Обзорная статья

Пару слов о программном обеспечении, разработанном на PHP

Эта статья посвящена относительно недавно вышедшему инструменту веб-разработуи - Zend Framework.

Стр. 12. Полнотекстовый поиск с Zend_Search_Lucene

Стр. 24. PHP в командной строке РHP традиционно используется только в интернете для работы ваших веб-сайтов, однако, это не единственная сфера его применения.

В этой статье рассматривается реализация полнотекстовой поисковой машины с использованием PHP5 и Zend Framework.

Стр. 29. Осторожно, данные! О каких данных идет речь? Да о любых: пользователь заполнил какую-то форму, вписал какие-то параметры в URL, и так далее. Вариантов, откуда могут поступить эти самые данные море.

Стр. 61. Практические стороны ООП в PHP Типичной ошибкой некоторых PHP-разработчиков, особенно начинающих, является слабое использование объектно-ориентированных возможностей PHP. Целью данной статьи является раскрытие преимуществ использования объектов, что должно стать необходимым условием изучения PHP.

Стр. 72. Интервью с Леонидом Лукиным Первый отечественный Zend Cerified Engeneer, с 1999 по 2005 год преподававший PHP в Центре компьютерного обучения «Специалист» при МГТУ им. Баумана, автор известного в среде PHP-программистов онлайн-экзамена «Online certified PHP specialist»

Стр. 41. Продвинутая репликация MySQL Одним из преимуществ кластера является то, что все узлы в системе равнозначны, в то время как при обычной репликации у вас есть ведущий узел (master) и несколько ведомых (slave), при этом приложения должны производить изменения только на ведущем узле.

Стр. 68. Тонкости PHP Их можно назвать непонятными, бессмысленными или “недоошибками”! Но как их не называй, в какойто момент все попадают в ловушку этих нелогичныx и странныx ошибок


PHP Inside'18 >> Новости и обзоры

Новости и обзоры TYPO3 association готовит пятую версию одноименнго CMF TYPO3 association – некоммерческое объединение разработчиков, работает над пятой версией своего CMF – TYPO3 (http://typo3.org/about/new-to-typo3/). Данный CMF и в своей четвертой реализации имеет внушительный список возможностей, начиная от современного функционала для редакторов контента (WYSIWYG редактор текста, история изменений с возможностью отката, набор мастеров и т.д.), до системы безопасности (история входов в систему, ограничения входа в систему только с конкретных IP-адресов, LDAPаутентификация и т.д.) и особенностей, важных для вебразработчика (продвинутая система кеширования, поддержка UTF-8, документированное API и многое другое). Одной из особенностей текущей стабильной версии TYPO3 является то, что страницы фронт-энда могут публиковаться в виде статичных HTML-файлов, что, несомненно, увеличивает производительность сайта. О своих планах на пятую версию рассказали Robert Lemke и Daniel Hinderink на состоявшейся в мае сего года конференции Linuxtag 2006.

Мониторинг хостов с новой версией limph 1.9.2 Разработчики limph 1.9.2 (http://sourceforge.net/projects/limph/) утверждают, что создали систему мониторинга не хуже чем в конкурентных проектах – Nagios (http://www.nagios.org/) и Node Runner (http://sourceforge.net/projects/node-runner/). Система мониторинга позволяет отслеживать состояние хостов в реальном времени (данные обновляются каждую минуту). Поддерживаются уведомления о событиях по электронной почте (при наличии в системе рабочего почтового сервера. Проверки осуществляются по протоколам UDP (отправка ICMP пакетов), TCP и со специальными агентами-скриптами.


PHP Inside'18 >> В фокусе >> Zend Framework. Обзорная статья

Zend Framework. Обзорная статья Александр Кудряшов

Э

та статья посвящена относительно недавно вышедшему Zend Framework. И является первой (и надеюсь не последней) из цикла статей, посвящённых данной технологии. Как указано в заглавии она носит обзорный характер, примеров кода в ней не будет (их я собираюсь приводить, если буду подробно писать про каждый из модулей), а будет рассомотрена лишь заявленная функциональность.

Завязка Целью своего проекта разработчики указывают написание качественной библиотеки кода, который будет поддерживаться и развиваться очень активно. Zend Framework должен обеспечить базу для написания сложных приложений без использования других библиотек и помочь PHP 5 стать индустриальным стандартом для написания коммерческих веб-ориентированных приложений. При разработки этого фрэймворка разработчики отталкивались от преимуществ, которые предоставляет PHP, а именно: простота использования, эффективность и понятность. Ну что ж, давайте посмотрим, что мы имеем на данный момент.

Развитие На момент написания статьи, последняя доступная версия была 0.1.2. Не бог весть что, но людям, который знакомы со средой *nix и *nux, должно быть известно, что столь малый номер версии не являются показателем неработоспособности приложения. Получить пакет Zend Framework можно совершенно безвозмездно на сайте framework.zend.com в разделе download. Предусмотрены архивы "zip" и "tar.gz". Рядом на сайте можно найти и документацию, но если вы решили скачивать фрэймворк, то в его комплект поставки документация предусмотрительно включена. Скачав пакет, и разархивировав его, вы заимеете папочку "Zend Framework-0.1.x"(x потому что я уверен, к моменту выхода статьи номер версии поменяется), в которой вы найдёте: 1. 2. 3. 4. 5. 6. 7. 8.

demos\ documentation\ incubator\ library\ tests\ licence.txt news.txt readme.txt

Давайте разбираться что есть что. В папке demos лежат несколько примеров использования Zend Framework. В папке documentation лежит, что характерно, документация по проекту в двух видах: описание API, сгенерированное при помощи phpDocumentor, и описание для пользователя, которое нам больше всего и пригодится. В папке incubator лежат части проекта, которые ещё сильно экспериментальны и морально не готовы к помещению в папку library, в которой лежат 6


PHP Inside'18 >> В фокусе >> Zend Framework. Обзорная статья работающие(почти наверняка) исходные тексты классов. Идём дальше, в папку tests лежат тесты PHPUnit2, но ещё не ко всем классам. Оставшиеся файлы носят говорящие имена и не подлежат рассмотрению, хотя вашего внимания, безусловно, заслуживают. Теперь, когда нам известно что где лежит, надо побыстрее посмотреть что же нам предлагают и как мы можем воспользоваться этим в кратчайшие сроки и с максимальной эффективностью. Инсталляция по-сути никакая не нужна. Единственное, что следует сделать - прописать путь к папке library в php.ini (в директиве include_path). Если не хочется трогать php.ini, то можно вызывать set_include_path() перед подключением файлов фрэймворка. N.B. В FAQ на официальном сайте нас предупреждают, что фрэймворк будет работать на PHP 5.0.4 и выше. Совместимость с PHP 4 не предусмотрена(вот так они и собираются насаждать PHP 5 в качестве стандарта de facto).

Кульминация Теперь перейдём к самому главному. К обзору функциональности. В этой статье я бегло пробегусь по всем основным узлам, коих всего-то 15. А рассмотрение каждого из них подробно выходит, пожалуй, за рамки ознакомительной статьи. Да, кстати, забыл упомянуть, что Zend Framework является набором объектно-ориентированных библиотек, и основной единицей измерения в нём является класс.

Zend Данный класс является основой всех основ для Zend Framework и содержит в себе только статические методы, которые активно используются многими компонентами фрэймворка.

Zend_Controller Этот класс предназначен для построения приложений по принципу MVC (Model-View-Controller) (Хотя я где-то читал, что при построении по данному принципу на PHP контроллер исключается, так как сам интерпретатор php выступает в его роли. Ну да ладно, поверим им на слово). Всё намного интереснее, чем я думал. Оказывается, что с использованием Zend_Controller, мы выходим на новый качественный уровень безопасности. С помощью этого модуля можно вынести все динамические файлы за пределы document_root, оставив там лишь файл index.php, в котором прописать только вызов Zend_Controller_Front, который в свою очередь будет управлять процессом приёма и обработки URL. Документация кончается на самом интересном месте, но в первой же подробной статье по классам я расскажу всё, что понял по данному перспективному и интересному классу.

Zend_DB Из названия, я думаю, понятно о чём идёт речь. Конечно, о базах данных. Нам предоставляют единый API, основанный на PDO(PHP Data Objects, встроен в PHP 5.1 и может быть загружен из PECL для PHP 5, несовместим с PHP 4), для доступа к любым SQL базам. Предоставляет множество удобных функций для работы с БД: •

"Закавычивание"{quoting} запросов во избежание SQL инъекций.

Прямые запросы к базе, без использования специфических функций Zend Framework

Управление транзакциями


PHP Inside'18 >> В фокусе >> Zend Framework. Обзорная статья •

Функции INSERT, UPDATE, DELETE

Функции fetch* для обработки результатов запросов

Функции доступа на уровне таблицы, столбца, набора столбцов{rowset}

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

Zend_Feed Этот набор классов позволяет непринуждённо работать с RSS и ATOM feed. Feed можно будет изменять по желанию и сохранять результат в том XML-виде, в котором нужно. В будущем предполагается поддержка для Atom Publishing Protocol. Вот что мы можем делать: •

Загружать feed из с веб-сайта, из файла, из строки PHP

Искать доступные feed на конкретной странице

В общем нам, предоставили всё ту же родную(кому как...) объектную модель для доступа к RSS и ATOM feeds.

Zend_Filter Информации пока немного. Ясно лишь, что это набор классов для обработки информации. Причём использовать его следует для не для массивов(для них есть Zend_Input_Filter), а как обычные строчные функции. Фильтров там очень много хороших и разных, но без сомнения полезных. В документации отображены далеко не все, для полного счастья надо залезать в исходный код класса. (Э-хе-хе...строчные функции-то не все запомнишь, какие что делают, а тут ещё десятка два полезных фильтров...без документации никуда...)

Zend_HttpClient Ещё один интересный модуль. Предназначен для общения с серверами на их родном языке. Много чего умеет уже сейчас. Например: •

Отсылать POST, PUT, DELETE запросы(всегда знал, что POST запросы можно отсылать, но никогда не знал как. И вот оно!)

Отсылать заголовки как по одному, так и сразу несколько

Оринимать ответы от сервера, с которым общаемся, позволяя нам создавать управляющую событийную логику

Идём дальше

Zend_InputFilter Этот набор классов предоставляет услуги фильтрации через единый API. Работает он по простому принципу: "Если данные верны - вернуть данные, если нет - вернуть FALSE". Работа по


PHP Inside'18 >> В фокусе >> Zend Framework. Обзорная статья фильтрации может производиться на массивом (например, $_POST), с простым доступом к его элементам. Осуществлены два подходя к фильтрации: Строгий (он же рекомендуемый) Если отправил массив фильтру, то он его обнулил, и ты или работаешь с его прошедшей фильтрацию копией, или не работаешь вообще. Хотя доступ к непроверенным данным из массива возможен, но это уже будет другой массив, что безопаснее. Нестрогий (он же совсем не рекомендуемый) Всё то же самое, только массив не будет обнулён. Продолжая структурировать всё и вся, определены(пока) три типа фильтров Blacklist Filtering (например, вырезать html-тэги из текста), Whitelist Filtering(например, проверить е-mail) и Blind Filtering(то же самое что и Blacklist Filtering, но здесь идёт список того, что надо оставить, а в Blacklist Filtering идёт списко того, что надо вырезать...тонкая такая разница). Для упрощения жизни всем нам, применяются строгие правила именования фильтров: •

Blacklist фильтры начинаются с "no"

Whitelist фильтры начинаются с "is"

Blind фильтры начинаются с "get"

Этот набор классов позиционируется(да и выглядит) как чрезвычайно полезный. В документации даже специально отмечено, что он будет полезен и разработчикам, и менеджерам проектов, и аудиторам кода.

Zend_Json Не кидайте в меня камни, но про JavaScript Object Notation я слышу впервые. Если есть ещё кто-то кроме меня, кому аббревиатура JSON не говорит ничего, можно вместе сходить на сайт www.json.org и почитать что же это за зверь. А учитывая, что рядом JSON в документации упоминается AJAX, надо туда просто срочно бежать, а то останимся не у дел при полновластном приходе WEB 2.0 и его интерфейсов. Умеет этот класс немного. Может из кода PHP создать код JSON, а может из кода JSON создать код PHP. Ещё нам предоставлена свобода выбора в вопросе отображения объектов JSON в PHP, можно их получать в виде ассоциативных массивов, а можно в виде объектов же PHP.

Zend_Log Хорошо проименованные классы - пол дела. Сразу понятно, что будем вести дневники. Дневники поддерживаются разные, консольные и файловые, можно объявлять сразу оба (имена только разные дать) и писать куда следует в случае чего. Для удобства последующих публичных чтений, предусмотрена система уровней опасности (DEBUG, WARNING, ERROR, SEVERE). Указывается в качестве дополнительного параметра (помимо самого сообщения) при записи.

Zend_Mail и Zend_Mime Здесь нам предоставляют карт-бланш по отправке корреспоненции. Отправлять её мы можем двумя способами: посредством встроенной в PHP функции mail(), или непосредственно создавая SMTP соединение. С самим письмом можно творить всё что угодно: отправлять его в форме html, присоединять файлы, добавлять заголовки(которые mail headers), устанавливать нужную кодировку. А с помощью Zend_Mime, можно управлять multipart MIME messages.


PHP Inside'18 >> В фокусе >> Zend Framework. Обзорная статья Zend_Pdf Один из самых богато документированных модулей. В версии Zend Framework 0.1.1 в документацию вложили десятимегабайтный файл с полным описанием формата PDF на тысячу страниц...можно полистать на досуге. Что же у нас здесь есть. Почти всё. •

Можно создавать документы и открывать уже созданные(если версия PDF v1.4)

Переставлять, добавлять, удалять страницы в документе

Рисовать различные примитивы(линии, окружности, четырёх- и многоугольники и даже эллипсы)

Писать 16 различными шрифтами всё, что захочется

Работать с изображениями(пока только JPG)

В общем всё о чём вы мечтали, но боялись попросить Adobe, и даже больше. Почему-то я не сталкивался с повальным использованием этих разнообразных функций...то ли у нас народ не доверяет PDF и признаёт только документы Microsoft Office...хотя теоретически, придумать практическое применение вполне возможно...видимо в различных не веб-ориентированных приложения для внутреннего пользования масштабов предприятия это может пригодиться. Поправьте меня, если я не прав, буду благодарен.

Zend_Search Пока этот модуль находится в инкубаторе, но в документации рассмотрен. Видимо включён будет в самое ближайшее время. В анонсе написано, что этот поисковый движок полностью написан на PHP 5, и для своей работы совершенно не требует наличия базы данных, умудряясь хранить свои индексные файлы в файловой системе. Своим происхождением этот движок обязан проекту Apache Lucene, который проживает по адресу http://lucene.apache.org/java/docs/. И в исходных кодах представлено два варианта исполнения на PHP и на Java. Насколько я понял(успел понять, точнее) работа с этим движком не так уж банальна. Индексные файлы надо создавать и обновлять, документы надо создавать, всё через объекты и напоминает SQLite. Хотя, если не доступна база данных, то уж лучше так, чем через txt-файлы... В данном номере журнала компонент Zend_Search_Lucene рассмотрен более детально.

Zend_Service Это модуль, который помогает make service(ДПП НН). Теперь серьёзно. Zend_Service является абстрактным классом для доступа к различным веб-сервисам(например XML-RPC). Если конкретнее, то поддерживаются любые REST-сервисы. К сервису можно подключиться, используя URI и поговорить с ним по душам на с использованием GET, POST, PUT и DELETE запросов. Для того, чтобы подстегнуть фантазию разработчиков, нам предоставили полноценные библиотеки доступа к таким замечательным ресурсам как Amazon, Flickr и Yahoo!..Что ж для примера вполне подойдёт, а там может кому и пригодится.


PHP Inside'18 >> В фокусе >> Zend Framework. Обзорная статья Zend_View Последний модуль в нашем сегодняшнем обзоре. Этот набор классов имеет непосредственное отношение к паттерну MVC(Model-View-Controller). А конкретно к отображению (View). Чем-то работа данного модуля похожа на шаблонизатор. Работа Zend_View проходит в два этапа: 1. В контроллере вы создаёте экземпляр Zend_View, присваиваете нужные значения переменных; 2.

Назначаете нужный вам шаблон(View), и он заполняется и отображается посредством Zend_View

Всё просто и со вкусом. Для удобства пользования существует фильтр, который позволяет экранировать все переменные, за которые вы боитесь, и тем самым избежать XSS(cross-site scripting). Если очень хочется облегчить жизнь своему дизайнеру, то можно использовать шаблоны с переменными в скобочках фигурных ( {test} ) и подгружать в них нужную информацию. Хотя Smarty умеет всё и даже больше, всегда стоит рассматривать возможность написать что-то быстрое, но негибкое под свои нужды. Но это ещё не всё. Присутствует класс вспомогательных функций (View Helpers), которые помогут вам выполнить рутинные построения на страницах, например полный функционал для работы с формами(вы ему параметры - он вам готовый html). Можно писать функции самому, складывать в свою папочку, и просить его искать именно там(по умолчанию он ищет в своей папочке). Вот так нас всех сразу приучат писать MVC-приложения.

Развязка Что же мы получили в итоге. А получили мы современный(ибо на PHP 5) и удобный репозиторий (а то и фрэймворк =) ) вспомогательных классов, весь пронизанный новой объектной моделью с реализацией исключений и прочих интерфейсов. Основываясь на Zend Framework, можно писать эффективные приложения, которые будут чётко структурированы и организованы. Следуя стандартам именования, которые предлагают нам разработчики, мы можем внести ясность в свои проекты (если у кого-то она уже была, то это прекрасно) и избежать головной боли при внесении изменений и доработке. Радует так же, что даже в первом релизе заложена очень даже реальная функциональность. Даже боязно предположить что же будет к версии 1.0.0. Поэтому, если вы хотите разобраться и начать использовать Zend Framework в своей повседневной программерской жизни, то лучше и проще это сделать сейчас =). Я не думаю, что изменения, которые будут вноситься в проект, резко изменят его структуру. Так что начинаем разбираться и пользоваться...должно пригодиться =).

Словарь терминов статьи API, Application Programming Interface программный интерфейс, предоставляемый библиотекой или приложением для взаимодействия с ними. MVC, Model-View-Controller – программная архитектура и шаблон проектирования, согласно которому производится разделение приложения на модель данных, пользовательский интерфейс представления, и компонент управления (контроля) логикой.


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene

Полнотекстовый поиск с Zend_Search_Lucene Автор: Квентин Зерваас, Перевод: Андрей Олищук

В

этой статье рассматривается реализация полнотекстовой поисковой машины с использованием PHP5 и Zend Framework. Мы будем использовать компонент Zend_Search_Lucene для создания индекса и поиска по нему.

Конечно же, Zend_Search_Lucene является не единственной библиотекой для решения нашей проблемы, но его преимущества состоят в том, что для начала работы нам не потребуется дополнительных модулей. К примеру, Xapian или Tsearch2 требуют дополнительное ПО для работы (Tsearch2 должен быть вкомпилирован в вашу СУБД PostgreSQL). Прежде чем начать, стоит отметить, что для работы нам понадобится как минимум PHP 5, так как Zend_Search_Lucene не будет работать на PHP 4. В статье мы рассмотрим следущие моменты: •

Как индексировать документы или серии документов.

Различные типы полей, которые могут быть проиндексированы.

Поиск по индексу.

Для демонстрации всей этой функциональности, мы рассмотрим создание поискового движка для сайта phpRiot. Прежде, администрация сайта использовала модуль Tsearch2, но с ним возникли проблемы, которые было сложно преодолеть.

Как работает полнотекстовая индексация и запросы Даже если вы ранее не встречались с рассматриваемой темой, я вкратце рассмотрю как и что работает. Индекс сохраняет специальную копию ваших данных, но вместо того, чтобы хранить все данные, он помещает в свою базу только список ключевых слов для каждого документа. Другими словами, для того чтобы создать индекс моего сайта phpRiot, я должен был бы циклически обойти все документы на сайте, обнаружить на них ключевые слова и добавить эти слова к документу. Так же, мы не можем заранее прописать все словосочетания ключевых слов, которые будет вводить пользователь при поиске, поэтому, в индексе нам придется сохранить еще и некоторую дополнительную информацию. В случае с phpRiot, мы сохраним заголовок документа, автора, URL документа и краткий обзор статьи. В качестве альтернативы, можно сохранить только ID документа, а затем обрабатывать документ по этому уникальному ID, получая остальные детали из базы. Возможно, такой вариант будет даже быстрее (меньше запросов), а так как заголовок и автор – это только пара слов для каждого документа, возникнет только небольшой недостаток в виде дублирования этих данных при 12


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene сохранении в индексе.

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

Поддержка актуальности индекса Если текст в базе сайта будет обновлен, то это приведет к устареванию поискового индекса. Это означает, что мы должны предпринять дополнительные меры для управления контентом, а точнее – переиндексировать этот документ. Здесь существует несколько подходов. К примеру, вы можете производить переиндексацию в реальном времени (как только документ на вашем сайте обновится), либо вы можете провести общую индексацию в определенное время суток. Каждый метод имеет собственные преимущества. Для таких сайтов как phpRiot, обновление индекса в реальном времени – наилучший выход, так как здесь данные не обновляются очень часто. Но с другой стороны, индексация каждого комментария может привести к чрезмерной нагрузке, так как перестроение индекса для каждого комментария будет лишний раз нагружать сервер, ведь в течение дня на сайте оставляются сотни комментариев.

Приступим Первым делом необходимо установить Zend Framework, если он еще не установлен. Он структурирован примерно так же как и PEAR. На данный момент ZF находится только на стадии «превью» и на момент написания статьи существовала версия 0.1.3. Вы можете скачать его отсюда: http://framework.zend.com/download либо, используя Subversion, можно получить наиболее свежую версию. Не могу сказать где для вас будет наиболее правильным разместить фреймворк на машине, но можно это сделать по образу PEAR в /usr/local/lib/php, либо как я – в /usr/local/lib/zend. 1.$ 2.$ 3.$ 4.$

cd /usr/local/src wget http://framework.zend.com/download/tgz tar -zxf ZendFramework-0.1.3.tar.gz mv ZendFramework-0.1.3/library /usr/local/lib/zend

Теперь, все что нам осталось – внести /usr/local/lib/zend в include path для PHP. К примеру, моя директива include_path в httpd.conf для phpRiot выглядит так: 1.php_value include_path .:/ var/www/phpriot.com/include:/usr/local/lib/php И теперь она превращается в: 1.php_value include_path .:/ var/www/phpriot.com/include:/usr/local/lib/php:/usr/local/lib/zend


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene

Создание первого индекса Начальный процесс создания индекса состоит из: •

Открытия индекса

Добавления каждого документа

Фиксации изменений в индексе

Индекс сохраняется в файловой системе, поэтому, когда вы открываете индекс, вы должны указать, где он должен хранится. Это достигается создание экземпляра класса Zend_Search_Lucene. 1.<?php 2. require_once('Zend/Search/Lucene.php'); 3. 4. $indexPath = '/var/www/phpriot.com/data/docindex'; 5. 6. $index = new Zend_Search_Lucene($indexPath, true); 7.?> Вы наверное заметили и второй параметр в конструкторе класса. Он означает, что индекс создается «с нуля». Если установить значение в FALSE (или опустить его), откроется уже существующий индекс. Это делается при обновлении индекса или запросах к нему. Так как на данном этапе мы создаем индекс, параметр необходимо указать.

Добавление документа в индекс После того, как мы открыли индекс, нужно в него добавить наши документы. Мы создаем новый документ используя следующий код: 1.<?php 2. $doc = new Zend_Search_Lucene_Document(); 3.?> Следующий шаг – определить, какие поля мы будем добавлять в наш индекс. Существует несколько различных типов полей, которые могут соответсвовать одному документу. Как мы говорили ранее, нам будет необходимо сохранить ключевые слова и некоторую дополнительную информацию. При желании, можно сохранить документ в индексе целиком, но это только излишне перегрузит наш индекс. Неплохим решением будет копирование в индекс коротких слов, вроде автора документа и его название. Начнем определять, какие поля необходимо добавить к индексу. •

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

Заголовок – обязательно нужно включить заголовок.

«Рекламу» документа – короткий обзор документа для вывода в результатах поиска.


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene •

Автора – это позволит видеть авторов статей уже в результатах поиска и можно будет представить сортировку документов по авторам.

Дата создания – время создания документа в формате timestamp.

Далее перейдем к Zend_Search_Lucene:

различным

типам

полей,

которые

могут

быть

использованы

в

UnIndexed (НеИндексировано) – Данные, недоступные для поиска, но хранящиеся вместе с документом («Реклама», ссылка на статью и время создания).

UnStored (НеСохранено) – Данные, доступные для поиска, но не сохраненные в индексе целиком (содержимое документа к примеру).

Text (Текст) – Данные, доступные для поиска и сохраненные целиком (заголовок и автор).

Существуют так же типы «Keyword» (ключевое слово) и «Binary» (бинарный объект), но в данном примере мы их рассматривать не будем. По бинарным объектам нельзя искать, но они могут быть сохранены, к примеру, как картинка к документу. Чтобы добавить поле к нашему индексируемому документу, мы используем метод addField(). Когда мы вызываем данный метод, мы должны передать данные, подготовленные в соответсвие с их типом. Для этого, создадим экземпляр класса Zend_Search_Lucene_Field с указанием типа поля в виде статического имени метода. Другими словами, это выглядит так: 1.<?php 2. $data = Zend_Search_Lucene_Field::Text('title', $docTitle); 3.?> Заметьте, что здесь же указывается и имя поля. В дальнейшем, мы можем ссылаться на эти данные по названию поля. Теперь добавим все типы данных, которые были рассмотрены выше, используя код: 1.<?php 2. $doc = new Zend_Search_Lucene_Document(); 3. $doc->addField(Zend_Search_Lucene_Field::UnIndexed('url', $docUrl)); 4. $doc->addField(Zend_Search_Lucene_Field::UnIndexed('created', $docCreated)); 5. $doc->addField(Zend_Search_Lucene_Field::UnIndexed('teaser', $docTeaser)); 6. $doc->addField(Zend_Search_Lucene_Field::Text('title', $docTitle)); 7. $doc->addField(Zend_Search_Lucene_Field::Text('author', $docAuthor)); 8. $doc->addField(Zend_Search_Lucene_Field::UnStored('contents', $docBody)); 9.?> Важное примечание: мы добавили главный контент с именем поля «contents», так как «content» является специальным словом в Zend_Search_Lucene, означающим по-умолчанию, что все запросы будут искать в этом поле.


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene И наконец, добавим документ в индекс, используя addDocument(): 1.<?php 2. $index->addDocument($doc); 3.?> Мы рассмотрим данный код более подробно чуть позже (включая возможности обхода нескольких документов и единоразового добавления их в индекс).

Фиксация/Сохранение индекса Как только документы добавлены к индексу, он должен быть сохранен. 1.<?php 2. $index->commit(); 3.?> Конечно, можно фиксировать изменения индекса и после добавления каждого документа, однако эта операция слишком «тяжела» и такое решение будет менее производительным, так как в последующем, системе придется опрашивать большее количество физических файлов (сегментов индекса). Если вы взгляните на вашу файловую систему, то сможете обнаружить сохраненный индекс в той директории, которую указали при открытии индекса. Эта директория содержит некоторое количество файлов, которые и будут использованы при дальнейшем обращении к индексу. Теперь попробуем собрать весь код в один скрипт, целью которого будет индексация всех статей на сайте phpRiot.

Индексация всех статей на сайте phpRiot Теперь, когда мы рассмотрели процесс создания простейшего индекса, немного расширим наш пример. Кроме того, мы расширим основной класс Zend_Search_Lucene_Document чтобы немного упростить свой код. Это позволит еще раз продемонстрировать удобство объектного стиля. Так как мы рассматриваем индексацию статей на сайте phpRiot, то нам придется использовать и класс DatabaseObject, используемый этим сайтом для извлечения статей из базы. Вам не обязательно знать тонкости работы класса, чтобы понять суть приведенного ниже примера. Но если вам интересно, то подробнее о DatabaseObject можете прочитать здесь: http://www.phpriot.com/d/articles/php/application-design/databaseobject-intro/index.html.

Расширяем Zend_Search_Lucene_Document Ранее, когда мы открывали индекс, мы создавали новый экземпляр Zend_Search_Lucene_Document, который хранил данные индекса для одного документа. Вместо того, чтобы обращаться к классу напрямую, мы немного расширим его. Другими словами, мы создадим addField в нашем классе, вместо того чтобы вызывать его каждый раз, когда создаем Zend_Search_Lucene_Document. 1.<?php 2. class PhpRiotIndexedDocument extends Zend_Search_Lucene_Document 3. { 4. /**


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene 5. * Constructor. Creates our indexable document and adds all 6. * necessary fields to it using the passed in DatabaseObject 7. * holding the article data. 8. */ 9. public function __construct(&$document) 10. { 11. $this->addField(Zend_Search_Lucene_Field::UnIndexed ('url', $document->generateUrl())); 12. $this->addField(Zend_Search_Lucene_Field::UnIndexed ('created', $document->getProperty('created'))); 13. $this->addField(Zend_Search_Lucene_Field::UnIndexed ('teaser', $document->getProperty('teaser'))); 14. 15. $this->addField(Zend_Search_Lucene_Field::Text('title', $document->getProperty('title'))); 16. $this->addField(Zend_Search_Lucene_Field::Text ('author', $document->getProperty('author'))); 17. 18. $this->addField(Zend_Search_Lucene_Field::UnStored ('contents', $document->getProperty('body'))); 19. } 20. } 21.?> Как вы можете видеть, мы создали простую обертку-враппер, который получает данные документа (используя DatabaseObject). Функция generateUrl() является специальным внутренним методом, который определяет полный URL документа. Мы сохраняем эти данные, когда строим индекс, так что нам не потребуется генерировать их каждый раз при поиске.

Построение полного индекса Теперь, когда у нас есть специальный класс, мы можем приступить к созданию нашего индекса путем обхода документов. После того, как документы добавлены к индексу, его следует зафиксировать. 1.<?php 2. require_once('Zend/Search/Lucene.php'); 3. require_once('DatabaseObject/PhpriotDocument.class.php'); 4. 5. // где сохранить индекс 6. $indexPath = '/var/www/phpriot.com/data/docindex'; 7. 8. // список всех ID 9. $doc_ids = PhpriotDocument::GetDocIds($db); 10. 11. // создаем индекс 12. $index = new Zend_Search_Lucene($indexPath, true); 13. 14. foreach ($doc_ids as $doc_id) { 15. 16. // загружаем объект базы 17. $document = new PhpriotDocument($db); 18. $document->loadRecord($doc_id); 19. 20. // создаем документ и добавляем его к индексу 21. $index->addDocument(new PhpRiotIndexedDocument($document)); 22. }


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene 23. 24. 25. 26.?>

// записываем индекс на диск $index->commit();

Индекс был создан! На операцию может потребоваться некоторое количество времени, если она будет производится с большим количеством документов, которые к тому же, содержат много данных. Мы при создании индекса используем PHP в командной строке, которая позволяет нам видеть прогресс в реальном времени, если это необходимо (мы можем выводить заголовок и статус каждого проиндексированного документа).

Как получать данные из индекса в Zend_Search_Lucene Пришло время затронуть наиважнейшую часть вопроса – поиск данных. Существует много опций, которые можно применять при запросах к индексу. Они позволяют контролировать процесс вывода результатов поиска. Роман Ковригин [_RVK_], сотрудник компании RBC, разработчик фреймворка ENVOS говорит о Zend Framework: Ни один из известных фреймворков не пользуется в России сколь нибудь ощутимой популярностью, поэтому ZFW сможет лишь занять пустующую нишу, а не подвинуть остальные. А вот сможет ли он стать популярным, это уже другой вопрос. Популярность того же mojavi на западе гораздо выше. Но ни один из PHP-фреймворков не сравнится по популярности с Rails для Ruby, вот потому я и считаю что популярность таких разработок низкая. Возможно ZFW и изменит ситуацию. Но в целом ZFW, конечно, будет популярнее того что было до этого, за счет хотя бы имени Zend. Нужны ли нам фреймворки? Думаю да, нужны. Нужен как бы один общий стандарт, одна объектная модель, некий общий стандартизированный механизм. Все это включает в себя понятие framework При подготовке индекса, мы создали шесть полей, но только по трем из них будет осуществляться реальный поиск: заголовок документа, текст документа и автор. Каждый из этих элементов сохраняется отдельно, что позволяет разделять поиск по этим полям. Синтаксис для осуществления запросов к каждой секции очень похож на синтаксис Google – вы указываете поле, затем двоеточие, затем искомый термин (все без пробелов). К примеру, чтобы искать по автору «Quentin», необходимо составить запрос «author:Quentin» (обратите внимание, что поиск не чувствителен к регистру, а чтобы сделать его чувствительным, нужно выполнить некоторые настройки при создании индекса). По тому же сценарию, для поиска слова «php» в заголовке, необходимо составить запрос следующим образом: «title:php». Как мы вкратце упомянули ранее, секция, в которой осуществляется поиск по умолчанию, называется contents. Следовательно, если вы хотите найти слово «Google» в теле документа, то можете использовать запросы «contents:google» или просто «google».

Включение и исключение терминов По-умолчанию, все указанные поисковые слова ищутся с использованием отношений между ними типа «ИЛИ». Это означает, что если хотя бы одно слово есть в документе, то он войдет в


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene результаты выборки. Чтобы заставить систему вернуть результаты поиска с конкретным словом, используется символ плюса. Если необходим обратный результат – нахождение документа, где слово НЕ используется, то перед ним ставится минус. При использовании знаков плюсов и минусов, вы можете указывать наименования полей, по которым производится поиск. К примеру: +author:Quentin или author:+Quentin (запросы идентичны).

Поиск фраз В Zend_Search_Lucene существует возможность поиска целых фраз. К примеру, если вы хотите найти точную фразу «Статьи о PHP» - поисковый движок с этим справится. Так как данная тема является комплексной и требующей подробностей, мы не будем рассматривать ее здесь, а отправим читателя к документации: http://framework.zend.com/manual/en/zend.search.queries.html.

Примеры запросов Вот некоторые примеры запросов к Zend_Search_Lucene: 1.php 2. // осуществляет поиск в индексе на наличие слова «php» 3. 4.php -author:quentin 5. // ищет статью любого автора, в которой есть слово «php», за исключением статей автора по имени Quentin 6. 7.author:quentin 8. // Находит все статьи автора по имени Quentin 9. 10.php -ajax 11. // находит все статьи со словом «php», без слова «ajax» 12. 13.title:mysql 14. // ищет все статьи со словом «MySQL» в заголовке 15. 16.title:mysql -author:quentin 17. // осуществляет поиск статей с «MySQL» в заголовке, 18. // где автора не зовут Quentin

Получение результатов Всем возвращаемым результатам присваиваются очки. Они являются единицей измерения, которая указывает на соответствие найденного документа поисковой фразе. Результаты запроса выводятся согласно количества очков, от большего к меньшему. Алгоритм подсчета очков можно настраивать по своему усмотрению и, соответственно, влиять на вывод результатов. Об этом читайте далее в статье.

Запросы к индексу Ранее мы рассмотрели как писать запросы, научились добавлять и исключать слова из поиска, а так же попробовали делать запросы к различным полям индекса. Теперь мы обратим внимание на сам процесс извлечения документов из индекса. Существует два способа выполнения запросов к индексу: отправка необработанного запроса, чтобы Zend_Search_Lucene мог самостоятельно обработать запрос (идеально для тех случаев, когда совершенно неизвестно, что пользователь будет искать) или построение запроса вручную с помощью API.


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene В любом случае нужно использовать метод query(). Данный метод возвращает результаты совпадения индекса и поискового запроса. 1.<?php 2. require_once('Zend/Search/Lucene.php'); 3. 4. $indexPath = '/var/www/phpriot.com/data/docindex'; 5. 6. $index = new Zend_Search_Lucene($indexPath); 7. 8. $hits = $index->query('php +author:Quentin'); 9.?> Данный пример осуществляет запрос к индексу на предмет поиска слова «php» и автора статьи по имени Quentin. Обратите внимание, когда мы открывали индекс, мы не передавали второй параметр, как это сделали бы при создании индекса. Это происходит по причине того, что мы запрашиваем индекс, а не создаем его. Такой же запрос мы можем обработать и при помощи API: 1.<?php 2. require_once('Zend/Search/Lucene.php'); 3. 4. $indexPath = '/var/www/phpriot.com/data/docindex'; 5. 6. $index = new Zend_Search_Lucene($indexPath); 7. 8. $query = new Zend_Search_Lucene_Search_Query_MultiTerm(); 9. $query->addTerm(new Zend_Search_Lucene_Index_Term('php'), null); 10. $query->addTerm(new Zend_Search_Lucene_Index_Term('Quentin', 'author'), true); 11. 12. $hits = $index->query($query); 13.?> Второй параметр для addTerm определяет, является ли данное поле обязательным или нет. Значение «true» указывает на обязательность (как будто бы мы в запросе поставили перед словом знак плюса), значение «false» запрещает наличие термина (словно перед ним поставлен значок минуса). Значение «null» говорит о том, что термин не является ни запрещенным, ни обязательным. Второй параметр у Zend_Search_Lucene_Index_Term указывает на поле, в котором следует искать термин. По умолчанию это поле contents. В целом, первый вариант с передачей необработанного запроса является более простым.

Работа с результатами поиска Результаты поискового запроса возвращаются в виде массива, поэтому, вы можете использовать php-функцию count() для подсчета количества документов. Каждое проиндексированное поле доступно как атрибут класса. Вот пример вывода результатов запроса: 1.<?php 2. require_once('Zend/Search/Lucene.php'); 3. $query = 'php +author:Quentin'; 4. $indexPath = '/var/www/phpriot.com/data/docindex'; 5. $index = new Zend_Search_Lucene($indexPath); 6. $hits = $index->query($query); 7. $numHits = count($hits); 8.?> <p> 9. Найдено <?= $hits ?> результатов по запросу <?= $query ?>.


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene 10.</p> 11.<?php foreach ($hits as $hit) { ?> 12. <h3><?= $hit->title ?> (score: <?= $hit->score ?>)</h3> 13. <p> 14. Автор: <?= $hit->author ?> 15. </p> 16. <p> 17. <?= $hit->teaser ?><br /> 18. <a href="<?= $hit->url ?>">Подробнее...</a> 19. </p> 20.<?php } ?> Здесь мы так же использовали поле «Очки» (score), чтобы вывести на экран количество баллов каждого найденного документа (напомним, что они начисляются согласно соответствию документа поисковому запросу и выводятся от большего к меньшему).

Создание простой поисковой машины Используя приведенный выше код, мы можем создать простую поисковую машину для сайта. Нам нужно только добавить форму запроса и встроить обработку полученной фразы. Назовем наш файл search.php: 1.<?php 2. require_once('Zend/Search/Lucene.php'); 3. $query = isset($_GET['query']) ? $_GET['query'] : ''; 4. $query = trim($query); 5. $indexPath = '/var/www/phpriot.com/data/docindex'; 6. $index = new Zend_Search_Lucene($indexPath); 7. if (strlen($query) > 0) { 8. $hits = $index->query($query); 9. $numHits = count($hits); 10. } 11.?> 12.<form method="get" action="search.php"> 13. <input type="text" name="query" value="<?= htmlSpecialChars ($query) ?>" /> 14. <input type="submit" value="Search" /> 15.</form> 16.<?php if (strlen($query) > 0) { ?> 17. <p> 18. Найдено <?= $hits ?> результатов по запросу <?= $query ?>. 19. </p> 20. 21. <?php foreach ($hits as $hit) { ?> 22. <h3><?= $hit->title ?> (score: <?= $hit->score ?>)</h3> 23. <p> 24. Автор <?= $hit->author ?> 25. </p> 26. <p> 27. <?= $hit->teaser ?><br /> 28. <a href="<?= $hit->url ?>">Подробнее...</a> 29. </p> 30. <?php } ?> 31.<?php } ?>

Обработка ошибок До сих пор мы не сталкивались с обработкой ошибок при поиске. К примеру, если мы напишем


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene «title:» без указания фразы, искомой в поле title, то система вернет ошибку. Чтобы этого не допустить, нужно перехватить исключение Zend_Search_Lucene_Exception. 1.<?php 2. require_once('Zend/Search/Lucene.php'); 3. $query = isset($_GET['query']) ? $_GET['query'] : ''; 4. $query = trim($query); 5. $indexPath = '/var/www/phpriot.com/data/docindex'; 6. $index = new Zend_Search_Lucene($indexPath); 7. try { 8. $hits = $index->query($query); 9. } 10. catch (Zend_Search_Lucene_Exception $ex) { 11. $hits = array(); 12. } 13. $numHits = count($hits); 14.?> Этот код возвращает ноль найденных результатов в случае, если происходит ошибка поиска. При этом, пользователь не информируется о том, что что-то пошло не так при поиске. Однако, ничто не мешает получить текст ошибки с помощью ($ex->getMessage()) и вывести его на экран.

Расширение Zend_Search_Lucene Чтобы получить поисковое решение, полностью соответствующее вашим требованиям, существует несколько аспектов для расширения Zend_Search_Lucene. Среди таких аспектов: •

Пользовательская система обработки документа для определения ключевых слов;

Настройка алгоритма начисления очков для найденных документов поисковому запросу;

Пользовательская настройка хранения индекса. Ваш индекс может храниться где угодно и как угодно.

Пользовательская система обработки Вот некоторые идеи использования пользовательской системы обработки: •

Обработчик PDF. Позволяют определять PDF документы и искать в них.

Обработчик для изображений. Если использовать Optical Character Recognition (OCR), то можно сохранять в индексе слова, размещенные на изображении (хранить саму картинку можно в бинарном поле).

Обработчик HTML, предназначенный для индексирования контента без HTML-тегов.

Настройка алгоритма начисления очков Вы можете настраивать алгоритм соответствия текста документа, искомой фразе. К примеру, вы можете указать, что «вес» заголовка (поля «title») гораздо выше, чем поля contents. Таким образом, документы с искомой фразой в заголовке, будут выводиться выше, чем документы с совпадениями в содержании. Процесс работы с алгоритмом хорошо описан здесь: http://framework.zend.com/manual/en/zend.search.extending.html#zend.search.extending.scoring.


PHP Inside'18 >> В фокусе >> Полнотекстовый поиск с Zend_Search_Lucene Настройка хранения индекса Метод хранения индекса на диске может так же быть гибко настроен с помощью классов Zend_Search_Lucene_Storage_Directory и Zend_Search_Lucene_Storage_File. Процесс подробно описан на http://framework.zend.com/manual/en/zend.search.extending.html#zend.search.extending.storage.

Заключение В данной статье мы поближе познакомились с Zend_Search_Lucene, частью Zend Framework. Сейчас он еще находится в стадии разработки, однако позволяет осуществлять полнотекстовый поиск на сайтах, написанных на PHP 5. Конечно же, данный материал не освещает все аспекты работы с поисковым компонентом, однако, мы надеялись дать вам несколько идей для собственного исследования.

Ссылки: http://framework.zend.com/ http://framework.zend.com/manual/en/zend.search.html - руководство http://www.sai.msu.su/~megera/postgres/gist/tsearch/V2/ - полнотекстовое расширение для поиска в PostgreSQL. http://www.xapian.org/ - инструмент для построения собственных поисковых движков Переведено с разрешения: http://phpriot.com

Словарь терминов статьи Framework, Фреймворк – в сфере разработки программного обеспечения фреймворком называется программная структура (набор классов, функций и т.п.), с помощью которой разрабатываются другие программные продукты. Полнотекстовый поиск – поиск совпадений по всему документу, а не только по его отдельным частям (к примеру, набору ключевых слов). PEAR, PHP Extension and Application Repository – структурированное хранилище PHP-кода, предназначенное для хранения и дистрибуции программных компонентов (классов, библиотек и приложений). Официальный сайт проекта: http://pear.php.net


PHP Inside'18 >> Идеи >> PHP в командной строке

PHP в командной строке Автор: Кевин Рамкишун, Перевод: Левинский Роман

P

HP традиционно используется только в интернете для работы ваших веб-сайтов, однако, это не единственная сфера его применения. Начиная с версии 4.3 PHP поставляется со специальной версией, которая может быть использована для выполнения скриптов в командной строке, решающих определенные системные задачи. Если вы пользователь Linux, вы наверное знаете что такое командная строка, но если вы пользователь Windows, вы можете этого и не знать. Командная строка используется для выполнения определенных системных команд. Для доступа к командной строке в Windows XP выполните Пуск -> Выполнить, и затем наберите 'cmd.exe'. Появится новое окно DOS и мигающая черта после C:\Windows. Что-нибудь, наподобие этого: 1.Microsoft Windows XP [Версия 5.1.2600] 2.(С) Корпорация Майкрософт, 1985-2001. 3.D:\Documents and Settings\Kings>_ Это командная строка, в которой вы можете вводить команды для выполнения определенных задач. Также возможно выполнение PHP скриптов с помощью интерпретатора PHP для командной строки, называемого PHP CLI. В это статье я хочу показать вам, как использовать PHP CLI, его возможности, и для чего он может быть использован. Начнем с самых простых вещей: как использовать PHP CLI.

Как использовать PHP CLI Для запуска PHP скрипта из командной строки в Linux, необходимо выполнить команду CHMOD, для того, чтобы PHP скрипт стал исполняемым ("chmod +x script.php") и затем его можно было использовать как программу. Просто кликните на скрипте, и он запустится в командной строке. В Windows это сделать немного сложнее. Для запуска PHP скрипта в командной строке откройте приглашение на ввод команды (Пуск -> Выполнить, 'cmd.exe'). Затем перейдите в директорию где находится ваш скрипт, используя обычные команды DOS. Например, если мой скрипт находится в D:\myscripts, я набираю следующее 1.Microsoft Windows XP [Версия 5.1.2600] 2.(С) Корпорация Майкрософт, 1985-2001. 3.D:\Documents and Settings\Kings>cd.. 4.D:\Documents and Settings>cd.. 5.D:\>cd myscripts 6.D:\myscripts\_ Когда вы в необходимой директории, введите сначала размещение вашего CLI php.exe (обычно это C:\PHP\CLI\php.exe), затем пробел и имя вашего скрипта, например, так 1.C:\PHP\CLI\php.exe script.php Затем нажмите Enter для запуска вашего скрипта в командной строке. Попробуем сделать это на примере простого Hello World: 1.#!/usr/bin/php


PHP Inside'18 >> Идеи >> PHP в командной строке 2.<?php 3. 4.?>

echo 'Hello World';

Обратите внимание на первую строку приведенного выше примера. Она называется "shebang" и используется только в Linux, сообщая интерпретатору командной строки где искать исполняемый файл PHP (убедитесь, что путь корректный). В Windows она игнорируется. Запустив приведенный выше пример, вы получите следующий результат: 1.Content-type: text/html 2.X-Powered-By: PHP/4.4.0 3.Hello World Как видите, он выводит 'Hello World'. Первые две строки — это стандартные заголовки, посылаемые PHP при обычном его использовании, но совершенно бесполезные в данном случае. Это не является проблемой, однако довольно просто скрыть их. Пользователи Linux могут изменить "shebang" на: 1.#!/usr/bin/php -q Пользователи Windows могут использовать -q в качестве аргумента, подобно этому: 1.G:\Projects\PHPit\content\php on the command line\demos>D:\PHP\CLI\php.exe -q simple.php. Это основы использования PHP CLI скриптов, теперь обратимся к особым возможностям CLI: аргументам.

Передача аргументов При использовании PHP CLI также возможна передача аргументов в скрипты, и использование этих аргументов в скрипте. Следующий пример демонстрирует это: 1.#!/usr/bin/php -q 2.<?php 3. echo "The following arguments were passed:\n"; 4. print_r ($_SERVER['argv']); 5.?> Для вызова скрипта с аргументами введите их после имени скрипта, например, так: 1.C:\PHP\CLI\php.exe -q arguments.php one two three Результатом будет: 1.Следующие аргументы были переданы: 2.Array 3.( 4. [0] => arguments.php 5. [1] => one 6. [2] => two 7. [3] => three 8.) Аргументы могут быть использованы для разнообразных целей, например, для передачи дополнительных значений или выполнения различных задач. Например, следующий скрипт — это простой PHP CLI калькулятор, демонстрирующий использование аргументов:


PHP Inside'18 >> Идеи >> PHP в командной строке 1.#!/usr/bin/php -q 2.<?php 3. echo "\n\nPHP CLI Calculator:\n\n"; 4. 5. $args = $_SERVER['argv']; 6. 7. if (count($args) != 4) { 8. echo 'Please pass two numbers 9. } 10. 11. $num1 = intval($args['2']); 12. $num2 = intval($args['3']); 13. 14. echo 'Answer: '; 15. 16. switch(strtolower($args['1'])) { 17. case 'add': 18. echo $num1 . ' + ' . $num2 . 19. break; 20. case 'subtract': 21. echo $num1 . ' - ' . $num2 . 22. break; 23. case 'multiply': 24. echo $num1 . ' * ' . $num2 . 25. break; 26. case 'divide': 27. echo $num1 . ' / ' . $num2 . 28. break; 29. } 30.?>

and an action';

' = ' . ($num1+$num2); ' = ' . ($num1-$num2); ' = ' . ($num1*$num2); ' = ' . ($num1/$num2);

Для использования вышеприведенного скрипта вы можете вызвать его с тремя аргументами: действие и два числа, например, так: 1.C:\PHP\CLI\php.exe 2.C:\PHP\CLI\php.exe 3.C:\PHP\CLI\php.exe 4.C:\PHP\CLI\php.exe

-q -q -q -q

calculator.php calculator.php calculator.php calculator.php

divide 3 4 add 1 7 subtract 10 3 multiply 6 2

Теперь посмотрим на другую уникальную возможность пользовательского ввода.

командной

строки:

ожидание

Получение входных данных При использовании командной строки также возможно запрашивать пользовательский ввод, читая его из STDIN. Это специальная константа, которая может быть использована для получения и обработки данных в вашем скрипте, введенных через командную строку. Следующий пример демонстрирует это: 1.#!/usr/bin/php -q 2.<?php 3. 4. echo "\n\nWhat is your name?\n"; 5. $name = fread(STDIN, 1024); 6. echo "\nWelcome $name"; 7.?>


PHP Inside'18 >> Идеи >> PHP в командной строке Этот скрипт запрашивает ваше имя, и затем выводит его обратно. Это может быть очень полезным для передачи значений, вместо использования аргументов. Сравним наш калькулятор, приведенный выше, и его переработанную версию: 1.#!/usr/bin/php -q 2.<?php 3. echo "\n\nPHP CLI Calculator:\n\n"; 4. echo "What would you like to do? (add|subtract|multiply| divide)\n"; 5. $action = strtolower(fread(STDIN, 1024)); 6. $action = trim($action); 7. $valid = array('add', 'subtract', 'multiply', 'divide'); 8. if (in_array($action, $valid) == false) { 9. echo "\nInvalid action"; 10. die(); 11. } 12. echo "\nEnter a number: "; 13. $num1 = trim(fread(STDIN, 1024)); 14. if (is_numeric($num1) == false) { 15. echo "\nInvalid number."; 16. die(); 17. } 18. echo "Enter a second number: "; 19. $num2 = trim(fread(STDIN, 1024)); 20. if (is_numeric($num2) == false) { 21. echo "\nInvalid number."; 22. die(); 23. } 24. echo "\nAnswer: "; 25. switch(strtolower($action)) { 26. case 'add': 27. echo $num1 . ' + ' . $num2 . ' = ' . ($num1+$num2); 28. break; 29. case 'subtract': 30. echo $num1 . ' - ' . $num2 . ' = ' . ($num1-$num2); 31. break; 32. case 'multiply': 33. echo $num1 . ' * ' . $num2 . ' = ' . ($num1*$num2); 34. break; 35. case 'divide': 36. echo $num1 . ' / ' . $num2 . ' = ' . ($num1/$num2); 37. break; 38. } 39.?> Если вы запустите этот скрипт, он не потребует никаких аргументов, вместо этого он спросит у вас действие и числа. Это точно такой же скрипт, как и предыдущий, но более простой для ввода значений.

Заключение В этой статье я показал вам как запускать PHP скрипты из командной строки и показал некоторые характерные особенности командной строки, такие как STDIN. Несмотря на то, что примеры в этой статье очень простые и не имеют большой практической ценности, PHP CLI в действительности может быть очень полезным, и выполнять почти все, что угодно. Главным образом потому, что PHP скрипты, запускаемые из командной строки имеют все


PHP Inside'18 >> Идеи >> PHP в командной строке системные возможности и имеют доступ едва ли не в любую директорию. Кандидатами скриптов для командной строки могут быть скрипты, решающие такие задачи, как резервное копирование, или те, которые необходимо выполнять ежечасно/ежедневно/еженедельно. Если вы объедините PHP CLI с Планировщиком Задач Windows или с Cron, вы можете выполнять определенные задачи автоматически, такие как очистка кэша данных, извлечение RSS потоков, или даже передвигаться по другим веб-сайтам.

Словарь терминов статьи CLI, Command Line Interface, Командная строка – Метод взаимодействия с компьютером через текстовый терминал. Команды, отдаваемые компьютеру вводятся в виде текстовых строк, обычно с клавиатуры. На практике противопоставляется Графическому Пользовательскому Интерфейсу (GUI, Graphical User Interface). CHMOD (change mode command) – команда в окружении Unix-подобных систем, меняющая режимы файлов и папок, управляя системой разграничения прав доступа к ним.

Переведено с разрешения: http://www.phpfreaks.com/


PHP Inside'18 >> Идеи >> Осторожно, данные!

Осторожно, данные! Кузьма Феськов

Т

акая экспрессия в названии статьи вполне допустима, потому что всякий, кто хоть раз сталкивался с реально работающими проектами, занимался их написанием и сопровождением, знает, что основную опасность несут именно они, этим самые неизвестные данные. О каких данных идет речь? Да о любых: пользователь заполнил какую-то форму, вписал какие-то параметры в URL, и так далее. Вариантов, откуда могут поступить эти самые данные море. В этом материале, я хочу рассмотреть их поближе, поговорить о той опасности, которую они могут с собой принести, а также обсудить пути устранения этих опасностей. В этом материале я буду считать, что вы используйте следующий софт с такими настройками: •

Apache 2.x

PHP 5.1.x

MySql 4.1.x

php.ini: •

register_globbals = off;

ini_set('display_errors', 1);

error_reporting(2047);

А, следовательно, NOTICE мы с вами тоже будем считать ошибкой!

Первое правило безопасности Первое, что вы должны научиться делать, это принимать данные из правильных источников. Основные источники данных в PHP – это суперглобальные массивы, в частности $_GET и $_POST. Приучайте себя брать данные именно из этих массивов. Также не лишним будет отметить, и тот факт, что параметр php.ini register_globbals должен быть off. Немного поясню последнее замечание о register_globbals. Ни раз мною замечалось – до сих пор многие программисты не понимаю почему этот параметр важно устанавливать в значение off. Многим кажется удобным, что переменные, переданные в скрипт методом POST или GET сразу появляются в пространстве скрипта и готовы к использованию. Однако, это удобство крайне обманчиво. Во-первых, если вы используете в своем скрипте какую-то переменную и заранее не позаботились об инициализации ее значения, то, дописав в URL ее название, я легко могу переопределить ее значение на нужное мне, в результате ваш скрипт этого даже не заметит. Вовторых, вы никогда не будете знать точно, каким именно образом вам передана та или иная переменная. Есть и другие менее значимые факторы. Исходя из всех этих замечаний, легко понять, что опасность подобного подхода весьма велика и он прямо ставит под угрозу написанные вами скрипты.


PHP Inside'18 >> Идеи >> Осторожно, данные! После небольшого отступления, давайте перейдем к практике. Итак, к нам поступила (или не поступила) переменная методом GET: 1.URL:/index.php?variable=10 О переменной мы заранее знаем, что она придет методом GET, будет содержать целое число. Первым делом, мы присваиваем значение локальной переменной. Естественно, многие сделали это следующим способом: 1.<?php 2.$variable = $_GET['variable']; 3.?> Однако, что произойдет в том случае, если переменная не пришла (возможно кто-то пытался испытать ваш скрипт на прочность и стер ее из URL)? Правильно – вы получите сообщение об ошибке (NOTICE мы договорились считать ошибкой). Чтобы этого не произошло, необходимо изменить наш код следующим образом: 1.<?php 2.$variable = isset($_GET['variable']) : (int)$_GET['variable'] : 0; 3.?> Для простоты кода и удобства, я применил в данном случае так называемую короткую нотацию оператора if. В обычном виде скрипт выглядел бы так: 1.<?php 2.if (isset($_GET['variable'])) { 3. $variable = (int)$_GET['variable']; 4.} else { 5. $variable = 0; 6.} 7.?> Но, согласитесь, эта запись слишком громоздка. Давайте вернемся к нашему коду. Мы с вами использовали команду isset, которая проверяет наличие переменной или индекса в массиве, и возвращает false если такого элемента нет, и true, если он есть. Что позволяет нам избежать обращения к несуществующему элементу. Поскольку мы заранее знаем, что эта переменная может содержать только целые числа, то, не анализируя ее содержимое, мы приводим его к целому числу. В данном случае мы применили короткую команду (int), которая эквивалентна intval(). Даже если вам передадут в этой переменной вместо числа строку, данная команда приведет ее к числу 0, что является целым числом и вполне удовлетворяет нашему требованию. Это, безусловно, очень простой пример, но он способен показать все хитросплетения и повороты, которые подстерегают нас на пути написания скрипта. Итак, первое правило: Всегда берите значения там, откуда они должны придти в скрипт. Всегда проверяйте переменные и индексы массивов на существование перед их использованием. Всегда инициализируйте значения переменных перед их использованием.

Правило второе С целыми значениями все довольно просто, но как быть, если нам необходимо получить строковые данные. Здесь есть несколько вариантов. Вариант первый: переменная приходит методом GET, переменная описывает один из возможных режимов работы при помощи ключевого слова.


PHP Inside'18 >> Идеи >> Осторожно, данные! 1.URL:/index.php?mode=calendar Поскольку переменная содержит название режима работы скрипта, а количество режимов и их названия, как правило, заранее известны, мы организуем простую проверку: 1.<?php 2.$all_mode = array('calendar', 'comments', 'news'); 3.if (isset($_GET['mode'] && in_array($_GET['mode'], $all_mode)) { 4. $mode = $_GET['mode']; 5.} else { 6. $mode = 'calendar'; 7.} 8.?> Давайте разберем код. Первым делом, мы создаем массив с перечнем всех возможных значений переменной $mode. Далее, помня о первом правиле, проверяем индекс массива на существование. Теперь, при помощи команды in_array, мы проверяем, является ли значение переменной $mode допустимым. Если ее значение присутствует в нашем массиве, то мы присваиваем локальной переменной $mode значение из $_GET['mode'], в противном случае, даем ей значение поумолчанию. Второй вариант: переменная приходит методом GET, передает букву для организации вывода данных по алфавиту. 1.URL:/index.php?letter=a

1.<?php 2.if (isset($_GET['mode'] && !empty($_GET['mode'])) { 3. $mode = substr($_GET['mode'], 0, 1); 4. if (!preg_match("/[a-zA-Z0-9]/", $mode) { 5. $mode = 'a'; 6. } 7.} else { 8. $mode = 'a'; 9.} 10.?> Конечно же надеяться на добросовестность пользователей – это хорошо, но на сотню другую добросовестных пользователей всегда найдется чудо-хакер, а потому мы подстрахуемся. Первым делом, мы выкусываем из переменной только первую букву командой substr (мало ли сколько текста нам могли написать в URL). Далее, мы проверяем оставшийся символ на вхождение в допустимый диапазон. В нашем случае, символы могут быть латинскими, а также цифры. В качестве замечания укажу, что проверить допустимость символа вы таже можете составив массив допустимых символов и применив команду in_array, использованную в предыдущем примере. Она отработает намного быстрее регулярного выражения. Итак, благодаря тому, что мы сумели типизировать наши данные, появилась возможность отказаться от кучи всяких проверок на правильность. Мы просто подогнали поступившие данные под нужные нам требования. Кто-то из вас может возразить, что это не совсем правильно, и нужно выдавать пользователю ошибку. Однако, смею вас заверить, что каждая надоедливая ошибка – это потерянный пользователь. К тому же в данных примерах совершенно нет никакого смысла выводить сообщение об ошибке, поскольку полученные нами данные не являются фатальными или архи-важными, и мы легко можем обойтись с ними таким образом.


PHP Inside'18 >> Идеи >> Осторожно, данные! Отсюда второе правило: стремитесь к максимальной типизации нужных вам данных.

Правило третье Вот мы с вами добрались и до третьего правила. Оно, правда, будет не совсем правилом, скорее памяткой. Частенько, данные, которые нам нужно получить, должны подчиняться какому-либо стандарту, например есть стандарт на e-mail адрес, на URL и так далее. Здесь я приведу для вас используемые мною функции для проверки разных стандартизированных данных.

Проверка e-mail адреса 1./** 2.* Проверяет e-mail адрес на соответствие стандарту 3.* 4.* @param string $email строка с e-mail адресом 5.* @return bool false если e-mail не соответствует стандарту 6.*/ 7.function EmailCheck($email) { 8. if (!preg_match ( "/^[-\w.]+@([A-z0-9][-A-z0-9]+\.)+[A-z]{2,4} $/", $email)) { 9. return false; 10. } 11. return true; 12.}

Проверка URL 1./** 2. * Проверка URL на соответствие стандарту 3. * 4. * @param string $url строка с URL 5. * @return string строку с URL или false, если неправильно 6. */ 7.function CheckUrl($url) { 8. $url = trim(preg_replace("/[^\x20-\xFF]/", '', strval($url))); 9. if (0 == strlen($url)) { 10. return false; 11. } 12. if (!preg_match("~^(?:(?:https?|ftp|telnet)://(?:[a-z0-9_-] {1,32}" 13. . "(?::[a-z0-9_-]{1,32})?@)?)?(?:(?:[a-z0-9-]{1,128}\.)+(?:com| net|" 14. . "org|mil|edu|arpa|gov|biz|info|aero|inc|name|[a-z]{2})|(?!0) (?:(?" 15. . "!0[^.]|255)[0-9]{1,3}\.){3}(?!0|255)[0-9]{1,3})(?:/[a-z09.,_@%&" 16. . "?+=\~/-]*)?(?:#[^ '\"&<>]*)?$~i", $url, $ok)) { 17. return false; 18. } 19. if (!strstr($url, '://')) { 20. $url = 'http://' . $url; 21. } 22. $url = preg_replace("~^[a-z]+~ie", "strtolower('\\0')", $url); 23. 24. return $url; 25.}


PHP Inside'18 >> Идеи >> Осторожно, данные! Обращаю ваше внимание на то, что функция CheckUrl не только проверяет URL на соответствие стандарту, но и ликвидирует простые несоответствия. В частности, она добавляет http://, если в исходном URL отсутствовало указание на протокол, также преобразует в нижний регистр написание домена. Следовательно, вам необходимо получить результат работы этой функции и присвоить его значение исходной переменной.

Проверка даты Сначала приведу пример, в котором мы проверим не саму дату, а правильность ее формата, иногда этого бывает достаточно: 1.<?php 2./** 3. * Проверка даты на соответствие формату YYYY-mm-dd 4. * 5. * @param string $date строка с датой (правильный формат YYYY-mm-dd) 6. * @return false если дата не совпадает с форматом 7. */ 8.function CheckDate($date) { 9. if (empty($date) || !preg_match("#[0-9]{4}\-[0-9]{2}\-[0-9]{2} #", $date)) { 10. return false; 11. } 12.} 13.?> Однако, часто этой проверки бывает недостаточно и нам, помимо всего, нужно быть точно уверенным, что такая дата действительно существует. Например, даты 31 апреля не существует и нам такая дата не подходит. 1.<?php 2./** 3. * Проверка даты на соответствие формату YYYY-mm-dd 4. * и ее реальность по григорианскому календарю. 5. * 6. * @param string $date строка с датой (правильный формат YYYY-mm-dd) 7. * @return false если дата не совпадает с форматом 8. */ 9.function CheckDate($date) { 10. $tmp = explode('-', $date); 11. if (!preg_match("#[0-9]{2}\.[0-9]{2}\.[0-9]{4}#", $date) || false === checkdate($tmp[1], $tmp[0], $tmp[2])) { 12. return false; 13. } 14.?> Во втором варианте функции мы помимо проверки правильности формата записи (регулярное выражение), воспользовавшись функцией checkdate, проверили ее правильность с точки зрения существования такой даты в календаре. Вот, пожалуй, основные вещи, которые используются чаще всего в наших приложениях и должны быть проверены. Правило третье: если ваши данные подчиняются четкому формату, описанному в спецификации, то, вероятнее всего, кто-то уже написал функцию для проверки этих данных на соответствие формату. Еще одно подправило, но очень важное: читайте комментарии на php.net к тем или иным функциям – часто ваша проблема там уже описана!


PHP Inside'18 >> Идеи >> Осторожно, данные!

Бои без правил Да, именно так! До сего момента мы с вами имели дело с данными, которые можно классифицировать, подогнать под стандарт, в общем, содержание которых нам было более или менее ясно. Теперь же мы поговорим с вами о данных другого рода – о данных неизвестного формата. Откуда могут появится такие загадочные данные? Все очень просто. Например, на вашем сайте есть гостевая книга или форум, а, следовательно, пользователь может ввести в них любой текст, и этот самый текст для нас с вами является черным ящиком, ибо предсказать что именно посчитал возможным ввести в форму пользователь не представляется возможным. Но любой черный ящик поддается расшифровке, и далее я предложу вам несколько способов приведения его содержимого в порядок. И так, прежде всего давайте подумаем, что нам может понадобиться, а что нам точно не нужно. Например, мы написали собственную гостевую книгу и хотим, чтобы пользователи оставляли в ней только текстовые сообщения, а теги HTML мы в ней видеть не хотим. В арсенале PHP есть специальная команда для вырезания тегов: strip_tags. 1.<?php 2.$string = strip_tags($string); 3.?> В результате вы получите текст, свободный от HTML тегов. Но бывают случаи, когда мы хотим дать пользователю возможность выделять какие-то слова, например делать шрифт жирным (<strong> или <b>) или наклонным (<i>). Пожалуйста, рассмотренная выше команда легко поможет нам в этом: 1.<?php 2.$string = strip_tags($string, '<strong><b><i>'); 3.?> Как видите, вторым аргументом мы можем перечислить те теги, которые желаем оставить. А, следовательно, пользователь, который их применит, увидит свой текст таким, каким он его задумал. Возможен и другой вариант – у вас свой форум, посвященный проблемам программирования и вырезать теги вам не только не надо вырезать, но надо их показать пользователю в том виде, в каком он их ввел. Здесь нам на помощь приходит еще одна функция htmlspecialchars, которая приводит все теги HTML к виду, пригодному для печати. 1.<?php 2.$html = '<b>Мой текст</b>'; 3.echo $html; 4.echo '<br>'; 5.echo htmlspecialchars($html); 6.?> Этот пример наглядно продемонстрирует вам действие этой функции. Также, эта функция будет незаменима, если пользователь захочет отредактировать, введенные ранее, данные. Например, в <input type='text' name='my_input' value=''> пользователь ввел какой-то текст, в котором присутствуют одинарные кавычки, которые мы использовали для ограничивания параметров, или другие теги. Боюсь, что просто вставить этот текст назад в данный INPUT у вас не получится. Однако, если вы примените перед вставкой текста в форму указанную выше функцию, все «недопустимые» символы будут специальным образом преобразованы, и пользователь увидит свой текст в том виде в каком он его ввел.


PHP Inside'18 >> Идеи >> Осторожно, данные! Все это хорошо, но на сегодняшний день сложился уже можно сказать стандарт, так называемые bb-коды, зная которые, пользователь легко может раскрашивать текст, вставлять в него разного рода форматирование не применяя для этого опасные для вас, как для автора сайта HTML теги и прочие, потенциально вредные команды. Кузьма Феськов, [kvf77], сотрудник компании RBC, автор нескольких статей о PHP приводит три правила обработки данных: 1. Всегда берите значения там, откуда они должны придти в скрипт. Всегда проверяйте переменные и индексы массивов на существование перед их использованием. Всегда инициализируйте значения переменных перед их использованием; 2. Стремитесь к максимальной типизации нужных вам данных; 3. Если ваши данные подчиняются четкому формату, описанному в спецификации, то, вероятнее всего, кто-то уже написал функцию для проверки этих данных на соответствие формату. Читайте так же комментарии на php.net к тем или иным функциям – часто ваша проблема там уже описана! Что такое bb-код? Это заранее оговоренный набор команд, которые, при выводе, ваш скрипт будет преобразовывать в набор HTML тегов, который, собственно, позволит пользователю иметь нужное ему оформление текста, а вам, быть относительно спокойными на предмет введения пользователем зловредных данных, которые вы попросту можете спокойно вырезать, оставляя лишь то, что было заранее оговорено. Чтобы отойти от словестного описания, давайте взглянем на несколько bb-кодов, которые наиболее распространены на сегодняшний день: •

[URL=http://mysite.ru]Мой сайт[/URL] = <a href='http://mysite.ru'>Мой сайт</a>

[B]Текст[/B] = <b>Текст</b>

[IMG]http://mysite.ru/mypic.gif[/IMG] = <img src=' http://mysite.ru/mypic.gif '>

[PHP]PHP код[/PHP] = здесь будет отображен PHP код, в котором будут подсвечены все переменные, ключевые слова и функция так, как будто вы видете его в профессиональном редакторе.

И так далее. Если у вас есть вопрос – а зачем все это нужно, почему нельзя сразу писать HTML теги, я отвечу – все просто, если позволить вашим пользователям писать теги, вы оставляете потенциальную «дыру» в своем сайте, поскольку пользователь может ввести JavaScript функции или другой зловредный код,который вполне может нанести ущерб вашим посетителям и вашему сайту. Применение bb-кодов позволяет вам найти некий компромисс между удобством и безопасностью. Впрочем, мы с вами заговорились, настало время посмотреть, каким образом все это работает, то есть посмотреть на код, который реализует замену bb-кодов на HTML. Хочу поблагодарить за помощь в подготовке этой части Ковригина Романа и разработчика плагина для Smarty Andre Rabold. Итак, первая функция преобразует обширный набор bb-кодов в их HTML эквивалент: 1.<?php 2.function bbcode2html($message) { 3. $preg = array( 4. '/(?<!\\\\)\[color(?::\w+)?=(.*?)\](.*?)\[\/color(?::\w+)?\]/si' => "<span style=\"color:\\1\">\\2</span>", 5. '/(?<!\\\\)\[size(?::\w+)?=(.*?)\](.*?)\[\/size(?::\w+)?\]/si'


PHP Inside'18 >> Идеи >> Осторожно, данные! => "<span style=\"font-size:\\1\">\\2</span>", 6. '/(?<!\\\\)\[font(?::\w+)?=(.*?)\](.*?)\[\/font(?::\w+)?\]/si' => "<span style=\"font-family:\\1\">\\2</span>", 7. '/(?<!\\\\)\[align(?::\w+)?=(.*?)\](.*?)\[\/align(?::\w+)?\]/si' => "<div style=\"text-align:\\1\">\\2</div>", 8. '/(?<!\\\\)\[b(?::\w+)?\](.*?)\[\/b(?::\w+)?\]/si' => "<span style=\"font-weight:bold\">\\1</span>", 9. '/(?<!\\\\)\[i(?::\w+)?\](.*?)\[\/i(?::\w+)?\]/si' => "<span style=\"font-style:italic\">\\1</span>", 10. '/(?<!\\\\)\[u(?::\w+)?\](.*?)\[\/u(?::\w+)?\]/si' => "<span style=\"text-decoration:underline\">\\1</span>", 11. '/(?<!\\\\)\[center(?::\w+)?\](.*?)\[\/center(?::\w+)?\]/si' => "<div style=\"text-align:center\">\\1</div>", 12. 13. // [code] & [sql] 14. '/(?<!\\\\)\[sql(?::\w+)?\](.*?)\[\/sql(?::\w+)?\]/sie' => "highlight_sql('\\1');", 15. '/(?<!\\\\)\[code(?::\w+)?\](.*?)\[\/code(?::\w+)?\]/sie' => "highlight_code('\\1');", 16. '/(?<!\\\\)\[php(?::\w+)?\](.*?)\[\/php(?::\w+)?\]/ise' => "highlight_php('\\1');", 17. // [email] 18. '/(?<!\\\\)\[email(?::\w+)?\](.*?)\[\/email(?::\w+)?\]/si' => "<a href=\"mailto:\\1\" class=\"bb-email\">\\1</a>", 19. '/(?<!\\\\)\[email(?::\w+)?=(.*?)\](.*?)\[\/email(?::\w+)?\]/ si' => "<a href=\"mailto:\\1\" class=\"bb-email\">\\2</a>", 20. // [url] 21. '/(?<!\\\\)\[url(?::\w+)?\]www\.(.*?)\[\/url(?::\w+)?\]/si' => "<a href=\"http://www.\\1\" target=\"_blank\" class=\"bburl\">\\1</a>", 22. '/(?<!\\\\)\[url(?::\w+)?\](.*?)\[\/url(?::\w+)?\]/si' => "<a href=\"\\1\" target=\"_blank\" class=\"bb-url\">\\1</a>", 23. '/(?<!\\\\)\[url(?::\w+)?=(.*?)?\](.*?)\[\/url(?::\w+)?\]/si' => "<a href=\"\\1\" target=\"_blank\" class=\"bb-url\">\\2</a>", 24. // [img] 25. '/(?<!\\\\)\[img(?::\w+)?\](.*?)\[\/img(?::\w+)?\]/si' => "<img src=\"\\1\" alt=\"\\1\" class=\"bb-image\" />", 26. '/(?<!\\\\)\[img(?::\w+)?=(.*?)x(.*?)\](.*?)\[\/img(?::\w+)?\]/ si' => "<img width=\"\\1\" height=\"\\2\" src=\"\\3\" alt=\"\\3\" class=\"bb-image\" />", 27. // [quote] 28. '/(?<!\\\\)\[quote(?::\w+)?\](.*?)\[\/quote(?::\w+)?\]/si' => "<div>Quote:<div class=\"bb-quote\">\\1</div></div>", 29. '/(?<!\\\\)\[quote(?::\w+)?=(?:"|"|\')?(.*?)["\']? (?:"|"|\')?\](.*?)\[\/quote\]/si' => "<div>Quote \\1:<div class=\"bb-quote\">\\2</div></div>", 30. // [list] 31. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[\*(?::\w+)?\](.*?)(?= (?:\s*<br\s*\/?>\s*)?\[\*|(?:\s*<br\s*\/?>\s*)?\[\/?list)/si' => "\n<li class=\"bb-listitem\">\\1</li>", 32. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[\/list(:(?!u|o)\w+)?\] (?:<br\s*\/?>)?/si' => "\n</ul>", 33. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[\/list:u(:\w+)?\] (?:<br\s*\/?>)?/si' => "\n</ul>", 34. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[\/list:o(:\w+)?\] (?:<br\s*\/?>)?/si' => "\n</ol>", 35. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[list(:(?!u|o)\w+)?\]\s* (?:<br\s*\/?>)?/si' => "\n<ul class=\"bb-list-unordered\">",


PHP Inside'18 >> Идеи >> Осторожно, данные! 36. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[list:u(:\w+)?\]\s* (?:<br\s*\/?>)?/si' => "\n<ul class=\"bb-list-unordered\">", 37. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[list:o(:\w+)?\]\s* (?:<br\s*\/?>)?/si' => "\n<ol class=\"bb-list-ordered\">", 38. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[list(?::o)?(:\w+)?=1\]\s* (?:<br\s*\/?>)?/si' => "\n<ol class=\"bb-list-ordered,bb-listordered-d\">", 39. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[list(?::o)?(:\w+)?=i\]\s* (?:<br\s*\/?>)?/s' => "\n<ol class=\"bb-list-ordered,bb-listordered-lr\">", 40. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[list(?::o)?(:\w+)?=I\]\s* (?:<br\s*\/?>)?/s' => "\n<ol class=\"bb-list-ordered,bb-listordered-ur\">", 41. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[list(?::o)?(:\w+)?=a\]\s* (?:<br\s*\/?>)?/s' => "\n<ol class=\"bb-list-ordered,bb-listordered-la\">", 42. '/(?<!\\\\)(?:\s*<br\s*\/?>\s*)?\[list(?::o)?(:\w+)?=A\]\s* (?:<br\s*\/?>)?/s' => "\n<ol class=\"bb-list-ordered,bb-listordered-ua\">", 43. // escaped tags like \[b], \[color], \[url], ... 44. '/\\\\(\[\/?\w+(?::\w+)*\])/' => "\\1" 45. 46. ); 47. $message = preg_replace(array_keys($preg), array_values($preg), $message); 48. return $message; 49.} 50.?> На входе функция принимает текст, содержащий bb-коды, на выходе вы получаете готовый HTML для вывода на экран. Для своей работы функция требует еще 3 функции: Первая функция занимается раскраской PHP кода, таким образом, как это делают профессиональные редакторы. На входе она получает bb-код [php][/php], на выходе, возвращает раскрашенный PHP код, содержащийся между этими тегами. 1.<?php 2.function highlight_php($str) { 3. $tags_exists = true; 4. 5. $res = htmlspecialchars($str); 6. $res = str_replace(array('<?php', '<?', '?>', '->', '=&gt'), array('<?php', '<?', '?>', '->', '=>'), $res); 7. 8. if (strpos('<?', $res) === false) { 9. $res = '<?php '.$res.' ?>'; 10. $tags_exists = false; 11. } 12. 13. $res = '<div class="bb-php">'.highlight_string($res, true). '</div>'; 14. if (!$tags_exists) { 15. $res = str_replace(array('<?php', '<?', '?>', '<br />'), array('', '', '', ''), $res); 16. } 17. 18. return $res;


PHP Inside'18 >> Идеи >> Осторожно, данные! 19.} 20.?> Вторая функция позволяет вывести на экран произвольный программный код или HTML так, чтобы видны были все теги ([code][/code]). Кстати, здесь мы использовали уже известную нам ранее команду htmlspecialchars. 1.<?php 2.function highlight_code($str) { 3. $res = '<div class="bb-code">'.htmlspecialchars($str).'</div>'; 4. return $res; 5.} 6.?> Третья функция позволяет вывести раскрашенный SQL запрос к базе данных ([sql][/sql]). 1.<?php 2.function highlight_sql($str) { 3. $words = array('add', 'auto_increment', 'all', 'alter', 'analyze', 'and', 4. 'as', 'asc', 'before', 'between', 'bigint', 'binary', 'blob', 5. 'both', 'by', 'cascade', 'case', 'change', 'character', 'check', 6. 'collate', 'column', 'columns', 'constraint', 'convert', 7. 'create', 'cross', 'current_date', 'current_time', 'current_timestamp', 8. 'current_user', 'database', 'databases', 'day_hour', 'day_microsecond', 9. 'day_minute', 'day_second', 'dec', 'decimal', 'default', 'delayed', 10. 'delete', 'desc', 'describe', 'distinct', 'distinctrow', 'div', 11. 'double', 'drop', 'dual', 'else', 'enclosed', 'escaped', 'exists', 12. 'explain', 'false', 'fields', 'float', 'float4', 'float8', 'for', 13. 'force', 'foreign', 'from', 'fulltext', 'grant', 'group', 'having', 14. 'high_priority', 'hour_microsecond', 'hour_minute', 'hour_second', 15. 'if', 'ignore', 'in', 'index', 'infile', 'inner', 'insert', 'int', 16. 'int1', 'int2', 'int3', 'int4', 'int8', 'integer', 'interval', 17. 'into', 'is', 'join', 'key', 'keys', 'kill', 'leading', 'left', 18. 'like', 'limit', 'lines', 'load', 'localtime', 'localtimestamp', 19. 'lock', 'long', 'longblob', 'longtext', 'low_priority', 'match', 20. 'mediumblob', 'mediumint', 'mediumtext', 'middleint', 21. 'minute_microsecond', 'minute_second', 'mod', 'natural', 'not', 22. 'no_write_to_binlog', 'null', 'numeric', 'on', 'optimize', 23. 'option', 'optionally', 'or', 'order', 'outer', 'outfile', 24. 'precision', 'primary', 'privileges', 'procedure',


PHP Inside'18 >> Идеи >> Осторожно, данные! 'purge', 'raid0', 25. 'read', 'real', 'references', 'regexp', 'rename', 'replace', 26. 'require', 'restrict', 'revoke', 'right', 'rlike', 'second_microsecond', 27. 'select', 'separator', 'set', 'show', 'smallint', 'soname', 'spatial', 28. 'sql_big_result', 'sql_calc_found_rows', 'sql_small_result', 'ssl', 29. 'starting', 'straight_join', 'table', 'tables', 'terminated', 'then', 30. 'tinyblob', 'tinyint', 'tinytext', 'text', 'to', 'trailing', 'true', 31. 'union', 'unique', 'unlock', 'unsigned', 'update', 'usage', 'use', 32. 'using', 'utc_date', 'utc_time', 'utc_timestamp', 'values', 'varbinary', 33. 'varcharacter', 'varchar', 'varying', 'when', 'where', 'with', 34. 'write', 'x509', 'xor', 'year_month', 'zerofill', 'char'); 35. 36. foreach ($words as $word) { 37. $patterns[] = '/\b'.$word.'\b/i'; 38. $WORDS[] = '<span class="bb-sql-word">'.strtoupper($word). '</span>'; 39. } 40. 41. $res = '<div class="bb-sql">'.preg_replace($patterns, $WORDS, $str).'</div>'; 42. return $res; 43.?> Ну, и, в финале, приведу список всех bb-кодов, которые умеет обрабатывать этот набор функций: •

[color=...]...[/color] – подкрашивает текст в указанный цвет

[size=...]...[/size] – задает шрифту определенный размер

[font=...]...[/font] – задает гарнитуру шрифта

[align=...]...[/align] – задает выравнивание текста (left, right, center, justify)

[b]...[/b] – жирный текст

[i]...[/i] – наклонный шрифт

[u]...[/u] – подчеркивание

[center]...[/center] – выравнивание по центру

[sql]...[/sql] – раскраска SQL запроса

[code]...[/code] – вывод на экран произвольного кода, в том числе и HTML

[php]...[/php] – раскраска PHP скрипта


PHP Inside'18 >> Идеи >> Осторожно, данные! •

[email=...]...[/email] или [email]...[/email] – ссылка на е-маил адрес

[url=...]...[/url] – ссылка на какой-то сайт

[img]http://...[/img] – вывод картинки

[quote]...[/quote] – цитата

[list=...]...[/list] – нумерованный список

Посмотрев, как реализован этот набор bb-кодов, вы легко сможете создать свои. До этого момента мы с вами говорили о всевозможных преобразованиях поступивших к нам данных. Однако, это лишь половина дела. Как правило с этими данными необходимо что-то делать дальше, например, вывести на экран или поместить в базу. Если с выводом на экран все понятно, то помещать данные в базу нужно аккуратно. Далее я рассмотрю, какие действия необходимо проделать с данными перед помещением их в базу и какие опасности нас могут подстерегать. Самое первое и главное правило – приводите все данные к нужному формату. Мы научились это делать в предыдущих частях нашего материала: целые числа необходимо приводить к целым числам, например функцией intval, также следует поступать со семи остальными данными, даты приводить к правильной дате, емаил-адрес к правильному и так далее. Что же нам делать с текстовыми данными неизвестного формата? Во-первых, если вы используете в оформлении текстов bb-коды, то помещайте эти данные именно с ними – то есть НЕ ПРЕОБРАЗОВЫВАЙТЕ bb-коды перед вставкой в базу данных. Все преобразования нужно делать непосредственно перед выводом данных на экран. Все строковые переменные должны быть заключены в кавычки. При этом не важно – двойные или одинарные. Но этого недостаточно. Например, в вашем тексте тоже есть кавычки. Как поступить в этом случае? Нам поможет специальная функция PHP mysql_real_escape_string. Эта функция проставляет слэши (специальный знак \) перед каждым символом, который мог бы помешать вашему запросу правильно выполняться. Обращаю также ваше внимание, что эти слэши будут отброшены базой и нужны только для формирования правильного запроса, то есть в базу они не попадут. Многие считают, что на этом работа закончена и приведенных выше действий достаточно. Однако, обращу ваше внимание на оператор LIKE, который позволяет организовывать поиск по вашим данным. Если вы прочитаете об этом операторе в документации, то обнаружите, что этот оператор обладает определенным набором специальных символов, которые управляют поиском. Например, %, который заменяет собой любой набор символов, символ _ (подчеркивание) заменяет собой любой символ. Приведенная выше команда не добавляет слэши к этим символам, а значит, пользователь, введя их в данные, легко может создать лишнюю нагрузку на базу, а, значит, подвергнуть ваш сервер опасности. Чтобы добавить скэши и к этому набору символов, воспользуемся другой функцией – addcslashes. Не перепутайте эту команду с командой addslashes. Вот пример использования указанной функции: 1.<?php 2.$variable = addCslashes($variable, '%_'); 3.?> После такой обработки эти символы перестанут быть управляющими и ваш сервер никак не пострадает, если пользователь использует их при вводе данных.


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL

Продвинутая репликация MySQL Автор: Джузеппе Максия, Перевод: Дмитрий Карпенко

Д

олжно быть, вы уже знаете про MySQL кластер – комплексное решение для построения высокодоступных и производительных систем. Одним из преимуществ кластера является то, что все узлы в системе равнозначны, в то время как при обычной репликации у вас есть ведущий узел (master) и несколько ведомых (slave), при этом приложения должны производить изменения только на ведущем узле. Главными недостатками MySQL кластера (в MySQL 5.0) являются: •

Вся база данных загружается в оперативную память, что требует больших ресурсов, чем при нормальной работе MySQL. (В MySQL 5.1 были введены пространства таблиц (table spaces), способные хранить неиндексированные данные на диске).

Некоторые функции не могут быть использованы в кластере, например, полнотекстовый поиск, связи, и изоляция трансакций не может быть выше уровня READ COMMITTED.

В некоторых случаях MySQL кластер является наилучшим решением, но для большинства повседневных задач репликация – это более удачный выбор. Однако репликация тоже несет в себе определенные проблемы: •

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

Проблема обхода отказа (fail-over). В момент отказа ведущего узла, ведомые готовы заменить его, но чтобы обнаружить отказ и своевременно среагировать на него, необходимо вмешательство администратора.

Цель данной статьи – помочь вам обойти эти проблемы. Используя возможности представленные в MySQL 5.0 и 5.1, мы можем построить гибкую систему репликации с механизмом обхода отказа, где каждый узел будет одновременно ведущим и ведомым.

Репликация с несколькими ведущими узлами Те из вас, кто не ознакомился с основами процесса репликации, могут сделать это в статье "Live backups of Mysql using replication" (http://www.onlamp.com/pub/a/onlamp/2005/06/16/MySQLian.html), а для более требовательных читателей существует "сухой", но исчерпывающий официальный мануал MySQL по репликации (http://dev.mysql.com/doc/refman/5.0/en/replication.html). Вернемся к теме. Рассмотрим ситуацию, когда вам нужна репликация с более чем одним ведущим узлом, так как в последнее время это наиболее распространенный случай. В книге "High


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL Performance MySQL" (http://www.oreilly.com/catalog/hpmysql/), Jeremy Zawodny, описывает одно из решений в главах 7 и 8, но во время публикации книги, необходимые для реализации технологии еще не были доступны. Александр Войцеховский [young], технический директор компании "IT Marketing" (г. Киев), один из авторов русскоязычной документации к PHP говорит о репликации: Репликация MySQL хороший способ повышения отказоустойчивости и распределения нагрузки, по большей части при большом количестве запросов на выборку. Также помогает избежать длительных блокировок для больших таблиц при создании бекапов (а они крайне необходимы даже при репликации). Крайне проста в настройке и использовании, никаких дополнительных требований к структуре таблиц и дизайну приложения не предьявляется. Одна из сложнорешаемых проблем при репликации с использованием нескольких ведущих узлов, это возможность возникновения конфликта при использовании самогенерирующихся (selfgenerated) ключей. AUTO_INCREMENT – это очень удобный функционал, но при репликации может обладать довольно разрушительными свойствами. Если узел А и узел B в одной и той же таблице, создадут запись с использованием автоинкрементного ключа, то появление конфликтов не заставит себя ожидать. Спасение пришло с новыми версиями MySQL. В MySQL 5 были введены несколько переменных (http://dev.mysql.com/doc/refman/5.0/en/replication-auto-increment.html), имеющих отношение к указанной проблеме и позволяющих реализовать репликацию децентрализованного поля узлов. Цитируя мануал: •

Переменная auto_increment_increment контролирует вложенными значениями поля AUTO_INCREMENT.

Переменная auto_increment_offset AUTO_INCREMENT.

определяет

шаг

отправное

инкремента значение

между для

успешно

полей

типа

Выбирая не-конфликтные значения для этих параметров на разных ведущих узлах, мы сможем избежать вышеописанного конфликта при занесении записей в одну и ту же таблицу двумя различными узлами, так как сервера не будут использовать конфликтные значения для полей AUTO_INCREMENT. Пример настройки для Х ведущих серверов: •

Установите auto_increment_increment в Х для каждого узла

Для каждого из Х узлов установите auto_increment_offset, используя значения 1, 2, …, Х.

Используя эти переменные так, как описано в мануале, вы можете быть уверены, что все узлы будут использовать разные последовательности автоинкрементарных чисел. Для примера, при настройке auto_increment_increment = 10 и auto_increment_offset = 3, при занесении трех записей будут сгенерированы числа 3, 13, 23. При настройке 10 и 7, числа будут равны 7,17,27 и т.д. Для моего четырех-узлового поля, я настроил auto_increment_increment = 10 для каждого узла и auto_increment_offset = 1 для первого узла, 2 - для второго и т.д. Теоретически все понятно, но все еще не ясно, как я собираюсь трансформировать эти сервера в децентрализованные узлы. Ответом на этот вопрос является так называемая, «круговая репликация» (circular replication), в которой каждый узел является ведущим для следующего в круге и ведомым для предыдущего.


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL

Круговая репликация с двумя узлами Вкратце, в круговой репликации задействованы два узла, каждый из которых является ведущим и одновременно ведомым для другого. (Рис 1.)

(Рисунок 1: Круговая репликация с двумя узлами) Для тестирования я использовал 2 сервера в моей компании (water и air; и скоро мы приобщим к тестам и fire с earth). Основная конфигурация серверов: 1.# node A - (water) setup 2.[mysqld] 3.server-id 4.# auto_increment_increment 5.# auto_increment_offset 6.master-host 7.master-user 8.master-password 9. 10.# node B - (air) setup 11.[mysqld] 12.server-id 13.# auto_increment_increment 14.# auto_increment_offset 15.master-host 16.master-user 17.master-password

= = = = = =

10 10 1 air.stardata.it nodeAuser nodeApass

= = = = = =

20 10 2 water.stardata.it nodeBuser nodeBpass

Обратите внимание на две магических переменных в конфигурационных файлах. Если вы исключите некоторые переменные (или закомментируете как в примере выше), может случиться неприятность, и это очень легко продемонстрировать. Помните о том, что репликация MySQL асинхронна. Это означает, что репликация на ведомом узле может состоятся позже чем данные изменятся на ведущем. Этот факт делает процесс репликации гибким и вы можете быть уверены, что даже при разрыве связи между ведущим и ведомым узлом, репликация будет возобновлена в момент восстановления связи. Но при этом существуют неприятные побочные эффекты, если вы пользуетесь автоинкрементарными значениями. Представьте себе, что у вас есть таблица подобной структуры: 1.CREATE TABLE x ( 2. id int(11) NOT NULL AUTO_INCREMENT, 3. c char(10) DEFAULT NULL, 4. PRIMARY KEY (id) 5. ) ENGINE=MyISAM DEFAULT CHARSET=latin1


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL Представьте также, что связь между узлами А и B нарушилась в какое-то время и следовательно репликация остановлена. В этот момент вы выполнили приказ INSERT на обоих серверах (переменные auto_increment_increment и auto_increment_offset не установлены):

1.[node A] 'ccc'); 2.[node B] 'zzz'); 3.

insert into x values (null, 'aaa'), (null, 'bbb'), (null, insert into x values (null, 'xxx'), (null, 'yyy'), (null,

Когда репликация будет возобновлена, вы получите blocking error на обоих узлах: 1.Error 'Duplicate entry '1' for key 'PRIMARY'' on query. database: 2. 'test'. Query: 'insert into x values (null, 'aaa')'

Default

Обнаружить причину ошибки очень просто: 1.[node A] select * from x; 2.+----+------+ 3.| id | c | 4.+----+------+ 5.| 1 | aaa | 6.| 2 | bbb | 7.| 3 | ccc | 8.+----+------+ 9. 10.[node B] select * from x; 11.+----+------+ 12.| id | c | 13.+----+------+ 14.| 1 | xxx | 15.| 2 | yyy | 16.| 3 | zzz | 17.+----+------+ 18. Оба узла создали одинаковые главные ключи (primary keys). Из-за чего, собственно, при возобновлении репликации СУБД сообщила, что произошла ошибка. Теперь попробуем активировать две вышеописанные переменные и посмотрим, что из этого выйдет: 1.[node 2.[node 3.[node 4.[node

A] A] B] B]

set set set set

auto_increment_increment auto_increment_offset auto_increment_increment auto_increment_offset

= 10; = 1; = 10; = 2;

Очистим ошибки, удалим содержимое тестовых таблиц и опять выполним INSERT (остановим репликацию, тем самым симулирую разрыв связи между узлами): 1.[node A] 2.[node B] 3.[node A] 4.[node A] 5.[node B] 6.[node A] 'ccc'); 7.[node B] 'zzz');

SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1; start slave; SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1; start slave; truncate x; stop slave ; stop slave ; insert into x values (null, 'aaa'), (null, 'bbb'), (null, insert into x values (null, 'xxx'), (null, 'yyy'), (null,


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL Таким образом, когда репликация будет возобновлена, она пройдет без ошибок. Выбор соответствующих значений для переменных auto_increment_increment и auto_increment_offset позволяет избежать конфликтов, связанных с использованием автогенерируемых ключей в данной круговой репликации. Что и требовалось доказать.

Добавление узлов Двух узлов может вполне хватать сегодня, но так как ваше приложение растет и вы должны учитывать этот рост при проектировке БД, то, возможно, вам понадобиться добавить еще несколько узлов в систему. В данном случае это можно сделать без особых усилий (Рис. 2)

Рисунок 2: Круговая репликация с четырьмя узлами В этой общей схеме water является ведущим узлом для air и ведомым для earth, который является ведомым для fire; последний, в свою очередь, является ведомым для air и таким образом замыкает круг. Числа в квадратах для каждого сервера означают: id – Идентификатор сервера, который должен быть уникальным для каждого узла, incr – значение auto_increment_increment, которое одинаково для всех узлов и offs – значение auto_increment_offset, которое гарантирует уникальность самогенерирующихся ключей. Ниже приведены полные настройки для всех узлов: 1.# node A - water 2.[mysqld] 3.server-id

= 10


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL 4.log-bin 5.log-slave-updates 6.replicate-same-server-id 7.auto_increment_increment 8.auto_increment_offset 9.master-host 10.master-user 11.master-password 12.report-host 13. 14.# Node B - air 15.[mysqld] 16.server-id 17.log-bin 18.log-slave-updates 19.replicate-same-server-id 20.auto_increment_increment 21.auto_increment_offset 22.master-host 23.master-user 24.master-password 25.report-host 26. 27.# Node C - fire 28.[mysqld] 29.server-id 30.log-bin 31.log-slave-updates 32.replicate-same-server-id 33.auto_increment_increment 34.auto_increment_offset 35.master-host 36.master-user 37.master-password 38.report-host 39. 40.# Node D - earth 41.[mysqld] 42.server-id 43.log-bin 44.log-slave-updates 45.replicate-same-server-id 46.auto_increment_increment 47.auto_increment_offset 48.master-host 49.master-user 50.master-password 51.report-host

= mysql-bin = = = =

0 10 1 earth.stardata.it = nodeAuser = nodeApass = nodeA

= 20 = mysql-bin = = = = = = =

0 10 2 water.stardata.it nodeBuser nodeBpass nodeB

= 30 = mysql-bin = = = = = = =

0 10 3 air.stardata.it nodeCuser nodeCpass nodeC

= 40 = mysql-bin = = = = = = =

0 10 4 fire.stardata.it nodeDuser nodeDpass nodeD

Некоторые переменные стоить описать отдельно. Первая из них, это log-slave-updates. Эта переменная указывает каждому узлу записывать в свой бинарный лог все изменения, которые он получил из бинарного лога своего ведущего узла. Без этого каскадная репликация просто не будет работать. С помощью переменной replicate-same-server-id мы избегаем бесконечного цикла репликации, так как каждый узел будет игнорировать свои собственные записи, вернувшиеся ему от ведущего сервера. Переменные auto_increment_increment и auto_increment_offset несут соответствующие значения, как было описано раннее. Все остальные настройки являются стандартными для репликации.


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL Вот пример отдельного использования: 1.[node A] 2.[node B] 3.[node C] 4.[node D] 5.[node A] 'aaa'); 6.[node B] 'bbb'); 7.[node C] 'ccc'); 8.[node D] 'ddd');

stop slave; stop slave; stop slave; stop slave; insert into test.x values (null, 'a'), (null, 'aa'), (null, insert into test.x values (null, 'b'), (null, 'bb'), (null, insert into test.x values (null, 'c'), (null, 'cc'), (null, insert into test.x values (null, 'd'), (null, 'dd'), (null,

При остановленной репликации, создадим три записи для каждого узла с независимо сгенерированными ключами. Результатом будут являться наборы не-конфликтных записей. 1.[node A] select * from test.x; 2.+----+------+ 3.| id | c | 4.+----+------+ 5.| 1 | a | 6.| 11 | aa | 7.| 21 | aaa | 8.+----+------+ 9.[node B] select * from test.x; 10.+----+------+ 11.| id | c | 12.+----+------+ 13.| 2 | b | 14.| 12 | bb | 15.| 22 | bbb | 16.+----+------+ 17.[node C] select * from test.x; 18.+----+------+ 19.| id | c | 20.+----+------+ 21.| 3 | c | 22.| 13 | cc | 23.| 23 | ccc | 24.+----+------+ 25.[node D] select * from test.x; 26.+----+------+ 27.| id | c | 28.+----+------+ 29.| 4 | d | 30.| 14 | dd | 31.| 24 | ddd | 32.+----+------+ Когда мы опять запустим процесс репликации, все узлы будут содержать одинаковую информацию: 1.[node 2.[node 3.[node 4.[node 5.[node

A] B] C] D] A]

start slave; start slave; start slave; start slave; select * from test.x;


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL 6.+----+------+ 7.| id | c | 8.+----+------+ 9.| 1 | a | 10.| 11 | aa | 11.| 21 | aaa | 12.| 4 | d | 13.| 14 | dd | 14.| 24 | ddd | 15.| 3 | c | 16.| 13 | cc | 17.| 23 | ccc | 18.| 2 | b | 19.| 12 | bb | 20.| 22 | bbb | 21.+----+------+ 22. 23.[node B] select count(*) from test.x; 24.+----------+ 25.| count(*) | 26.+----------+ 27.| 12 | 28.+----------+ 29. 30.[node C] select count(*) from test.x; 31.+----------+ 32.| count(*) | 33.+----------+ 34.| 12 | 35.+----------+ 36. 37.[node D] select count(*) from test.x; 38.+----------+ 39.| count(*) | 40.+----------+ 41.| 12 | 42.+----------+ Но не стройте иллюзий, круговая репликация такая же хрупкая, как и нормальная, если речь пойдет о конфликтах ключей. Создание одинакового не-автоматически сгенерированного главного (primary) или уникального (unique) ключа на разных узлах, нарушит репликацию точно также как и в случае обыкновенной репликации - ведущий-ведомый (master-slave). При асинхронной репликации это может случиться тоже, но вам должно сильно не везти, чтобы это произошло. Придерживаясь хорошего стиля программирования, вы сможете избежать большинства проблем (за исключением таких как, например, короткие обрывы связи). Отсюда можно сделать два главных вывода: во-первых, вы можете, без особых усилий, использовать круговую репликацию там, где сейчас используете обыкновенный сервер БД, и, вовторых, что производительность вполне отвечает нашим потребностям.

Оценка производительности круговой репликации Как использовать круговую репликацию понятно, но как быть с производительностью? Чтобы ответить на этот вопрос я должен был сделать кое-какие измерения. Чтобы не захламлять эту


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL статью, я сошлюсь на недавнюю запись в блоге, содержащую специфический вывод (Измерение скорости репликации http://datacharmer.blogspot.com/2006/04/measuring-replication-speed.html, вместе с соответствующими исходниками http://forge.mysql.com/snippets/view.php?id=5). Эта утилита показывает, что репликация 1000 байт по всему репликационному полю занимает 0.001150 секунды. Для сравнения: простое занесение данных в таблицу на одном узле (без репликации) заняло 0.000015 секунды, тогда как транспорт тех же данных между двумя узлами занял всего лишь 0.000065 секунды. Также, я хочу продемонстрировать более реальный опыт, чтонибудь, более доступное обычному программисту. Метод, который я использовал для достижения более точных результатов. Как бы то ни было, скорость репликации может быть достаточно высокой, даже для очень требовательных задач. Рассмотрим экстремальный пример. Внесем три записи размером более 1000 байт и сразу после этого попробуем получить эти записи на узле, который является ведущим для того, на котором мы эти записи занесли в базу. Чтобы стать доступными, эти записи должны пройти все поле репликации. Код теста написанный на Perl: 1.#!/usr/bin/perl 2.use strict; 3.use warnings; 4. 5.use English qw( -no_match_vars ); 6.use DBI; 7.use Time::HiRes qw/ usleep /; 8. 9.my @configuration_files = (); 10.my $max_config_index = 3; 11.my $current_config = 0; 12. 13.for ( 'A' .. 'D' ) { 14. push @configuration_files, "$ENV{HOME}/ circular_replica/my.node$_.cnf"; 15.} 16. 17.sub get_connection { 18. my ($config_files) = @_; 19. my $config_file = $config_files->[$current_config]; 20. $current_config++; 21. if ($current_config > $max_config_index) { 22. $current_config = 0; 23. } 24. 25. my $dbh; 26. eval { 27. 28. $dbh=DBI->connect("dbi:mysql:test" 29. . ";mysql_read_default_file=$config_file", 30. undef, 31. undef, 32. {RaiseError => 1}) 33. or die "Can't connect: $DBI::errstr\n"; 34. }; 35. if ( $EVAL_ERROR ) { 36. print STDERR $EVAL_ERROR; 37. return; 38. } 39. return $dbh; 40.} 41.


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL 42.my $dbh = get_connection(\@configuration_files); 43.$dbh->do(qq{truncate x}); 44.my $bigtext = 'a' x 1000; 45. 46.for my $loop (1 .. 10) 47.{ 48. my $dbh1 = get_connection(\@configuration_files); 49. my $dbh2 = get_connection(\@configuration_files); 50. my ($server1) = $dbh1->selectrow_array(qq{select \@\@server_id}); 51. my ($server2) = $dbh2->selectrow_array(qq{select \@\@server_id}); 52. for (1 .. 3) { 53. $dbh2->do( qq{insert into x values (null, concat("server ", ? ), ?) } , undef , $server1, $bigtext ); 54. } 55. usleep(1); 56. my $count = $dbh1->selectrow_array(qq{ select count(*) from x }); 57. print "inserted a record from server $server2\n ", 58. "retrieved $count records from $server1\n"; 59.} После удаления всех записей из тестовой таблицы, у которой теперь появился третий столбец (MEDIUMTEXT), код проходит по узлам, на каждом шаге получая доступ к узлу ($dbh2) и к его ведущему узлу ($dbh1). Сразу после занесения записей, скрипт останавливается на 1 микросекунду (usleep), после чего подсчитывает количество записей на ведущем узле. Результат работы примера: 1.inserted a record from server 30 2. retrieved 3 records from 20 3.inserted a record from server 10 4. retrieved 6 records from 40 5.inserted a record from server 30 6. retrieved 9 records from 20 7.inserted a record from server 10 8. retrieved 12 records from 40 9.inserted a record from server 30 10. retrieved 15 records from 20 11.inserted a record from server 10 12. retrieved 18 records from 40 13.inserted a record from server 30 14. retrieved 21 records from 20 15.inserted a record from server 10 16. retrieved 24 records from 40 17.inserted a record from server 30 18. retrieved 27 records from 20 19.inserted a record from server 10 20. retrieved 30 records from 40 Я надеюсь, с этим простым примером, я показал, что круговая репликация в MySQL 5 является вполне жизнеспособной альтернативой дорогим кластерам. Однако, круговая репликация довольно плохо масштабируется. Если количество узлов вырастет больше 10, скорость репликации может перестать соответствовать вашему представлению достаточной скорости работы. Помните об этом. Репликационное поле может оказаться идеальным решением для вас, если для работы вам хватает малого количества серверов.


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL

Автоматический обход отказа Теперь я могу описать второе неудобство репликации, а именно, отсутствие механизма обхода отказа. У опытного администратора, изменение ведущего узла для группы ведомых займет всего несколько минут. При этом вы должны учитывать, что процедура изменения ведущего узла подразумевает замену ведущего узла. Потребность в этой замене может возникнуть в неблагоприятное время, и несколько минут квалифицированной работы могут обернуться часами потерянного сна. Добавьте к этому возможность плохих отношений с друзьями после резкого ухода с тусовки со звонящим телефоном в руке, опасное вождение, и, в конечном итоге, ваши постоянные переживания. Более опытные администраторы интегрировали системы мониторинга, способные на ходу обнаружить ошибку и запустить скрипт, который сменит ведущий узел без вмешательства со стороны персонала. Однако работа этих систем очень сильно зависит от операционной системы на сервере и доступности инструментов, необходимых для корректной работы таких скриптов. Одной из новых функциональных возможностей, которая была представлена в MySQL 5.1, является, встроенный в СУБД планировщик событий, появление которого подтолкнуло многих к изучению возможности создать самодостаточный механизм для решения проблемы обхода отказа. Эта статья не является исключением. Начнем с того, что рассмотрим обход отказа на нормальной (не круговой) репликации, потому как на этом примере проще понять, и, во многих случаях, это как раз то, чего не хватает для полного счастья. В большинстве случаев схема репликации примерно следующая: один ведущий узел, пара ведомых и узел-кандидат в ведущие, который также может являться обычным ведомым узлом. Для простоты рассмотрите ситуацию на рис. 3:

Рисунок 3: Обычная схема репликации Для

достижения

поставленной

цели

вам

понадобятся

две

из

недавно

введенных


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL функциональностей, федеративные таблицы (federated tables http://dev.mysql.com/doc/refman/5.0/en/federated-storage-engine.html) представленные в MySQL 5.0 и планировщик событий MySQL (MySQL event scheduler http://dev.mysql.com/doc/refman/5.1/en/events.html) доступный начиная с MySQL 5.1.6. Описание этих нововведений не является целью этой статьи. К счастью, кое-кто уже написал статьи по этим темам, и усердный читатель может найти краткое введение в федеративный движок MySQL (http://dev.mysql.com/tech-resources/articles/mysql-federated-storage.html) и планировщик событий MySQL (http://dev.mysql.com/tech-resources/articles/event-feature.html). В этой статье будет достаточным сказать, что федеративные таблицы, по сути, являются ссылками на существующие таблицы на удаленном сервере. Структура таблицы на локальном сервере должна быть полностью идентична структуре таблицы с удаленного сервера. В приказ CREATE TABLE вы должны добавить специальную связывающую строку (connection string), чтобы локальный сервер знал, где лежат соответствующие данные. Планировщик событий является движком, запускающим приказы SQL в ответ на происходящие в системе события. Под событием подразумевается объект в схеме БД, объединяющий в себе событие (event) и акцию (action), которую нужно выполнить при наступлении этого события. Для события можно указать необязательный параметр времени повтора, а для акции соответствующий SQL приказ. События могут создаваться, модифицироваться, быть включенными или выключенными, или удаленными из DDL (Data Definition Language). Работу данного функционала обеспечивает процесс сервера ДБ, который не использует для этого какие-либо сторонние утилиты, как CRON под Linux или планировщик задач под Windows. Не забывайте, что на момент написания этой статьи, MySQL 5.1 все еще находится в стадии бета версии. Тогда как техника круговой репликации может быть использована на рабочих серверах, возможности, которые я буду описывать ниже, все еще могут содержать баги. Не торопитесь с использованием их в работе, пока MySQL 5.1 не выйдет под стабильной версией. До этого можете смело экспериментировать. С этими двумя инструментами в руках, вы можете построить механизм восстановления системы репликации в случае отказа ведущего узла. Принцип предельно прост. На ведущем узле, в базе replica существует таблица who, которая состоит из одного поля. Названия базы и таблицы не критичны. Существование этой таблицы преследует единственную цель: ведомые узлы могут ссылаться на нее с помощью федеративной таблицы master_who (Рис. 4)


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL 1.# на ведущем узле 2.CREATE TABLE who ( 3. server_id int 4.) ENGINE=MyIsam 5. 6.# на каждом из ведомых 7.CREATE TABLE master_who ( 8. server_id int 9.) ENGINE=Federated 10. CONNECTION='mysql://username:password@172.16.1.10:3306/replica/wh o'; 11. 12. 13.Главной сутью этого сценария является повторяющееся событие с вызовом приказа check master_conn, настроенное так, чтобы повторяться каждые 30 секунд. 14. 15.create event check_master_conn 16. on schedule every 30 second 17. enable 18. do call check_master(); 19. 20. 21.Это также легко, как прописать задание в crontab (я думаю, намного легче). Каждые 30 секунд это событие вызывает хранимую процедуру, которая тестирует есть-ли связь с ведущим сервером. 22. 23.create procedure check_master() 24.deterministic 25.begin 26. declare master_dead boolean default false; 27. declare curx cursor for 28. select server_id from replica.master_who; 29. declare continue handler for SQLSTATE 'HY000' 30. set master_dead = true; 31. 32. open curx; 33. # a failure to open the cursor occurs here 34. # setting the master_dead variable to true 35. 36. if (master_dead) then 37. stop slave; 38. change master to 39. master_host='172.16.1.40', 40. master_log_file='mysql-bin.000001', 41. master_log_pos=0; 42. start slave; 43. alter event check_master_conn disable; 44. end if; 45.end Чтобы проверить доступность ведущего узла, процедура открывает курсор на таблице master_who (федеративная таблица, связанная с удаленным сервером). Если открыть курсор не удалось, обработчик соответственного статуса настроит переменную master_dead в true. Если это произойдет, процедура остановит репликацию, сменит ведущий сервер на сервер-кандидат, опять запустит репликацию и выключит событие проверки ведущего узла, так как оно уже выполнило свою роль (Рис. 5).


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL

Рисунок 5: Проверка отказа ведущего узла с помощью федеративных таблиц В течении 30 секунд отказа ведущего узла (или меньше, если вы решите уменьшить интервал проверки связи) старый ведущий узел будет забыт и новый успешно займет его место (Рис. 6).

Рисунок 6: После отказа ведущего узла, ведомые переключаются на новый узел Теперь администратор спасен от разборок с друзьями, стрит-рейсинга и нервного срыва. Я вижу, что у аудитории появились вопросы. Как насчет второстепенного механизма обхода? Что если второй ведущий узел тоже упадет? Вдохните поглубже и будьте реалистами. Это всего лишь «демонстрация идеи», что такой механизм реализуем, и он может спасти вас от тяжелой работы и усилий. Настройка после отказа все еще требует кое-какой мануальной работы. Вы просто больше не напрягаетесь, вот и все.


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL Хотя второстепенные отказы (или цепочки отказов) достаточно маловероятны, не стоит упускать их из виду. Мое личное мнение – как можно больше усилий при реакции на первый отказ и вмешательство персонала при второстепенных отказах, но, конечно же, существуют случаи, когда рекомендуется и даже необходимо считаться с второстепенными отказами и обеспечить автоматический обход для них тоже. Это вопрос комплексного подхода и, в конце концов, финансов, потому как чем больше времени вы потратите на планирование многослойных обходов отказа, тем дороже будет результат. Вы или ваша компания (или ваш заказчик, если вы консультант) должны сами решить, насколько вы подвержены паранойе. Базируясь на вышеописанных основах, вы можете взять простое решение и подогнать его под себя, вдвое (или втрое) углубив систему обхода отказов. Все реализуемо. По моему опыту, системы с излишними планами восстановления становятся слишком сложными и дорогими. Также их описание выходит за рамки этой статьи.

Обход отказа при круговой репликации Осталась нераскрытой еще одна важная часть: как обойти отказ в круговой топологии, т.е. в репликационном поле с главными возможностями полноценного кластера. Принцип не изменился, но интрига увеличилась вместе с количеством узлов (Рис. 7).

Рисунок 8: проверка пульса узлов при круговой репликации Вместо одной таблицы who добавим по такой таблице на каждом из узлов и по одной федеративной таблице master_who, связанной с соответствующим ведущим узлом. Структура таблицы ничем не отличается от рассмотренной прежде. Изменилась только связывающая строка (connection string). Для настройки соответствующей таблицы на каждом из узлов используйте приведенную хранимую процедуру: 1.create procedure make_connections() 2.begin 3. drop table if exists master_who; 4. case @@server_id 5. when 10 then


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL 6. CREATE TABLE master_who 7. ( 8. server_id int not null primary key 9. ) ENGINE = federated 10. CONNECTION = 'mysql://username:password@earth:3306/replica/who'; 11. when 20 then 12. CREATE TABLE master_who 13. ( 14. server_id int not null primary key 15. ) ENGINE = federated 16. CONNECTION = 'mysql://username:password@water:3306/replica/who'; 17. when 30 then 18. CREATE TABLE master_who 19. ( 20. server_id int not null primary key 21. ) ENGINE = federated 22. CONNECTION = 'mysql://username:password@air:3306/replica/who'; 23. when 40 then 24. CREATE TABLE master_who 25. ( 26. server_id int not null primary key 27. ) ENGINE = federated 28. CONNECTION = 'mysql://username:password@fire:3306/replica/who'; 29. else 30. select "unhandled server id " as "error"; 31. end case; 32.end 33. 34. 35.Событие ничем не отличается от события при нормальной схеме репликации. Вызываемая хранимая процедура изменилась: 36. 37.create procedure check_master() 38.deterministic 39.begin 40. declare master_dead boolean default false; 41. declare curx cursor for 42. select server_id from replica.master_who; 43. declare continue handler for SQLSTATE 'HY000' 44. set master_dead = true; 45. open curx; 46. 47. if (master_dead) then 48. stop slave; 49. case @@server_id 50. when 10 then 51. change master to 52. master_host = 'fire'; 53. when 20 then 54. change master to 55. master_host = 'earth'; 56. when 30 then 57. change master to 58. master_host = 'water';


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL 59. when 40 then 60. change master to 61. master_host = 'air'; 62. else 63. -- report the error in a log table 64. insert into check_master_log values (now(), @@server_id, 65. "not handled server id"); 66. end case; 67. start slave; 68. alter event check_master_conn disable; 69. end if; 70.end Сценарий обхода отказа не отличается от предыдущей схемы. Но в отличие от предыдущего примера, где все ведомые узлы имели одинаковый механизм обхода и действовали одинаково, в этом случае только один узел должен как-то среагировать, чтобы восстановить репликацию. Остальные узлы продолжают работать, как будто ничего не произошло (Рис. 8).

Рисунок 8: отказ узла при круговой репликации Обратите внимание, что во всех примерах этой статьи, я использую имена хостов для ясности. В то время как на рабочих серверах я всегда использую IP адреса из-за производительности (не тратится время на перевод хостов в IP), и часто в моем распоряжении находится высокоскоростная выделенная линия для внутренней связи репликационного поля. Узел 40 обнаружил, что узел 30 недоступен. Его процедура check_master сменит ведущий узел на узел 20, air, для которого изменения останутся незаметными (Рис. 9).


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL

Рисунок 9: Восстановление при отказе узла в круговой репликации Обратите внимание, что после восстановления узел 40 перестал тестировать доступность ведущего узла. Это сделано из тех же принципов, что и в обсуждении цепочки отказов при нормальной репликации. В обоих случаях есть возможность сделать второй уровень событий и процедур восстановления, которые начнут вызываться после первого отказа, благодаря чему система переживет и второй отказ. Я оставляю эту задачу для упражнения прилежному читателю.

Обход отказа на стороне клиента До сих пор я показывал, как заменить отказавший сервер другим. Со стороны репликационного поля все отлично, но как на это реагирует бедняга клиент, который пытается соединиться с отказавшим ведущим сервером и все время получает ошибки соединения? Тут нас поджидают плохие новости, так как универсального решения этой проблемы не существует. Вам придется найти решение на стороне клиента, так как эта проблема касается только этой стороны. Существует несколько решений на стороне сервера, но их действие ограничено только для ситуации с использованием двух узлов или же они сильно зависят от специфических возможностей операционных систем (как, например, CARP http://www.openbsd.org/faq/faq6.html#CARP, UCARP – http://www.ucarp.org/project/ucarp, или Linux-HA - http://www.linux-ha.org/) Утешительная новость, что у кластера MySQL те же проблемы. Многих это удивит, но ответом для высокодоступности MySQL (http://lists.mysql.com/cluster/740) является использование нескольких источников данных (multiple data sources) в Java. И пока не произойдет интеграции MySQL и операционной системы, в ситуации, когда вам нужно построить высокодоступную систему, вы можете рассчитывать только на себя. Однако есть и хорошие новости. Если круговая репликация полностью отвечает вашим потребностям, то простенькая система распределения нагрузки (load balancer) гарантирует вам как хорошее управление ресурсами, так и высокодоступность. Вы можете купить распределитель нагрузки на аппаратном уровне или написать свой собственный для своего приложения. Вы также можете просматривать логи системы обхода отказов на предмет обнаружения «мертвых» узлов, и затем исключать их из балансировки. При любом подходе, будьте внимательны и распределяйте


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL подключения по трансакциям, а не по отдельным приказам SQL. Даже если вы не используете трансакции, используйте одно подключение для группы приказов, например, с одной вебстраницы, функции модуля или административной программы. Позвольте мне объяснить это чуть подробнее. Вы используете какое-то устройство, которое выбирает наугад узел и передает подключение к нему. Вы можете использовать такое устройство для каждого приказа в вашем приложении и таким образом распределять нагрузку между узлами БД. Однако такая экстремальная свобода выбора подключений может сыграть с вами злую шутку. Может случиться что вы проведете приказ INSERT на одном узле, а потом SELECT уже на другом узле, тогда как результат вашего INSERTа еще не успел среплицироваться. Чтобы этого избежать, ваше приложение должно получить подключение к БД и провести в его рамках все логически связанные приказы.

О чем не было сказано Я надеюсь в этой статье, мне удалось показать, как создать более гибкую систему репликации. Но нам необходимо заполнить еще несколько пробелов, чтобы эта демонстрация идеи переросла в приличный движок. Во-первых, я должен упомянуть то, что MySQL AB планирует расширить репликацию MySQL (http://www.planetmysql.org/robin/?p=12) и ввести кое-какие устройства для разрешения конфликтов (как, например, конфликт уникальных ключей). Процесс все еще на ранних стадиях разработки, но вы уже можете присмотреться к нему. Также стоит упомянуть некоторые полусырые возможности в MySQL 5.x. Вы знаете, что одним из главных нововведений в MySQL 5.0, была информационная схема БД (information schema database), которая предоставляет доступ к метаданным БД. К сожалению, недостающим звеном в коллекции этих метаданных является отсутствие всех данных касательно репликации. Поэтому получить доступ к данным о статусе репликации, вы можете только с помощью приказов SHOW ЧТО-ТО (на сегодня, в MySQL 5.1.9), и хранимые процедуры не имеют доступа к этой информации. Еще одна неприятность заключается в том, что параметром приказа CHANGE MASTER TO может быть только строка. Переменные недопустимы, что снижает гибкость хранимых процедур. Разработчики были уведомлены об этой несообразности так что, будем надеяться, они как-то отреагируют. В настоящее время, все это означает, что вы можете добиться нормальной репликации только с помощью внешних процедур. (В действительности, это не совсем так. Существуют некоторые недокументированные и не рекомендуемые к использованию возможности как обойти эти ограничения. Я продемонстрирую эту технику на пользовательской конференции MySQL 2006 в докладе Higher Order MySQL http://www.mysqluc.com/cs/mysqluc2006/view/e_sess/8015). Улучшенная круговая репликация предлагает к использованию дополнительные возможности, которые я здесь не описал. В отдельных случаях возможен обмен сообщениями между серверами. Это можно использовать при замене ведущего узла, когда ведомый узел «попросит» ведущий выполнить RESET MASTER, перед восстановлением репликации. Я оставлю эти прелести для другой статьи, чтобы не перегружать эту. Как бы то ни было, позвольте напомнить, что данная статья является всего лишь «демонстрацией идеи» и требует некоторых доработок перед началом использования в работе. Реальное приложение должно дважды проверить, действительно ли ведущий сервер недоступен, перед переключением на заменяющий его узел, который, в свою очередь, должен быть опрошен на предмет готовности взять на себя функции ведущего узла; все остальные узлы должны быть оповещены о произошедших изменениях и т.д. Вы можете построить систему репликации, базируясь на текущих возможностях, но лучше


PHP Inside'18 >> Идеи >> Продвинутая репликация MySQL дождаться выхода MySQL 5.1 с плановыми нововведениями, с помощью которых сделать задуманное можно будет намного проще и эффективней.

Эксперименты с системой Экспериментировать с репликацией довольно сложно. Для это нужно иметь возможность оперировать несколькими серверами с возможностью настраивать их в ведущем и ведомом режимах. Для тестирования механизма обхода отказов вам понадобиться, как минимум, три сервера. Поскольку не каждый может позволить себе работать с такой кучей железа, я предлагаю вам использовать MySQL 5 Replication Playground (http://www.stardata.it/projects/mysql_replication_playground.html), где все узлы собраны в одном пакете и используют разные порты и сокеты для симуляции разных серверов. Если вы захотите воспользоваться этим пакетом, все что вам понадобиться, это наинсталлировать MySQL 5.1 и потом установить пакет в свою домашнюю директорию (Вам не понадобиться администраторский доступ). Скачайте пакет, внимательно прочитайте readme, запустите инсталляцию и можете начинать тестировать.

Благодарности Выражаю благодарность Patrizio Tassone и Alberto Coduti за графику. Спасибо Roland Bouman, Jay Pipes и Lars Thalmann за исправление ошибок и полезные комментарии. Я в неоплатном долгу перед ними также за исправления и редакцию моего текста. Спасибо Massimiliano Stucchi за предоставление площадки для тестирования под FreeBSD и исправление некоторых ошибок.

Автор Giuseppe Maxia консультант по базам данных и технический директор итальянской компании StarData.it, специализирующейся на Open Source продуктах. Переведено с разрешения: http://onlamp.com

Словарь терминов статьи Кластер представляет собой несколько объединенных компьютеров, управляемых и используемых как единое целое. Они называются узлами и могут быть одно- или мультипроцессорными. В классической схеме при работе с приложениями все узлы разделяют внешнюю память на массиве жестких дисков, используя внутренние дисковые накопители для специальных функций (например, системных). - by wikipedia Репликация - механизм синхронизации содержимого нескольких копий объекта (например, содержимого базы данных). Репликация — это процесс, под которым понимается копирование данных из одного источника на множество других и наоборот. - by wikipedia Load Balancer, Система Распределения Нагрузки – выделенный компьютер или специальный коммутатор, в задачи которого входит оптимальное распределение нагрузки между объединенными серверами.


PHP Inside'18 >> Идеи >> Практические стороны ООП в PHP

Практические стороны ООП в PHP Автор: Девид Дэй, Перевод: Дмитрий Мезенцев

Т

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

Ловушка для разработчиков Я тоже попал в эту западню в начале своего изучения PHP, но воссиял свет, и мои глаза увидели объектно-ориентированные расширения PHP. Под этим я поздразумеваю именно использование классов, а не простую группировку функций. Много начинающих разработчиков считают классы бесполезными, неуклюжими и отнимающими много времени. Уникальность PHP в том, что можно вставить код в любую точку страницы и немедленно начать его использовать. Если дело обстоит именно так, можно включить файл с функциями в начало страницы - и наслаждаться жизнью. Использование классов, не слишком отличаясь от подобного подхода в некоторых аспектах, дает много преимуществ.

Что такое объекты Для начинающих разработчиков классы - или объектно-ориентированное программирование (ООП) - могут показаться слишком запутанными или необычными. Это краткое введение для тех, кто сталкивается с ними впервые. Эта статья рассказывает о PHP 4. Обратите внимание, что объектная модель в PHP 5 значительно изменена, более удобная и содержит новые возможности. Для более подробной информации см. официальную документацию PHP. Рассмотрим пример определения класса из файла exampleclass.php, который выглядит следующим образом: 1.<?php 2.class Example 3.{ 4. //Переменные класса 5. var $var1; 6. var $va2; 7. 8. //Функция, получающая два числа 9. function set_numbers($number1, $number2) 10. { 11. $this->var1 = $number1; 12. $this->var2 = $number2; 13. } 14. 15. //Функция, возвращающая их сумму


PHP Inside'18 >> Идеи >> Практические стороны ООП в PHP 16. 17. 18. 19. 20.} 21.?>

function add_numbers() { return ($this->var1 + $this->var2); }

В самом верху после комментария идут объявления переменных класса var $var1 и var $var2. Они будут доступны только внутри классу. Обратиться к ним можно используя синтаксис $this>variablename. Функция set_numbers() присваивает значения параметров $number1 и $number2 переменным класса $var1 и $var2 соответственно. Функция add_numbers() возвращает сумму $var1 и $var2. Для его использования создадим экземпляр класса Example (см. файл exampleuse.php): 1.<?php 2./* Используйте require_once для подключения файла класса. 3. Это остановит обработку страницы при его отсутствии 4. и не позволит его повторное включение во избежание ошибок 5. Вы также можете использовать require(), include(), include_once() 6.*/ 7.require_once("exampleclass.php"); 8. 9.//создаем экземпляр объекта и 10.//присваиваем его переменной 11.$exampleobject = new Example;

или

Здесь создается экземпляр объекта с помощью вызова функции new. Синтаксис весьма прост $objectvariable = new ClassName;. Example - это имя класса, ранее определенного в файле exampleclass.php; это имя используется при создании объектной переменной. После того как у вас появился экземпляр объекта, можно вызывать его функции (или методы) 12.$exampleobject->set_numbers(1,3); 13.echo($exampleobject->add_numbers()); 14.//выведет на экран: 4 15.?> Вызов методов класса очень похож на оператор $this->, за исключением того что $this-> заменяется на объектную переменную. В нашем примере это $exampleobject. $exampleobject>set_numbers(1,3) и $exampleobject->add_numbers() вызывают методы нашего экземпляра класса Example. Для более подробной информации http://www.php.net/docs.php.

о

классах

см.

официальную

документацию

PHP

Когда имеет смысл использовать ООП? Существует несколько факторов, которые помогут определиться с необходимостью использования ООП. К примеру, для одних проектов это может быть обязательным условием успешного завершения, тогда как в других это будет только ненужной нагрузкой для разработчиков. Вам следует использовать OOП в вашем проекте, если вы: •

Используете сложные функции или код в проекте


PHP Inside'18 >> Идеи >> Практические стороны ООП в PHP •

Ваш проект содержит большое количество модулей/блоков кода

Необходимо держать код упорядоченным

Возможно расширение проекта в будущем

Планируете повторно использовать этот код в других проектах

Повторный вызов определенных функции больше одного раза на странице приводит к логическим ошибкам

Являетесь настоящим фанатом ООП в PHP :-)

Использование ООП не имеет особого смысла, если: •

Проект состоит всего из несколько строк кода;

Не будет расширяться позднее;

Код не будет повторно использоваться;

Срок сдачи близок, и ваш проект не сложен;

Ваш босс запрещает вам это делать;

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

Преимущества OOП в PHP Среди многочисленных преимуществ использования ООП особенно выделяются три производительность, организация, и эффективность. Производительность ускоряет разработку или экономит время. Упорядоченность облегчает управление кодом. Эффективность упрощает последующую разработку.

Производительность Основное преимущество использования OOП в PHP - увеличение производительности, возможность сэкономить время или ускорить процесс разработки. После создания файла класса всякий раз, когда вы повторно используете его в проекте, Вы избавляетесь от необходимости возвращаться и перепрограммировать что-либо. Может показаться, что библиотека функций тоже достаточно хороша, чтобы сберечь ваше время - поначалу и я был жертвой этого заблуждения - но в конечном счете библиотеку прийдется специфически подстраивать для каждого нового проекта. Потраченному времени можно найти лучшее применение - библиотека классов, однажды тщательно запрограммированная, может позднее сберечь Вам многие часы повторяющегося (если не сказать скучного) кодирования. Возьмем, к примеру, маленькую утилиту для выборки из базы данных, написанную мною на скорую руку некоторое время тому назад. Первоначально она извлекала одну строку в формате, который понимают файлы Flash, позднее я добавил в нее и html-вывод (устав набирать


PHP Inside'18 >> Идеи >> Практические стороны ООП в PHP повторяющиеся SQL-операторы). Чтбы было интереснее следить за развитием событий, начнем с класса, умеющего выводить только HTML. Кстати, весь используемый в статье код можно загрузить в приложении к журналу.

Повторное использование Повторное использование в нашем случае - это процесс использования одного и того же файла класса в нескольких проектах. Используете ли вы повторно свой код в других проектах? Большинство разработчиков используют, и верите вы или нет, иногда достаточно сложно перенести свой код из одного проекта в другой. Функции могут быть разбросаны по нескольким страницам, сложный код может быть встроен в HTML или другие неприятности мешают повторному использованию. ООП дает решение этих проблем. Создание экземпляра класса предоставляет простой способ использовать готовый код в новых или уже существующих проектах. Вам не нужно выдергивать небольшие фрагменты существующего кода, собирая требуемую функциональность. Весь ваш код находится в одном файле, который можно легко включить в любую страницу и сразу начать использовать. Вот пример использования класса DataExtraction для веб-сайта A (sitea.php): 1.<?php 2.require_once("dataextractionclass.php"); 3.$data = new DataExtraction(); 4.$data->deWhereStatement("WHERE id = '2'") 5.$data->deExtractDataHtml("users_table", "id", "username", "email"); 6.echo("Hello, ".$test->deGetData("username")."!"); 7.?> Если в таблице users_table ID пользователя TestUser равен 2, будет выведен текст Hello, TestUser!. Если веб-сайту нужен подобный подход с точки зрения быстрого получения информации, просто используйте его многократно Следующий пример - для веб-сайта B (siteb.php): 1.<?php 2.require_once("dataextractionclass.php"); 3.$data = new DataExtraction(); 4.$data->deWhereStatement("WHERE email = 'test@example.com'") 5.$data->deExtractDataHtml("clientlist_table", "id", "balance"); 6.echo("The balance for ".$test->deGetData("email")); 7.echo(" is $".$test->deGetData("balance")."!"); 8.?>

"email",

Предположим, что почтовый адрес test@example.com содержится в таблице clientlist_table с остатком $2,041, тогда будет напечатано: The balance for test@example.com is $2,041! Как вы видите, ООП в PHP предоставляет простой и быстрый способ повторного использования кода.

Множественность Однако у класса DataExtraction есть недостаток. Приведенный в качестве примера класс группирует вывод HTML в массив. Этот массив, будучи ассоциативным, может хранить только одну строку данных из каждой таблицы базы данных. Добавление второй строки данных из той же самой таблицы в массив перезапишет её поверх первоначальной строки. То же произойдет при


PHP Inside'18 >> Идеи >> Практические стороны ООП в PHP добавлении сроки с именами столбцов, идентичными уже имеющимся - более старая информация будет стерта. Это можно предотвратить более аккуратным программированием, но я хочу показать пример, как можно исправить это, не изменяя код самого класса. Здесь нам очень пригодится множественность. Множественность - практика использования нескольких экземпляров одного и того же класса. Это избавляет от лишнего программирования. Предположим, нужно сказать "Привет!" двум пользователям вместо одного. Код siteaimproved.php включает: 1.<?php 2.require_once("dataextractionclass.php"); 3.$data1 = new DataExtraction(); 4.$data2 = new DataExtraction(); 5.$data1->deWhereStatement("WHERE id = '2'") 6.$data1->deExtractDataHtml("users_table", "id", "username", "email"); 7.$data2->deWhereStatement("WHERE id = '3'") 8.$data2->deExtractDataHtml("users_table", "id", "username", "email"); 9.echo("Hello, ".$data1->deGetData("username")."! "); 10.echo("Hello, ".$data2->deGetData("username")."!"); 11.?> Если в таблице users_table для пользователя TestUser id=2 и для пользователя AnotherUser id=3, мы получим текст Hello, TestUser! Hello, AnotherUser! Как вы видите, создавая несколько экземпляров класса, вы можете хранить несколько строк данных из одной таблицы. Эти экземпляры являются отдельными сущностями и существую независимо друг от друга. Но помните, что создание тысяч объектов может быстро исчерпать системные ресурсы. Хотя вы вряд ли будете использовать этот класс таким образом, просто я хочу обратить ваше внимание на этот момент.

Универсальность Предположим, начинается новый проект. Он использует Flash и будет показывать одну строку информации из базы данных. Можно писать код "с нуля" и тратить драгоценное время на повторную реализацию уже написанных функций, а можно просто модифицировать исходный класс. Вот новая функция, добавленная в dataextractionclass.php: 1.// Использование: deExtractDataFlash("db_table", "field1", "field2", "field3") 2.function deExtractDataFlash($table) 3.{ 4. $args = func_get_args(); 5. foreach($args as $k=>$v) 6. { 7. if($k != 0) 8. { 9. $fields .= trim($v).", "; 10. } 11. } 12. 13. //убираем заключительные запятые и пробелы 14. $fields = substr($fields, 0, (strlen($fields) - 2)); 15. $query = "SELECT $fields FROM $table $this->whereStatement"; 16. //возможный вариант: WHERE waterId='1' 17. 18. $this->dbDataExtraction->dbConnect_Db(); 19. //вызываем метод внешнего класса 20. $result = mysql_query($query) or die("MySQL Query Error:


PHP Inside'18 >> Идеи >> Практические стороны ООП в PHP ".mysql_error()); 21. 22. while($row = mysql_fetch_array($result)) 23. { 24. foreach($args as $k=>$v) 25. { 26. if($k != 0) 27. { 28. //выводим в понятном для Flash формате 29. echo("&".$v."=".$row[$k-1]); 30. } 31. } 32. } 33.} Теперь вы можете вызывать эту функцию из dataextractionclass.php и использовать ее совместно с файлами Flash. Все что вам нужно сделать - это вставить текст новой функции в исходный код класса. Иногда может возникнуть необходимость сохранить исходный класс неизменным. Тогда следует использовать ключевое слово extends для расширения существующего класса без модификации его исходного кода. К примеру, исходное имя класса было DataExtraction. Используя extends, вы можете расширить его следующим образом: 1.<?php 2.class FlashExtraction extends DataExtraction 3.{ 4./* Flash Extraction Function Here */ 5.} 6.?> Классы-потомки, в отличии от функций класса, могут находиться в отдельных файлах. При использовании данной методики вы должны определить (или включить) код класса-предка на вашей странице до класса-потомка Для более подробной информации см. официальную документацию PHP.

Упорядоченность OOП - лучший способ сохранить ваши файлы структурированными и организованными. Единственный файл класса поможет обуздать хаос кода, разбросанного по многочисленным страницам HTML. Вместо изменения переменной или добавления проверки в нескольких местах (с риском забыть какую-то страницу) теперь можно изменить только файл класса. Чем меньше файлов загромождают ваш Web-сервер, тем меньше вы будете волноваться, когда прийдет время вносить изменения. IT-шников нельзя назвать самыми организованными, так что даже порядок в структуре каталогов здесь пригодится. Это важный фактор как производительности, так и эффективности.

Эффективность OOП может разительно увеличить эффективность. Что я под этим подразумеваю? В этом случае это целостность кода - наличие законченного и отлаженного проекта. Если Вы повторно используете функции, внедренные в HTML-код, велика вероятность забыть или потерять некоторый фрагмент. Это похоже на потерю носка при стирке всего кучей. С OOП код остается в пределах одного файла и трудно что-либо потерять, потому что все, что нужно сделать - вызвать require_once. Возвращаясь к аналогии со стиркой, это похоже на стирку ваших носков в отдельной загрузке, где проще сохранить их всех вместе.


PHP Inside'18 >> Идеи >> Практические стороны ООП в PHP

Недостатки ООП Не хочу показаться необъективным поклонником ООП - в его использовании есть и неудобства. Их не очень много, и основной из них - это время разработки. Как правило, написание класса в PHP занимает больше времени, независимо от вашего уровня знаний. Однако ваша производительность и эффективность возрастают, как только вы начинаете эти классы повторно использовать. Если вам приходится повторно использовать код в схожих проектах, то ООП определенно ваш выбор. Однако если сроки поджимают или вы никогда не вернетесь к этому коду, ООП - не самое разумное решение. Изучить ООП в PHP достаточно просто. Там нет никаких радикальных изменений, но если вы ранее не сталкивались с ООП или новичок в программировании, для начала использования может потребоваться некоторое время.

Заключение Надеюсь, что чтение данной статьи позволит вам освоить новую технику и расширит ваш опыт программирования на PHP. Важно помнить, что ООП - только способ, а не панацея. Это всего лишь еще один инструмент в арсенале разработчика. И именно вам решать, подходит ли ООП для использования в вашем проекте.

Словарь терминов статьи ООП, Объектно Ориентированное Программирование – парадигма программирования, которая рассматривает программу, как набор отдельных самостоятельных частей – объектов, имеющих собственные свойства и модели поведения. Противопоставляется функциональному программированию, как простой коллекции программных инструкций.


PHP Inside'18 >> Идеи >> Тонкости PHP

Тонкости PHP Автор: Джон Херрен, Перевод: http://iluha.net

И

х можно назвать непонятными, бессмысленными или “недоошибками”! Но как их не называй, в какой-то момент все попадают в ловушку этих нелогичныx и странныx ошибок. В связи с тем, что PHP слабо-типизированный язык, в процессе написания программ могут проявиться некоторые странные вещи. PHP легкий для освоения язык, но всегда существует возможность подловить случайного программиста на тех вещах, который вроде бы “должны работать”. Не все приходят в PHP, имея за плечами большой опыт программирования, по этой причине здесь приводятся несколько интересных примеров того, как PHP может заманить вас в ловушку, если вы не аккуратны. Приготовтесь запоминать - информация идет к вам!

Иголки в стоге сена Скажем вам надо проверить входит ли одна подстрока в другую, т.е. фактически - найти иголку в стоге сена! Это достаточно простая задача. Давайте используем функцию strpos(), чтобы ее решить: 1.<?php 2.if (strpos(‘abcdefg’,‘abc’)) 3.{ 4.echo “found it!”; 5.} 6.else 7.{ 8.echo “didn’t find it..”; 9.} 10.?> Секундочку! Как мы видим - вхождение подстроки существует. Почему тогда функция strpos() вернула нам false? Посмотрим в руководство: strpos() — Возвращает позицию первого вхождения подстроки 1.int strpos ( string haystack, mixed needle [, int offset] ) О! strpos() возвращает число, обозначающее позицию (отсюда такое название)! Так как наша подстрока находится в начале строки, в которой проходит поиск, функция вернет 0, который преобразуется в false. Кроме того в руководстве сказано: Если подстрока needle не найдена, strpos () возвращает FALSE. Отлично, исправим наш пример следующим образом: 1.<?php 2.if (strpos(‘abcdefg’,‘abc’) != false) 3.{ 4.echo “found it!”; 5.} 6.else 7.{ 8.echo “didn’t find it..”; 9.} 10.?>


PHP Inside'18 >> Идеи >> Тонкости PHP Хм…!? Возвращенное значение 0 не равно false… или равно? Равно, т.к. число 0 приводится к false. Значит мы должны проверять значение еще и по типу: 1.<?php 2.if (strpos(‘abcdefg’,‘abc’) !== false) 3.{ 4.echo “found it!”; 5.} 6.else 7.{ 8.echo “didn’t find it..”; 9.} 10.?> Наконец, мы получили правильный результат результат. Оператор == сравнивает значения, а оператор === (и обратный от него !==) сравнивает и значение и тип. Если вы смотрели PHP bug tracker, то видели что данный случай упоминается очень много раз, и все время помечается как фиктивный.

Головоногие в моем коде? Еще одна занятная особенность, которую я узнал пока готовился к экзамену Zend: 1.<?php 2.var_dump(0123 == 123); //выведет bool(false) 3.?> Это ввело меня в абсолютное замешательство. Затем я нашел - 0 в начале числа обозначает, что число является восьмеричным, и в десятичной системе счисления равно 83. Восьмеричная нотация была введена в PHP, для облегчения работы с правами на файлы. Обратитесь к разделу руководства, посвященному chmod(). Не путайте вышесказанное со следующим примером: 1.<?php 2.var_dump(“0123″ == 123); //выведет bool(true) 3.?> Строка “0123″ будет преобразована при стравнении в число 123.

Стойкие константы Вопрос. Что выведет нижеследующий код? 1.<?php 2.//MY_UNDEFINED_CONSTANT действительно не определена 3.if (MY_UNDEFINED_CONSTANT) 4.{ 5.echo “Kelly Clarkson”; 6.} 7.else 8.{ 9.echo “Kellie Pickler”; 10.} 11.?>


PHP Inside'18 >> Идеи >> Тонкости PHP Если вы решите что это будет Pickler, вы будете неправы. PHP выведет в данном случае ошибку типа E_NOTICE, но если в конфигурации такой тип ошибок отключен, то вы должны помнить, что неопределенная константа трактуется как имя этой константы, как будто вы используете имя в качестве строки. А стока “MY_UNDEFINED_CONSTANT” будет означать true, когда приводется к типу boolean.

Врал ли мой учитель математики? Посадите третьекласника в одну комнату с компьютерным специалистом и дайте им следующуюю задачку: 1.<?php 2.var_dump(0.7 + 0.1 == 0.8); //выведет bool(false) 3.?> PHP не проводит логические операции, когда сравнивает два числа с плавающей точкой, это связано с внутренним представлением чисел в языке. Совет прост - никогда не сравнивайте числа с плавающей точкой на равенство! Используйте расширение BCMath.

Профессор, читавший “Введение в логические системы”, тоже врал? 1.<?php 2.$x = true and false; 3.var_dump($x); //выводит bool(true) 4.?> Почему ‘and‘ работает как оператор ‘or‘? Встали в тупик? Может быть это поможет: 1.<?php 2.$y = ($x = true and false); 3.var_dump($x); //выведет bool(true) 4.var_dump($y); //выведет bool(false) 5.?> Это случай ошибочного приоритета операторов. В данном примере, оператор присваивания (=) имеет большй приоритет чем оператор ‘and‘, таким образом переменной $x присваивается значение true. Второй пример немного проясняет ситуацию. При первом присваивании переменной $x присваивается (true), а через ‘and‘ переменной $y присваивается false.

__toString() иль не __toString? В PHP 5 нам доступен новый “волшебный метод” - __toString(), который позволяет делать следующее: 1.<?php 2.class Person 3.{ 4.private $name; 5. 6.function __construct($name=“John Doe”) 7.{ 8.$this->name = $name; 9.} 10.


PHP Inside'18 >> Идеи >> Тонкости PHP 11.function __toString() 12.{ 13.return (string) $this->name; 14.} 15.} 16. 17.$bob = new Person(‘Bob’); 18.echo $bob; //выведет Bob 19. 20.?> Это удобно и понятно. Но, что если я захочу добавить восклицательный знак: 1.<?php 2.echo $bob . “!”; //выведет Object id #1! 3.?> Что случилось с Бобом? Оператору конкатенации (.) имеет больший приоритет, и мой метод __toString() никогда не будет вызван. Что если я приведу мой объект к строковому типу: 1.<?php 2.$name = (string) $bob; 3.echo $name . “!”; //выведет Object id #1! 4.?> Опять не повезло. В руководстве сказано __toString вызывается только непосредственно в связке с echo() или print(). Таким образом, попробуем послать наш объект и восклицательный знак как параметры к echo(). 1.<?php 2.echo $bob , “!”; //выводит Bob! 3.?> Вуоля! Наконец, наш метод __toString() вызван.

Словарь терминов статьи PHP Bug tracker, http://bugs.php.net/ - база данных, хранящая ошибки и «псевдоошибки» PHP. Здесь можно как найти причины «неправильного» поведения PHP, так и сообщить об обнаруженных ошибках.


PHP Inside'18 >> Люди >> Интервью с Леонидом Лукиным

Интервью с Леонидом Лукиным Интервьюировал: Андрей Олищук Первый отечественный Zend Cerified Engeneer, с 1999 по 2005 год преподававший PHP в Центре компьютерного обучения «Специалист» при МГТУ им. Баумана, автор известного в среде PHPпрограммистов онлайн-экзамена «Online certified PHP specialist», ныне независимый PHPразработчик и консультант по веб-технологиям, Леонид Лукин любезно согласился дать интервью нашему журналу. Главным образом, мы обсудили вопросы получения PHP-образования в России и СНГ, поговорили о программе сертификации Zend и затронули тему о состоянии дел на рынке труда веб-разработчиков. Более подробную информацию о Леониде можно найти на его блоге: http://blog.phpworld.ru/. Вы работали в сфере IT-образования, скажите, много ли сейчас существует мест в России, где учат непосредственно PHP? Леонид Лукин (ЛЛ): На данный момент в России достаточно учебных центров, где можно учиться различным веб-технологиям, в том числе и PHP. Естественно, что большинство таких учебных центров расположено в крупных городах, таких как Москва и Санкт-Петербург. При этом почти никто не предложит изучать PHP дистанционно. С другой стороны, учебные организации, как правило, дают возможность изучать PHP не в рамках отдельного самостоятельного курса, а как составляющую часть курсов, представляющих несколько смежных веб-технологий, т.е. позволяют изучать Andi Gutmans (слева) и Леонид Лукин (справа) PHP только обзорно и лишь самые основы языка. И в этом я вижу сегодня одну из главных проблем профессиональной подготовки PHP-специалистов. Что можно посоветовать при выборе места обучения PHP? В первую очередь, желательно отдавать предпочтение солидным организация с большим опытом работы, с набором авторизаций по различным сертифицированным учебным программам. Как правило, качество обучение в таких заведениях гораздо выше, чем в учебных центрах-однодневках. Попросите в учебной организации копию программы курса, поинтересуйтесь ее авторами. Известны случаи, когда учебные организации копируют программы своих конкурентов, но при этом не в состоянии реализовать адекватное обучение по ним в рамках своих курсов. Именно в таких ситуациях вам предоставят возможность просмотреть программу и обсудить ее с менеджером или будущем преподавателем с большой неохотой. Однако, на мой взгляд, главное правило, которое позволит овладеть любой технологией, в том числе и PHP, максимально успешно и эффективно, заключается в том, чтобы выбирать не столько организацию, сколько непосредственно преподавателя, того человека от умений и навыков которого будет зависеть две трети успеха, если не больше. При этом как выбрать преподавателя? Желательно пообщаться с кем-то из тех, кто ранее обучался у данного преподавателя, уточнить


PHP Inside'18 >> Люди >> Интервью с Леонидом Лукиным его уровень квалификации, наличие сертификатов, в том числе и сертификата IT-инструктора. Не бойтесь задавать вопросы о практическом опыте преподавателя в области веб-разработок. Идеальная ситуация складывается, когда преподаватель успешно совмещает практическую деятельность с работой в учебной организации. Если же речь идет именно о преподавателе по PHP, то крайне желательно, чтобы он имел статус Zend Certified Engineer, единственной официальной сертификации по PHP. Список тех, кто получил такой статус, так называемые «Желтые страницы экспертов по PHP», всегда можно просмотреть в онлайне на сайте компании Zend. Не побоюсь высказать мнение, что, на мой взгляд, лучшим преподавателем PHP в России на сегодняшний день является Никитин Иван Геннадьевич, ныне работающий в центре обучения “Специалист”. Уверен, что мои слова смогут подтвердить практически все, кто прошел у него такое обучение, а это не одна сотня слушателей, многие из которых после обучения успешно находят высокооплачиваемую работу по специальности, без особых проблем сдают сертификационные экзамены, да и просто чувствуют себя гораздо увереннее в области вебразработок. Итак, как вы уже поняли обучаться PHP не так и просто. Правильный выбор зависит от многих факторов. Однако, если же все эти аспекты упустить из виду - очень часто деньги, отданные за обучение, могут быть просто «выброшены на ветер». А это, как правило, деньги не маленькие. Одной из основных особенностей PHP является то, что для него написано много модулей и расширений. Когда Вы преподавали в центре "Специалист", на чем именно строили обучение? Что входило в программу и почему именно это? ЛЛ: В учебном центре "Специалист" при подготовке программы по PHP мы ориентировались на несколько ключевых моментов. Самый важный из них заключается в том, что изучать PHP приходят люди с абсолютно разной начальной подготовкой. В этой связи было принято решение предложить сразу 2 курса, базовый – для начинающих, и профессиональный – для тех, кто знает основы PHP, имеет опыт работы с этой технологией, и хотел бы совершенствовать и углублять свои знания. Базовый курс «Основы PHP программирования» дает начальные сведения о языке PHP, рациональной области использования самой технологии PHP, много времени уделяется вопросам установки и настройки, совместной работе с Apache и MySQL. Фактически, курс готовит специалистов, которые смогут свободно использовать возможности ядра PHP, писать вполне серьезные, но типовые, приложения на PHP, такие как форумы, гостевые книги, электронные магазины и т.п. Что же касается использования возможностей дополнительных расширений и классов PEAR, то в рамках данного курса дается только краткое введение в назначение упомянутых элементов. Второй же курс – «Профессиональное программирование на PHP5» - предназначен для углубленного изучения программирования на PHP, в частности уже базируется на PHP версии 5. В курсе рассматриваются особенности объектно-ориентированного программирования, работа PHP с SQLite и XML-технологиями на примере расширений SAX, DOM, XSLT, SimpleXML. Слушатели курса учатся создавать SOAP клиенты и серверы, так что по окончании курса работа с вебсервисами на PHP для выпускников курса это вовсе не проблема. Также рассматриваются особенности работы с графикой на PHP на примере использования расширения GD. Много времени на курсе уделяется вопросам безопасности и производительности при программировании на PHP. А главное – выпускники курса получают обширную базу знаний, которая позволит им в дальнейшем самостоятельно разобраться и эффективно использовать любые расширения PHP, изучение которых не входило в программу курса. Я думаю, и уверен, что вы со мной согласитесь,


PHP Inside'18 >> Люди >> Интервью с Леонидом Лукиным это и является залогом будущей успешной карьеры любого специалиста. Также замечу, что при написании программ мы активно консультировались непосредственно с отделом обучения и сертификации компании Zend, после чего программы даже были одобрены Zend и рекомендованы для подготовки к сдаче экзамена на получение статуса Zend Certified Engineer. В свое время «Специалист» даже стал третьим официальным авторизированным учебным центром Zend в мире, однако в этом году по разногласиям экономического характера «Специалист» утерял данный статус. Таким образом, на сегодняшний день на территории России нет, к сожалению, не одного учебного центра авторизированного Zend. Многих разработчиков интересует вопрос определения уровня своей квалификации. Существуют ли лично у Вас какие либо системы или подходы для определения класса разработчика? В чем они заключаются? ЛЛ: Да, такая система у меня лично имеется. Уверен, что ее использует большое количество специалистов, а потому я в этом вопросе наверняка не буду оригинален. В первую очередь я обращаю внимание на качество написания исходного кода в плане логичного и систематизированного форматирования кода (не столько важно как именно оформлен код, сколько должно проявляться единообразие и обоснованность, соблюдаться правила именования переменных, функций и т.п.). Очень критично наличие четких и подробных комментариев, даже в случае, если разработчик писал код только для себя. Важное значение имеет простота кода и его безопасность. Код должен писаться со знанием дела, с учетом попыток взлома (я уже и не упоминаю о register_globals, хотя на эти грабли многие наступают повсеместно) и в ряде случаев повышенной загруженности приложений. Лично для меня одного взгляда на код достаточно для того, чтобы понять – имеет ли программист четкое понимание того, как работает PHP, понимает ли он идеологию и философию этой технологии. К сожалению должен констатировать, что большинство очень далеко от такого понимания. И что самое печальное – авторы множества книг по PHP, которые сейчас можно в изобилии встретить на полках книжных магазинов, великолепно демонстрируют свою чудовищную некомпетентность. А отсюда, как вы понимаете, она и приобретает массовость среди разработчиков. Справедливости ради хочу заметить, что в основном грешат этим только отечественные авторы, переводным же изданиям в большинстве случаев удается оставаться вполне пригодными для изучения PHP «с нуля», хотя и встречаются случаи, когда великолепные книги отличных авторов портятся некачественным переводом. Вот и еще одна причина, почему задача «правильного» освоения такой, на первый взгляд простой технологии, как PHP, является в России очень и очень не простой. О сертификации Zend уже было многое сказано, но не могли бы Вы нам еще раз рассказать, что она собой представляет? Где и как можно записаться на сдачу экзаменов, сколько стоит прохождение и подготовка, как к ним готовиться, в чем заключается сам экзамен, какие сложности при сдаче, какие советы вы можете дать? ЛЛ: Сертификация Zend дает возможность PHP-разработчику официально подтвердить свою квалификацию, успешно сдав экзамен и получив статус Zend Certified Engineer. Сдать экзамен можно в любом центре тестирования VUE (http://vue.com/zend/), которых в России насчитывается не один десяток. Запись на экзамен начинается с сайта компании Zend (http://www.zend.com/education/certification), где вы сможете подать заявку на сдачу экзамена и оплатить ваучер, без которого сдача экзамена невозможна. Стоимость экзамена составит $125 + налоги + стоимость сдачи экзамена в том или ином центре тестирования. Если у вас нет возможности оплатить ваучер кредитной картой, то центр тестирования может приобрести его для вас, однако, прежде всего вам необходимо будет самостоятельно зарегистрироваться на сайте Zend, после чего предоставить в центр тестирования информацию о полученной там вами учетной записи.


PHP Inside'18 >> Люди >> Интервью с Леонидом Лукиным Что собой представляет экзамен? Вам будет предложено в течение 90 минут ответить на 70 вопросов экзамена. Вопросы бывают трех типов: несколько вариантов ответа с одним правильным, несколько вариантов ответа с несколькими верными (необходимо указать все правильные), открытые вопросы, ответы на которые нужно не выбирать из предложенных, а написать самим. Вопросы последнего типа, естественно и являются наиболее сложными, но к счастью таких меньшинство. Все вопросы не выше среднего уровня сложности, однако, ориентированы на программистов с достаточным опытом работы и касаются только основных аспектов использования ядра PHP версии 4, т.е. вопросы по различным расширениям PHP отсутствуют. В ближайшее время появится еще одна версия экзамена, ориентированная на PHP 5, однако и предыдущий вариант будет доступен до конца 2006 года, так что сдающий сам сможет выбрать тот или иной вариант экзамена. Естественно, сдавать экзамен придется только на английском языке. Перевод экзамена на русский язык не предвидится и по заверениям Zend Education Advisory Board вряд ли вообще будет допустим, так как одним из требований, предъявляемым к специалисту по PHP, является уверенное владение английским языком, хотя бы на уровне чтения и понимания технической документации. Для успешной подготовки к экзамену я бы рекомендовал использовать ряд официальных учебных пособий - Zend PHP Certification Study Guide и Zend PHP Certification Practice Test Book, приобрести которые можно во многих электронных магазинах и на сайте компании Zend. Личное мнение об экзамене – экзамен не сложный, однако не стоит рисковать, если вы не имеете хотя бы годичного опыта работы с технологией PHP и не работали с официальной документацией, опубликованной на www.php.net. К сожалению, обычная литература, и об этот я уже упоминал, не позволит вам успешно сдать этот экзамен. Тем более что ни один из отечественных авторов книг по PHP еще не смог сдать такой экзамен. Чем сертификат будет полезен в реальной жизни отечественному разработчику? И как лично Вы относитесь к программам сертификации PHP-специалистов Brainbench и "online certified PHP specialist" от центра "Специалист"? Насколько и чем эти программы отличаются от сертификации Zend? Существуют ли другие программы сертификации PHPразработчиков? ЛЛ: Наличие международного сертификата Zend Certified Engineer решает одну из главных проблем российских PHP-специалистов. А проблема эта заключается в том, что очень трудно подтвердить свою квалификацию PHP-программиста и выделиться среди огромного числа тех, кто, осваивая PHP по книгам из серии «за 24 часа», претендует на участие в серьезных проектах и высокий уровень оплаты труда. Хотя, именно по этой причине он, этот уровень, и не очень высок. Не только новички, но и те, кто годами программирует на PHP, в большинстве случаев имеют серьезные пробелы в знании технологии, что критическим образом сказывается на безопасности и производительности создаваемых ими приложений. Подчас заказчик не может понять, почему то или иное приложение по оценкам разных команд разработчиков может отличаться по бюджету в несколько раз. Кажется, зачем платить больше, если есть знакомый студент, владеющий ПэХаПэ, который напишет все за пару ночей и очень и очень недорого возьмет за свою работу. К сожалению, эта ситуация очень типична и многие не осознают потенциал проблемы. Не осознают ее иногда и сами разработчики. За последний год лично мне, как фрилансеру, в большинстве случаев доводилось большее время потратить на переработку таких вот приложений, написанных горе-специалистами, чем на разработку совершенно новых проектов. Таким образом, популярность программ сертификации может дать только положительный эффект. Говоря простыми словами, если у вас есть такая сертификация, то ваша ценность как специалиста резко увеличивается, вы практически без проблем сможете найти высокооплачиваемую работу,


PHP Inside'18 >> Люди >> Интервью с Леонидом Лукиным уехать работать по контракту за рубеж, да и в любом случае, сможете чувствовать себя более уверенно. А что касается программ сертификации, не относящихся к Zend, то я бы расценивал их только как великолепную возможность попробовать свои силы перед сдачей «настоящего» экзамена. К сожалению, ничего конкретного не смогу сказать про экзамен Brainbench. А вот об экзамене "Online certified PHP specialist" от центра "Специалист" могу рассказать практически все, так как именно я являлся его автором. И главную проблему этого теста сейчас вижу в том, что он ужасно устарел, так как писался уж очень много лет назад. Необходимость его обновления в "Специалисте" осознают, однако выход нового варианта, боюсь, вряд ли можно ожидать в ближайшее время. В любом случае, пока только экзамен Zend является наиболее адекватным в плане проверки уровня знаний и навыков PHP-специалиста, постоянно обновляемым и корректируемым, что делает его единственным официально признанным международным сообществом PHP экзаменом. Уверен, что уровень этой программы сертификации настолько высок, что вряд ли кто-то уже и смеет думать о том, чтобы создать ее конкуренцию. Как Вы считаете, необходимо ли в России и СНГ развивать систему PHP-образования, или для PHP не требуется каких либо образовательных программ и рынок труда все равно получит специалистов, так как язык несложен в изучении? ЛЛ: Я не стану спорить с тем, что язык PHP несложен в изучении, однако, многие считают, что он даже значительно проще, чем есть в действительности. Важно понимать, что сложен не столько PHP, сколько среда, в которой он используется. Я говорю о системе World Wide Web, которая не прощает упрощения. Если бы было все так просто – имели бы мы столько мощных расширений PHP, PEAR классов, Zend Framework, наконец? Я использую PHP вот уже более 6 лет и все равно почти каждодневно обращаюсь к документации, читаю литературу, статьи, знакомлюсь с опытом других разработчиков. А вы посмотрите на темы, которые затрагивает ваш журнал? Разве можно назвать их банальными? И с каждым годом все вокруг будет только усложняться и усложняться. В этой ситуации ценность качественных образовательных программ будет неуклонно повышаться. Я в этом уверен. Другой вопрос в том, кто и как будет их развивать? На мой взгляд, все изменится только тогда, когда возрастет экономическая отдача от веб-отрасли, вкладывать в веб-разработки станет выгоднее, чем сейчас, а потому и выгодны будут вложения в хороших специалистов. Я думаю, это уже не за горами. Верите ли Вы в корпоративное будущее открытых технологий, ведь сейчас все проприетарные технологии (от Microsoft, SUN...) имеют хорошую основу для подготовки специалистов, в то время как Open Source еще не достиг такого уровня? Ждет ли Open Source (и PHP в частности) бум образовательных программ уровня предприятий (а не просто "основы PHP"), либо этого не произойдет никогда? ЛЛ: Это уже происходит. И в Zend это поняли два года назад. В результате мы с вами имеем возможность принять участие в программе PHP-сертификации. Кстати, замечу, что PHP уже давно по сути почти что проприетарная технология. Вы разве этого не заметили? Как и многие другие Open Source проекты. Не извлекать из них прибыль не разумно. И тут я считаю, что как только PHP-сообщество получило компанию Zend в качестве флагмана технологии, все от этого оказались только в выигрыше. Несомненно. Также значительная поддержка PHP оказывается сейчас компанией IBM. На мой взгляд, такая модель бизнеса в сфере IT оказывается наиболее эффективной в современных условиях. Однако, будущее покажет… Расскажите немного и о себе. Как Вы проводите свободное время, чем увлекаетесь, чем занимаетесь во время отпуска и выходных?


PHP Inside'18 >> Люди >> Интервью с Леонидом Лукиным ЛЛ: Что сказать о себе? Я очень люблю то, чем занимаюсь. Считаю, что мне очень повезло в том, что я окунулся в WWW почти с момента его возникновения. Долгое время мне приходилось совмещать научную и преподавательскую деятельность с работой разработчика веб-приложений и SEO-специалиста. Это совмещение имеет множество недостатков, вот почему некоторое время назад я полностью посвятил себя последнему. По примеру многих моих зарубежных знакомых разработчиков PHP, с которыми я тесно общался на различных конференциях по PHP, я решил посвятить себя фрилансу. Как фрилансер, я имею такие громадные преимущества по сравнению с большинством людей моей, да и других профессий, что это просто тема отдельной статьи. Благодаря этому только в последнее время я смог по настоящему ощутить определенную независимость, почувствовать такое явление в своей жизни, как свободное время. На что я его трачу? Я посвящаю его семье, друзьям, музыке, совершенствованию в игре на электрогитаре. Есть определенные планы значительно расширить круг интересов и увлечений. И, конечно, совершенствоваться в PHP! Спасибо!


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.