На прошлом занятии мы освоили Composer, переместили Fenom в зависимости, и переписали наш проект для поддержки автозагрузки стандарта PSR-4.

А сегодня мы добавим в проект xPDO, напишем схему, сгенерируем модель, создадим таблицу и выведем новости на сайте.Для этого нам придётся внести много изменений в код проекта.

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

Но, сначала мы меняем наш composer.json:


{
  "name": "Brevis",
  "autoload": {
    "psr-4": {
      "Brevis\\": "core/"
    }
  },
  "require": {
    "php": ">=5.3.0",
    "fenom/fenom": "2.*",
    "xpdo/xpdo": "3.0.*@dev"
  },
  "scripts": {
    "post-install-cmd": "\\Brevis\\Core::cleanPackages",
    "post-update-cmd": "\\Brevis\\Core::cleanPackages"
  }
}

В блоке scripts я добавил вызов нового метода для очистки ненужные директорий устанавливаемых пакетов (тесты, примеры, документация и т.п.). Они только замедляют синхронизацию проекта с удалённым сервером.

Очистка пакетов

Добавляем и новый статичный метод cleanPackages:


    /**
     * Удаление ненужных файлов в пакетах, установленных через Composer
     *
     * @param mixed $base
     */
    public static function cleanPackages($base = '') {
        // Composer при вызове метода передаёт внутрь свой объект, но нам это не нужно
        // Значит, если передана не строка, то это первый запуск и мы стартуем от директории вендоров
        if (!is_string($base)) {
            $base = dirname(dirname(__FILE__)) . '/vendor/';
        }
        // Получаем все директории и
        if ($dirs = @scandir($base)) {
            // Проходим по ним в цикле
            foreach ($dirs as $dir) {
                // Символы выхода из директории нас не интересуют
                if (in_array($dir, array('.', '..'))) {
                    continue;
                }
                $path = $base . $dir;
                // Если это директория, а не файл
                if (is_dir($path)) {
                    // И она в следующем списке
                    if (in_array($dir, array('tests', 'test', 'docs', 'gui', 'sandbox', 'examples', '.git'))) {
                        // Удаляем её, вместе с поддиректориями
                        Core::rmDir($path);
                    }
                    // А если не в списке - рекурсивно проверяем её дальше, этим же методом
                    else {
                        // Просто передавая в него нужный путь
                        Core::cleanPackages($path . '/');
                    }
                }
                // А если это файл, то удаляем все, кроме php
                elseif (pathinfo($path, PATHINFO_EXTENSION) != 'php') {
                    unlink($path);
                }
            }
        }
    }

Данный метод - отличный пример рекурсивной работы с директориями и файлами. Мы проходим по всем установленным пакетам и удаляем нам ненужное.

Обратите внимание, что метод объявлен как public static. Статичный означает, что он может быть вызван без инициализации класса. То есть, он является собственностью самого класса, а не его экземпляра. Например в языке Swift подобные методы так и называются - class func.

Метод обязан быть статичным, чтобы Composer мог вызывать его после установки или обновления пакетов. Ведь он же не будет инициализировать наш класс - просто не знает, как это делать.

Так как статичный метод работает не в экземпляре класса, он не может вызывать другие не статичные методы. То есть, никакого $this->method() нет и быть не может! Поэтому используемый им rmDir мы тоже делаем public static и меняем его вызов в методе clearCache:


    public function clearCache() {
        Core::rmDir($this->config['cachePath']);
        mkdir($this->config['cachePath']);
    }

Теперь можно вызвать composer update и у нас установится xPDO, после чего ненужные файлы и у него, и у Fenom будут удалены.

Если вам не хочется заморачиваться с очисткой пакетов, то просто уберите блок scripts из composer.json:


  "scripts": {
    "post-install-cmd": "\\Brevis\\Core::cleanPackages",
    "post-update-cmd": "\\Brevis\\Core::cleanPackages"
  }

Но лично меня мегабайты ненужных данных в проекте очень напрягают.

Вот коммит с нашими изменениями.

Выносим настройки в файл

