Date post: | 10-May-2015 |
Category: |
Technology |
Upload: | ross-tuck |
View: | 11,611 times |
Download: | 2 times |
Ross Tuck
Extending Doctrine 2For Your Domain Model
June 9th, DPC
Who Am I?
Ross Tuck
Team Lead at IbuildingsCodemonkeyREST nutHat guyToken Foreigner
America, &@*! Yeah
@rosstuck#dashinglyHandsome
Quick But Necessary
Doctrine 101
Doctrine 102
Doctrine 102.5
/** @Entity */
class Article {
Model
/** @Id @GeneratedValue
* @Column(type="integer") */
protected $id;
/** @Column(type="string") */
protected $title;
/** @Column(type="text") */
protected $content;
}
$article = $em->find('Article', 1);
$article->setTitle('Awesome New Story');
$em->flush();
Controller
Filters
We need a way to approve comments before they appear to all users.
TPS Request
$comments = $em->getRepository('Comment')->findAll();
foreach($comments as $comment) {
echo $comment->getContent()."\n";
}
Controller
Output:Doug isn't humanDoug is the best
$comments = $em->getRepository('Comment')->findAll();
foreach($comments as $comment) {
echo $comment->getContent()."\n";
}
Controller
$comments = $em->createQuery('SELECT c FROM Comment c');
foreach($comments->getResult() as $comment) {
echo $comment->getContent()."\n";
}
Controller
$comments = $em->find('Article', 1)->getComments();
foreach($comments as $comment) {
echo $comment->getContent()."\n";
}
Controller
Approx 100 places in your code!
FiltersNew in 2.2
use Doctrine\ORM\Query\Filter\SQLFilter;
class CommentFilter extends SQLFilter {
public function addFilterConstraint($entityInfo, $alias) {
if ($entityInfo->name !== 'Comment') {
return "";
}
return $alias.".approved = 1";
}
}
Filter
$em->getConfiguration()
->addFilter('approved_comments', 'CommentFilter');
Bootstrap
Handy key
Class name
$em->getConfiguration()
->addFilter('approved_comments', 'CommentFilter');
$em->getFilters()->enable('approved_comments');
Bootstrap
if ($this->isNormalUser()) {
$em->getConfiguration()
->addFilter('approved_comments', 'CommentFilter');
$em->getFilters()->enable('approved_comments');
}
Bootstrap
$comments = $em->getRepository('Comment')->findAll();
foreach($comments as $comment) {
echo $comment->getContent()."\n";
}
Controller
As Normal User
Output:Doug is the best
Controller
As the AdminOutput:
Doug isn't humanDoug is the best
$comments = $em->getRepository('Comment')->findAll();
foreach($comments as $comment) {
echo $comment->getContent()."\n";
}
Parameters
$filter = $em->getFilters()->getFilter('approved_comments');
$filter->setParameter('level', $this->getUserLevel());
Bootstrap
use Doctrine\ORM\Query\Filter\SQLFilter;
class CommentFilter extends SQLFilter {
public function addFilterConstraint($entityInfo, $alias) {
if ($entityInfo->name !== 'Comment') {
return "";
}
$level = $this->getParameter('level');
return $alias.".approved = 1";
}
}
Filter
use Doctrine\ORM\Query\Filter\SQLFilter;
class CommentFilter extends SQLFilter {
public function addFilterConstraint($entityInfo, $alias) {
if ($entityInfo->name !== 'Comment') {
return "";
}
$level = $this->getParameter('level');
return $alias.".approved = ".$level;
}
}
Filter
use Doctrine\ORM\Query\Filter\SQLFilter;
class CommentFilter extends SQLFilter {
public function addFilterConstraint($entityInfo, $alias) {
if ($entityInfo->name !== 'Comment') {
return "";
}
$level = $this->getParameter('level');
return $alias.".approved = ".$level;
}
}
Filter
Already escaped
Limitations
Events
prePersistpostPersistpreUpdatepostUpdatepreRemovepostRemove
postLoadloadClassMetadatapreFlushonFlushpostFlushonClear
Insert
Callbacks Listeners
On the model External objects
Lifecycle Callbacks
/** @Entity */
class Article {
Model
/** @Id @GeneratedValue
* @Column(type="integer") */
protected $id;
/** @Column(type="string") */
protected $title;
/** @Column(type="text") */
protected $content;
}
$article = $em->find('Article', 1);
$article->setTitle('Awesome New Story');
$em->flush();
Controller
Articles must record the last date they were modified in any way.
TPS Request
$article = $em->find('Article', 1);
$article->setTitle('Awesome New Story');
$article->setUpdatedAt(new DateTime('now'));
$em->flush();
Controller
+ 100 other files+ testing+ missed bugs
/** @Entity */
class Article {
Model
/** @Id @GeneratedValue
* @Column(type="integer") */
protected $id;
/** @Column(type="string") */
protected $title;
}
/** @Entity */
class Article {
Model
/** @Id @GeneratedValue
* @Column(type="integer") */
protected $id;
/** @Column(type="string") */
protected $title;
/** @Column(type="datetime") */
protected $updatedAt;
}
/** @Entity */
class Article {
Model
// ...
public function markAsUpdated() {
$this->updatedAt = new \Datetime('now');
}
}
/** @Entity @HasLifecycleCallbacks */
class Article {
Model
// ...
/** @PreUpdate */
public function markAsUpdated() {
$this->updatedAt = new \Datetime('now');
}
}
Event mapping
$article = $em->find('Article', 1);
$article->setTitle('Awesome New Story');
$em->flush();
echo $article->getUpdatedAt()->format('c');
// 2012-05-29T10:48:00+02:00
Controller
NoChanges!
/** @Entity @HasLifecycleCallbacks */
class Article {
Model
// ...
/** @PreUpdate */
public function markAsUpdated() {
$this->updatedAt = new \Datetime('now');
}
}
What else can I do?
prePersistpostPersistpreUpdatepostUpdatepreRemovepostRemove
postLoadloadClassMetadatapreFlushonFlushpostFlushonClear
prePersistpostPersistpreUpdatepostUpdatepreRemovepostRemove
postLoadloadClassMetadatapreFlushonFlushpostFlushonClear
Protip:Only fires if you're dirty
new MudWrestling();
$article = $em->find('Article', 1);
$article->setTitle('Awesome New Story');
$em->flush();
// 2012-05-29T10:48:00+02:00
// 2012-05-29T10:48:00+02:00
Controller
2X
$article = $em->find('Article', 1);
$article->setTitle('Awesome New Story');
$em->flush();
Controller
'Awesome New Story' === 'Awesome New Story'
No update
$article = $em->find('Article', 1);
$article->setTitle('Fantabulous Updated Story');
$em->flush();
Controller
'Awesome New Story' !== 'Fantabulous Updated Story'
Update, yeah!
$article = $em->find('Article', 1);
$article->setTitle('Awesome New Project'.uniqid());
$em->flush();
Controller
So, here's the thing.
The Thing
Events are cool.
Callbacks?
Eeeeeeeeeeeeeeeeeeh.
• Limited to model dependencies• Do you really need that function?• Protected function? Silent error• Repeated Code• Performance implications
Listeners
class UpdateTimeListener {
Listener
public function preUpdate($event) {
}
}
$em->getEventManager()->addEventListener(
array(Doctrine\ORM\Events::preUpdate),
new UpdateTimeListener()
);
Bootstrap
Invert
Invert
$em->getEventManager()->addEventListener(
array(Doctrine\ORM\Events::preUpdate),
new UpdateTimeListener()
);
Bootstrap
$em->getEventManager()->addEventSubscriber(
new UpdateTimeListener()
);
Bootstrap
class UpdateTimeListener {
Listener
public function preUpdate($event) {
}
public function getSubscribedEvents() {
return array(Events::preUpdate);
}
}
use Doctrine\Common\EventSubscriber;
class UpdateTimeListener implements EventSubscriber {
Listener
public function preUpdate($event) {
}
public function getSubscribedEvents() {
return array(Events::preUpdate);
}
}
Functionally?Design?
No differenceSubscriber
$em->getEventManager()->addEventSubscriber(
new ChangeMailListener()
);
Bootstrap
$em->getEventManager()->addEventSubscriber(
new ChangeMailListener($mailer)
);
Bootstrap
class UpdateTimeListener implements EventSubscriber {
Listener
public function getSubscribedEvents() {
return array(Events::preUpdate);
}
public function preUpdate($event) {
}
}
public function preUpdate($event) {
Listener
$em = $event->getEntityManager();
$model = $event->getEntity();
if (!$model instanceof Article) { return; }
$model->setUpdatedAt(new \Datetime('now'));
$uow = $event->getEntityManager()->getUnitOfWork();
$uow->recomputeSingleEntityChangeSet(
$em->getClassMetadata('Article'),
$model
);
}
$article = $em->find('Article', 1);
$article->setTitle('Awesome New Story');
$em->flush();
echo $article->getUpdatedAt()->format('c');
// 2012-05-29T10:48:00+02:00
Controller
Whoopity-doo
Theory Land
interface LastUpdatedInterface {
public function setUpdatedAt(\Datetime $date);
public function getUpdatedAt();
}
public function preUpdate($event) {
Listener
$em = $event->getEntityManager();
$model = $event->getEntity();
if (!$model instanceof Article) { return; }
$model->setUpdatedAt(new \Datetime('now'));
$uow = $event->getEntityManager()->getUnitOfWork();
$uow->recomputeSingleEntityChangeSet(
$em->getClassMetadata('Article'),
$model
);
}
public function preUpdate($event) {
Listener
$em = $event->getEntityManager();
$model = $event->getEntity();
if (!$model instanceof LastUpdatedInterface) { return; }
$model->setUpdatedAt(new \Datetime('now'));
$uow = $event->getEntityManager()->getUnitOfWork();
$uow->recomputeSingleEntityChangeSet(
$em->getClassMetadata('Article'),
$model
);
}
public function preUpdate($event) {
Listener
$em = $event->getEntityManager();
$model = $event->getEntity();
if (!$model instanceof LastUpdatedInterface) { return; }
$model->setUpdatedAt(new \Datetime('now'));
$uow = $event->getEntityManager()->getUnitOfWork();
$uow->recomputeSingleEntityChangeSet(
$em->getClassMetadata(get_class($model)),
$model
);
}
The POWAH
Flushing And Multiple Events
OnFlush
After every update to an article, I want an email of the changes sent to me.
Also, bring me more lettuce.
TPS Request
Listener
class ChangeMailListener implements EventSubscriber {
protected $mailMessage;
public function getSubscribedEvents() {
return array(Events::onFlush, Events::postFlush);
}
}
Listener
public function onFlush($event) {
$uow = $event->getEntityManager()->getUnitOfWork();
foreach($uow->getScheduledEntityUpdates() as $model) {
$changeset = $uow->getEntityChangeSet($model);
}
}
array(1) {
["title"]=>
array(2) {
[0]=> string(16) "Boring Old Title"
[1]=> string(16) "Great New Title!"
}
}
Listener
public function onFlush($event) {
$uow = $event->getEntityManager()->getUnitOfWork();
foreach($uow->getScheduledEntityUpdates() as $model) {
$changeset = $uow->getEntityChangeSet($model);
$this->formatAllPrettyInMail($model, $changeset);
}
}
Listener
public function onFlush($event) {
$uow = $event->getEntityManager()->getUnitOfWork();
foreach($uow->getScheduledEntityUpdates() as $model) {
$changeset = $uow->getEntityChangeSet($model);
$this->formatAllPrettyInMail($model, $changeset);
}
}
public function postFlush($event) {
if (!$this->hasMessage()) { return; }
$this->mailTheBoss($this->message);
}
That's really all there is to it.
Listener
public function formatAllPrettyInMail($model, $changes) {
if (!$model instanceof Article) {
return;
}
$msg = "";
foreach($changes as $field => $values) {
$msg .= "{$field} ----- \n".
"old: {$values[0]} \n".
"new: {$values[1]} \n\n";
}
$this->mailMessage .= $msg;
}
Advice:Treat your listeners like controllers
Keep it thin
Crazy Town
Write the change messages to the database instead of emailing them.
TPS Request
Model
/** @Entity */
class ChangeLog {
/** @Id @GeneratedValue
* @Column(type="integer") */
protected $id;
/** @Column(type="text") */
protected $description;
}
Listener
class ChangeLogger implements EventSubscriber {
public function getSubscribedEvents() {
return array(Events::onFlush);
}
public function onFlush($event) {
}
}
Listener::onFlush
public function onFlush($event) {
$em = $event->getEntityManager();
$uow = $em->getUnitOfWork();
foreach($uow->getScheduledEntityUpdates() as $model) {
if (!$model instanceof Article) { continue; }
$log = new ChangeLog();
$log->setDescription($this->meFormatPrettyOneDay());
$em->persist($log);
$uow->computeChangeSet(
$em->getClassMetadata('ChangeLog'), $log
);
}
Shiny
Yes, I really used PHPMyAdmin there
Shiny
Also, wow, it worked!
UnitOfWork API
$uow->getScheduledEntityInsertions();
$uow->getScheduledEntityUpdates();
$uow->getScheduledEntityDeletions();
$uow->getScheduledCollectionUpdates();
$uow->getScheduledCollectionDeletions();
UnitOfWork API
$uow->scheduleForInsert();
$uow->scheduleForUpdate();
$uow->scheduleExtraUpdate();
$uow->scheduleForDelete();
UnitOfWork API
And many more...
postLoad
/** @Entity */
class Article {
Model
//...
/** @Column(type="integer") */
protected $viewCount;
}
MySQL Redis
+
Move view counts out of the database into a faster system.
And don't break everything.
TPS Request
$article = $em->find('Article', 1);
echo $article->getViewCount();
Controller
/** @Entity */
class Article {
Model
//...
/** @Column(type="integer") */
protected $viewCount;
}
Listener
class ViewCountListener implements EventSubscriber {
public function getSubscribedEvents() {
return \Doctrine\ORM\Events::postLoad;
}
public function postLoad($event) {
$model = $event->getEntity();
if (!$model instanceof Article) {
return;
}
$currentRank = $this->getCountFromRedis($model);
$model->setViewCount($currentRank);
}
}
$article = $em->find('Article', 1);
echo $article->getViewCount();
Controller
Many, many other uses.
Class Metadata
/** @Entity */
class Article {
Model
/** @Id @GeneratedValue
* @Column(type="integer") */
protected $id;
/** @Column(type="string") */
protected $title;
/** @Column(type="datetime") */
protected $updatedAt;
}
Where dothey go?
/** @Entity */
class Article {
Model
/** @Id @GeneratedValue
* @Column(type="integer") */
protected $id;
/** @Column(type="string") */
protected $title;
/** @Column(type="datetime") */
protected $updatedAt;
}
Doctrine\ORM\Mapping\ClassMetadata
???
$metadata = $em->getClassMetadata('Article');
???
$metadata = $em->getClassMetadata('Article');
echo $metadata->getTableName();
Output:Article
???
$metadata = $em->getClassMetadata('Article');
echo $metadata->getTableName();
Output:articles
Doctrine 2.3 will bring NamingStrategy
???
$conn = $em->getConnection();
$tableName = $metadata->getQuotedTableName($conn);
$results = $conn->query('SELECT * FROM '.$tableName);
Tip of the Iceberg
???
array(8) { fieldName => "title" type => "string" length => NULL precision => 0 scale => 0 nullable => false unique => false columnName => "title"}
$metadata->getFieldMapping('title');
???
$metadata->getFieldMapping('title');
$metadata->getFieldNames();
$metadata->getReflectionClass();
$metadata->getReflectionProperty('title');
$metadata->getColumnName('title');
$metadata->getTypeOfField('title');
Simple Example
Symple Example
class ArticleType extends AbstractType {
public function buildForm($builder, array $options) {
$builder
->add('title')
->add('content');
}
}
Form in Symfony2
$entity = new Article();
$form = $this->createForm(new ArticleType(), $entity);
Controller in Symfony2
class ArticleType extends AbstractType {
public function buildForm($builder, array $options) {
$builder
->add('title')
->add('content');
}
}
Form in Symfony2
Symfony Doctrine Bridge
switch ($metadata->getTypeOfField($property)) {
case 'string':
return new TypeGuess('text', ...);
case 'boolean':
return new TypeGuess('checkbox', ...);
case 'integer':
case 'bigint':
case 'smallint':
return new TypeGuess('integer', ...);
case 'text':
return new TypeGuess('textarea', ...);
default:
return new TypeGuess('text', ...);
}
Cool, huh?
Cool, huh?
???
array(15) { fieldName => "comments" mappedBy => "article" targetEntity => "Comment" cascade => array(0) {} fetch => 2 type => 4 isOwningSide => false sourceEntity => "Article" ...
$metadata->getAssociationMapping('comments');
???
$metadata->getAssociationMapping('comments');
$metadata->getAssociationMappings();
$metadata->isSingleValuedAssociation('comments');
$metadata->isCollectionValuedAssociation('comments');
$metadata->getAssociationTargetClass('comments');
class ArticleType extends AbstractType {
public function buildForm($builder, array $options) {
$builder
->add('title')
->add('content')
->add('comments');
}
}
Form in Symfony2
Oh yeah.
You can set all of this stuff.
100~ public functions
But is there a good reason?Unless you're writing a driver, probably not.
Listener
class MyListener {
public function loadClassMetadata($event) {
echo $event->getClassMetadata()->getName();
}
}
Where's the manatee?
You're the manatee.
Epilogue & Service Layers
Model
Controller
View
Model
Service Layer
Controller
View
Be smart.
Be smart.
Be simple
Don't overuse this.
Test
Test test test
test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test
RTFM.
Go forth and rock.
You're the manatee.
Thanks to:
@boekkooi#doctrine (freenode)
Wikipedia
OwnMoment (sxc.hu)