PHP
Website development
July 2015 27

Быстрый старт с PHPixie 3

Tutorial
image
После двух лет разработки закончена третья версия фреймворка PHPixie. Почему так долго? На самом деле за это время было написано не меньше трех ORM и шаблонизаторов, которые удалялись и переписывались опять, потому что «ааа, можно ведь сделать лучше». Особенно много времени ушло на тесты, без которых огромное количество улучшений просто не было бы замечено. Много раз хотелось просто оставить это дело, остановиться на второй версии и добавлять в нее модули. Но сейчас, когда все эти итерации были пройдены я могу уверенно сказать что это лучшая имплементация которую я знаю ( и на какую был способен ). Вот чем вас порадует PHPixie 3:

  • Следование стандартам PSR-2 и PSR-4
  • Поддержка PSR-7 запросов и библиотека для удобной работы с ними
  • Шаблонизатор использующий простой PHP, но с поддержкой наследования и блоков как у Twig. Позволяющий легко добавлять свои расширения и другие форматы, например HAML итд.
  • ORM который прост в использовании как ActiveRecord, но при этом разбивающий логику запросов, сущностей и репозиториев отдельно. Поддерживающий связи с коллекциями MongoDB и оптимизацию запросов над многими сущностями одновременно (например можно связать несколько статей с несколькими тэгами одним запросом)
  • Подход с процессорами вместо привычных контроллеров позволяет создать произвольную архитектуру.
  • Компонент конфигураций позволяющий разбивать настройки по в глубину по папкам (например ключ languages.en.plural.mouse может обратится к ключу plural.mouse в файле languages/en.php)
  • Система бандлов позволяющая легко использовать один код в нескольких проектах и делится ним с другими пользователями. Бандлы устанавливаются через композер как любая другая библиотека.


А сейчас короткий туториал, который покажет вам все что надо знать чтобы начать разработку с PHPixie 3:


Инсталляция

Сначала создаем скелет проекта используя Composer:

php composer.phar create-project phpixie/project your_project_folder 3.*-dev


Настройте ваш HTTP сервер на папку /web directory. В случае Nginx вам также понадобится добавить это в конфиг:

location / {
            try_files $uri $uri/ /index.php;
}


Если вы используете Windows вам надо будет создать ярлык /web/bundles/app указывающий на /bundles/app/web.
На Linux и MacOS ярлык будет работать из коробки. Он нужен для того чтобы открыть доступ к веб файлам дефолтного бандла (сейчас все объясню).

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

Бандлы

PHPixie 3 поддерживает систему бандлов, как например делает Symfony2. Если вы еще с ними не работали представьте себе что сайт можно теперь разбить на логические части, которые потом легко переносить в другие проекты и делится через Composer. Например аутентификация пользователей может быть создана как отдельный бандл со своими шаблонами, стилями и изображениями а затем использована на нескольких проектах. Бандлы «монтируются» в проект как например в линуксе диски монтируются в файловою систему.

По началу проект создается с единым бандлом 'app' в '/bundles/app'. И хоть структура проекта в третьей версии кажется сложнее чем во второй, но пока у вас только один бандл вы редко будете делать что-то вне этой директории.

Процессоры

Привычная концепция MVC Контроллеров сильно расширена в PHPixie, она теперь поддерживает бесконечную вложенность и произвольную архитектуру. Идея в том что «контроллер» с точки зрения фреймворка есть только один. Но он может например делегировать свои функции «субконтроллерам» и вообще делать что ему захочется. Интерфейс процессора ( как они теперь называются) состоит всего из одного метода process($request). Это позволило создать несколько базовых классов процессоров, которые по разному обрабатывают запросы. Конечно-же один из вариантов полностью похож на привычные контроллеры, так что можно все делать по-старинке, что и делает стандартный Greet процессор, который показал нам приветствие после установки.

Теперь создадим новый процессор Quickstart, на котором и будем все пробовать:

// bundles/app/src/Project/App/HTTPProcessors/Quickstart.php