Сам xPDO уже установлен, теперь нужно подключить его в проект. Для его инициализации нужно указать настройки соединения с БД, и забивать из прямо в PHP коде неправильно сразу нескольким причинам: - Код нашего проекта выгружается на GitHub, зачем кому-то видеть настройки соединения с БД?

  • Наш проект может работать на разных серверах - нужны разные настройки с минимальным изменением кода
  • Незнакомый с проектом человек может и вовсе не найти эти настройки, зашитые в коде класса

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

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

Итак, суммируем все требуемые изменения: 1. Все настройки из переменной Core::config переезжают в файл core/Config/config.inc.php 2. Сам файл config.inc.php мы указываем в .gitignore, чтобы его никто кроме нас не видел. Но рядом кладём config_sample.inc.php - для примера. 3. Переписываем конструктор класса Core так, чтобы он загружал указанный файл с конфигом

Еще я считаю, что в отличии от Fenom, xPDO нужно загружать сразу же, при инициализации класса Core, потому что работа с БД - важнейшая часть ядра. К тому же, мы будем использовать логирование и кэширование xPDO в своём проекте. То есть, без него ядро запускать никак нельзя.

xPDO становится очень важной частью системы, поэтому конструктор класса Core переписываем так:


    /**
     * Конструктор класса
     *
     * @param string $config Имя файла с конфигом
     */
    function __construct($config = 'config') {
        if (is_string($config)) {
            $config = dirname(__FILE__) . "/Config/{$config}.inc.php";
            if (file_exists($config)) {
                require_once $config;
                /** @var string $database_dsn */
                /** @var string $database_user */
                /** @var string $database_password */
                /** @var array $database_options */
                try {
                    $this->xpdo = new xPDO($database_dsn, $database_user, $database_password, $database_options);
                    $this->xpdo->setPackage(PROJECT_NAME, PROJECT_MODEL_PATH);
                    $this->xpdo->startTime = microtime(true);
                }
                catch (Exception $e) {
                    exit($e->getMessage());
                }
            }
            else {
                exit('Не могу загрузить файл конфигурации');
            }
        }
        else {
            exit('Неправильное имя файла конфигурации');
        }
    }

Или мы загружаем xPDO, или отказываемся работать вовсе.

Понятное дело, что в конфиге у нас просто обязаны быть объявлены требуемые переменные:


<?php
// Строка соединения с БД. Тип, хост, имя БД и кодировка
$database_dsn = 'mysql:host=127.0.0.1;dbname=s1889;charset=utf8';
// БД юзер
$database_user = 's1889';
// Пароль юзера для БД
$database_password = 'VqSBKfZRf19m';
// Настройки xPDO: кэширование и загрузка свойств и связанных объектов
// Тут лучше ничего не трогать, оставить по умолчанию
$database_options = array(
    \xPDO\xPDO::OPT_CACHE_PATH => PROJECT_CACHE_PATH,
    \xPDO\xPDO::OPT_HYDRATE_FIELDS => true,
    \xPDO\xPDO::OPT_HYDRATE_RELATED_OBJECTS => true,
    \xPDO\xPDO::OPT_HYDRATE_ADHOC_FIELDS => true,
);

Между настройками соединения и xPDO мы должны определить еще пути к директориям, потому что в конфиге xPDO уже используется PROJECT_CACHE_PATH:


if (!defined('PROJECT_BASE_PATH')) {
    define('PROJECT_BASE_PATH', strtr(realpath(dirname(dirname(dirname(__FILE__)))), '\\', '/') . '/');
}

if (!defined('PROJECT_CORE_PATH')) {
    define('PROJECT_CORE_PATH', PROJECT_BASE_PATH . 'core/');
}

if (!defined('PROJECT_TEMPLATES_PATH')) {
    define('PROJECT_TEMPLATES_PATH', PROJECT_CORE_PATH . 'Templates/');
}

if (!defined('PROJECT_CACHE_PATH')) {
    define('PROJECT_CACHE_PATH', PROJECT_CORE_PATH . 'Cache/');
}

Заодно мы вынесли в настройки и параметры Fenom:


if (!defined('PROJECT_FENOM_OPTIONS')) {
    define('PROJECT_FENOM_OPTIONS', Fenom::AUTO_RELOAD | Fenom::FORCE_VERIFY);
}

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

