doctrine in the real world

93
Doctrine in the Real World Real world examples doctrine Tuesday, February 8, 2011

Upload: jonathan-wage

Post on 06-May-2015

11.214 views

Category:

Documents


5 download

DESCRIPTION

Real world Doctrine examples from http://shopopensky.com and http://sociallynotable.com

TRANSCRIPT

Doctrine in the Real WorldReal world examples

doctrine

Tuesday, February 8, 2011

My name isJonathan H. Wage

Tuesday, February 8, 2011

• PHP Developer for 10+ years

• Long time Symfony and Doctrine contributor

• Published Author

• Entrepreneur

• Currently living in Nashville, Tennessee

Tuesday, February 8, 2011

I work full-time for OpenSky

http://shopopensky.com

Tuesday, February 8, 2011

Previously employed by SensioLabs

Tuesday, February 8, 2011

What is OpenSky?

Tuesday, February 8, 2011

A new way to shop

• OpenSky connects you with innovators, trendsetters and tastemakers. You choose the ones you like and each week they invite you to their private online sales.

Tuesday, February 8, 2011

We Love OpenSource• PHP 5.3

• Apache2

• Symfony2

• Doctrine2

• jQuery

• mule, stomp, hornetq

• MongoDB

• nginx

• varnish

Tuesday, February 8, 2011

We don’t just use open source projects

Tuesday, February 8, 2011

We help build them

Tuesday, February 8, 2011

OpenSky has some of the top committers in Symfony2 and other

projects

Tuesday, February 8, 2011

Symfony2 OpenSky Committers

• 65 Kris Wallsmith

• 52 Jonathan H. Wage

• 36 Jeremy Mikola

• 36 Bulat Shakirzyanov

• 6 Justin Hileman

Tuesday, February 8, 2011

Doctrine MongoDB Committers

• 39 Jonathan H. Wage

• 11 Bulat Shakirzyanov

• 2 Kris Wallsmith

Tuesday, February 8, 2011

MongoDB ODM Committers

• 349 Jonathan H. Wage

• 226 Bulat Shakirzyanov

• 17 Kris Wallsmith

• 13 Steven Surowiec

• 2 Jeremy Mikola

Tuesday, February 8, 2011

Sorry to bore you

Tuesday, February 8, 2011

Moving on to the stuff you came here for

Tuesday, February 8, 2011

OpenSky uses Doctrine ORM and ODM

Tuesday, February 8, 2011

Why?

Tuesday, February 8, 2011

We are an eCommerce site

Tuesday, February 8, 2011

Actions involving commerce need

transactions

Tuesday, February 8, 2011

ORM and MySQL

• Order

• Order\Transaction

• Order\Shipment

Tuesday, February 8, 2011

ODM and MongoDB

• Product

• Seller

• Supplier

• User

• ... basically everything else that is not involving $$$ and transactions

Tuesday, February 8, 2011

Blending the Two

Tuesday, February 8, 2011

Defining our Product Document

Tuesday, February 8, 2011

/** @mongodb:Document(collection="products") */class Product{ /** @mongodb:Id */ private $id;

/** @mongodb:String */ private $title;

public function getId() { return $this->id; }

public function getTitle() { return $this->title; }

public function setTitle($title) { $this->title = $title; }}

Tuesday, February 8, 2011

Defining our Order Entity

Tuesday, February 8, 2011