namespace Project\App\HTTPProcessors;

use PHPixie\HTTP\Request;

//  Расширяем класс который действует как привычный контроллер
class Quickstart extends \PHPixie\DefaultBundle\Processor\HTTP\Actions
{
    /**
     * Builder будет использоваться повсюду
     * чтобы достукаться в разные части фреймворка
     * @var Project\App\Builder
     */
    protected $builder;
    
    public function __construct($builder)
    {
        $this->builder = $builder;
    }
    
    // Дефолтное действие
    public function defaultAction(Request $request)
    {
        return "Quickstart tutorial";
    }
    
    //Уже скоро мы добавим сюда новых методов
}


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


// bundles/app/src/Project/App/HTTPProcessors.php

//...
    protected function buildQuickstartProcessor()
    {
        return new HTTPProcessors\Quickstart(
            $this->builder
        );
    }
//...


Зайдя на localhost/quickstart теперь мы увидим сообщение «Quickstart tutorial».

Теперь можно попробовать другие части фреймворка.

Роутинг

Часто приходится создавать ссылки типа /quickstart/view/4 включающие id или имя страницы или товара. Для этого сначала создадим простенькое действие в нашем процессоре:

// bundles/app/src/Project/App/HTTPProcessors/Quickstart.php

//...
    public function viewAction($request)
    {
        //Выведем параметр 'id' 
        return $request->attributes()->get('id');
    }
    
//...
}


Теперь так же понадобится добавить правило с этим параметром к тем что уже прописаны в конфиге. Но сначала рассмотрим как он выглядит:

// bundles/app/assets/config/routeResolver

return array(
    // тут описана группа роутов
    // который будут пробоваться
    // пока какой-то не подойдет 
    'type'      => 'group',
    'resolvers' => array(
        
        //...вот сюда мы добавим наши модификации
        
        //Дефолтный роут
        'default' => array(
            'type'     => 'pattern',
            
            // скобки обозначают необязательную часть
            'path'     => '(<processor>(/<action>))',
            
            //Дефолтные параметры
            //Например если ссылка просто /greet
            //То параметр 'action' будет 'default'
            'defaults' => array(
                'processor' => 'greet',
                'action'       => 'default'
            )
        )
    )
);


Часть которую мы хотим добавить выглядит вот так:

'view' => array(
    'type'     => 'pattern',
    
    //Поскольку параметр id обязателен
    //то скобок не ставим
    'path'     => 'quickstart/view/<id>',
    'defaults' => array(
        'processor' => 'quickstart',
        'action'    => 'view'
    )
)


Поскольку роуты подбираются по порядку то важно чтобы более специфические правила были в конфиге выше чем общие.

Теперь зайдя на localhost/quickstart/view/5 вы увидите '5' в ответ.

Как быстрый пример чего можно добиться в настройках попробуем прописать общий префикс для нескольких роутов со своими параметрами. Ничего страшного если это покажется сложным, на данный момент нам оно не важно:

array(
    
    //Создаем префикс
    'type'      => 'prefix',
    
    // У префикса может быть свой паттерн
    // со своими параметрами
    'path'   => 'user/<userId>/',
    'resolver' => array(
        'type'      => 'group',
        'resolvers' => array(
        
            //направит /user/5/friends to Friends::userFriends()
            'friends' => array(
                'path'  => 'friends',
                'defaults' => array(
                    'processor' => 'friends',
                    'action'    => 'usersFriends'
                )
            ),
            
            //направит /user/5/profile to Profile::userProfile()
            'profile' => array(
                'path'  => 'profile',
                'defaults' => array(
                    'processor' => 'profile',
                    'action'    => 'userProfile'
                )
            )
        )
    )
);



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

Ввод и вывод

Как вы уже заметили, каждое действие получает запрос как параметр и возвращает какой-то ответ. Вот как мы можем получить разную информацию из запроса:

//$_GET['name'] 
$request->query()->get('name');

//$_POST['name'] 
$request->data()->get('name');

//Параметры роутинга
$request->attributes()->get('name');