Остаётся только переписать наш Core для использования новых настроек. Смотрим итоговый коммит с этими изменениями. С константами код даже немного компактнее.

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

  • Мы не сможем перенести в этот файл настройки вывода ошибок PHP
  • Тот же XML не очень удобно редактировать (да и JSON тоже, хоть уже и привыкли)
  • Такая структура конфига очень похожа на MODX, что просто приятно.

Еще должен возникнуть вопрос, а почему настройки подключения к БД указаны переменными, а пути к директориям константами? Очень просто - переменные будут видны только внутри метода __construct() и после создания экземпляра класса его методы уже не получат к ним доступа, а вот константы видны отовсюду.

Следовательно, мы прячем чувствительные данные (логин и пароль к БД), не объявляя их глобально, в отличии от констант с настройками.

Наш index.php теперь выглядит так:

<?php

require_once dirname(__FILE__) . '/vendor/autoload.php';
$Core = new \Brevis\Core();

$req = !empty($_REQUEST['q'])
    ? trim($_REQUEST['q'])
    : '';

if (!defined('PROJECT_API_MODE') || !PROJECT_API_MODE) {
    $Core->handleRequest($req);
}

Настройка вывода ошибок переехала в файл конфигурации, а метод handleRequest() теперь вызывается только если не был объявлен режим работы через API. Он пригодится нам в следущей главе.

Подключать другой файл с настройками мы можем, просто указывая его при инициализации ядра:

$Core = new \Brevis\Core('new_config');

По умолчанию там просто config.

Пишем схему и генерируем модель xPDO

Я рассчитываю на то, что вы знаете, что такое xPDO и зачем оно вообще нужно. Если же нет, то прочитайте, пожалуйста, в этом уроке с первого курса.

Создаём 2 директории: /core/Model и /core/Schema. В первой у нас будут готовые файлы модели, а вторая нужна только для их генерации. xPDO сам не использует схему. Он работает только с моделью. Схема нужна нам, чтобы её сгенерировать.

Создаём файл /core/Schema/brevis.schema.xml:


<?xml version="1.0" encoding="UTF-8"?>
<model package="Brevis\Model" platform="mysql" defaultEngine="MyISAM" version="3.0">

    <object class="News" table="news" extends="xPDO\Om\xPDOSimpleObject">
        <field key="pagetitle" dbtype="varchar" phptype="string" precision="100" null="true" default="" />
        <field key="longtitle" dbtype="varchar" phptype="string" precision="255" null="true" default="" />
        <field key="text" dbtype="longtext" phptype="string" null="true" default="" />
        <field key="alias" dbtype="varchar" precision="100" phptype="string" null="true" default="" />

        <index alias="alias" name="alias" primary="false" unique="false" type="BTREE">
            <column key="alias" length="" collation="A" null="false" />
        </index>
    </object>

</model>

Документация по схемам xPDO находится вот здесь, но она уже устарела. С версией 3.0 мы учимся работать по примеру из репозитория.

Основные отличия, как я вижу, это: 1. Указание version="3.0" в объявлении модели 2. Так как мы придерживаемся PSR-4, у нас package="Brevis\Model" 3. Указание всех классов с пространством имён, типа extends="xPDO\Om\xPDOSimpleObject"

Всё остальное как раньше: указываем имя класса, таблицу, колонки, индексы и связи (их у нас пока нет).

Теперь нам нужен скрипт для генерации модели. Обзываем его build.model.php и кладём рядом со схемой:


<?php

define('PROJECT_API_MODE', true);
use \xPDO\xPDO as xPDO;

$base = dirname(dirname(dirname(__FILE__))) . '/';
require_once $base . 'index.php';
$xPDO = $Core->xpdo;

$xml = dirname(__FILE__) . '/' . PROJECT_NAME_LOWER . '.schema.xml';

/** @var \xPDO\Om\xPDOManager $manager */
$manager = $xPDO->getManager();
/** @var \xPDO\Om\xPDOGenerator $generator */
$generator = $manager->getGenerator();

$generator->parseSchema($xml, PROJECT_MODEL_PATH);
$xPDO->log(xPDO::LOG_LEVEL_INFO, 'Модель сгенерирована');

