Приветствую, хабралюди.

onPHP OnPHP — это фреймворк, как не трудно догадаться, написанный на PHP. Как и любой подобный инструмент он обладает своими преимуществами и недостатками и, как я считаю, первых у него больше.

Как ни странно, на хабре я не нашел ни одного топика, посвященного этому фреймоврку. Видимо причина в том, что у фрейморка напрочь отсутствует документация, отчего он напоминает «фреймворк в себе» и «для своих». Однако это не совсем так, но об этом чуть позже.



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

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

Введение.


Фреймворк родился и вырос в команде людей, которая создавала его и развивала для собственных нужд, насколько я понял, с 2005-го года. Самая ранняя версия, доступная публично, это версия 0.2, датированная 3 сентября 2005 года. С тех пор фреймворк сильно изменялся, в него добавлялись новые возможности и исправлялись ошибки. Сейчас onPHP — это достойная кандидатура на роль фундамента для веб-проекта любого масштаба. Последняя стабильная версия — 1.0.

Достоинства и недостатки.


Как я уже говорил, у любого инструмента (а скорее просто — у любого) есть свои достоинства и недостатки, и onPHP не исключение. Среди достоинств я смог выделить следующие:

1. Быстрая разработка приложений.

Инструменты onPHP позволяет очень быстро создать нужные классы и связи между ними, построить структуру БД и проверить зависимости. Все это делается с помощью схемы в формате XML, описывающей сущности проекта. Подробнее об этом ниже. Кроме того, имеется удобные инструменты для доступа к данным: Criteria (обдуманный порт из Hibernate) и OSQL.

2. Горизонтальная масштабируемость.

Глубоко проработан вопрос кэширования. Как я понимаю, это гордость фрейморка. Кэшировать можно по разному и очень умно (файловая система, memcached, etc). Кроме того, слой работы с базами данных поддерживает работу с несколькими соединениями, а каждой заведенной сущности можно указать какое соединение использовать, т.е. фактически прозрачно хранить данные в разных БД.

3. Гибкость.

Она заключается в том, что фрейморк в подавляющем большинстве случаев, не навязывает конкретных шаблонов действия для создания какой-либо функциональности в вашем проекте. В то же самое время, фреймворк предоставляет все необходимые возможности для создания того, что вы хотите. В архитектуре фреймворка используютсся многие общеизвестные шаблоны проектирования, а сам фрейморк предоставляет возможности для «ошаблонивания» ваших собственных классов (StaticFactory, Singleton).

4. Скорость.

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

5. Качество и надежность.

Код покрыт unit-тестами. Каждая новая функциональность, которая появляется во фрейморке, покрывается тестами. По опыту использования, я был приятно удивлен стабильностью и предсказуемостью поведения построенной системы.

6. Качественный, чистый и понятный код.

Возможно, это субъективно, но это мое мнение и, видимо, создателей тоже. Строгий coding slyle, к которому быстро привыкаешь, способствует быстрому восприятию кода и даже некоему эстетическому наслаждению, которое возникает всякий раз, кода заглядываешь в код.

Пожалуй из достоинств это все. Недостатки:

1. Отсутствие документации.

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

2. Небольшое сообщество.

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

3. Конфликт классов.

Классы фреймворка не имеют префикса, что создает проблемы при миграции проектов на этот фреймворк. Поэтому приходится иногда выдумывать другие имена тем классам, имена которых заняты в onPHP.

С чего начать?


В мастер-ветке git'а, есть очень краткий tutorial, в котором есть рекомендации по типовой структуре проекта, точке входа (index.php), использованию базы данных, использованию форм (мощный инструмент для приема и валидации входных данных) и недописанная заметка про ACL, поскольку он еще в инкубаторе.

Инкубатор.


Эта та часть onPHP, в которой находятся те части фрейморка, которые официально еще не приобрели окончательной формы и в которых возможны изменения, неработающий функционал и прочие беспорядки. Здесь ведется разработка, отладка и тестирование. Новые части фрейморвка проходят через разработку в инкубаторе, а после стабилизации попадают в основной раздел.
Так, в инкубаторе на текущий момент находятся Acl, Tsearch (полнотекстовый поиск для PostgreSQL), FormRendering (централизованное отображение форм — объекта Form и его примитивов), Application (приложение и совокупность его настроек для удобной настройки) и еще несколько пакетов.

Meta.


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

Пример схемы:
<?xml version="1.0"?>
<!DOCTYPE metaconfiguration SYSTEM "meta.dtd">
<metaconfiguration>
<classes>
        <class name="Author" table="authors">
                <properties>
                        <identifier/>
                        <property name="name" type="String" size="50" required="true"/>
                </properties>
                <pattern name="StraightMapping"/>
        </class>
        <class name="Book" table="books">
                <properties>
                        <identifier/>
                        <property name="title" type="String" size="50" required="true"/>
                        <property name="author" type="Author" relation="OneToOne" required="true" fetch="lazy"/>
                </properties>
                <pattern name="StraightMapping"/>
        </class>