А теперь немного интереснее:

$data = $request->data();

// С заданием дефолтного значения
$data->get('name', 'Trixie');

// Выбросит исключение
// если параметр 'name' не задан
$data->getRequired('name');

// Получение вложенного поля
// $_POST['users']['pixie']['name']
$data->get('users.pixie.name');

// Можно также 'разрезать' параметры 
// чтобы не писать долгие ключи много раз
$pixie = $data->slice('users.pixie');
$pixie->get('name');

// Получить данные простым массивом
$data->get();

// Получить массив ключей
$data->keys();

// А можно сразу итерировать
// как по массиву
foreach($data as $key => $value) {

}

// Кстати если вам нравится такой синтаксис
// посмотрите на библиотеку phpixie/slice
// которую можно использовать с любыми массивами


JSON запросы тоже автоматически парсяться в $request->data(), что уже облегчает работу с AJAX запросами


Вывод еще проще:

// Просто текст
return 'hello';

// Чтобы вывести JSON
// просто возвращаем массив или объект
return array('success' => true);

// Или строим произвольные ответы
// используя библиотеку HTTP
$http = $this->builder->components()->http();
$httpResponses = $http->responses();

// Например редирект
return $httpResponses->redirect('http://phpixie.com/');

// Задаем хедеры и код ответа
return $httpResponses->stringResponse('Not found', $headers = array(), 404);

// Создаем загрузку файла
return $httpResponses->downloadFile('pixie.jpg', 'image/png', $filePath);

// Загрузка файла из строки
// Например для CSV
return $httpResponses->download('report.csv', 'text/csv', $contents);


Шаблоны


Шаблонизатор PHPixie поддерживает наследование шаблонов, блоки и возможность легко добавлять свои расширения и даже форматы.
По умолчанию стандартный бандл подгружает шаблоны из папки bundles/app/assets/templates.
В ней уже лежат два файла которые использовались стандартным процессором Greet.

Начнем с того что создадим простой шаблон:

<!-- bundles/app/assets/templates/quickstart/message.php -->

<!--
Функция $_() кодирует текст как HTML, чтобы символы
'<', '>' и т.д. не ломали верстку, а так же защищает от
большинства XSS атак.
-->
<p><?=$_($message)?></p>


Теперь добавим в процессор еще одно действие:

// bundles/app/src/Project/App/HTTPProcessors/Quickstart.php

//...
    public function renderAction($request)
    {
        $template = $this->builder->components()->template();
        
        return $template->render(
            'app:quickstart/message',
            array(
                'message' => 'hello'
            )
        );
    }
//...
}


И видим результат по ссылке localhost/quickstart/render

Если вам нравится передавать параметры в шаблон динамически, а не массивом, то такой подход так же доступен:

$template = $this->components()->template();

$container = $template->get('app:quickstart/message');
$container->message = 'hello';
return $container->render();

// Или можно возвратить сам контейнер
// и он автоматически отрендерится
return $container;


Наследование шаблонов


Добавим базовый родительский шаблон

<!-- bundles/app/assets/templates/quickstart/layout.php -->

<h1>Quickstart</h1>

<div>
    <!-- Тут вставится контент дочернего шаблона -->
    <?=$this->childContent();?>
</div>


и используем его в нашем message.php

<!-- bundles/app/assets/templates/quickstart/message.php -->

<?php $this->layout('app:quickstart/layout');?>

<p><?=$_($message)?></p>


Теперь по адресу localhost/quickstart/render наш шаблон будет уже обернут в родительский. Кстати родительский шаблон в свою очередь тоже может иметь своих родителей.

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

<!-- bundles/app/assets/templates/quickstart/layout.php -->

<!-- Опишем блок 'header' -->
<?php $this->startBlock('header'); ?>
    <h1>Quickstart</h1>
<?php $this->endBlock(); ?>

<!-- И выведем его -->
<?=$this->block('header') ?>

<div>
    <!-- Тут будет контент дочернего шаблона -->
    <?=$this->childContent();?>