$files = scandir(PROJECT_MODEL_PATH . PROJECT_NAME . '/Model/');
foreach ($files as $file) {
    $src = PROJECT_MODEL_PATH . PROJECT_NAME . '/Model/' . $file;
    $dst = PROJECT_MODEL_PATH . $file;
    if ($file == 'metadata.mysql.php') {
        @unlink($dst);
        rename($src, $dst);
    }
    elseif (!file_exists($dst)) {
        rename($src, $dst);
    }
}
$Core::rmDir(PROJECT_MODEL_PATH . PROJECT_NAME);
$xPDO->log(xPDO::LOG_LEVEL_INFO, 'Файлы перенесены');

На https://modhost.pro по умолчанию нельзя запускать файлы из директории core через браузер - в целях безопасности. Так что вам нужно зайти в консоль сервера и там выполнить

php ~/www/core/Schema/build.model.php

По нашей схеме xPDO cгенерирует файлы по стандарту PSR-0. То есть, вот так:


/core
    /Model
        /Brevis
            /Model
                /mysql
                    News.php
                News.php

А у нас в проекте принято PSR-4. То есть, нам нужно вот так:


/core
    /Model
        /mysql
            News.php
        News.php

Вложенный Brevis\Model нам ни к чему. Ведь все новые классы у нас в пространстве имён Brevis\Model и мы хотим их указывать как Brevis\Model\News - так ведь?


Brevis\Core - ядро
Brevis\Controllers\Home - контроллер страницы Home
Brevis\Model\News - объект новости в БД.

Если вы уже забыли прошлый урок и чем отличаются PSR-0 от PSR-4, напоминаю.

Поэтому в скрипте генерации модели дописан перенос свежесгенерированных файлов на 2 директории выше.

Если что-то здесь непонятно - задавайте вопросы в комментариях. Но вообще, этот скрипт лучше принять как данность. Вот так - работает хорошо, лучше не трогать =)

Итак, простенькая схема написана, модель сгенерирована, можно проверять. Вот коммит со всеми изменениями.

Пробуем работать с xPDO

Для тестирования работы xPDO я предлагаю использовать наш контроллер Test. Чтобы мы видели все сообщения xPDO нужно их включить. Я предлагаю вынести настройки логирования в наш файл конфигурации:


if (!defined('PROJECT_LOG_LEVEL')) {
    define('PROJECT_LOG_LEVEL', \xPDO\xPDO::LOG_LEVEL_INFO);
}

if (!defined('PROJECT_LOG_TARGET')) {
    define('PROJECT_LOG_TARGET', 'HTML');
}

Дописываем в конец Core::__constructor():


    $this->xpdo->setLogLevel(defined('PROJECT_LOG_LEVEL') ? PROJECT_LOG_LEVEL : xPDO::LOG_LEVEL_ERROR);
    $this->xpdo->setLogTarget(defined('PROJECT_LOG_TARGET') ? PROJECT_LOG_TARGET : 'FILE');

а сразу после запуска xPDO добавляем нашу модель (xPDO загружает данные из /core/Model/):

$this->xpdo->setPackage('Model', PROJECT_CORE_PATH);

И вот теперь с ней можно работать.

Первым делом, нам создаём нашу таблицу новостей в контроллере Test. Временно дописываем в метод run():


    $manager = $this->core->xpdo->getManager();
    $manager->createObjectContainer('Brevis\Model\News');

И проверяем

Если запустить скрипт еще раз - будет ошибка, что такая таблица уже есть. Удалить её можно так:

$manager = $this->core->xpdo->getManager();
$manager->removeObjectContainer('Brevis\Model\News');

В будущем, думаю, стоит выделить в Core отдельный метод для создания всех нужных таблиц при установке, а пока сойдёт и вручную.