</classes>
</metaconfiguration>

Что здесь интересного:

— Для каждой сущности указывается какими свойствами она обладает и какой их тип. На основе этого, meta-builder порекомендует какие изменения в схеме БД нужно сделать, а также этим можно воспользоваться для валидации данных объектов по форме (Form).

— Можно сразу указать связи между сущностями, которые затем будут отражены в сгенерированных классах. Доступны к использованию OneToOne, OneToMany и ManyToMany. Так получилось, что я всегда использовал связь OneToOne, но хотелось бы узнать про остальные типы связей (имеется ввиду не теория, с этим все в порядке, а именно реализация в onPHP).

— Pattern — представляет собой шаблон будущего класса. Особенностей всех шаблонов я, к сожалению, не знаю, однако вот некоторые из них:
  1. StraightMapping — из названия понятно, что это прямой маппинг объекта на одну запись в таблице БД. Это ничто иное, как ActiveRecord М.Фаулера.
  2. EnumerationClass — перечисление. Класс, который не имеет таблицы в БД, а является линейным списком (id => name). Используется для тех данных, которые никогда или практически никогда не меняются (например, месяца, знаки зодиака, и т. д.)
  3. AbstractClass — абстрактный класс. Используется для определения в схеме абстрактных классов, от которых в этой же схеме другие классы будет наследоваться.
Остальные паттерны (DictionaryClass, SpookedClass, SpookedEnumeration, ValueObject и InternalClass) мне пока неведомы, надеюсь знающие расскажут, а я запишу.

— fetch=«lazy» — это указание на то, что при сборке объекта, свойство Author загружать не нужно до обращения к нему. В приведенном примере, если я выгружу объект Book по ID:
$book = Book::dao()->getById(1);
$author = $book->getAuthor();
то для сборки объекта Author будет сделан запрос в БД на получение значений его параметров. При fetch=«cascade» все данные по всем связям выгребаются из БД одним запросом, после чего связанные объекты собираются и доступны для использования без дополнительных запросов.

Это далеко не все, что можно использовать в meta, но для краткого обзора достаточно. Начнем генерировать классы.

Кодогенерация.


В папке meta есть скрипт bin/build.php, который с помощью рядом находящихся классов и превращает схему в классы. Посмотрим как его использовать:
Usage: build.php [options] [project-configuration-file.inc.php] [metaconfiguration.xml]

Meta у нас есть, а вот файла конфигурации — нет. Создадим его.

В корне есть файл global.inc.php.tpl, который является шаблоном для файла конфигурации. Копируем его и изменяем следующим образом:
а) добаляем константу PATH_CLASSES — там, где у вас будут лежать сгенерированные классы.
б) Добавляем в set_include_path():
.PATH_CLASSES.PATH_SEPARATOR
.PATH_CLASSES.'Auto'.PATH_SEPARATOR
.PATH_CLASSES.'Auto'.DIRECTORY_SEPARATOR.'Business'.PATH_SEPARATOR
.PATH_CLASSES.'Auto'.DIRECTORY_SEPARATOR.'DAOs'.PATH_SEPARATOR
.PATH_CLASSES.'Auto'.DIRECTORY_SEPARATOR.'Proto'.PATH_SEPARATOR
.PATH_CLASSES.'Business'.PATH_SEPARATOR
.PATH_CLASSES.'DAOs'.PATH_SEPARATOR
.PATH_CLASSES.'Proto'.PATH_SEPARATOR
в) добавляем параметры соединения с БД:
DBPool::me()->setDefault(
DB::spawn('PgSQL', 'user', 'password', 'localhost', 'dbname')
->setEncoding('utf-8')
);

Затем, запускаем build.php:

$ ./meta/bin/build.php ./global.inc.php ./schema/project.xml 

onPHP-1.1: MetaConfiguration builder.

Known internal classes: Range, DateRange, TimestampRange, IdentifiableObject, IdentifiableTree, NamedObject, NamedTree, that's all.

Building classes:
	Author:
		AutoProtoAuthor (classes/Auto/Proto/AutoProtoAuthor.class.php)
		ProtoAuthor (classes/Proto/ProtoAuthor.class.php)
		AutoAuthor (classes/Auto/Business/AutoAuthor.class.php)
		Author (classes/Business/Author.class.php)
		AutoAuthorDAO (classes/Auto/DAOs/AutoAuthorDAO.class.php)
		AuthorDAO (classes/DAOs/AuthorDAO.class.php)

	Book:
		AutoProtoBook (classes/Auto/Proto/AutoProtoBook.class.php)
		ProtoBook (classes/Proto/ProtoBook.class.php)
		AutoBook (classes/Auto/Business/AutoBook.class.php)
		Book (classes/Business/Book.class.php)
		AutoBookDAO (classes/Auto/DAOs/AutoBookDAO.class.php)
		BookDAO (classes/DAOs/BookDAO.class.php)

Building containers: 

Building DB schema:
		schema.php (classes/Auto/schema.php)

