Easyweb: Новогоднее обновление

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

    Поскольку до конца года осталось менее суток, то позвольте рассказать о том, что еще удалось сделать в этом году.


    XML facilities


    Среди прочих задач, поставленных перед Easyweb-ом, была необходимость сделать работу с XML на стороне PHP максимально чистой, изящной, компактной и простой для понимания. В данный момент дописаны все основные методы XML-фасилитей. Описание публичных методов и небольшие примеры использования:

    Как и всегда, любую дополнительную функциональность можно попросить через feature request на GitHub-е.

    Приведу пример решения одной и той же задачи на стандартном PHP DOM API, и на Easyweb XML facilities. Суть такова. Нужно загрузить файл с описанием книг, и сделать с каждой книгой следующее: обнулить цену, заменить код валюты на его написание в нижнем регистре, а также установить заданные идентификаторы для автора и категории книги (включая проверку ошибок).

    Было:
    <?php
    function replace($author_id, $category_id)
    {
        $xml = new DOMDocument();
        if(!$xml->load('/var/www/html/mywebsite/xml/library.xml'))
        {
            throw new Exception('Error loading XML file');
        }
        $xpath = new DOMXPath($xml);
        foreach($xpath->query('/books/book') as $book)
        {
            $price = $xpath->query('price', $book);
            if($price->length != 1)
            {
                throw new Exception('Node "price" should be unique for the book');
            }
            $price->item[0]->nodeValue = 0;
    
            $currency = $price->getAttribute('currency');
            if($currency)
            {
                $book->setAttribute('currency', strtolower($currency));
            }
            else
            {
                throw new Exception('Attribute "currency" not found');
            }
    
            $book->setAttribute('author_id', $author_id);
            $book->setAttribute('category_id', $category_id);
        }
    }
    ?>
    

    Стало:
    <?php
    function replace($author_id, $category_id)
    {
        $xml = xml::load('/xml/library.xml');
        foreach($xml->query('/books/book') as $book)
        {
            $book['price'] = 0;
            $book['price/@currency'] = strtolower($book['price/@currency']);
            $book['@author_id'] = $author_id;
            $book['@category_id'] = $category_id;
        }
    }
    ?>
    

    Easyweb XML фасилити умеют конструироваться от нативных PHP DOM resource handle, а также выдавать их пользователю через мембер-функцию ::get(), в связи с чем можно легко интегрироваться со сторонними библиотеками, работающими через нативные PHP DOM объекты.


    XPath-расширение www:paginate


    www:paginate($page, $count, $size)

    Функция предназначена для упрощения отрисовки статического пагинатора.
    $page — текущая страница.
    $count — общее количество страниц.
    $size — размер пагинатора.
    Текущая страница отмечается атрибутом current. На первой странице не будет ноды <previous />, на последней — ноды <next />.

    Вызов функции www:paginate(15, 85, 10) вернет вот такой XML:
    <pages>
        <previous>14</previous>
        <page>10</page>
        <page>11</page>
        <page>12</page>
        <page>13</page>
        <page>14</page>
        <page current="current">15</page>
        <page>16</page>
        <page>17</page>
        <page>18</page>
        <page>19</page>
        <next>16</next>
    </pages>
    

    Пример верстки:
    <xsl:template match="/">
        <xsl:apply-templates select="www:paginate(15, 85, 10)/pages/*" />
    </xsl:template>
    <xsl:template match="previous">
        <a href="/page/{.}/" class="page">← Previous</a>
    </xsl:template>
    <xsl:template match="next">
        <a href="/page/{.}/" class="page">Next →</a>
    </xsl:template>
    <xsl:template match="page[@current]">
        <span class="page current"><xsl:value-of select="." /></span>
    </xsl:template>
    <xsl:template match="page">
        <a href="/page/{.}/" class="page"><xsl:value-of select="." /></a>
    </xsl:template>
    

    Возможный результат:
    image


    Кеширование блоков


    Теперь результат XSL-расширения www:xslt можно кешировать в файлы. Для этого нужно добавить атрибут cache="true". Также имеется два необязательных атрибута cache-args и cache-lifetime, первый из которых позволяет передать в закешированный блок список простых параметров, а второй — ограничить время жизни закешированных данных. Пример использования:
    <www:xslt
        xsl="/books.xsl"
        xml="book:list(author_id -> {$author_id})"
        args="page -> {$page}, count -> 10"
        cache="true"
        cache-args="domain -> '{$domain}'"
        cache-lifetime="600" />
    


    XQuery


    Первый вариант поддержки XQuery в Easyweb. На данный момент его возможности сильно ограничены: нельзя передать параметры, нельзя использовать XSL- и XPath-расширения Easyweb-а. Главная проблема сейчас заключается в том, что хорошую XQuery-библиотеку для PHP не удалось найти в принципе. Если вы можете помочь мудрым советом, то буду рад услышать его здесь: habrahabr.ru/qa/31087

    На данный момент XQuery сделан через XQuery Lite (http://phpxmlclasses.sourceforge.net/xquery_lite.html), который был сделан и заброшен его автором еще в 2002-м году. XQuery Lite выложен в репозиторий Easyweb в связи с тем, что его пришлось допилить напильником, чтобы он заработал в PHP5.

    Сейчас поддержка XQuery заключается во введении XSL-расширения www:xquery:
    <div>
        <h1>External Resources</h1>
        <www:xquery src="/tpl/links.xq" />
    </div>
    


    Пользовательские XSL-расширения


    Теперь пользователь может регистрировать в движке свои собственные XSL-расширения. Для своего собственного расширения нужно указать пространство имен, а также его URI. То же самое нужно сделать в XSL-шаблонах страниц. Пример регистрации XSL-расширения, реализующего некоторую обработку текста (например — какая-то своя разметка):
    $www = www::create('en', 'us');
    
    $www->register_xsl('http://supermarkup.com/about', 'sm', 'block', function($node)
    {
        $xml = new xml();
        foreach($node->children() as $child)
        {
            $xml->append($xml->import($child));
        }
        foreach($xml->query('//text()') as $text)
        {
            $parent = $text->parent();
            $parent->append(supermarkup($text->value()));
            $parent->remove($text);
        }
        return $xml;
    });
    

    Использование в шаблоне:
    <?xml version="1.0" encoding="utf-8" ?>
    
    <xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:php="http://php.net/xsl"
        xmlns:www="https://github.com/nyan-cat/easyweb"
        xmlns:sm="http://supermarkup.com/about"
        exclude-result-prefixes="php www sm">
        <xsl:template match="/">
            <sm:block>
                <xsl:copy-of select="message" />
            </sm:block>
        </xsl:template>
    </xsl:stylesheet>
    

    Функция-обработчик расширения работает через Easyweb XML фасилити. Она принимает на вход XML-ноду, являющуюся расширением, и возвращает либо XML-ноду, либо XML-документ, которыми будет заменена нода расширения.

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


    GeoIP


    В движке появилась поддержка GeoIP. Для его работы потребуется установить PHP PECL GeoIP. Поддержка GeoIP сделана через интерфейс абстрактных процедур Easyweb-а. Пример описания GeoIP-процедуры в конфиге сайта:
    <procedure name="geoip:record" datasource="geoip" method="record" root="record">
        <param name="host" type="string" />
    </procedure>
    

    Теперь эту поцедуру можно использовать в любом месте, где используются абстрактиные процедуры Easyweb-а: при рендере страницы, при рендере блока, при вычислении групп системы прав доступа, при вызове XPath-расширения www:query, или же из PHP через инстанс класса Easyweb. Пример вызова:
    $record = $www->query('geoip:record', array
    (
        'host' => $www->variable('user:ip')
    ));
    

    Возможный результат:
    <?xml version="1.0"?>
    <record>
        <country>
            <alpha2>US</alpha2>
            <alpha3>USA</alpha3>
            <name>United States</name>
        </country>
        <region>NC</region>
        <city>Charlotte</city>
        <latitude>35.206001281738</latitude>
        <longitude>-80.829002380371</longitude>
    </record>
    

    Не забудьте, что GeoIP-базу качать нужно отдельно:


    Полнотекстовый и фасетный поиск


    Появилась первая пробная версия полнотекстового и фасетного поиска. Поиск выполняется через Apache Solr (http://lucene.apache.org/solr/). Для использования поиска потребуется установить Java, Servlet container (например — Tomcat или Jetty), сам Solr, а также PHP PECL SolrClient. Простая и доступная статья по установке Solr-а на CentOS: http://blog.nexcess.net/2011/12/30/installing-apache-solr-on-centos/.

    Как и в случае GeoIP, Solr-поиск реализован через абстрактные процедуры Easyweb, которые можно использовать в разных подсистемах движка. Пример конфига:
    <datasource
        name="metadata"
        type="solr"
        server="localhost"
        port="8080"
        url="/solr/"
        username="admin"
        password="samplepassword" />
    <!-- ... -->
    <procedure name="guestbook:add" datasource="guestbook" core="guestbook" method="add">
        <param name="author_id" type="natural" />
        <param name="message" type="author" />
        <param name="host" type="ipv4" />
    </procedure>
    <!-- ... -->
    <procedure
        name="guestbook:list"
        datasource="guestbook"
        core="guestbook"
        method="query"
        root="messages"
        item="message">
        *:*
    </procedure>
    

    Пока что конфигурить Solr нужно самостоятельно. В будущем планируется сделать генератор схемы Solr-а налету.


    Заключение


    Итак, в движке реализованы все основные функции уровня ядра, котрые были ранее запланированы. В более долгосрочных планах остаются LL(1)-парсеры, ORM-система и фреймворк для полнодуплексного общения клиента и сервера.

    Планы на начало следующего года — заняться переездом одного Большого® Коммерческого© Сайта™, использующего все функции Easyweb-а, на новую версию движка, параллельно с чем будут устраняться обнаруженные ошибки.

    С большой радостью приму ваши фичреквесты, багрепорты, и просто вопросы по установке и использованию Easyweb-а.

    С Новым годом!
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 9

      +1
      Вы бы хоть какой-нибудь пример типа бложека сделали, а то одинокий hello world и больше никаких намёков на то, на что же будет похож итоговый код.
        0
        В качестве учебного пособия есть гостевая книга: github.com/nyan-cat/easybook, ее логика такая: регистрации нет, автор сообщения может редактировать или удалять сообщения. Автор определяется по правилу: если IP тот же, и если сообщение не старше 10 минут (конечно же это просто для примера, в жизни так не делают). Или вы ее и назвали hello world-ом? Реальный сайт будет примерно так и выглядеть, только будет больше файликов в каждой папке. Объемы файликов будут такие же, или чуть больше. Разве что последние фичи, описанные в этом посте, там еще не используются. Easybook должна дать полное понимание того, как делается сайт на Easyweb. В репе лежит easybook.sql — его надо импортнуть в базу, создать MySQL-юзера, прописать его логин-пароль в datasource.xml, и все — сайт заработает. Если же примера Easybook вам недостаточно, то напишите, что именно вы хотели бы увидеть, и я буду дорабатывать этот пример.

        P.S. Хабротипограф — фи. Тире нужно отбивать неразрывным пробелом, а не обычным.
        0
        делаю нечто похожее, так что небольшой обмен опытом)

        1. для парсера нужно уметь указывать baseURI относительно которого будут вычисляться relativeURI. использовать везде абсолютные ссылки — не предлагать)

        2. get — слишком абстрактный интерфейс, лучше его конкретизировать: getDOMNode, getSimpleXML и тп. у меня у каждого узла просто есть свойства DOMNode и DOMDocument.

        3. www:paginate — опять же префикс слишком абстрактен, лучше использовать конктретный типа easyweb:paginate. и использовать его не только в имени функции, но и в имени режимов (easyweb:previous,easyweb:next,...). основная цель пространств имён — не указать предназначение, а избежать конфликтов и недопониманий. впрочем, там действительно нужна эта функция? сдаётся мне всё это реализуемо не слишком сложными шаблонами. к тому же, почему нельзя указать откуда брать данные? или предполагается для каждого блока на странице вручную прогонять по xslt преобразованию? не слишком практичное решение.

        4. инвалидация по времени — зло. неужели так сложно сделать инвалидацию по событиям? да и вообще, что там кешировать? xslt преобразования и так реактивны. а если уж совсем жалко процессорного времени фронтенда, то можно вообще переложить его на плечи клиента. для примера сайт: hyoo.ru/?article=XStyle;author=Nin+Jin — статейку тоже почитайте, может заинтересует)

        5. «В репе лежит easybook.sql — его надо импортнуть в базу, создать MySQL-юзера, прописать его логин-пароль в datasource.xml, и все — сайт заработает.» — фу как сложно) вы же наверняка используете какой-нибудь pdo, так что какая проблема по умолчанию автоматом создавать sqlite базу с нужной структурой?

        6. почему нету checked_children, кидающего исключение в случае отсутствия детей, а также checked_alone_children проверяющего, что он такой один?) я это к чему — ошибка в данных не является настолько исключительной ситуацией, чтобы раскручивать стек. и все такие ошибки должны быть обработаны и зачастую могут быть заменены дефолтными значениями или другими источниками получения. а если нужна логика «либо всё верно и работаем, либо не верно и падаем», то лучше применить какую-нибудь схему валидации с более совершенными механизмами чем проверка «в элементе есть хотябы один узел»)
          0
          Ох, сколько всего. Отвечу на каждый пункт отдельно, чтобы было удобнее обсуждать.

          1. В PHP-конфиге сайта указывается константа website_root — все пути внутри сайта (за исключением пары особых ситуаций) считаются относительно этого пути: github.com/nyan-cat/easybook/blob/master/php/config.php
            0
            2. Вопрос именования сущностей — это тема отдельного курса лекций, обосновать свою позицию в несколько предложений не так просто. Но я все же попробую :) Имена сущностей должны нести минимум информации, которой, в свою очередь, должно быть достаточно для того, чтобы понять, как этой сущностью пользоваться, и не более. Имена не должны сообщать «как это устроено», только — «как этим пользоваться». Например, в Boost-е аналогичная функция назвается native_handle, и у сокетов, и у файлов, и у тредов. Я надеюсь, вы доверяете коммьюнити буста в вопросе имен? native_handle не говорит, что именно вы получите. Об этом можно догадаться по имени агрегирующего класса, и убедиться наверняка из документации. Именно поэтому в C++ шаблоны, конкретизированные параметрами, берут в typedef. Пользователю нужно знать, как этим пользоваться, а не как оно устроено. В 90% ситуаций пользователю достаточно знать про контейнер лишь то, что это контейнер, и его можно проитерировать через range-based for-loop. То есть, сосредоточиться стоит на том, «для чего мне это», а не «как это устроено». Я вижу всего один случай, когда действительно нужно называть функции getDOMNode / getSimpleXML — это когда фасилити умеют выдавать сразу несколько встроенных типов ресурсов.
              0
              3. Пространства имен XML — это не просто способ разрешения коллизии имен. Подразумевается, что за пространством имен стоит какая-то библиотека, или часть очень большой библиотеки (как в случае EXSLT). Функция введена просто для удобства, т.к. отрисовка пагинатора — задача каждого второго сайта. Все XPath-расширения, предоставляемые движком: github.com/nyan-cat/easyweb/wiki/XPath-extensions
              Что же касается самого имени пространства имен «www» — предполагается, что XSL- и XPath-расширения Easyweb реализуют всю необходимую сайтостроительную функциональность и должны покрыть 95% всех вебмастеропотребностей, поэтому мне кажется совершенно логичным выбрать имя, претендующее на дефолтность.
                0
                4. Это например по каким событиям? Можете привести пару примеров? Теперь что касается кеширования. Необходимость решения возникла как раз в реальном проекте. Суть такова: на каждой странице сайта есть ссылка «Выбрать город». По нажатию появляется окно со списком городов. Всего городов порядка ~1500, а их XSL-отверстывание выполняется с группировкой по первой букве названия. По ТЗ нужнто было чтобы этот блок был именно в HTML, а не подгружался аяксом, и был на каждой странице сайта. При этом Google Bot ходит на сайт ~50 раз в секунду. Речь именно про страницы сайта, а не про скрипты или там favicon. Такой интенсивности оказалось достаточно, чтобы достаточно сильно напрячь деда на предпоследней линейке Intel Xeon. Введение кеша снизило общую нагрузку в разы. Вот вам и пример из реальной жизни ;-)
                  0
                  5. Easyweb — это не CMS. В ней нет ничего по умолчанию: нет готовых баз, готовых шаблонов, или чего-то еще готового. Голый сайт на Easyweb — это голый сайт впринципе, в котором нет вообще ничего. Считайте так: Easyweb — это XSLT-верстка + специальная шина данных. То есть, вы устанавливаете сторонние хранилища данных (MySQL, PostgreSQL, Apache Solr, GeoIP, и т.д.), конфигурите их как хотите, дизайните их внутренние контейнеры данных (базы, таблицы, индексы, и т.д.) так, как хотите, без каких-либо ограничений, и уже после этого конфигурите Easyweb так, чтобы он умел брать нужные данные из нужных мест. Далее вам остается только верстать на XSL, а Easyweb уже сам разберется, куда пойти, и что откуда взять.
                    0
                    6. Рекомендую еще раз посмотреть на Boost. Функции по работе с некоторыми типами системных ресурсов (например — файлы или сокеты) представлены в двух экземплярах — одна в случае ошибки кидает исключение, другая — возвращает ошибку через параметр-ссылку. Поскольку в PHP нет перегрузки функций, я решил сделать префикс checked_. Смысл в том, что одной и той же функцией можно воспользоваться с разными ожиданиями. В одном случае логика программы может сразу же предполагать, что файла может и не быть. То есть — это не является ошибкой, а является одним из валидных кейсов. В таком случае пользовател пишет что-то вроде:
                    if($file = fs::read('...'))
                    {
                        # ...
                    }
                    

                    Но может быть ситуация, когда файл обязан быть, и его отсутствие означает невалидность каких-либо дальнейших действий. В таком случае пользователь пишет:
                    $file = fs::checked_read('...');
                    # ...
                    

                    и получает гарантию, что «либо все, либо ничего».
                    checked_children не нужен, т.к. в случая отсутствия детей функция вернет пустой range, из-за которого foreach не сломается, а просто будет выполнено ноль итераций. checked_ функций не так много: это fs::checked_read, nodeset::checked_first и nodeset::checked_last. Кажется — все. Это просто редкие ситуации, когда одна и та же функция в разных ситуациях может вызываться с разными ожиданиями.

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

                  Самое читаемое