Давайте создадим запись в БД, потом выберем её, покажем, изменим и удалим:


    public function run() {
        $content = '';
        $news = $this->core->xpdo->newObject('Brevis\Model\News');
        $news->fromArray(array(
            'pagetitle' => 'Новость 1',
            'alias' => 'news1'
        ));
        $news->save();
        $content .= '<pre>' . print_r($news->toArray(), true) . '</pre>';

        $news = $this->core->xpdo->getObject('Brevis\Model\News', array('alias' => 'news1'));
        $news->set('longtitle', rand());
        $news->save();
        $content .= '<pre>' . print_r($news->toArray(), true) . '</pre>';

        $news->remove();

        return $this->template('test', array(
            'title' => 'Тестовая страница',
            'pagetitle' => 'Тестовая страница',
            'content' => $content,
        ), $this);
    }

Результат: -

Вы видите, что записи сохраняются, потому что их id увеличивается. Коммит с изменённым контроллером Test. Проверить можно тут - http://s1889.bez.modhost.pro/test/.

Заключение

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

Скрипты эти писать повторно на других проектах не придётся, их можно свободно копировать туда-сюда. Что скрипт очистки пакетов от ненужных файлов, что скрипт генерации и перемещения модели.

Перемещение модели, помимо очевидного удобства от меньшей вложенности директорий, даёт нам автозагрузку классов xPDO:

$news = new \Brevis\Model\News($this->core->xpdo);
$news->fromArray(array(
    'pagetitle' => 'Новость 1',
    'alias' => 'news1'
));
print_r($news->toArray());

Не знаю, зачем, но сама возможность греет.

На следующем уроке напишем и выведем пару новостей на сайте.

Комментарии (7)
Семён Лобачевский
09.06.2015 12:24

Очень крутая штука этот xPDO!!! Всё получилось, только не очень понятно одно, таблица создалась, но записей в ней нет, это почему так? Она создаётся и сразу удаляется, чтобы не засорять на данном этапе таблицу?

bezumkinВасилий Наумкин
09.06.2015 12:26

А у меня же в тестовом контроллере запись удаляется в конце.

Убери $news->remove() или закомментируй.

Семён Лобачевский
09.06.2015 12:29

Ага, всё понял, проглядел.

Антон Фомичёв
02.11.2015 23:36

Остался за кадром вопрос использования побитовых операторов и битовых масок вот тут:


define('PROJECT_FENOM_OPTIONS', Fenom::AUTO_RELOAD | Fenom::FORCE_VERIFY);
bezumkinВасилий Наумкин
03.11.2015 03:10

Там же ссылочка есть на документацию.

Или вопрос именно в битовых масках? Я их сам, честно говоря, не очень-то понимаю. В pdoTools опции указываются через массив.

Антон Фомичёв
03.11.2015 04:44

С документацией-то понятно:)) Я просто тоже не очень понимаю все эти битовые операции. То есть, воо вроде читаешь - понятно. Начинаешь читать - вроде боль-мень понятно. Сталкиваешься на практике - нифига не понятно! Сказывается гуманитарное образование:)

bezumkinВасилий Наумкин
03.11.2015 04:48

Вот такая же фигня, без шуток.

ЕвгенийК
09.04.2022 03:35
Это хорошо, что такая возможность есть и может быть использована. А то тенденция, мания, что-то в по...
begoodco1
07.04.2022 05:49
Зарегистрировался чтобы выразить благодарность за доступное и подробное описание процесса. Была возм...
bezumkin
Василий Наумкин
18.03.2022 12:35
Авторизация есть из коробки, для входа в базовую админку. Можно установить через composer и собрать ...
bezumkin
Василий Наумкин
10.03.2022 12:08
Ну, я имел в виду, что по закону можно =) А в реальности с валютой очевидные проблемы.
Сергей Лелеко
04.03.2022 06:12
О как! не знал! спасибо
bezumkin
Василий Наумкин
01.03.2022 15:32
Я делал одного бота на botman/botman, но из-за своей универсальности конкретно с Телеграм на нём раб...
bezumkin
Василий Наумкин
25.02.2022 09:22
P.S. Кажется цитаты у тебя никак не стилизуются в комментариях... Спасибо, поправил!
Electrica
Михаил
08.02.2022 11:19
Работает!
Алексей
09.01.2019 10:55
Насыщенный год ) От души поздравляю с ДР! Счастья, успехов и семейного благополучия! Жаль лимит заме...
septa rose
28.05.2018 22:16
hmmm, keren abis