</div>


Теперь добавим контент в этот блок с нашего message.php:
<!-- bundles/app/assets/templates/quickstart/message.php -->
<?php $this->layout('app:quickstart/layout');?>

<?php $this->startBlock('header'); ?>
    <h2>Message</h2>
<?php $this->endBlock(); ?>

<p><?=$_($message)?></p>


По умолчанию контент в блоке идет в порядке добавления. Это позволяет удобно организовать например подключение стилей и скриптов в зависимости от страницы. Но мы также можем сказать родительскому шаблону добавлять свой контент в блок только если дочерний не добавил его сам.

<!-- bundles/app/assets/templates/quickstart/layout.php -->

<?php if($this->startBlock('header', true)): ?>
    <h1>Quickstart</h1>
<?php $this->endBlock(); endif;?>

<!-- ... -->


Или например добавить контент в начало блока а не в конец:
$this->startBlock('header', false, true);


Подшаблоны


Включение подшаблона тоже возможно:

<?php include $this->resolve('some:template');?>


Генерация ссылок


Для генерации ссылок в шаблоне существуют два метода httpPath и httpUri:

<?php $url=$this->httpPath(
        'app.default',
        array(
            'processor' => 'hello',
            'action'    => 'greet'
        )
    );
    ?>
<a href="<?=$_($url)?>">Hello</a>


Базы данных и ORM



Соединения к базам данных описываются в глобальной конфигурации, вне бандлов. Например соединение к MySQL могло бы выглядеть вот так:

// assets/config/database.php

return array(
    'default' => array(
        'driver' => 'pdo',
        'connection' => 'mysql:host=localhost;dbname=quickstart',
        'user'     => 'pixie',
        'password' => 'pixie'
    )
);


PHPixie поддерживает не только реляционные базы данных, но и **MongoDB**.
Вы можете даже описать связи (one-to-one, one-to-many, many-to-many) между реляционными таблицами и коллекциями MongoDB и работать с ними используя тот же синтаксис. На данный момент никакой другой PHP ORM не поддерживает такого уровня интеграции.

Наполним базу данными. Например допустим мы хотим создать трекер для проектов, какие состоят из заданий. Схема могла бы выглядеть вот так:

CREATE TABLE `projects`(
    `id`         INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `name`       VARCHAR(255),
    `tasksTotal` INT DEFAULT 0,
    `tasksDone`  INT DEFAULT 0
);

CREATE TABLE `tasks`(
    `id`        INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `projectId` INT NOT NULL,
    `name`      VARCHAR(255),
    `isDone`    BOOLEAN DEFAULT 0
);

INSERT INTO `projects` VALUES
(1, 'Quickstart', 4, 3),
(2, 'Build a website', 3, 0);

INSERT INTO `tasks` VALUES
(1, 1, 'Installing', 1),
(2, 1, 'Routing', 1),
(3, 1, 'Templating', 1),
(4, 1, 'Database', 0),

(5, 2, 'Design', 0),
(6, 2, 'Develop', 0),
(7, 2, 'Deploy', 0);


И мы уже можем использовать ORM для доступа к этим данным. Добавим действие orm в процессор:

// bundles/app/src/Project/App/HTTPProcessors/Quickstart.php

//...
    public function ormAction(Request $request)
    {
        $orm = $this->builder->components->orm();
        
        $projects = $orm->query('project')->find();
        
        // Этот метод превратит сущности
        // в простые объекты.
        // Как уже говорилось, если
        // возвратить объекты, то они
        // перекодируются в JSON
        return $projects->asArray(true);
    }
//...


Теперь зайдя на localhost/quickstart/orm вы увидите JSON ответ с данными проектов.
Перед тем как глубже заглянуть в возможности новой ORM настроим связь один-ко-многим
между проектами и их заданиями.

// bundles/app/assets/config/orm.php

<?php

