Как работать с XML в PHP и почему иногда этого не нужно делать…

Статья предполагает что Вы знаете:

  • PHP на хорошем базовом уровне
  • Что такое классы и объекты
  • Что такое рекурсия
  • Что такое XML

Когда я учился в Московском Авиационном Институте у нас были лекции по теории программирования. Их нам читал Юрий Анатольевич Голубков, немолодой уже человек со своеобразным чувством юмора.

Так вот на первой лекции он рассказал нам (тогда еще юным балбесам) что при проектировании любых систем нужно стараться придерживаться одного очень важного приниципа. Принцип называется KISS. Если Вы пошли по ссылке то уже догадились, что речь идет не о поцелуях, и не рок-группе средней руки. Речь идет о принципе проектирования систем который звучит как: «Keep it simple, Stupid!»?, что по-русски звучит как: «Делай проще, тупица!»

Это действительно очень важный принцип, поскольку человеку в силу определенных причин свойственно самого себя запутывать, а вложенная в него Богом жажда творчества, не управляемая здравым смыслом часто приводит к печальным последствиям, в том числе в виде плохо работающих, трудно поддерживаемых, сложно рассширяемых программных систем и в конце-концов головной боли в районе Вашей точке, что прямодушные американцы очень точно называют: «pain-in-the-ass».

1. Предыстория
2. Как работать с XML
3. Когда не следует этого делать

Предыстория

Сегодня я хочу рассказать Вам о том как сам попался на удочку собственного формализма и желания «все сделать правильно».

Я очень не люблю обычные констаты php и ручной include_once, на мой взгляд они выглядят уродливо и малопривлекательно. Но зато я очень люблю константы/статические переменые классов и __autoload, за то что они стройные и красивые. :)

Поэтому для своих настройек я завел отдельный класс, примерно такой:

class M_Settings {
    // Net settings
    public static $use_proxy              = false;

    public static $is_local_server        = true;

    public static $get_web_timeout        = 150;
    public static $traceroute_timeout     = 200;
    public static $curl_timeout           = 20;

    public static $proxy_reconnect_count_overhead  = 4;
    public static $proxy_timeout_overhead          = 10;

    public static $reconnect_count        = 2;

    public static $net_data_dir           = "data/html";
    public static $net_data_dir_read      = "data/html/current";

    public static $exec_out_charset       = 'CP866';
    public static $exec_out_convert       = true;

    public static $save_net               = false;
    public static $no_net                 = false;

    public static $proxy                  = array();
    public static $proxy_auth             = array();

    // Site settings
    public static $show_stats             = false;
    public static $strip_spaces_in_views  = false;

    public static $site_name              = "Stroy-Market.ru";
    public static $site_title             = "Stroy-Market.ru";

    public static $database               = array(); 

    public static $producers_on_page_count            = 20;
    public static $articles_on_page_count             = 12;
    public static $news_on_page_count_main            = 3;
    public static $news_on_page_count                 = 10;
    public static $best_or_new_articles_on_page_count = 8;

    public static $price_news_on_page_count           = 3;
    public static $papers_on_page_count               = 8;
    public static $news_brief_size                    = 200;

    // Admin settings
    public static $admin_items_on_page_count          = 20;
    public static $admin_page_title_length            = 40;
    public static $admin_page_desc_length             = 150;

    // Other settings
    public static $statistics_dir         = "stat";
    public static $internal_charset       = 'UTF-8';

    // Path settings
    public static $doc_root               = '';
    public static $image_dir              = 'view/content';
    public static $image_catalog_dir      = 'catalog';
}

Я это сделал для того чтобы в любом месте где мне нужна настройка я мог простро написать что-то вроде: M_Settings::$use_proxy и получить то что мне нужно. К тому же если хочется изменить настройку
по умолчанию (например для отладки) я просто меняю ее значение в точке входа (index.php), что на мой взгляд очень удобно.

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

2. Как работать с XML

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

Для начала я придумал формат моего XML-файла настроек, примерно такой:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings>
    <group name="net">
        <group name="proxy">
            <item name="use"              value="no"/>
            <item name="overhead"         value="10"/>
            <group name="list">
                <item name="proxy1" host="192.168.1.1" port="123" user="user" pass="pass"/>
            </group>
        </group>
        <group name="timeout">
            <item name="curl"            value="20"/>
            <item name="reconnect_count" value="2"/>
        </group>
    </group>
    <group name="site">
        <item name="show_stats"             value="yes"/>
        <item name="strip_spaces_in_views"  value="false"/>
        <item name="internal_charset"       value="utf-8"/>
        <group name="paths">
            <item name="doc_root" value=""/>
            <item name="images"   value="view/content"/>
        </group>
    </group>
    <group name="db">
        <item name="bm" host="localhost" user="root" pass="" db="bm"/>
    </group>
