Date post: | 06-May-2015 |
Category: |
Technology |
Upload: | jonathan-wage |
View: | 8,423 times |
Download: | 0 times |
Doctrine in the Real WorldReal world examples
doctrine
Friday, March 4, 2011
My name isJonathan H. Wage
Friday, March 4, 2011
• PHP Developer for 10+ years
• Long time Symfony and Doctrine contributor
• Published Author
• Entrepreneur
• Currently living in Nashville, Tennessee
Friday, March 4, 2011
Previously employed by SensioLabs
Friday, March 4, 2011
Partnered with ServerGrove
MongoDB Hosting
http://mongodbhosting.com
Friday, March 4, 2011
Today, I work full-time for OpenSky
http://shopopensky.com
Friday, March 4, 2011
What is OpenSky?
Friday, March 4, 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.
Friday, March 4, 2011
OpenSky Loves OpenSource
• PHP 5.3
• Apache2
• Symfony2
• Doctrine2
• jQuery
• mule, stomp, hornetq
• MongoDB
• nginx
• varnish
Friday, March 4, 2011
We don’t just use open source projects
Friday, March 4, 2011
We help build them
Friday, March 4, 2011
OpenSky has some of the top committers in Symfony2 and other
projects
Friday, March 4, 2011
Symfony2 OpenSky Committers
• 65 Kris Wallsmith
• 52 Jonathan H. Wage
• 36 Jeremy Mikola
• 36 Bulat Shakirzyanov
• 6 Justin Hileman
Friday, March 4, 2011
Doctrine MongoDB Committers
• 39 Jonathan H. Wage
• 11 Bulat Shakirzyanov
• 2 Kris Wallsmith
Friday, March 4, 2011
MongoDB ODM Committers
• 349 Jonathan H. Wage
• 226 Bulat Shakirzyanov
• 17 Kris Wallsmith
• 13 Steven Surowiec
• 2 Jeremy Mikola
Friday, March 4, 2011
OpenSky uses both the Doctrine ORM and ODM
Friday, March 4, 2011
Why?
Friday, March 4, 2011
We are an eCommerce site
Friday, March 4, 2011
Actions involving commerce need
transactions
Friday, March 4, 2011
ORM and MySQL
• Order
• Order\Transaction
• Order\Shipment
Friday, March 4, 2011
ODM and MongoDB
• Product
• Seller
• Supplier
• User
• ... basically everything else that is not involving $$$ and transactions
Friday, March 4, 2011
Blending the Two
Friday, March 4, 2011
Defining our Product Document
Friday, March 4, 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; }}
Friday, March 4, 2011
Defining our Order Entity
Friday, March 4, 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; // ...}
Friday, March 4, 2011
Setting the Product
public function setProduct(Product $product){ $this->productId = $product->getId(); $this->product = $product;}
Friday, March 4, 2011
• $productId is mapped and persisted
• but $product which stores the Product instance is not a persistent entity property
Friday, March 4, 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
Friday, March 4, 2011
Loading Product ODM reference in Order
Entity
Friday, March 4, 2011
Lifecycle Events to the Rescue
Friday, March 4, 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
Friday, March 4, 2011
$eventListener = new OrderPostLoadListener($dm);$eventManager = $em->getEventManager();$eventManager->addEventListener( array(\Doctrine\ORM\Events::postLoad), $eventListener);
Add EventListener
Friday, March 4, 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>
Friday, March 4, 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); }}
Friday, March 4, 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);
Friday, March 4, 2011
Seamless
• Documents and Entities play together like best friends
• Because Doctrine persistence remains transparent from your domain this is possible
Friday, March 4, 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)
Friday, March 4, 2011
Example from Blog
• This example was first written on my personal blog http://jwage.com
• You can read the blog post here http://jwage.com/2010/08/25/blending-the-doctrine-orm-and-mongodb-odm/
Friday, March 4, 2011
MongoDB ODM SoftDelete Functionality
Friday, March 4, 2011
I like my deletes soft, not hard
Friday, March 4, 2011
Why?
Friday, March 4, 2011
Deleting data is dangerous business
Friday, March 4, 2011
Flickr accidentally deleted a pro members
account and 5000 pictures
Friday, March 4, 2011
They were able to restore it later but it
took some time
Friday, March 4, 2011
Instead of deleting, simply set a deletedAt field
Friday, March 4, 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
Friday, March 4, 2011
Autoload Extension
$loader = new UniversalClassLoader();$loader->registerNamespaces(array( // ... 'Doctrine\\ODM\\MongoDB\\SoftDelete' => __DIR__.'/vendor/doctrine-mongodb-odm-softdelete/lib',));$loader->register();
Friday, March 4, 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
Friday, March 4, 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
Friday, March 4, 2011
Autoload the Bundle
$loader = new UniversalClassLoader();$loader->registerNamespaces(array( // ... 'Doctrine\\ODM\\MongoDB\\Symfony\\SoftDeleteBundle' => __DIR__.'/vendor/doctrine-mongodb-odm-softdelete-bundle',));$loader->register();
Friday, March 4, 2011
Register the Bundle
public function registerBundles(){ $bundles = array( // ...
// register doctrine symfony bundles new Doctrine\ODM\MongoDB\Symfony\SoftDeleteBundle\SoftDeleteBundle() );
// ...
return $bundles;}
Friday, March 4, 2011
Enable the Bundle
// app/config/config.yml
doctrine_mongodb_softdelete.config: ~
Friday, March 4, 2011
SoftDeleteManager
$sdm = $container->get('doctrine.odm.mongodb.soft_delete.manager');
Friday, March 4, 2011
SoftDeleteable
interface SoftDeleteable{ function getDeletedAt();}
ODM Documents must implement this interface
Friday, March 4, 2011
User implements SoftDeletable
/** @mongodb:Document */class User implements SoftDeleteable{ /** @mongodb:Date @mongodb:Index */ private $deletedAt;
public function getDeletedAt() { return $this->deletedAt; }}
Friday, March 4, 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();
Friday, March 4, 2011
Query Executed
db.users.update( { _id : { $in : [new ObjectId('1234567891011123456')] } }, { $set : { deletedAt: new Date() } })
Friday, March 4, 2011
Restore a User
// now again later we can restore that same user$user = $dm->getRepository('User')->findOneByUsername('jwage');$sdm->restore($user);$sdm->flush();
Friday, March 4, 2011
Query Executed
db.users.update( { _id : { $in : [new ObjectId('1234567891011123456')] } }, { $unset : { deletedAt: true } })
Friday, March 4, 2011
Limit cursors to only show non deleted users
$qb = $dm->createQueryBuilder('User') ->field('deletedAt')->exists(false);$query = $qb->getQuery();$users = $query->execute();
Friday, March 4, 2011
Get only deleted users
$qb = $dm->createQueryBuilder('User') ->field('deletedAt')->exists(true);$query = $qb->getQuery();$users = $query->execute();
Friday, March 4, 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();
Friday, March 4, 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);
Friday, March 4, 2011
PHP Daemons
Friday, March 4, 2011
Symfony2 and supervisor
http://supervisord.org/
Friday, March 4, 2011
What is supervisor?
Friday, March 4, 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
Friday, March 4, 2011
Daemonize a Symfony2 Console Command
with supervisor
Friday, March 4, 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?
Friday, March 4, 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
Friday, March 4, 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;
// ...}
Friday, March 4, 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
Friday, March 4, 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();
// ... } // ...}
Friday, March 4, 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
Friday, March 4, 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(); }}
Friday, March 4, 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
Friday, March 4, 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);}
Friday, March 4, 2011
Tailable Cursor Bundle
https://github.com/doctrine/doctrine-mongodb-odm-tailable-cursor-bundle
Friday, March 4, 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.
Friday, March 4, 2011
Install supervisorhttp://supervisord.org/installing.html
$ easy_install supervisor
Friday, March 4, 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
Friday, March 4, 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
Friday, March 4, 2011
Where do I use supervisor?
• http://sociallynotable.com
• Keeps daemon running that watches twitter
• Indexes tweets with links to amazon products
• Maintains tweet statistics and ranks the popular products
Friday, March 4, 2011
Thanks!I hope this presentation was useful to you!
Friday, March 4, 2011
Questions?
- http://jwage.com- http://twitter.com/jwage- http://facebook.com/jwage- http://about.me/jwage- http://shopopensky.com- http://mongodbhosting.com- http://servergrove.com- http://sociallynotable.com
Friday, March 4, 2011