return array(
    'relationships' => array(
        array(
            'type'  => 'oneToMany',
            'owner' => 'project',
            'items' => 'task',
            
            // При удалении проектов
            // сразу удалять их задания
            'itemsOptions' => array(
                'onOwnerDelete' => 'delete'
            )
        )
    )
);


Сущности


Создание, изменение и удаление сущностей вполне интуитивно:

$orm = $this->builder->components->orm();

// Создание сущности через ее репозиторий
$projectRepository = $orm->repository('project');
$project = $projectRepository->create();

// или быстрый подход 
$project = $orm->createEntity('project');

// Изменение и сохранение
$project->name = 'Buy Groceries';
$project->save();

$task = $orm->createEntity('task');
$task->name = 'Milk';
$task->save();

// Добавление задания в проект
$project->tasks->add($task);

// Удаление проекта
$project->delete();

// Итерация по загрузчиках
$projects = $orm->query('project')->find();
foreach($projects as $project) {
    foreach($project->tasks() as $task) {
        //...
    }
}


Запросы


Вот только асть того что сейчас возможно с ORM запросами:

$orm = $this->builder->components->orm();

// Найти проект по имени
$orm->query('project')->where('name', 'Quickstart')->findOne();

// Найти по id
$orm->query('project')->in($id)->findOne();

// Найти по массиву id
$orm->query('project')->in($ids)->findOne();

// Добавление нескольких условий
$orm->query('project')
    ->where('tasksTotal', '>', 2)
    ->or('tasksDone', '<', 5)
    ->find();
    
// Группировка условий
//WHERE name = 'Quickstart' OR ( ... )
$orm->query('project')
    ->where('name', 'Quickstart')
    ->or(function($query) {
        $querty
            ->where('tasksTotal', '>', 2)
            ->or('tasksDone', '<', 5);
    })
    ->find();

// Альтернативный синтаксис
$orm->query('project')
    ->where('name', 'Quickstart')
    ->startWhereConditionGroup('or')
        ->where('tasksTotal', '>', 2)
        ->or('tasksDone', '<', 5)
    ->endGroup()
    ->find();
    
// Для сравнение полей в базе, добавьте '*' к оператору
// Найти проекты у которых tasksTotal = tasksDone
$orm->query('project')
    ->where('tasksTotal', '=*', 'tasksDone')
    ->find();

// Найти проекты с хотя бы одним заданием
$orm->query('project')
    ->relatedTo('task')
    ->find();
    
// Найти проект с конкретным заданием
$orm->query('project')
    ->where('tasks.name', 'Routing')
    ->findOne();

// Или так
$orm->query('project')
    ->orRelatedTo('task', function($query) {
        $query->where('name', 'Routing');
    })
    ->findOne();

// Подгрузить задания для проектов
// во время выборки (eager loading) 
$orm->query('project')->find(array('task'));

// Изменить данные во всех проектах
$orm->query('project')->update(array(
    'tasksDone' => 0
));

// Подсчитать законченные проекты
// и потом сделать выборку тем же запросом.
// Например для разбиения на страницы
$query = $orm->query('project')
    ->where('tasksTotal', '=*', 'tasksDone');
    
$count = $query->count();

$query
    ->limit(5)
    ->offset(0)
    ->find();


PHPixie ORM включает множество оптимизация для уменьшения количества запросов к базе.
Например можно связать несколько заданий к проекту одним запросом:

$orm = $this->builder->components->orm();

// Запрос проекта
$projectQuery = $orm->query('project')
    ->where('name', 'Quickstart');

// Запрос обозначающий первые 5 заданий
$tasksQuery = $orm->query('task')->limit(5);

// К этому времени к базе вызовов еще не было

// Теперь свяжем из вместе одним запросом
$projectQuery->tasks->add($tasksQuery);


Использование запросов вместо работы с сущностями резко уменьшает количество запросов. Это особенно заметно в ситуации со связями многие-ко-многим. Как и с поддержкой MongoDB, никакая другая ORM этого не позволяет.

Расширение сущностей


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

Вот простенький враппер:

// bundles/app/src/Project/App/ORMWrappers/Project.php;