Suggested DB-schema changes: 
table 'authors' not found, skipping.
table 'books' not found, skipping.

Checking for stale files: 

Trying to compile all known classes... done.

Checking sanity of generated files: 

	Author (2/1/
ERROR:  42P01: relation "authors" does not exist - SELECT "authors"."id", "authors"."name" FROM "authors" WHERE ("authors"."id" IS NOT NULL) ORDER BY "authors"."id" LIMIT 1

[exception trace вырезан]

Что произошло: builder взял конфигурационный файл и по схеме создал классы (раздел Building classes), создал файл схемы (раздел Building DB schema), проверил наличие устаревших файлов, проверил синтаксис во всех файлах проекта и, про��еряя правильность сгенерированных классов, нашел ошибку, что таблицы-то мы еще не создали. Исправимся. Заходим на базу и выполняем 2 запроса:
CREATE TABLE authors (id serial PRIMARY KEY);
CREATE TABLE books (id serial PRIMARY KEY);

Запускаем еще раз build.php:

$ ./meta/bin/build.php ./global.inc.php ./schema/project.xml 

onPHP-1.1: MetaConfiguration builder.

Known internal classes: Range, DateRange, TimestampRange, IdentifiableObject, IdentifiableTree, NamedObject, NamedTree, that's all.

Building classes:
	Author:
		AutoProtoAuthor 
		AutoAuthor 
		AutoAuthorDAO 

	Book:
		AutoProtoBook 
		AutoBook 
		AutoBookDAO 

Building containers: 

Building DB schema:
		schema.php 

Suggested DB-schema changes: 
ALTER TABLE "authors" ALTER COLUMN "id" TYPE BIGINT;
ALTER TABLE "authors" ADD COLUMN "name" CHARACTER VARYING(50) NOT NULL;

ALTER TABLE "books" ALTER COLUMN "id" TYPE BIGINT;
ALTER TABLE "books" ADD COLUMN "title" CHARACTER VARYING(50) NOT NULL;
ALTER TABLE "books" ADD COLUMN "author_id" BIGINT NOT NULL REFERENCES "authors"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
CREATE INDEX "author_id_idx" ON "books"("author_id");


Checking for stale files: 

Trying to compile all known classes... done.

Checking sanity of generated files: 

	Author (2/1/F/C/H/T), Book (3/1/
ERROR:  42703: column books.title does not exist - SELECT "books"."id", "books"."title", "books"."author_id" FROM "books" WHERE ("books"."id" IS NOT NULL) ORDER BY "books"."id" LIMIT 1

Обратите внимание на Suggested DB-schema changes, выполним эти запросы и снова запустим build.php:

$ ./meta/bin/build.php ./global.inc.php ./schema/project.xml 

onPHP-1.1: MetaConfiguration builder.

Known internal classes: Range, DateRange, TimestampRange, IdentifiableObject, IdentifiableTree, NamedObject, NamedTree, that's all.

Building classes:
	Author:
		AutoProtoAuthor 
		AutoAuthor 
		AutoAuthorDAO 

	Book:
		AutoProtoBook 
		AutoBook 
		AutoBookDAO 

Building containers: 

Building DB schema:
		schema.php 

Suggested DB-schema changes: 

Checking for stale files: 

Trying to compile all known classes... done.

Checking sanity of generated files: 

	Author (2/1/F/C/H/T), Book (3/1/F/C/H/T), done.

Всё, классы сгенерированы. Давайте посмотрим на них. В каталоге, который был указан в константе PATH_CLASSES сейчас находятся 4 подкаталога:
  • Auto: классы, которые содержат не подлежащие изменению классы, которые перегенерируются всякий раз при запуске build.php;
  • Business: бизнес-объекты, которые можно и нужно изменять, дополняя необходимой логикой;
  • DAOs: Data Access Objects, объекты доступа к данным, в которые можно поместить часто используемые выборки;
  • Proto: классы, описывающие прототипы объектов, их свойства.

Пример использования созданных классов.


Код:
require "global.inc.php";

DBPool::me()->getLink()->begin();

$author = Author::create()
->setName('Martin Fowler');
Author::dao()->add($author);

$book = Book::create()
->setTitle('Patterns of Enterprise Application Architecture')
->setAuthor($author);
Book::dao()->add($book);

DBPool::me()->getLink()->commit();

Результат:

dbname=> select * from authors;
 id |     name      
----+---------------
  1 | Martin Fowler
(1 row)

dbname=> select * from books;
 id |                      title                      | author_id 
----+-------------------------------------------------+-----------
  1 | Patterns of Enterprise Application Architecture |         1
(1 row)


На этом пока все, в следующий раз планирую написать про MVC в onPHP и полноценный пример приложения, а также осветить вопросы, которые будут заданы. Надеюсь тема onPHP найдет позитивный и конструктивный отклик в виде комментариев к этой заметке.
Спасибо за внимание.

UPD:
Ссылки:
— onphp.org
GIT
блог Дениса Габайдулина, одного из создателей onPHP