</settings>

Все достаточно просто есть два типа узлов group и item: group – это группа настроек, а item – это конкретная настройка. Причем item бывают 2х видов простой: когда у него есть скалярно значение (value) и сложный когда в качестве значения задается массив в виде набора .

Осталось написать класс который будет это дело загружать и формировать из него обычный php-array. Самое главное в этом классе это функция которая будет преобразовывать XML в Array. И прежде чем писать сам класс я решил просто написать эту функцию, чтобы оттестировать ее не отвлекаясь на мелочи, а потом уже оформить в виде класса-одиночки, к которому уже будут обращаться другие классы системы. Получилось примерно что-то такое:

// Заголовок что это будет обычный текстовый файл, а не html
// Кстати очень удобно для тестовых целей
header("Content-type: text/plain; charset=utf-8");

echo "-----------------------------\n";
echo " Reading XML-file ";
echo "\n-----------------------------\n\n";

// Волшебная функция модуля php SimpleXML
// которая формирует из файла объект класса SimpleXMLElement
// http://ru.php.net/manual/ru/function.simplexml-load-file.php
// http://ru.php.net/manual/ru/book.simplexml.php
$xml = simplexml_load_file('settings.cfg');

// Наша функция которая принимает на вход объект класса SimpleXMLElement и формирует из него array
function loadNode($node) {
    $res = array();

    // Проходим по всем детям данного узла
    // При первом запуске $node это указывает на верхний узел
    // Соответственно все children это всего  следующего уровня (net, site, db)
    foreach($node->children() as $child) {
        // Получаем имя узла (мы ведь не знаем что именно у нас за узел)
        switch($child->getName()) {
            // Это группа? Вызываем рекурсивно нашу же функцию и результат кладем в массив
            // и ключом равны атрибуту данного узла
            // Т.е на 1ом уровне в нашем массиве будут элементы
            // array(
            //    'net'  => '...',
            //    'site' => '...',
            //    'db'   => '...'
            //)
            case 'group':
                $res["{$child['name']}"] = loadNode($child);
            break;
            // При последующих вызовах мы в конце-концов доберемся до узлов типа item
            case 'item':
                // Получаем имя настройки (т.е. атрибут name) и приводим его к типу string
                // (изначально он типа SimpleXMLElement, а нам именно нужна строка)
                // подстановка в двойные кавычки "" производит это преобразование автоматически.
                $name  = (string)$child['name'];            

                // Определяем подтип нашего узла
                // это скалярное значение?
                if(isset($child['value'])) {
                    // Отлично берем его как строку
                    $value      = (string)$child['value'];
                    // Пытаемся преобразовать его в значение типа bool
                    $bool       = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
                    // Если получилось то присваиваем именно булево значение, если нет оставляем строку
                    $res[$name] = ($bool === null) ? $value : $bool;
                // Это вектроное значение
                } else {
                    $arr = array();
                    // Отлично! Проходим по всем атрибутам и сохраняем их во временный массив
                    foreach($child->attributes() as $key => $value) {
                        // Разумеется атрибут name нам не нужен, поскольку он будет ключом
                        // массива который мы здесь формируем
                        if($key == 'name') continue;
                        // Сохраняем значение как строки
                        $arr[$key] = (string)$value;
                    }
                    // Записываем наше значение как в наш массив с нужным ключом (значение атрибута
                    // name нашего узла item)
                    $res[$name] = $arr;
                }
            break;
            // Это какой-то другой узел! Такого быть не должно, сразу выдаем ошибку.
            default:
                trigger_error('Wrong node type!', E_USER_ERROR);
            break;
        }
    }
    return $res;
}

// Показываем результат работы
$settings = loadNode($xml);
var_dump($settings);

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

3. Когда не следует этого делать

Так все же почему все это не нужно было делать?
Да все выглядит прикольно и круто, но увы совершенно излишне.
Что я получаю в конце всего этого?
Правильно: array.

Ну и зачем городить огород, если можно тот же самый array можно просто написать руками и сделать статическим членом нужного мне класса?

Увы это простая мысль пришла мне не сразу и я несколько часов на написание того, что вряд ли будет использоваться. Но правда в этом есть небольшой плюс, Вы читаете эту статью, и я надеюсь умеете теперь немного работать с XML с помощью очень удобного и действительно простого класса SimpleXMLElement. За дополнительными сведениями по работе с ним отсылаю Вас к документации.

P.S. Исходные коды можно скачать тут.

Обсуждение закрыто.