namespace Project\App\ORMWrappers;

class Project extends \PHPixie\ORM\Wrappers\Type\Database\Entity
{
    // Добавим метод который скажет нам
    // закончен проект или нет
    public function isDone()
    {
        return $this->tasksDone === $this->tasksTotal;
    }
}


Теперь зарегистрируем его в бандле:

// bundles/app/src/Project/App/ORMWrappers.php;
namespace Project\App;

class ORMWrappers extends \PHPixie\ORM\Wrappers\Implementation
{
    // Прописываем имена моделей для которых есть врапперы
    protected $databaseEntities = array(
        'project'
    );
    
    // И метод который его строит
    public function projectEntity($entity)
    {
        return new ORMWrappers\Project($entity);
    }
}


Все, теперь можем пробовать:

//Find the first project
$project = $orm->query('project')->findOne();

//Check if it is done
$project->isDone();

Также можно объявить врапперы для Запросов и Репозиториев чтобы добавить или изменить из функционал. Например можно добавить к запросу метод добавляющий сразу несколько условий.

Есть больше!



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

Меня и так все устраивало !

Если система бандлов, подход к процессорам и другие изменения кажутся вам лишними, и хотелось бы работать как во второй версии, то не пугайтесь, вы дальше можете использовать тот же подход:

  • Забудьте обо всем вне папки /bundles/app/
  • Поскольку бандлы вы использовать не будете, не создавайте ярлык в папке /web, а просто смело кидайте в нее ваши веб-файлы как делали во второй версии. Если вы потом передумаете и захотите работать с бандлами, просто перенесете файлы с /web в /bundles/app/web. А до этого времени не заморачивайтесь.
  • Чуть чуть расширив стандартные классы легко можете добиться поведения второй версии:


Делаем так чтобы процессоры строились автоматически, по неймспейсу, без надобности прописывать каждый в HTTPProcessor:
// /bundles/app/src/Project/App/HTTPProcessor.php

namespace Project\App;
class HTTPProcessor extends \PHPixie\DefaultBundle\Processor\HTTP\Builder
{
    protected $builder;
    protected $attribute = 'processor';
    
    public function __construct($builder)
    {
        $this->builder = $builder;
    }
    
    public function processor($name)
    {
        if(!array_key_exists($name, $this->processors)) {
            $class = '\Project\App\HTTPProcessors\\'.ucfirst($name);
            if(!class_exists($class)) {
                return null;
            }
            $this->processors[$name] = new $class($this->builder);
        }
        
        return $this->processors[$name];
    }
}


Добавляем методы before() и after(), как было в контроллерах. Как дополнительный бонус, если before() возвратит какой-то результат то уже не вызывать само действие:

// /bundles/app/src/Project/App/HTTPProcessors\Controller.php

namespace Project\App\HTTPProcessors;

asbtarct class Controller extends \PHPixie\DefaultBundle\Processor\HTTP\Actions
{
    protected $builder;
    protected $attribute = 'processor';
    
    public function __construct($builder)
    {
        $this->builder = $builder;
    }
    
   public function process($request)
   {
      $result = $this->before($request);
      if($result !== null) {
         return $result;
      }
      
      $result = parent::process($request);
      
      return $this->after($request, $result);
   }
   
   public function before($request) {
   
   }
   
   public function after($request, $result) {
      return $result;
   }

}


Конец



Теперь у вас должно быть все чтобы начать работать с PHPixie 3, но есть еще много не прочитанной документации, как на сайте, так и тут на хабре. У большинства компонентов уже есть своя документация на обновленном сайте или даже русская версия здесь. Все компоненты могут использоваться без фреймворка, покрыты тестами на 100%, и почти все имеют максимальную метрику качества на CodeClimate.

Кстати сайт фреймворка тоже обновился, с более серьезным дизайном, так что теперь будет легче убедить тим лида писать как раз на нем. А если у вас будут еще вопросы то меня можно найти фактически постоянно в нашем чате.

Бенчмарки



image
+20
24.1k 120
Comments 37