А сегодня мы добавим в проект 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, а рядом положить файл-пример с ненастоящими данными.
Заодно мы укажем в этом файле с настройками и имя нашего сайта, и адрес к нему, и пути к директориям и что еще нам понадобится. Эти данные не могут меняться в процессе выполнения программы, поэтому мы используем константы.
Итак, суммируем все требуемые изменения:
- Все настройки из переменной Core::config переезжают в файл core/Config/config.inc.php
- Сам файл config.inc.php мы указываем в .gitignore, чтобы его никто кроме нас не видел. Но рядом кладём config_sample.inc.php — для примера.
- Переписываем конструктор класса Core так, чтобы он загружал указанный файл с конфигом
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 мы учимся работать по примеру из репозитория.
Основные отличия, как я вижу, это:
- Указание version=«3.0» в объявлении модели
- Так как мы придерживаемся PSR-4, у нас package=«Brevis\Model»
- Указание всех классов с пространством имён, типа 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, 'Файлы перенесены');На 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. Проверить можно тут — 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());Не знаю, зачем, но сама возможность греет.
На следующем уроке напишем и выведем пару новостей на сайте.
← Следующая заметка
Выводим новости
Выводим новости
Предыдущая заметка →
Осваиваем Composer
Осваиваем Composer
Всё получилось, только не очень понятно одно, таблица создалась, но записей в ней нет, это почему так?
Она создаётся и сразу удаляется, чтобы не засорять на данном этапе таблицу?
Убери $news->remove() или закомментируй.
Или вопрос именно в битовых масках? Я их сам, честно говоря, не очень-то понимаю. В pdoTools опции указываются через массив.
Я просто тоже не очень понимаю все эти битовые операции. То есть, воо вроде читаешь — понятно. Начинаешь читать — вроде боль-мень понятно. Сталкиваешься на практике — нифига не понятно!
Сказывается гуманитарное образование:)