doctrine 2
DESCRIPTION
Валерий Рабиевский Team Leader, stfalcon.comTRANSCRIPT
Doctrine 2
Who am I?
Валерий РабиевскийTeam lead веб-студии stfalcon.com
Активный разработчик Open Source движка ZFEngine (ZF + Doctrine)
Более 4 лет опыта работы с PHP
Почему?
Doctrine 2
Библиотеки
— Common— DBAL (включает Common)— ORM (включает DBAL+Common)
— Migrations (часть DBAL)
— Object Document Mapper: MongoDB CouchDB
github.com/doctrine
Entities
— Легкая модель (самый простой PHP класс)
— Не нужно наследование от базового класса
— Есть возможность наследовать модель от своих базовых классов
— Конструктор модели можно использовать для своих нужд
— Данные хранятся непосредственно в свойствах объекта
Пример модели
namespace Entities; class User { private $id; private $name;
public function getId() { return $this->id; }
public function getName() { return $this->name; }
public function setName($name) { $this->name = $name; } }
namespace Entities; class User { private $id; private $name;
public function getId() { return $this->id; }
public function getName() { return $this->name; }
public function setName($name) { $this->name = $name; } }
EntityManager
EntityManager является центральной точкой доступа к функциям ORM
— Управление обновлением сущностей
— Доступ к репозиториям сущностей
— Используется паттерн UnitOfWork
ZF2 + D2
protected function _initAutoload(){ $loader = new \Zend\Loader\StandardAutoloader(); $loader->registerNamespace('Doctrine', '/path/to/Doctrine'); $loader->registerNamespace('Symfony', '/path/to/Symfony'); $loader->register();}
protected function _initAutoload(){ $loader = new \Zend\Loader\StandardAutoloader(); $loader->registerNamespace('Doctrine', '/path/to/Doctrine'); $loader->registerNamespace('Symfony', '/path/to/Symfony'); $loader->register();}
Добавляем автозагрузку в Bootstrap
Настройка Doctrine 2
./application/configs/application.xml
<!-- production --><doctrine> <connection><!-- user, password, database, etc --></connection> <paths>
<entities>path/to/entities</entities><proxies>path/to/proxies</proxies>
</paths> <proxiesNamespace value="Application\Model\Proxies" /> <autogenerateProxyClasses value="0" /> <cacheAdapter value="Doctrine\Common\Cache\ApcCache" /></doctrine>
<!-- development -->... <autogenerateProxyClasses value="1" /> <cacheAdapter value="Doctrine\Common\Cache\ArrayCache" />…
<!-- production --><doctrine> <connection><!-- user, password, database, etc --></connection> <paths>
<entities>path/to/entities</entities><proxies>path/to/proxies</proxies>
</paths> <proxiesNamespace value="Application\Model\Proxies" /> <autogenerateProxyClasses value="0" /> <cacheAdapter value="Doctrine\Common\Cache\ApcCache" /></doctrine>
<!-- development -->... <autogenerateProxyClasses value="1" /> <cacheAdapter value="Doctrine\Common\Cache\ArrayCache" />…
Подключение EntityManager
protected function _initEntityManager() { if (is_null($this->_em)) { $options = $this->getOption('doctrine'); $cache = new $options['cacheAdapter']; $config = new Configuration(); $driverImpl = $config ->newDefaultAnnotationDriver($options['paths']['entities']); $config->setMetadataCacheImpl($cache); $config->setMetadataDriverImpl($driverImpl); $config->setQueryCacheImpl($cache); $config->setProxyNamespace($options['proxiesNamespace']); $config->setProxyDir($options['paths']['proxies']); $config->setAutoGenerateProxyClasses( $options['autogenerateProxyClasses'] ); $this->_em = EntityManager::create($options['connection'], $config); } return $this->_em; }
protected function _initEntityManager() { if (is_null($this->_em)) { $options = $this->getOption('doctrine'); $cache = new $options['cacheAdapter']; $config = new Configuration(); $driverImpl = $config ->newDefaultAnnotationDriver($options['paths']['entities']); $config->setMetadataCacheImpl($cache); $config->setMetadataDriverImpl($driverImpl); $config->setQueryCacheImpl($cache); $config->setProxyNamespace($options['proxiesNamespace']); $config->setProxyDir($options['paths']['proxies']); $config->setAutoGenerateProxyClasses( $options['autogenerateProxyClasses'] ); $this->_em = EntityManager::create($options['connection'], $config); } return $this->_em; }
Mapping
Basic Mapping
— Docblock Annotations
— XML
— YAML
— PHP
Association Mapping
— One-To-One & Many-To-Many:— Unidirectional— Bidirectional— Self-referencing
— Many-To-One, Unidirectional— One-To-Many:
— Unidirectional with Join Table— Bidirectional— Self-referencing
Inheritance Mapping
— Mapped Superclasses
— Single Table Inheritance
— Class Table Inheritance
Mapping
... /** * @ManyToOne(targetEntity="Address", inversedBy="users") * @JoinColumn(name="address_id", referencedColumnName="id") */ private $address; ...
... /** * @ManyToOne(targetEntity="Address", inversedBy="users") * @JoinColumn(name="address_id", referencedColumnName="id") */ private $address; ...
... /** @OneToMany(targetEntity="User", mappedBy="address") */ private $user; ...
... /** @OneToMany(targetEntity="User", mappedBy="address") */ private $user; ...
Entities/User
Entitites/Address
Console
Console
... $em = $application->getBootstrap()->getResource('EntityManager'); ... $helpers = array( 'db' => new DBAL\Helper\ConnectionHelper($em->getConnection()), 'em' => new ORM\Helper\EntityManagerHelper($em), 'dialog' => new \Symfony\Component\Console\Helper\DialogHelper(), ); ... $cli = new \Symfony\Component\Console\Application( 'Doctrine Command Line Interface', Doctrine\Common\Version::VERSION); $cli->setCatchExceptions(true); ... $cli->addCommands(array( new DBAL\Command\RunSqlCommand(), new ORM\Command\ValidateSchemaCommand(), new Migrations\Command\VersionCommand() )); $cli->run();
... $em = $application->getBootstrap()->getResource('EntityManager'); ... $helpers = array( 'db' => new DBAL\Helper\ConnectionHelper($em->getConnection()), 'em' => new ORM\Helper\EntityManagerHelper($em), 'dialog' => new \Symfony\Component\Console\Helper\DialogHelper(), ); ... $cli = new \Symfony\Component\Console\Application( 'Doctrine Command Line Interface', Doctrine\Common\Version::VERSION); $cli->setCatchExceptions(true); ... $cli->addCommands(array( new DBAL\Command\RunSqlCommand(), new ORM\Command\ValidateSchemaCommand(), new Migrations\Command\VersionCommand() )); $cli->run();
Console
$ ./doctrine Doctrine Command Line Interface version 2.0.0RC3-DEV Usage: [options] command [arguments] dbal :import :run-sql orm :convert-d1-schema :convert-mapping :generate-proxies :generate-repositories :run-dql :validate-schema orm:clear-cache :metadata :query :result
$ ./doctrine Doctrine Command Line Interface version 2.0.0RC3-DEV Usage: [options] command [arguments] dbal :import :run-sql orm :convert-d1-schema :convert-mapping :generate-proxies :generate-repositories :run-dql :validate-schema orm:clear-cache :metadata :query :result
Console: ORM
$ ./doctrine orm:ensure-production-settings Proxy Classes are always regenerating.
$ ./doctrine orm:ensure-production-settings SQLSTATE[28000] [1045] Access denied for user 'root'@'localhost'
$ ./doctrine orm:ensure-production-settings Environment is correctly configured for production.
$ ./doctrine orm:ensure-production-settings Proxy Classes are always regenerating.
$ ./doctrine orm:ensure-production-settings SQLSTATE[28000] [1045] Access denied for user 'root'@'localhost'
$ ./doctrine orm:ensure-production-settings Environment is correctly configured for production.
Проверка корректности настроек для production
Console: ORM
Валидация модели
$ ./doctrine orm:validate-schema
[Mapping] FAIL - The entity-class 'Entities\Address' mapping is invalid: * The field Entities\Address#user is on the inverse side of a bi-directional Relationship, but the specified mappedBy association on the target-entity Entities\User#address does not contain the required 'inversedBy' attribute.
[Database] FAIL - The database schema is not in sync with the current mapping file.
$ ./doctrine orm:validate-schema
[Mapping] FAIL - The entity-class 'Entities\Address' mapping is invalid: * The field Entities\Address#user is on the inverse side of a bi-directional Relationship, but the specified mappedBy association on the target-entity Entities\User#address does not contain the required 'inversedBy' attribute.
[Database] FAIL - The database schema is not in sync with the current mapping file.
Migrations
Migrations
Что нужно:— стандартный скрипт для подключения консоли— в папку с скриптом добавить migrations.xml (или yaml)
<doctrine-migrations> <name>Doctrine Migrations</name> <migrations-namespace>
DoctrineMigrations </migrations-namespace> <table name="migration_versions" /> <migrations-directory>/path/to/migrations/</migrations-directory></doctrine-migrations>
<doctrine-migrations> <name>Doctrine Migrations</name> <migrations-namespace>
DoctrineMigrations </migrations-namespace> <table name="migration_versions" /> <migrations-directory>/path/to/migrations/</migrations-directory></doctrine-migrations>
Migrations
Доступные команды
$ ./doctrine ... migrations :diff :generate :status :execute :migrate :version ...
$ ./doctrine ... migrations :diff :generate :status :execute :migrate :version ...
Migrations
Фиксируем изменения в миграции
$ ./doctrine migrations:diff Generated new migration class to "path/to/migrations/Version20101124201328.php" from schema differences.
$ ./doctrine migrations:diff Generated new migration class to "path/to/migrations/Version20101124201328.php" from schema differences.
namespace DoctrineMigrations; class Version20101124201328 extends AbstractMigration { public function up(Schema $schema) { $this->_addSql('CREATE TABLE users (...) ENGINE = InnoDB'); }
public function down(Schema $schema) { $this->_addSql('DROP TABLE users'); } }
namespace DoctrineMigrations; class Version20101124201328 extends AbstractMigration { public function up(Schema $schema) { $this->_addSql('CREATE TABLE users (...) ENGINE = InnoDB'); }
public function down(Schema $schema) { $this->_addSql('DROP TABLE users'); } }
Migrations
Накатывание миграции
$ ./doctrine migrations:migrate --dry-run
Executing dry run of migration up to 20101124201328 from 0
++ migrating 20101124201328
-> CREATE TABLE users ( ... ) ENGINE = InnoDB
++ migrated (0.01s)
------------------------
++ finished in 0.01 ++ 1 migrations executed ++ 1 sql queries
$ ./doctrine migrations:migrate --dry-run
Executing dry run of migration up to 20101124201328 from 0
++ migrating 20101124201328
-> CREATE TABLE users ( ... ) ENGINE = InnoDB
++ migrated (0.01s)
------------------------
++ finished in 0.01 ++ 1 migrations executed ++ 1 sql queries
Migrations
Генерируем заготовку миграции
$ ./doctrine migrations:generate --editor-cmd=netbeans Generated new migration class to "path/to/migrations/Version20101124201328.php"
$ ./doctrine migrations:generate --editor-cmd=netbeans Generated new migration class to "path/to/migrations/Version20101124201328.php"
namespace DoctrineMigrations; class Version20101124201328 extends AbstractMigration { public function up(Schema $schema) { // $this->_addSql('CREATE TABLE users (...) ENGINE = InnoDB'); $table = $schema->createTable('users'); $table->addColumn('username', 'string'); }
public function down(Schema $schema) { $schema->dropTable('users'); } }
namespace DoctrineMigrations; class Version20101124201328 extends AbstractMigration { public function up(Schema $schema) { // $this->_addSql('CREATE TABLE users (...) ENGINE = InnoDB'); $table = $schema->createTable('users'); $table->addColumn('username', 'string'); }
public function down(Schema $schema) { $schema->dropTable('users'); } }
Использование
Пример работы с моделями
$em = $this->getInvokeArg('bootstrap') ->getResource('EntityManager');
$address = new Entities\Address(); $address->setStreet('Киевская, 1');
$user = new Entities\User(); $user->setName('Ваня'); $user->setAddress($address);
$em->persist($address); $em->persist($user); $em->flush();
$em = $this->getInvokeArg('bootstrap') ->getResource('EntityManager');
$address = new Entities\Address(); $address->setStreet('Киевская, 1');
$user = new Entities\User(); $user->setName('Ваня'); $user->setAddress($address);
$em->persist($address); $em->persist($user); $em->flush();
Использование
Пример работы с моделями
$user = $em->find('Entities\User', 1); $user->getAddress(); // → object Proxies\EntitiesAddressProxy
$user->getName(); // Ваня $user->setName('Петя');
$em->flush();
...
$user = $em->find('Entities\User', 1); $user->getName(); // Петя
$user = $em->find('Entities\User', 1); $user->getAddress(); // → object Proxies\EntitiesAddressProxy
$user->getName(); // Ваня $user->setName('Петя');
$em->flush();
...
$user = $em->find('Entities\User', 1); $user->getName(); // Петя
Doctrine Query Language
Doctrine 1— Не было реального парсера DQL
Doctrine 2— Abstract Syntax Tree
Behaviors
Behaviors
Нет и не будет расширений «из коробки»
Events & Subscribers
+−
Events
namespace Entities;
/** * @HasLifecycleCallbacks */ class User { … /** @PrePersist */ public function updateCreatedAt() { $this->createdAt = date('Y-m-d H:m:s'); } }
namespace Entities;
/** * @HasLifecycleCallbacks */ class User { … /** @PrePersist */ public function updateCreatedAt() { $this->createdAt = date('Y-m-d H:m:s'); } }
Lifecycle Events
— pre/postRemove
— pre/postPersist
— pre/postUpdate
— postLoad
— loadClassMetadata
— onFlush
Event Listeners
Простейший подписчик на события
class MyEventSubscriber implements EventSubscriber { public function getSubscribedEvents() { return array( Events::preUpdate ); } public function preUpdate(PreUpdateEventArgs $eventArgs) { if ($eventArgs->getEntity() instanceof User) { if ($eventArgs->hasChangedField('name')) { /*наш код*/ } } } }
$entityManager->getEventManager() ->addEventSubscriber(new MyEventSubscriber());
class MyEventSubscriber implements EventSubscriber { public function getSubscribedEvents() { return array( Events::preUpdate ); } public function preUpdate(PreUpdateEventArgs $eventArgs) { if ($eventArgs->getEntity() instanceof User) { if ($eventArgs->hasChangedField('name')) { /*наш код*/ } } } }
$entityManager->getEventManager() ->addEventSubscriber(new MyEventSubscriber());
Behavioral Extensions
goo.gl/Mgnwg(www.doctrine-project.org/blog/doctrine2-behavioral-extensions)
... /** * @gedmo:Timestampable(on="create") * @Column(type="date") */ private $created;
/** * @gedmo:Timestampable(on="update") * @Column(type="datetime") */ private $updated; ...
... /** * @gedmo:Timestampable(on="create") * @Column(type="date") */ private $created;
/** * @gedmo:Timestampable(on="update") * @Column(type="datetime") */ private $updated; ...
Репликация
Doctrine 1
$connections = array( 'master' => 'mysql://root:@master/dbname', 'slave_1' => 'mysql://root:@slave1/dbname', 'slave_2' => 'mysql://root:@slave2/dbname',);
foreach ($connections as $name => $dsn) { Doctrine_Manager::connection($dsn, $name);}
$connections = array( 'master' => 'mysql://root:@master/dbname', 'slave_1' => 'mysql://root:@slave1/dbname', 'slave_2' => 'mysql://root:@slave2/dbname',);
foreach ($connections as $name => $dsn) { Doctrine_Manager::connection($dsn, $name);}
Репликация
Doctrine 1
Doctrine 2
:(
$connections = array( 'master' => 'mysql://root:@master/dbname', 'slave_1' => 'mysql://root:@slave1/dbname', 'slave_2' => 'mysql://root:@slave2/dbname',);
foreach ($connections as $name => $dsn) { Doctrine_Manager::connection($dsn, $name);}
$connections = array( 'master' => 'mysql://root:@master/dbname', 'slave_1' => 'mysql://root:@slave1/dbname', 'slave_2' => 'mysql://root:@slave2/dbname',);
foreach ($connections as $name => $dsn) { Doctrine_Manager::connection($dsn, $name);}
Репликация
В Doctrine 2 все действия с моделью происходят через EntityManager
Значит можно:— создать несколько EM на каждое
подключение;— расширить стандартный EM поддержкой
репликаций;
Предпоследняя
Исходный код моих экспериментов с ZF2 и Doctrine 2 скоро появится на GitHub'e:
github.com/ftrrtf
Спасибо за внимание!
Есть вопросы?
Валерий Рабиевский[email protected]
twitter.com/ftrrtffacebook.com/ftrrtf