Есть шутка о типичной карьере разработчика:

  1. Не использует фреймворки
  2. Обнаруживает фреймворки
  3. Пишет свои фреймворки
  4. Не использует фреймворки

Пункт 4, конечно же, ссылается на новоприобретенные способности использовать Composer для построения собственных фреймворков, создавая их из различных компонентов третьих сторон.

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

Webiny

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

Webiny StdLib


Нет, не то чтобы «std». StdLib лежит в основе всех остальных суб-компонентов фреймворка. Библиотека служит вспомогательным звеном, таким же, как и внедрение зависимостей или Symfony/Yaml в любом другом PHP проекте.

Среди прочего, StdLib, как и многие другие до нее, делает работу со скалярами значительно проще, добавляя свободный объектно-ориентированный интерфейс и некоторые вспомогательные методы. Например, есть облегченный URLObject, который содержит несколько методов для работы с редиректами, схемы, порты и т.д. Помимо помощи с ОО обертками, библиотека также предлагает базовую валидацию, методы, помогающие при сборке других Webiny-компонентов, простой трейт для реализации Singleton и многое другое.

Основное отличие StdLib заключается в том, что она реализована в виде набора трейтов — очень сильно недооцененной части современной разработки PHP. Например, вышеупомянутый URLObject создается вот так: $this->url('http://www.webiny.com/');, потому что трейт StdObject может быть добавлен к любому классу, которому необходима данная функциональность. Большинство других компонентов Webiny также реализованы на трейтах. Ввиду изолированного характера фреймворка, команда выбрала такой подход для упрощения иерархии классов и некоторого расширения возможностей.

StdLib можно использовать автономно, напрямую из Composer или же вытянуть ее по зависимостям в качестве составной части всего фреймворка. Давайте посмотрим, что же она предлагает нам?

Особенности


Внутри StdLib состоит из двух суб-библиотек. Одна из них — Exception, используется только если вы намерены создавать дополнительные Webiny-компоненты. Другая — StdObject, тут и содержится функционал, о котором мы говорили.

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

ComponentTrait

ComponentTrait полезен только, если вы создаете дополнительные Webiny-компоненты, чего мы пока делать не будем. Пропустим.

FactoryLoaderTrait

FactoryLoaderTrait полезен в вызове экземпляров класса, которые были динамически определены. Например:

$className = 'myNameSpace\myClass';
$argument1 = 'Hello';
$argument2 = 'World';
$mustBe = 'myNameSpace\mySubClass';

Чтобы инстанцировать класс myClass с аргументами «Hello» и «World», убедившись, что он является экземпляром mySubClass можно использовать следующие подходы:

// standard PHP
 
try {
    $instance = new $className($argument1, $argument2);
    if (!($instance instanceof $mustBe)) {
        throw new Exception("Instances don't match!");
    }
} catch (Exception $e) {
    // Handle exception
}

// FactoryLoaderTrait approach
 
try {
    $instance = $this->factory($className, $mustBe, [$argument1, $argument2]);
} catch (Exception $e) {
    // Handle exception
}

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

SingletonTrait

SingletonTrait мгновенно превращает ваш класс в Singleton. В интернете есть множество обширных дискуссий о «плохой природе Singleton» — не просто древние топики, но и несколько наших старых дискуссий об этом. На всякий случай, если вдруг не будет возможности реализовать нормальный DI-контейнер, то тут-то этот трейт нам и пригодится.

Имейте ввиду одну вещь — __construct кофликты. Трейт не реализует свою имплиментацию метода __construct, он может быть использован как в классах, которые имеют свою реализацию метода, так и в тех, которые используют другие реализации Singleton. Но не забывайте, если вы решили использовать этот трейт, вы должны адаптировать свой конструктор класса в соответствии с паттерном.

Если вы новичок в работе с синглтонами, этот пост может помочь.

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

Это полезно, так как вы можете использовать его в качестве механизма пост-инициализации для дальнейшего использования в качестве Singeton-объекта без необходимости полагаться на свой конструктор. Возможно, ваш класс все же имеет свой конструктор, и все что вам нужно — превратить его в Singleton, но такая магия нуждается в несколько более тонкой настройке. Как раз для подобных случаев идеально подходит этот трейт.

StdLibTrait

Трейт является сочетанием StdObjectTrait и ValidatorTrait. Сам по себе StdLibTrait содержит некоторые функции помощи кодирования/декодирования JSON, но они довольно скудны на данный момент, а как сериализовывать в JSON знают и другие PHP-аналоги. Возможно в этом месте лучше использовать Serializer из Symfony — компонент, хорошо опробованный сообществом и имеющий поддержку мультиформатов.

ValidatorTrait

Начнем с того, что ValidatorTrait — простая коллекция родных для PHP методов проверки типа, обернутая в трейт. Я не уверен в его полезности и в причинах, которыми руководствовались авторы, т.к. API остается практически идентичным (self::isArray() vs is_array()), но предположу, что это как-то связано с возможностью расширять компонент, перегружать методы, реализуя собственный функционал.

В одном месте валидатор использует StdObjectWrapper для обеспечения плавного интерфейса проверки этих типов, который в свою очередь является частью подбиблиотеки StdObject, использующейся в Webiny в качестве объектно-ориентированной обертки для скаляров и форматирования URL.

StdObjectTrait

Это ядро компонента StdLib, основная его часть. Этот трейт обеспечивает базовыми возможностями стандартного класса такие классы, как ArrayObject, UrlObject, StringObject и DateTimeObject.

StringObject

Пожалуй, самый простой из связки. Этот объект позволяет использовать строки в качестве объектов.

$string = $this->str("My string");

Экземпляр предоставляет возможность удобной смены кодировки (по умолчанию в кодировке UFT8) и некоторые вспомогательные методы, такие как length, wordCount и subStringCount, а также обертки некоторых встроенных PHP функций. Все это довольно полезно иметь под рукой, прямо внутри класса. Через ManipulatorTrait, общим для всех StdObjects, в StringObject также имеется доступ к методам, которые изменяют его: например, trim — самая частоиспользуемая и полезная фича при работе со строками.

Опять же, огромным преимуществом работы с трейтами является не только возможность иметь цепочку вызовов над string-объектом, но и автоподстановка в IDE:


$string = $this->str("This is a string");
echo $string->hash()->padLeft(45, "testing");

Вот так, в одну строчку мы взяли хэш и добавили слева testing до набора 45 символов. И вот результат — testif72017485fbf6423499baf9b240daa14f5f095a1. Врядли вы сможете сделать это еще проще и понятнее, чем в примере выше.

В качестве другого примера, посмотрите на этот код:

$string = new StringObject('Some test string.');
$string->caseUpper()->trimRight('.')->replace(' test'); // SOME STRING

Конечно, объект реализует метод __toString для непосредственной подготовки его к использованию в виде строки. Все другие объекты StdLib также имеют имплементацию этого метода для непосредственного вывода на печать. На выходе вы всегда сможете получить примитив для дальнейшей работы.

Эти методы слишком многочисленны, чтобы их перечислять; для получения более подробной информации просто посмотрите сами в исходниках.

ArrayObject

Аналогично StringObject, ArrayObject предлагает удобный интерфейс для работы с массивом. Естественно, он подходит для итераций через foreach и, вообще, ведет себя почти как родной массив.

$array = new ArrayObject(['one', 'two', 'three']);
$array->first(); // StringObject 'one'
$array->append('four')->prepend('zero'); // ['zero', 'one', 'two', 'three', 'four']

Обратите внимание, что при получении элементов из этого массива возвращаются экземпляры StdObject, а не фактические скаляры. Возвращаемое значение всегда будет иметь тип StdObjectAbstract. Строки производят StringObject, массивы производят ArrayObject. Цифры, что может быть несколько непоследовательным, производят экземпляры StdObjectWrapper со свойством _value, в котором и задано наше число. Если элемент — это класс, то этот класс также будет обернут. Например:

$array = $this->arr([1, "2", new myClass(1, 2)]);
var_dump($array->last());

Вот что мы получим:

image

Я не уверен в своих ощущениях по поводу этого. С одной стороны — это суперпоследовательно, мы всегда получаем StdObjectWrapper в той или иной форме. Но с другой стороны, если я имею дело с массивом классов, я не могу просто забыть об обертке и работать как раньше: каждый раз нужно уделять некоторое внимание этому аспекту. Конечно, есть способ получить реальные значения из любого StdLib-объекта путем вызова метода val, который вытаскивает базовое значение, содержащееся внутри.

Для разнообразных манипулятивных приемов с массивами, базирующимися на ArrayObject, смотрите на соответствующий трейт. Я нахожу синтаксис по принципу цепочки особенно полезным, ведь можно спуститься в многомерный массив указав key1.key2.key3 в качестве индекса, что, в свою очередь, является довольно впечатляющей экономией времени:

$array = [
            'k1' => 'test',
            'k2' => [
                'k3' => [
                    'k4' => 'deepest'
                ]
            ]
        ];
         
$a = new ArrayObject($array);
$a->key('k2.k3.k4', 'webiny');
$a->key('k2.k3.k5', 'anotherElement');
 
echo $a->key('k2.k3.k4'); // webiny
echo $a->key('k2.k3.k5'); // anotherElement

URLObject

Этот объект помогает сделать синтаксис вашего кода, работающего с URL, чище. Он поддерживает некоторые распространенные коды ответа, методы подписи запросов, работу с портами и другими переменными, а также может легко манипулировать — переключать схемы, хосты, домены и т.д. с помощью одной строчки надежного кода.

Странно, что URLObject не наследуется от StringObject. Для меня наличие наследования имело бы смысл, по сути, они оба бы выиграли от от манипулятивных приемов друг друга.

Существуют библиотеки для работы с URL-адресами и получше, так что я не вижу особых преимуществ именно у этой. Первый пример, который приходит на ум — великолепная PHP League URL library, которая делает все это и многое другое, имеет отличное качество кода, который полностью протестирован и активно поддерживается более чем десятком высококвалифицированных разработчиков.

DateTimeObject

Наконец, есть DateTimeObject. Это обертка над объектом datetime, обеспечивающая более свободный и естественный интерфейс работы, а также помогающая решить большую часть проблем, возникающих при работе со временем:

$dt = new DateTimeObject('3 months ago');
echo $dt; // 2013-02-12 17:00:36
$dt->add('10 days')->sub('5 hours'); // 2013-02-22 12:00:36

Само собой, DateTimeObject будет очень удобен, если вы пока еще не используете какую-либо библиотеку для работы со временем, но для более сложных манипуляций с datetime я бы порекомендовал Carbon, немного приправленный библиотекой Period.

Нельзя сказать, что DateTimeObject совсем уж бесполезен — он по умолчанию поддерживает работу с часовыми поясами, у него удобный формат вывода даты, простые и легкие diffs в качестве альтернанивы Period, удобочитаемое «x времени назад», зависимое от часовых поясов и многое другое.

Вывод


Имея все это, Webiny-фреймворк позволяет разработчикам входить в проект со своим нативным PHP кодом. Все обертки ведут себя именно так, как ожидает разработчик. Слишком амбициозно? Возможно. Но я рассматриваю это как интересный подход, определяющий стандарты кодирования перед началом работы над проектом — он позволяет обойтись без проблем, возникающих в ходе дальнейшего проектирования, экономя время в долгосрочной перспективе. Единственный недостаток, который я вижу — необходимость использовать use в каждом отдельном классе, в котором вы хотите иметь поддержку StdObject.

Проект Webiny масштабный и интересный. Фреймворк впечатляет, его компоненты охватывают обширные области. Вопрос о том, действительно ли это необходимо, я оставляю на вас. Если вам интересно, почему я так много внимания ему уделяю внимания, и зачем весь этот пост, то ответ будет простым — мне нравится играть с новыми вещами; мне немного любопытно как они работают изнутри.

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

Комментарии (8)


  1. Fesor
    22.04.2015 01:03
    +1

    Хм… мне одному кажется что зависимость от AWS SDK на уровне фреймворка это не хорошо? Ну или зависимость от апишки sendgrid… или imagine… да и в целом от ext-mongo…

    Короче какая-то явно для своих нужд собранная штука.


    1. iGusev Автор
      22.04.2015 01:13
      +1

      Основной репозиторий монолитен и аггрегирует в себе сразу все компоненты, в том числе и те, которые работают с AWS SDK, sendgrid, imagine и ext-mongo, отсюда и представленные зависимости.

      Ничего не мешает использовать только необходимые части, как в Symfony.


      1. Fesor
        22.04.2015 01:51
        +1

        Ну да, соглашусь… но

        Мне как-то жутко от того что вот это используется повсеместно во всех компонентах фреймворка… напоминает Yii1

        github.com/Webiny/StdLib/blob/master/ComponentTrait.php
        github.com/Webiny/StdLib/blob/master/FactoryLoaderTrait.php
        github.com/Webiny/StdLib/blob/master/StdLibTrait.php

        Вот это зачем нужно — вообще не понятно…
        github.com/Webiny/StdLib/blob/master/StdObject/DateTimeObject/DateTimeObject.php

        Ну да ладно, это все куча утилит… Но… Вместо того что бы использовать нормальный DiC или что-либо, что же мы видим:

        ServiceManager::getInstance()->registerService('MyService', new ConfigObject($config));
        


        А судя по примерам даже Symfony/DependencyInjection удобнее… не говоря уж про PHP-DI.

        Порадовал только UserInterface…

        А ну и да, увидев такое обилие логики в трейтах сразу стало любопытно как это все тестами покрыто… а тестов то и нет.


        1. iGusev Автор
          22.04.2015 02:42

          В других компонентах с тестами получше. Дайте им время, фреймворку всего полгода, пока еще не набрал достаточное количество активных сторонников


          1. Fesor
            22.04.2015 14:18
            +2

            И не наберет с таким подходом… выглядит просто как поделка и агитирует писать плохо.


  1. pepelsbey
    22.04.2015 02:56
    +2

    Ве?бини, Веби?ни, Веба?йни? Лучше сразу поясните, а то будет поздно — неочевидно читается.


    1. iGusev Автор
      22.04.2015 03:01

      Гугл предпочитает первый вариант (Ве?бини)


  1. kix
    22.04.2015 11:25
    -2

    Хмм, вообще, трейты — не лучшая идея, если честно. Это всё по большому счету статика, и с тестированием будут проблемы. А так, конечно, selling point у ребят мощный.