Upload
stepan-tanasiychuk
View
7.735
Download
2
Embed Size (px)
DESCRIPTION
Citation preview
Чем я занимаюсь?
Web разработкой занялся в 2003 году
С Zend Framework начал работать в 2008 году
Руковожу собственной веб-студией с 2009 года
Активный участник сообщества zendframework.ru
Люблю прикольные смайлы :]
Содержание доклада
Подключение Doctrine к ZF проекту Скрипт для работы с Doctrine_Cli Генерация моделей по YAML схемам Механизм миграций Наследование в моделях Шаблоны расширений Адаптер для Zend_Auth Адаптер для Zend_Paginator ZFEngine и использование Doctrine в модульном ZF
приложении
Несколько слов о Doctrine
ORM библиотека для PHP 5.2.3+ Использует паттерны Active Record, Data Mapper и Metadata
Mapping Собственный язык запросов — DQL (по мотивам HQL) Связи один-к-одному, один-ко-многим и многие-к-многим Автогенерация моделей по yaml схемам Экспорт и импорт из/в yaml Механизм миграций Шаблоны поведений (l18n, Versionable, NestedSet, etc.)
Подключаем Doctrine к ZF проекту
Размещаем Doctrine в library/Doctrine:$ svn export http://svn.doctrine-project.org/tags/1.2.1/lib/Doctrine/ ./library/Doctrine
Прописываем следующие настройки в application.ini:autoloadernamespaces[] = "Doctrine"
Parables_Application_Resource_Doctrine
Matthew Lurz добавил в Zend Framework proposal application-ресурс для подключения Doctrine.
Его класс называется Parables_Application_Resource_Doctrine и лежит здесь http://github.com/mlurz71/parables
ZFEngine_Application_Resource_Doctrine
Мы немного изменили код Parables_Application_Resource_Doctrine для работы с Doctrine 1.2.x и храним его в репозитории ZFEngine как ZFEngine_Application_Resource_Doctrine
ZFEngine это сборная солянка классов, которые мы используем при разработке проектов на ZF. Лежит все здесь: http://zfengine.com
В основном код наш. Также есть чужой, но с некоторыми изменениями. Надеюсь, что это все в рамках закона ^_~.
Подключаем ZFEngine к ZF проекту
Размещаем ZFEngine в library/ZFEngine:$ svn export http://svn2.assembla.com/svn/zfengine/trunk/library/ZFEngine/ ./library/ZFEngine
Прописываем следующие настройки в application.ini:autoloadernamespaces[] = "ZFEngine"
pluginPaths.ZFEngine_Application_Resource = "ZFEngine/Application/Resource"
Настраиваем подключение к БД
resources.doctrine.connections.primary.dsn.adapter = "mysql"
resources.doctrine.connections.primary.dsn.username = "root"
resources.doctrine.connections.primary.dsn.password = "******"
resources.doctrine.connections.primary.dsn.host = "localhost"
resources.doctrine.connections.primary.dsn.dbname = "zfconf"
resources.doctrine.connections.primary.options.charset = "utf8"
resources.doctrine.connections.primary.options.collate = "utf8_unicode_ci"
Настраиваем Doctrine_Manager
resources.doctrine.manager.attributes.attr_autoload_table_classes = 1
resources.doctrine.manager.attributes.attr_use_native_enum = 1
resources.doctrine.manager.attributes.attr_quote_identifier = 1
resources.doctrine.manager.attributes.attr_auto_free_query_objects = 1
resources.doctrine.manager.attributes.attr_auto_accessor_override = 1
resources.doctrine.manager.attributes.attr_model_loading = "model_loading_conservative"
MODEL_LOADING_PEAR
В Doctrine 1.2 появился новый режим для автозагрузки моделей — MODEL_LOADING_PEAR, но при использовании этого режима не работает generate-migration-diff :(.Я заметил это уже в процессе подготовки доклада и пока просто написал в багрепорт Doctrine.
Для проектов с НЕмодульной структурой
Указываем путь к директории с моделями:resources.doctrine.manager.models_path = APPLICATION_PATH "/models"
Настраиваем кеширование
resources.doctrine.manager.*.attributes.attr_result_cache.driver = "memcache"
.attributes.attr_result_cache.lifespan = 3600
.attributes.attr_result_cache.options.servers.host = "localhost"
.attributes.attr_result_cache.options.servers.port = 11211
.attributes.attr_result_cache.options.servers.persistent = 1
.attributes.attr_result_cache.options.compression = 0
Настраиваем Doctrine_Cli
doctrine_cli.data_fixtures_path = APPLICATION_PATH "/configs/doctrine/data/fixtures"
doctrine_cli.models_path = APPLICATION_PATH "/models"
doctrine_cli.migrations_path = APPLICATION_PATH "/configs/doctrine/migrations"
doctrine_cli.sql_path = APPLICATION_PATH "/configs/doctrine/data/sql"
doctrine_cli.yaml_schema_path = APPLICATION_PATH "/configs/doctrine/schema"
doctrine_cli.generate_models_options.generateBaseClasses = 1
doctrine_cli.generate_models_options.baseClassesDirectory = "Base"
doctrine_cli.generate_models_options.generateTableClasses = 1
Cкрипт для работы с Doctrine_Cli
./application/sripts/common.php<?php
define('APPLICATION_ENV', 'development');
define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/..'));
set_include_path(implode(PATH_SEPARATOR, array(
realpath(APPLICATION_PATH . '/../library'),
get_include_path(),
)));
Cкрипт для работы с Doctrine_Cli
./application/sripts/doctrine#!/usr/bin/env php
<?php
require_once 'common.php';
require_once 'Zend/Application.php';
$application = new Zend_Application(
APPLICATION_ENV,
APPLICATION_PATH . '/configs/application.ini'
);
$application->getBootstrap()
->bootstrap();
$cli = new Doctrine_Cli($application->getOption('doctrine_cli'));
$cli->run($_SERVER['argv']);
Проверяем как работает
Запускаем скрипт без параметров:$ ./application/sripts/doctrine
Doctrine Command Line Interface
./application/sripts/doctrine generate-sql
./application/sripts/doctrine create-db
./application/sripts/doctrine generate-yaml-models
./application/sripts/doctrine dql
./application/sripts/doctrine generate-migrations-models
./application/sripts/doctrine generate-yaml-db
./application/sripts/doctrine generate-models-yaml
./application/sripts/doctrine generate-migrations-diff
./application/sripts/doctrine generate-migration
./application/sripts/doctrine create-tables
./application/sripts/doctrine drop-db
./application/sripts/doctrine generate-migrations-db
... и ещё 9ть команд, которые не поместились на этом слайде (:
Создадим схему модели User
./application/configs/doctrine/schema/User.yml
User:
tableName: users
options:
type: INNODB
collate: utf8_unicode_ci
charset: utf8
columns:
id:
type: integer(4)
primary: true
autoincrement: true
login: string(32)
email: string(255)
Генерируем модели по YAML схемам
Запускаем скрипт с параметром generate-models-yaml:$ ./application/sripts/doctrine generate-models-yaml
generate-models-yaml - Generated models successfully from YAML schema
Получаем готовые модели:./application/models
|-- Base
| `-- BaseUser.php
|-- User.php
`-- UserTable.php
Важная деталь: сами YAML схемы можно сгенерировать непосредственно с структуры БД используя команду generate-yaml-db.
Сгенерированный код базовой модели User
./application/models/Base/BaseUser.php<?php
abstract class BaseUser extends Doctrine_Record
{
public function setTableDefinition()
{
$this->setTableName('users');
$this->hasColumn('id', 'integer', 4, array('type' => 'integer', 'unsigned' => true, 'primary' => true, 'autoincrement' => true, 'length' => '4'));
// Здесь было описание полей login и email ...
$this->option('type', 'INNODB');
$this->option('collate', 'utf8_unicode_ci');
$this->option('charset', 'utf8');
}
public function setUp()
{
parent::setUp();
}
}
Сгенерированный код модели User и маппера UserTable
./application/models/User.php
<?php
class User extends BaseUser
{
}
./application/models/UserTable.php<?php
class UserTable extends Doctrine_Table
{
}
Напишем свой сеттер для поля email
./application/models/User.php
<?php
/**
* User model
*/
class User extends BaseUser
{
/**
* Set email adress into lowercase
*
* @param string $email
* @return void
*/
public function setEmail($email)
{
$this->_set('email', strtolower($email));
}
}
Пишем экшн для проверки работы
./application/controllers/IndexController.php<?php
class IndexController extends Zend_Controller_Action
{
/**
* Simple action
*
* @return void
*/
public function indexAction()
{
$user = new User();
$user->login = 'stfalcon';
$user->email = '[email protected]';
Zend_Debug::dump($user->toArray());
}
}
Запускаем в браузере
array
'id' => null
'login' => string 'stfalcon' (length=8)
'email' => string '[email protected]' (length=16)
Миграции
Сгенерируем первый класс миграций. Его можно генерировать из классов моделей или БД (см. мануал к Doctrine).$ ./application/sripts/doctrine generate-migrations-models
generate-migrations-models - Generated migration classes successfully from models
Получаем готовую модель миграций:./application/configs/doctrine/
|-- data
| |-- fixtures
| `-- sql
|-- migrations
| `-- 1268942153_adduser.php
`-- schema
`-- User.yml
Сгенерированный код первой модели миграций
./application/configs/doctrine/migrations/1268942153_adduser.php<?php
class Adduser extends Doctrine_Migration_Base
{
public function up()
{
$this->createTable('user', array('id' => array('type' => 'integer', 'unsigned' => true, 'primary' => true, 'autoincrement' => true, 'length' => 4),
// Здесь были параметры для создания полей login и email ...
), array('type' => 'INNODB', 'indexes' => array(), 'primary' => array(0 => 'id'), 'collate' => 'utf8_unicode_ci', 'charset' => 'utf8'));
}
public function down()
{
$this->dropTable('user');
}
}
Создадим БД и накатим на неё наши изменения
Создаем БД (например на production сервере):mysql> CREATE DATABASE `zfconf`;
Query OK, 1 row affected (0,00 sec)
Накатываем на неё миграцию:$ ./application/sripts/doctrine migrate
migrate - migrated successfully to version #1
Выведем список таблиц:mysql> SHOW TABLES;
migration_version
users
Проверяем работу скрипта
Структура таблицы в которой хранится номер миграции:mysql> SHOW CREATE TABLE `migration_version`;
CREATE TABLE `migration_version` (
`version` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1
Структура таблицы пользователей:mysql> SHOW CREATE TABLE `users`;
CREATE TABLE `users` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`login` varchar(32) COLLATE utf8_unicode_ci DEFAULT NULL,
`email` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Наследование в YAML схемах
./application/configs/doctrine/schema/Administrator.yml## Administrator schema
Administrator:
tableName: administrators
inheritance:
extends: User
type: concrete
columns:
password_hash: string(32)
password_salt: string(8)
actAs: [Timestampable]
Работаем с Doctrine_Cli
В первую очередь делаем migration-diff — он генерирует классы миграций на основе различий между кодом моделей и YAML схемами:$ ./application/sripts/doctrine generate-migrations-diff
generate-migrations-diff - Generated migration classes successfully from difference
./application/configs/doctrine
|-- data
| |-- fixtures
| `-- sql
|-- migrations
| |-- 1268942153_adduser.php
| `-- 1268942505_version2.php
`-- schema
|-- Administrator.yml
`-- User.yml
Работаем с Doctrine_Cli
Генерируем код моделей:$ ./application/sripts/doctrine generate-models-yaml
generate-models-yaml - Generated models successfully from YAML schema
Накатываем изменения на БД:$ ./application/sripts/doctrine migrate
migrate - migrated successfully to version #2
Работаем с Doctrine_Cli
Смотрим, что получилось:mysql> SHOW CREATE TABLE `administrators`;
CREATE TABLE `administrators` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`login` varchar(32) COLLATE utf8_unicode_ci DEFAULT NULL,
`email` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`password_hash` varchar(32) COLLATE utf8_unicode_ci DEFAULT NULL,
`password_salt` varchar(8) COLLATE utf8_unicode_ci DEFAULT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
По-моему пора сделать авторизацию
Сначала напишем сеттер для password:./application/models/Administrator.php
<?php
class Administrator extends BaseAdministrator
{
// Здесь был phpDoc блок ...
public function setPassword($password)
{
if (strlen($password)) {
$passwordSalt = substr(md5(mktime()), 0, rand(5,8));
$passwordHash = md5($password . $passwordSalt);
$this->_set('password_hash', $passwordHash);
$this->_set('password_salt', $passwordSalt);
}
}
}
Сгенерируем аккаунт для админа и сохраним его в БД
./application/controllers/IndexController.php
<?php
class IndexController extends Zend_Controller_Action
{
// Здесь был phpDoc блок ...
public function indexAction()
{
$administrator = new Administrator();
$administrator->email = '[email protected]';
$administrator->login = 'stfalcon';
$administrator->password = 'qwerty';
$administrator->save();
Zend_Debug::dump($administrator->toArray());
}
}
Проверяем содержимое таблицы administrators
mysql> SELECT * FROM `administrators`;
+----+----------+------------------+----------------------------------+---------------+---------------------+---------------------+
| id | login | email | password_hash | password_salt | created_at | updated_at |
+----+----------+------------------+----------------------------------+---------------+---------------------+---------------------+
| 1 | stfalcon | [email protected] | bcd3987603a947d54480285c16f06fde | fc1ed | 2010-03-18 23:04:11 | 2010-03-18 23:04:11 |
ZendX_Doctrine_Auth_Adapter
./application/controllers/IndexController.php
public function indexAction()
{
$authAdapter = new ZendX_Doctrine_Auth_Adapter(
Doctrine_Core::getConnectionByTableName('Administrator'));
$authAdapter->setTableName('Administrator a')
->setIdentityColumn('a.login')
->setCredentialColumn('a.password_hash')
->setCredentialTreatment('MD5(CONCAT(?,a.password_salt))')
->setIdentity('stfalcon')->setCredential('qwerty');
$auth = Zend_Auth::getInstance();
$result = $auth->authenticate($authAdapter);
if ($result->isValid()) {
echo '<h1>OK</h1>';
} else {
echo '<h1>FAIL</h1>';
}
}
Открываем страницу в браузере
Все ОК :) И не забудьте сохранить данные авторзации в хранилище:
$data = $authAdapter->getResultRowObject(null, array('password_hash', 'password_salt'));
$auth->getStorage()->write($data);
Увековечим учетную запись администратора
./application/configs/doctrine/data/fixtures/users.yml
Administrator:
Admin_1:
login: stfalcon
email: [email protected]
password_hash: bcd3987603a947d54480285c16f06fde
password_salt: fc1ed
# Admin_2:
# login: stfalcon
# ...
Сделаем глобальный reload
$ ./application/sripts/doctrine build-all-reload
build-all-reload - Are you sure you wish to drop your databases? (y/n)
y
build-all-reload - Successfully dropped database for connection named 'primary'
build-all-reload - Generated models successfully from YAML schema
build-all-reload - Successfully created database for connection named 'primary'
build-all-reload - Created tables successfully
build-all-reload - Data was successfully loaded
mysql> SELECT * FROM `administrators`;
| id | login | email | password_hash | password_salt | created_at | updated_at |
+----+----------+------------------+----------------------------------+---------------+---------------------+---------------------+
| 1 | stfalcon | [email protected] | bcd3987603a947d54480285c16f06fde | fc1ed | 2010-03-18 23:04:11 | 2010-03-18 23:04:11 |
Адаптер для Zend_Paginator
Мы используем ZFEngine_Paginator_Adapter_Doctrine, этонемного переработанный с учетом наших потребностей и изменений в Doctrine 1.2 SmartL_Zend_Paginator_Adapter_Doctrine http://code.google.com/p/smart-framework/
Ещё раз пропиарю наш ZFEngine :)http://zfengine.com
ZFEngine_Paginator_Adapter_Doctrine
Давайте выведем список администраторов с постраничной навигацией. Для этого создадим в таблице administrators 10 случайных записей:
Расширяем функционал AdministratorTable
Создадим метод getQueryToFetchAll(), который будет возвращать запрос на выборку всех администраторов:./application/models/AdministratorTable.php<?php
class AdministratorTable extends UserTable
{
/**
* Query to fetch all administrators
* @return Doctrine_Query
*/
public function getQueryToFetchAll()
{
return $this->createQuery('a')
->orderBy('a.created_at');
}
}
Работаем с пагинатором
./application/controllers/IndexController.php
<?php
class IndexController extends Zend_Controller_Action
{
public function indexAction()
{
$query = Doctrine_Core::getTable('Administrator')
->getQueryToFetchAll();
$paginator = new Zend_Paginator(
new ZFEngine_Paginator_Adapter_Doctrine($query));
$paginator->setCurrentPageNumber($this->_getParam('page', 1));
$paginator->setItemCountPerPage(4);
$this->view->paginator = $paginator;
}
}
Оформляем вывод списка в view шаблоне
./application/views/scripts/index/index.phtml
<h1>
<?php echo $this->translate('Администраторы'); ?>:
</h1>
<?php if (count($this->paginator)): ?>
<ul>
<?php foreach ($this->paginator as $administrator): ?>
<li>
<?php echo $administrator->login; ?>
<<?php echo $administrator->email; ?>>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php echo $this->paginationControl($this->paginator, 'Sliding', 'digg.phtml'); ?>
digg.phtml
digg.phtml я выложил здесь — http://pastie.org/832023 (за основу взят шаблон с ZendPaginationHelper)
Открываем страницу в браузере
И наслаждаемся результатом :)
ZFEngine и использование Doctrine в модульном ZF приложении
Мы написали несколько тасков (собственно таски написал Валерий Рабиевский, а я только немного порефакторил) для Doctrine, которые позволяют генерировать модели и использовать механизм миграций в ZF проектах с модульной архитектурой.
При этом между моделями разных модулей работает связывание и наследование.
Также работает механизм миграций для проекта в целом.
Пример структуры модульного ZF проекта
./application/
|-- Bootstrap.php
|-- configs
| `-- application.ini
|-- layouts
| `-- scripts
| |-- admin.phtml
| `-- index.html
`-- modules
|-- products
`-- users
Настройки для модульной структуры
Прописываем следующие настройки в application.ini:; Указываем, где находятся наши модули для Zend
resources.frontController.moduleDirectory =
APPLICATION_PATH "/modules"
; и для Doctrine_Cli
doctrine_cli.modules_path = APPLICATION_PATH "/modules/"
; а также прописываем путь к папке, где будут хранится yaml-схемы предыдущих версий (old), и новые (temp), собранные с модулей в одну папку. Именно по различиям между ними и будут генерироваться миграции.
doctrine_cli.old_schema_path = APPLICATION_PATH "/configs/doctrine/schema/old/"
doctrine_cli.temp_schema_path = APPLICATION_PATH "/../tmp/schema/"
resources.modules[] = "" ; подгружаем ресурс для подержки модулей
; И убираем строки, где задавали расположение моделей:
; resources.doctrine.manager.models_path = APPLICATION_PATH "/models"
; doctrine_cli.models_path = APPLICATION_PATH "/models"
; так как теперь модели подгружаются самим Zend'ом
Cтруктура модуля users
./application/modules/users/
|-- Bootstrap.php
|-- configs
| `-- doctrine
| |-- data
| | |-- fixtures
| | `-- sql
| `-- schema
| `-- User.yml
|-- controllers
| `-- IndexController.php
|-- models
`-- views
`-- scripts
`-- index
`-- index.phtml
Схема User.yml
## User schema
Users_Model_User:
tableName: users
options:
type: INNODB
collate: utf8_unicode_ci
charset: utf8
columns:
id:
type: integer(4)
unsigned: true
primary: true
autoincrement: true
login: string(32)
email: string(255)
Cтруктура модуля products
./application/modules/products/
|-- Bootstrap.php
|-- configs
| |-- acl.php
| |-- doctrine
| | `-- schema
| | `-- Product.yml
| `-- routes.xml
|-- controllers
| `-- IndexController.php
|-- forms
|-- models
`-- views
|-- helpers
`-- scripts
`-- index
`-- index.phtml
Схема Product.yml
## Product schema
Products_Model_Product:
tableName: products
options:
...
columns:
id:
type: integer(4)
unsigned: true
primary: true
autoincrement: true
user_id:
type: integer(4)
unsigned: true
name: string(255)
description: string
actAs: [Timestampable]
...
Схема Product.yml (продолжение)
...
# Прописываем связь один-ко-многим
# User и Products – алиасы, через которые мы сможем обращаться
# из одной модели к другой
relations:
User:
class: Users_Model_User
foreign: id
local: user_id
foreignAlias: Products
onUpdate: CASCADE
onDelete: CASCADE
Новый скрипт для Doctrine_Cli
Скрипт для работы с Doctrine_Cli в модульном ZF приложении лежит в репозитории ZFEngine.
Единственное его отличие от обычного скрипта, это наличие кода для подключения тасков с ZFEngine и справка по командам ZFEngine при запуске скрипта с ключем info:$ ./application/scripts/doctrine info
zfengine-generate-migrations-models -> для генерации новой миграций
zfengine-generate-migrations-diff -> для генерации изменений миграций
zfengine-generate-models-yaml -> для генерация моделей из yaml-файлов
zfengine-prepare-schema-files-for-migrations -> для копирования shema-файлов для сравнения при генерации миграций
Очередность действий:
При создании новой миграции:
zfengine-generate-models-yaml
zfengine-generate-migrations-models
migrate
При создании изменений миграции: ...
Генерируем модели по YAML схемам
Все также как в предыдущих примерах, только команда с префиксом zfengine:$ ./application/sripts/doctrine zfengine-generate-models-yaml
Generated models for module "Products" successfully
Generated models for module "Users" successfully
Generated models finished
Получаем готовые модели:./application/modules/users/
|-- models
|-- Base
| `-- User.php
|-- User.php
`-- UserTable.php
Только теперь модели именуются согласно стандартам ZF и подгружаются родным автозагрузчиком:BaseUser → Users_Model_Base_User
User → Users_Model_User
Сгенерированые модели
Между моделями из разных модулей сгенерировались связи:
./application/modules/users/models/Base/User.php<?php ...
public function setUp() {
$this->hasMany('Products_Model_Product as Products', array(
'local' => 'id', 'foreign' => 'user_id'));
}
./application/modules/products/models/Base/Product.php
<?php ...
public function setUp() {
$this->hasOne('Users_Model_User as User', array(
'local' => 'user_id','foreign' => 'id',
'onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE'));
}
При работе с моделью пользователя коллекция моделей продуктов будет подгружена только при необходимости. Например при получении всех продуктов пользователя:
$products = $user->Products;
Миграции
Сгенерируем миграции:Первую миграцию (на новом проекте) делаем через:
$ ./application/sripts/doctrine zfengine-generate-migrations-models
Так миграции генерируются на основании существующих классов моделей, а последующие — уже на основании изменений в yaml-схемах командой:
$ ./application/sripts/doctrine zfengine-generate-migrations-diff
И накатываем миграции на базу:$ ./application/sripts/doctrine migrate
migrate - migrated successfully to version #3
Структура таблицы `products`
Смотрим, что получилось в БД:mysql> SHOW CREATE TABLE `products`;
CREATE TABLE `products` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned DEFAULT NULL,
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`description` text COLLATE utf8_unicode_ci,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `products_user_id_users_id` (`user_id`),
CONSTRAINT `products_user_id_users_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci