Подключаем xPDO
На прошлом занятии мы освоили 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
- Сам файл config.inc.php мы указываем в .gitignore, чтобы его никто кроме нас не видел. Но рядом кладём config_sample.inc.php - для примера.
- Переписываем конструктор класса 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" в объявлении модели
- Так как мы придерживаемся 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, 'Файлы перенесены');
На 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());
Не знаю, зачем, но сама возможность греет.
На следующем уроке напишем и выведем пару новостей на сайте.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
2 959
09.06.2015, 12:56:00
7 комментариев
Семён Лобачевский
09.06.2015, 18:24:11
Очень крутая штука этот xPDO!!! Всё получилось, только не очень понятно одно, таблица создалась, но записей в ней нет, это почему так? Она создаётся и сразу удаляется, чтобы не засорять на данном этапе таблицу?
Василий Наумкин
09.06.2015, 18:26:51
А у меня же в тестовом контроллере запись удаляется в конце.
Убери $news->remove() или закомментируй.
Семён Лобачевский
09.06.2015, 18:29:03
Ага, всё понял, проглядел.
Антон Фомичёв
03.11.2015, 05:36:57
Остался за кадром вопрос использования побитовых операторов и битовых масок вот тут:
Василий Наумкин
03.11.2015, 09:10:42
Там же ссылочка есть на документацию.
Или вопрос именно в битовых масках? Я их сам, честно говоря, не очень-то понимаю. В pdoTools опции указываются через массив.
Антон Фомичёв
03.11.2015, 10:44:08
С документацией-то понятно:)) Я просто тоже не очень понимаю все эти битовые операции. То есть, воо вроде читаешь - понятно. Начинаешь читать - вроде боль-мень понятно. Сталкиваешься на практике - нифига не понятно! Сказывается гуманитарное образование:)
Василий Наумкин
03.11.2015, 10:48:15
Вот такая же фигня, без шуток.
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Василий Наумкин
23.12.2024, 05:33:00
В MODX сначала создали проблему, автоматически генерируя адреса, а потом "решили" заморозкой.
Так ч...
Дмитрий
14.12.2024, 09:10:38
Василий, прошу прощения, тупанул, не разобрался сразу. Фреймворк отличный! "Чистый лист" на vue, рис...
Василий Наумкин
05.12.2024, 20:01:14
В итоге основная ошибка была в неправильном общем root в Nginx, из-за чего запросы не улетали на фай...
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500
Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи.
...
Уровни подписки
Спасибо!
500 ₽ в месяц
Эта подписка ничего не даёт, просто возможность сказать спасибо за мои заметки. Подписчики отмечаются зелёненьким цветом в комментариях.
Большое спасибо!
1 000 ₽ в месяц
И эта подписка не даёт ничего, кроме оранжевого цвета в комментариях и возможности сказать спасибо, но уже большое!