/** * @orm:Entity * @orm:Table(name="orders") * @orm:HasLifecycleCallbacks */class Order{ /** * @orm:Id @orm:Column(type="integer") * @orm:GeneratedValue(strategy="AUTO") */ private $id;

/** * @orm:Column(type="string") */ private $productId;

/** * @var Documents\Product */ private $product; // ...}

Tuesday, February 8, 2011

Setting the Product

public function setProduct(Product $product){ $this->productId = $product->getId(); $this->product = $product;}

Tuesday, February 8, 2011

• $productId is mapped and persisted

• but $product which stores the Product instance is not a persistent entity property

Tuesday, February 8, 2011

Order has a reference to product?

• How?

• Order is an ORM entity stored in MySQL

• and Product is an ODM document stored in MongoDB

Tuesday, February 8, 2011

Loading Product ODM reference in Order

Entity

Tuesday, February 8, 2011

Lifecycle Events to the Rescue

Tuesday, February 8, 2011

EventManager

• Event system is controlled by the EventManager

• Central point of event listener system

• Listeners are registered on the manager

• Events are dispatched through the manager

Tuesday, February 8, 2011

$eventListener = new OrderPostLoadListener($dm);$eventManager = $em->getEventManager();$eventManager->addEventListener( array(\Doctrine\ORM\Events::postLoad), $eventListener);

Add EventListener

Tuesday, February 8, 2011

In Symfony2 DI

<?xml version="1.0" encoding="utf-8" ?><container xmlns="http://www.symfony-project.org/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">

<parameters> <parameter key="order.post_load.listener.class">OrderPostLoadListener</parameter> </parameters>

<services> <service id="order.post_load.listener" class="%order.post_load.listener.class%" scope="container"> <argument type="service" id="doctrine.odm.mongodb.default_document_manager" /> <tag name="doctrine.orm.default_event_listener" event="postLoad" /> </service> </services></container>

Tuesday, February 8, 2011

OrderPostLoadListeneruse Doctrine\ODM\MongoDB\DocumentManager;use Doctrine\ORM\Event\LifecycleEventArgs;

class OrderPostLoadListener{ public function __construct(DocumentManager $dm) { $this->dm = $dm; }

public function postLoad(LifecycleEventArgs $eventArgs) { // get the order entity $order = $eventArgs->getEntity();

// get odm reference to order.product_id $productId = $order->getProductId(); $product = $this->dm->getReference('MyBundle:Document\Product', $productId);

// set the product on the order $em = $eventArgs->getEntityManager(); $productReflProp = $em->getClassMetadata('MyBundle:Entity\Order') ->reflClass->getProperty('product'); $productReflProp->setAccessible(true); $productReflProp->setValue($order, $product); }}

Tuesday, February 8, 2011

All Together Now// Create a new product and order$product = new Product();$product->setTitle('Test Product');$dm->persist($product);$dm->flush();

$order = new Order();$order->setProduct($product);$em->persist($order);$em->flush();

// Find the order later$order = $em->find('Order', $order->getId());

// Instance of an uninitialized product proxy$product = $order->getProduct();

// Initializes proxy and queries the monogodb databaseecho "Order Title: " . $product->getTitle();print_r($order);

Tuesday, February 8, 2011

Seamless

• Documents and Entities play together like best friends

• Because Doctrine persistence remains transparent from your domain this is possible

Tuesday, February 8, 2011

Order Object( [id:Entities\Order:private] => 53 [productId:Entities\Order:private] => 4c74a1868ead0ed7a9000000 [product:Entities\Order:private] => Proxies\DocumentProductProxy Object ( [__isInitialized__] => 1 [id:Documents\Product:private] => 4c74a1868ead0ed7a9000000 [title:Documents\Product:private] => Test Product )

)

print_r($order)

Tuesday, February 8, 2011

MongoDB ODM SoftDelete Functionality

Tuesday, February 8, 2011

I like my deletes soft, not hard

Tuesday, February 8, 2011

Why?

Tuesday, February 8, 2011

Deleting data is dangerous business

Tuesday, February 8, 2011

Flickr accidentally deleted a pro members

account and 4000 pictures

Tuesday, February 8, 2011

They were able to restore it later but it

took some time

Tuesday, February 8, 2011

Instead of deleting, simply set a deletedAt field

Tuesday, February 8, 2011

Install SoftDelete Extension for Doctrine

MongoDB ODM

$ git clone git://github.com/doctrine/mongodb-odm-softdelete src/vendor/doctrine-mongodb-odm-softdelete

http://github.com/doctrine/mongodb-odm-softdelete

Tuesday, February 8, 2011

Autoload Extension

$loader = new UniversalClassLoader();$loader->registerNamespaces(array( // ... 'Doctrine\\ODM\\MongoDB\\SoftDelete' => __DIR__.'/vendor/doctrine-mongodb-odm-softdelete/lib',));$loader->register();

Tuesday, February 8, 2011

use Doctrine\ODM\MongoDB\SoftDelete\Configuration;

use Doctrine\ODM\MongoDB\SoftDelete\UnitOfWork;use Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteManager;use Doctrine\Common\EventManager;

// $dm is a DocumentManager instance we should already have

$config = new Configuration();$uow = new UnitOfWork($dm, $config);$evm = new EventManager();$sdm = new SoftDeleteManager($dm, $config, $uow, $evm);

Raw PHP Configuration

Tuesday, February 8, 2011

Symfony2 Integrationhttp://github.com/doctrine/mongodb-odm-softdelete-bundle

$ git clone git://github.com/doctrine/mongodb-odm-softdelete-bundle.git src/vendor/doctrine-mongodb-odm-softdelete-bundle

Tuesday, February 8, 2011

Autoload the Bundle

$loader = new UniversalClassLoader();$loader->registerNamespaces(array( // ... 'Doctrine\\ODM\\MongoDB\\Symfony\\SoftDeleteBundle' => __DIR__.'/vendor/doctrine-mongodb-odm-softdelete-bundle',));$loader->register();

Tuesday, February 8, 2011

Register the Bundle

public function registerBundles(){ $bundles = array( // ...

// register doctrine symfony bundles new Doctrine\ODM\MongoDB\Symfony\SoftDeleteBundle\SoftDeleteBundle() );

// ...

return $bundles;}

Tuesday, February 8, 2011

Enable the Bundle

// app/config/config.yml

doctrine_mongodb_softdelete.config: ~

Tuesday, February 8, 2011

SoftDeleteManager

$sdm = $container->get('doctrine.odm.mongodb.soft_delete.manager');

Tuesday, February 8, 2011

SoftDeleteable

interface SoftDeleteable{ function getDeletedAt(); function isDeleted();}

ODM Documents must implement this interface

Tuesday, February 8, 2011

User implements SoftDeletable

/** @mongodb:Document */class User implements SoftDeleteable{ /** @mongodb:Date @mongodb:Index */ private $deletedAt;

public function getDeletedAt() { return $this->deletedAt; }

public function isDeleted() { return $this->deletedAt !== null ? true : false; }}

Tuesday, February 8, 2011

SoftDelete a User$user = new User('jwage');// ...$dm->persist($user);$dm->flush();

// later we can soft delete the user jwage$user = $dm->getRepository('User')->findOneByUsername('jwage');$sdm->delete($user);$sdm->flush();

Tuesday, February 8, 2011

Query Executed

db.users.update( { _id : { $in : [new ObjectId('1234567891011123456')] } }, { $set : { deletedAt: new Date() } })

Tuesday, February 8, 2011

Restore a User

// now again later we can restore that same user$user = $dm->getRepository('User')->findOneByUsername('jwage');$sdm->restore($user);$sdm->flush();

Tuesday, February 8, 2011

Query Executed

db.users.update( { _id : { $in : [new ObjectId('1234567891011123456')] } }, { $unset : { deletedAt: true } })

Tuesday, February 8, 2011

Limit cursors to only show non deleted users

$qb = $dm->createQueryBuilder('User') ->field('deletedAt')->exists(false);$query = $qb->getQuery();$users = $query->execute();

Tuesday, February 8, 2011

Get only deleted users

$qb = $dm->createQueryBuilder('User') ->field('deletedAt')->exists(true);$query = $qb->getQuery();$users = $query->execute();

Tuesday, February 8, 2011

Restore several deleted users

$qb = $dm->createQueryBuilder('User') ->field('deletedAt')->exists(true) ->field('createdAt')->gt(new DateTime('-24 hours'));$query = $qb->getQuery();$users = $query->execute();

foreach ($users as $user) { $sdm->restore($user);}$sdm->flush();

Tuesday, February 8, 2011

Soft Delete Events

- preDelete- postDelete- preRestore- postRestore

class TestEventSubscriber implements \Doctrine\Common\EventSubscriber{ public function preSoftDelete(LifecycleEventArgs $args) { $document = $args->getDocument(); $sdm = $args->getSoftDeleteManager(); }

public function getSubscribedEvents() { return array(Events::preSoftDelete); }}

$eventSubscriber = new TestEventSubscriber();$evm->addEventSubscriber($eventSubscriber);

Tuesday, February 8, 2011

Symfony2 and supervisor

http://supervisord.org/

Tuesday, February 8, 2011

What is supervisor?

Tuesday, February 8, 2011

Supervisor is a client/server system that allows its users to monitor and control a number of processes on UNIX-like operating systems.

http://supervisord.org

Tuesday, February 8, 2011

Daemonize a Symfony2 Console Command

with supervisor

Tuesday, February 8, 2011

Scenario

• You want to send an e-mail when new users register in your system.

• But, sending an e-mail directly from your action introduces a failure point to your stack.

• ....What do you do?

Tuesday, February 8, 2011

Tailable Cursor

• Use a tailable mongodb cursor

• Tail a NewUser document collection

• Insert NewUser documents from your actions

• The daemon will instantly process the NewUser after it is inserted and dispatch the e-mail

Tuesday, February 8, 2011

Define NewUsernamespace MyCompany\Bundle\MyBundle\Document;

/** * @mongodb:Document(collection={ * "name"="new_users", * "capped"="true", * "size"="100000", * "max"="1000" * }, repositoryClass="MyCompany\Bundle\MyBundle\Document\NewUserRepository") */class NewUser{ /** @mongodb:Id */ private $id;

/** @mongodb:ReferenceOne(targetDocument="User") */ private $user;

/** @mongodb:Boolean @mongodb:Index */ private $isProcessed = false;

// ...}

Tuesday, February 8, 2011

Create Collection• The NewUser collection must be capped in

order to tail it so we need to create it.

• Luckily, Doctrine has a console command for it.

• It will read the mapping information we configured and create the collection

$ php app/console doctrine:mongodb:schema:create --class="MyBundle:NewUser" --collection

Tuesday, February 8, 2011

Insert NewUser upon Registration

public function register(){ // ...

$user = new User(); $form = new RegisterForm('register', $user, $validator);

$form->bind($request, $user); if ($form->isValid()) { $newUser = new NewUser($user); $dm->persist($newUser); $dm->persist($user); $dm->flush();

// ... } // ...}

Tuesday, February 8, 2011

The Daemon Console Command

• You can find the console command code to use to tail a cursor here:

• https://gist.github.com/812942

Tuesday, February 8, 2011

Executing Console Command

$ php app/console doctrine:mongodb:tail-cursor MyBundle:NewUser findUnProcessed new_user.processor

• The command requires 3 arguments:

• document - the name of the document to tail

• finder - the repository finder method used to get the cursor

• processor - the id of the service used to process the new users

Tuesday, February 8, 2011

findUnProcessed()

• We need the findUnProcessed() method to return the unprocessed cursor to tail

class NewUserRepository extends DocumentRepository{ public function findUnProcessed() { return $this->createQueryBuilder() ->field('isProcessed')->equals(false) ->getQuery() ->execute(); }}

Tuesday, February 8, 2011

NewUserProcessor

use Swift_Message;use Symfony\Component\Console\Output\OutputInterface;

class NewUserProcessor{ private $mailer;

public function __construct($mailer) { $this->mailer = $mailer; }

public function process(OutputInterface $output, $document) { }}

We need a service id new_user.processor with a process(OutputInterface $output, $document) method

Tuesday, February 8, 2011

Send the e-mailpublic function process(OutputInterface $output, $document){ $user = $document->getUser();

$message = Swift_Message::newInstance() ->setSubject('New Registration') ->setFrom('[email protected]') ->setTo($user->getEmail()) ->setBody('New user registration') ; $this->mailer->send($message);

$document->setIsProcessed(true);}

Tuesday, February 8, 2011

Daemonization

• Now, how do we really daemonize the console command and keep it running 24 hours a day, 7 days a week?

• The answer is supervisor, it will allow us to configure a console command for it to manage the process id of and always keep an instance of it running.

Tuesday, February 8, 2011

Install supervisorhttp://supervisord.org/installing.html

$ easy_install supervisor

Tuesday, February 8, 2011

Configure a Profile• We need to configure a profile for supervisor to

know how to run the console command

[program:tail-new-user]numprocs=1

startretries=100directory=/

stdout_logfile=/path/to/symfonyproject/app/tail-new-user-supervisord.logautostart=true

autorestart=trueuser=root

command=/usr/local/bin/php /path/to/symfonyproject/app/console doctrine:mongodb:tail-cursor MyBundle:NewUser

findUnprocessed new_user.processor

[program:tail-new-user]numprocs=1

startretries=100directory=/

stdout_logfile=/path/to/symfonyproject/app/tail-new-user-supervisord.logautostart=true

autorestart=trueuser=root

command=/usr/local/bin/php /path/to/symfonyproject/app/console doctrine:mongodb:tail-cursor MyBundle:NewUser

findUnprocessed new_user.processor[program:tail-new-user]numprocs=1startretries=100directory=/stdout_logfile=/path/to/symfonyproject/app/logs/tail-new-user-supervisord.logautostart=trueautorestart=trueuser=rootcommand=/usr/local/bin/php /path/to/symfonyproject/app/console doctrine:mongodb:tail-cursor MyBundle:NewUser findUnprocessed new_user.processor

$ vi /etc/supervisor/conf.d/tail-new-user.conf

Tuesday, February 8, 2011

Start supervisord

• Start an instance of supervisord

• It will run as a daemon in the background

• The tail-new-user.conf will always be running

$ supervisord

Tuesday, February 8, 2011

Where do I use supervisor?

• sociallynotable.com

• Indexes tweets with links to Amazon.com products

• Maintains statistics on each product and lets you shop the popular products each day

Tuesday, February 8, 2011

sn:watch-twitter console command

Tuesday, February 8, 2011

I can manually start it

Tuesday, February 8, 2011

But, what if it crashes or stops unexpectedly?

Tuesday, February 8, 2011

This is exactly what supervisor is for

Tuesday, February 8, 2011

Setup a configuration profile for supervisorand it will ensure the console command is

always running

Tuesday, February 8, 2011

[program:watch-twitter]numprocs=1startretries=100000000000directory=/stdout_logfile=/var/www/vhosts/sociallynotable.com/socially-notable/sociallynotable/logs/watch-twitter-supervisord.logautostart=trueautorestart=trueuser=rootcommand=/usr/local/bin/php /var/www/vhosts/sociallynotable.com/socially-notable/sociallynotable/console sn:watch-twitter

Tuesday, February 8, 2011

Now when I start supervisor the twitter watcher will always

remain running. Even if I kill the pid myself, it will start back up.

Tuesday, February 8, 2011

Thanks!I hope this presentation was useful to you!

Tuesday, February 8, 2011

Questions?- http://jwage.com- http://shopopensky.com- http://sociallynotable.com- http://twitter.com/jwage- http://facebook.com/jwage- http://about.me/jwage

Tuesday, February 8, 2011