Предыстория
Решив запустить небольшой сервис на подаренном мне хостинге, оказалось, что там нету ни одного xml-парсера: ни SimpleXML, ни DOMXML, а только libxml и xml-rpc. Недолго думая, я решил написать свой. Мне требовался разбор не сложных rss-лент, поэтому хватило достаточно просто класса xml => array.[1]
Но для интересной статьи этого было явно не достаточно, поэтому сейчас мы напишем свою замену для SimpleXML. А заодно пробежимся по многим интересным возможностям PHP 5.
Постановка задачи
Доступ к элементам у нас будет осуществляться как доступ к свойствам класса, например $xml->element, а доступ к атрибутам элемента, как к массиву, те $xml->element['attr'], также реализуем проверку на существование атрибута при помощи isset() и итерацию по элементам при помощи foreach. И так, начнем.
Немного магии?
В PHP 5 для классов определены некоторые ‘магические’ методы, они начинаются с двойного подчеркивания ‘__’ и вызываются при происхождении определенного действия.[2] Нам понадобятся следующие:
- void __construct ([ mixed $args [, $... ]] ) — самый известный магический метод, вызывается после создания класса оператором new.
- mixed __get ($name) – вызывается при обращении к свойствам класса, если соответствующее поле не было найдено, например $obj->element вызовет __get('element'), если element не был объявлен как поле класса.
- void __set ($name, $value) – соответственно вызывается при изменении свойства класса, например $obj->element = $some_var вызовет __set('element', $some_var).
- string __toString() — вызывается при любых операциях над классом, как над строкой, допустим echo $obj или strval($obj). Этот метод нам потребуется для получения содержимого элемента. К сожалению, методов возвращающих не строку нету, поэтому чтобы преобразовать элемент в число придется делать так: intval(strval($obj)).
SPL
Standard PHP Library – стандартная библиотека PHP, как и STL из мира C++, создавалась для того, чтобы дать разработчику инструменты для решения типовых задач.[3]
Нам потребуется реализовать следующие интерфейсы:
- ArrayAccess – для доступа к классу, как к массиву, например $obj['name'] или isset($obj['name']).
- IteratorAggregate – для возможности итерации по классу при помощи foreach.
- Countable — чтобы узнать количество потомков у элемента.
XML и expat
Это стандартные библиотеки для работы с XML и создания XML-парсеров.[4] То, что надо для решения нашей задачи. Ради интереса можете написать разбор xml-файла вручную, допустим на регулярных выражениях.
Больше всего в expat нас интересуют следующие функции:
- bool xml_set_element_handler (resource $parser, callback $start_element_handler, callback $end_element_handler) – устанавливает функции, вызываемые при нахождении открытого и закрытого тегов соответственно.
- bool xml_set_character_data_handler (resource $parser, callback $handler) – вызывает функцию, передавая ей символьное содержание элемента, причем даже если там ничего не было, она все равно вызывается.
Примечание: callback в php это либо имя функции, переданное как строка, либо массив с двумя значениями – первое это название класса, а второе название метода этого класса.
Указатели
Указатели в PHP работают не совсем так, как в C или в C++.[5] Фактически, конструкция $a =& $b всего лишь означает, что теперь $a указывает на ту же область с данными, что и $b, причем изменить адрес куда указывает $b через $a невозможно, те можно сказать, что изменение адреса имеет один уровень вложенности.
Начиная с пятой версии, в PHP все переменные передаются в функцию по указателю, но как только вы изменяете ее значение – выделяется память под новую. В нашем случае указатели пригодятся для указания на родительский элемент.
Кодинг
С теорией закончили, теперь приступим непосредственно к написанию парсера.
Каждый объект будет представлять один xml-элемент, поэтому ему потребуются такие свойства, как имя тега, атрибуты, данные, ссылка на родителя и массив с потомками, кроме того, потребуется переменная-указатель на текущий элемент. Из методов нам потребуется реализовать все интерфейсы, добавление потомка, установку ссылки на родителя, присвоение содержимого элемента и три функции, требуемые для парсера — открытие и закрытие тега и получение содержимого элемента.
Сделаем набросок будущего класса:
class XML implements ArrayAccess, IteratorAggregate, Countable {
private $pointer;
private $tagName;
private $attributes = array();
private $cdata;
private $parent;
private $childs = array();
public function __construct($data) { }
public function __toString() { return; }
public function __get($name) { return; }
public function offsetGet($offset) { return; }
public function offsetExists($offset) { return; }
public function offsetSet($offset, $value) { return; }
public function offsetUnset($offset) { return; }
public function count() { return; }
public function getIterator() { return; }
public function appendChild($tag, $attributes) { return; }
public function setParent(XML $parent) {}
public function getParent() { return; }
public function setCData($cdata) {}
private function parse($data) {}
private function tag_open($parser, $tag, $attributes) {}
private function cdata($parser, $cdata) {}
private function tag_close($parser, $tag) {}
}
Теперь примемся за реализацию функций. По порядку, начнем с конструктора. В нашем случае он может принимать два типа значений – строку (xml) или массив из двух элементов (название элемента, атрибуты), так как перегрузки одного метода с разными параметрами в php нету – придется вручную проверять тип.
public function __construct($data) {
if (is_array($data)) {
list($this->tagName, $this->attributes) = $data;
} else if (is_string($data))
$this->parse($data);
}
Как уже упоминалось – при помощи магического метода __toString() пользователь сможет получить данные элемента в виде строки, а затем преобразовать ее в любой требуемый ему тип, к сожалению, напрямую возвращать, что хочется, не получится, поэтому только так.
Заодно разберем следующий магический метод __get($name), при помощи него будет осуществляться доступ к потомкам текущего элемента. Вполне логично, что если потомок всего лишь один, то его сразу и вернуть, без необходимости обращаться по 0 индексу массива. Например: $xml->rss->channel->item[5]->url, вместо $xml->rss[0]->channel[0]->item[5]->url[0], если элементы rss, channel и url существуют в единственном экземпляре на своем уровне вложенности.
public function __toString() {
return $this->cdata;
}
public function __get($name) {
if (isset($this->childs[$name])) {
if (count($this->childs[$name]) == 1)
return $this->childs[$name][0];
else
return $this->childs[$name];
}
throw new Exception(«UFO steals [$name]!»);
}
Функции offsetGet, offsetExists, offsetSet и offsetUnset реализуют интерфейс ArrayAccess, для доступа к объекту как к массиву. Мы его используем для доступа к атрибутам элемента. offsetSet и offsetUnset оставим пока заглушками.
public function offsetGet($offset) {
if (isset($this->attributes[$offset]))
return $this->attributes[$offset];
throw new Exception(«Holy cow! There is'nt [$offset] attribute!»);
}
public function offsetExists($offset) {
return isset($this->attributes[$offset]);
}
А теперь мы столкнулись с проблемой из-за принятого недавно решения. Если вдруг мы захотим запустить цикл foreach по единственному элементу, то он запустится по самому xml-объекту! Поэтому придется пожертвовать возможностью простым способом использовать foreach для атрибутов элемента и реализовать метод getAttributes(). А итератор и количество элементов мы будем возвращать для массива элементов, к которому принадлежит вызываемый, а если у него нету родителя, то итератор по массиву из одного текущего элемента. Таким образом, будут реализованы интерфейсы IteratorAggregate и Countable.
public function count() {
if ($this->parent != null)
return count($this->parent->childs[$this->tagName]);
return 1;
}
public function getIterator() {
if ($this->parent != null)
return new ArrayIterator($this->parent->childs[$this->tagName]);
return new ArrayIterator(array($this));
}
Добавление потомка простая функция, интересно в ней разве только то, что после добавления элемента, она возвращает ссылку на него.
public function appendChild($tag, $attributes) {
$element = new XML(array($tag, $attributes));
$element->setParent($this);
$this->childs[$tag][] = $element;
return $element;
}
Теперь реализуем сам парсер. Для создания древовидной структуры будем использовать указатель на текущий элемент. В начале он устанавливается непосредственно на текущий элемент, при открытии тега – на открытый элемент, для того, чтобы все содержащиеся в нем элементы добавились ему к потомкам, а при закрытии тега – на его родительский элемент.
private function parse($data) {
$this->pointer =& $this;
$parser = xml_parser_create();
xml_set_object($parser, $this);
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
xml_set_element_handler($parser, «tag_open», «tag_close»);
xml_set_character_data_handler($parser, «cdata»);
xml_parse($parser, $data);
}
private function tag_open($parser, $tag, $attributes) {
$this->pointer =& $this->pointer->appendChild($tag, $attributes);
}
private function cdata($parser, $cdata) {
$this->pointer->setCData($cdata);
}
private function tag_close($parser, $tag) {
$this->pointer =& $this->pointer->getParent();
}
Все. Парсер готов к работе. Дабы не раздувать статью еще больше, полностью исходный код с комментариями я загрузил на Google Docs и пример использования тоже.[6]
Что дальше?
Это все еще не полная замена для SimpleXML, наш парсер до сих пор не умеет создавать xml-документ из данных, находящихся в нем. Добавление нужных функций не сложная задача, поэтому я ее оставлю, для тех, кому это интересно, как домашнее задание :)
Ссылки
1) Первая версия xml=>array парсера.
2) Документация по магическим методам (eng) (рус).
3) Документация по SPL.
4) Описание функций xml-парсера.
5) Документация по указателям (eng) (рус).
6) Окончательная версия парсера и простой пример использования.