На прошлом занятии мы освоили 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 . 'mysql');
rename(PROJECT_MODEL_PATH . PROJECT_NAME . '/Model/mysql', PROJECT_MODEL_PATH . 'mysql');
$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.h3.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


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

  1. Семён Лобачевский 09 июня 2015, 15:24 # 0
    Очень крутая штука этот xPDO!!!
    Всё получилось, только не очень понятно одно, таблица создалась, но записей в ней нет, это почему так?
    Она создаётся и сразу удаляется, чтобы не засорять на данном этапе таблицу?
    1. Василий Наумкин 09 июня 2015, 15:26 # 0
      А у меня же в тестовом контроллере запись удаляется в конце.

      Убери $news->remove() или закомментируй.
      1. Семён Лобачевский 09 июня 2015, 15:29 # +1
        Ага, всё понял, проглядел.
    2. Антон Фомичёв 03 ноября 2015, 02:36 # 0
      Остался за кадром вопрос использования побитовых операторов и битовых масок вот тут:
      define('PROJECT_FENOM_OPTIONS', Fenom::AUTO_RELOAD | Fenom::FORCE_VERIFY);
      
      1. Василий Наумкин 03 ноября 2015, 06:10 # 0
        Там же ссылочка есть на документацию.

        Или вопрос именно в битовых масках? Я их сам, честно говоря, не очень-то понимаю. В pdoTools опции указываются через массив.
        1. Антон Фомичёв 03 ноября 2015, 07:44 # 0
          С документацией-то понятно:))
          Я просто тоже не очень понимаю все эти битовые операции. То есть, воо вроде читаешь — понятно. Начинаешь читать — вроде боль-мень понятно. Сталкиваешься на практике — нифига не понятно!
          Сказывается гуманитарное образование:)
          1. Василий Наумкин 03 ноября 2015, 07:48 # 0
            Вот такая же фигня, без шуток.
      Добавление новых комментариев отключено.