Since the start of the Doctrine MongoDB Object Document Mapper project people have asked how it can be integrated with the ORM. This blog post demonstrates how you can integrate the two transparently, maintaining a clean domain model.
This example will have a Product that is stored in MongoDB and the Order stored in a MySQL database.
Defining our Document and Entity
First lets define our Product document:
<?php
namespace Documents;
/** @Document */
class Product
{
/** @Id */
private $id;
/** @String */
private $title;
public function getId()
{
return $this->id;
}
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
}
Next create the Order entity that has a $product and $productId property linking it to the Product that is stored with MongoDB:
<?php
namespace Entities;
use Documents\Product;
/**
* @Entity
* @Table(name="orders")
* @HasLifecycleCallbacks
*/
class Order
{
/**
* @Id @Column(type="integer")
* @GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @Column(type="string")
*/
private $productId;
/**
* @var Documents\Product
*/
private $product;
public function getId()
{
return $this->id;
}
public function getProductId()
{
return $this->productId;
}
public function setProduct(Product $product)
{
$this->productId = $product->getId();
$this->product = $product;
}
public function getProduct()
{
return $this->product;
}
}
Event Subscriber
Now we need to setup an event subscriber that will set the $product property of all Order instances to a reference to the document product so it can be lazily loaded when it is accessed the first time. So first register a new event subscriber:
<?php
$eventManager = $em->getEventManager();
$eventManager->addEventListener(
array(\Doctrine\ORM\Events::postLoad), new MyEventSubscriber($dm)
);
So now we need to define a class named MyEventSubscriber and pass a dependency to the DocumentManager. It will have a postLoad() method that sets the product document reference:
<?php
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\Event\LifecycleEventArgs;
class MyEventSubscriber
{
public function __construct(DocumentManager $dm)
{
$this->dm = $dm;
}
public function postLoad(LifecycleEventArgs $eventArgs)
{
$order = $eventArgs->getEntity();
$em = $eventArgs->getEntityManager();
$productReflProp = $em->getClassMetadata('Entities\Order')
->reflClass->getProperty('product');
$productReflProp->setAccessible(true);
$productReflProp->setValue(
$order, $this->dm->getReference('Documents\Product', $order->getProductId())
);
}
}
The postLoad method will be invoked after an ORM entity is loaded from the database. This allows us to use the DocumentManager to set the $product property with a reference to the Product document with the product id we previously stored.
First create a new Product:
<?php
$product = new \Documents\Product();
$product->setTitle('Test Product');
$dm->persist($product);
$dm->flush();
Now create a new Order and link it to a Product in MySQL:
<?php
$order = new \Entities\Order();
$order->setProduct($product);
$em->persist($order);
$em->flush();
Later we can retrieve the entity and lazily load the reference to the document in MongoDB:
<?php
$order = $em->find('Order', $order->getId());
// Instance of an uninitialized product proxy
$product = $order->getProduct();
// Initializes proxy and queries the database
echo "Order Title: " . $product->getTitle();
If you were to print the $order you would see that we got back regular PHP objects:
<?php
print_r($order);
The above would output the following:
Order Object
(
[id:Entities\Order:private] => 53
[productId:Entities\Order:private] => 4c74a1868ead0ed7a9000000
[product:Entities\Order:private] => Proxies\DocumentsProductProxy Object
(
[__isInitialized__] => 1
[id:Documents\Product:private] => 4c74a1868ead0ed7a9000000
[title:Documents\Product:private] => Test Product
)
)
That is it! It is not a very abstract example right now but it demonstrates how to utilize the events to do some very interesting things with the Doctrine persistence libraries! I hope that now someone will inspired to create an extension that offers an abstract solution for blending the ORM and ODM together!
Posted in: Doctrine, MongoDB, PHP
Tags: doctrine, mongodb, odm, orm, php
Yesterday I added a few new chapters to the Doctrine MongoDB ODM Documentation:
I hope it is helpful to someone!
Posted in: Uncategorized
The Doctrine Annotations library was born from a need in the Doctrine2 ORM to allow the mapping information to be specified inside the doc-blocks of classes, properties and methods. The library is independent and can be used in your own libraries to implement doc block annotations.
Setup and Configuration
To use the annotations library is simple, you just need to create a new AnnotationReader
instance:
<?php
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
Usage
Using the library API is simple. Imagine you had some annotation classes that looked like the following:
<?php
namespace MyCompany\Annotations;
class Foo extends \Doctrine\Common\Annotations\Annotation
{
public $bar;
}
class Bar extends \Doctrine\Common\Annotations\Annotation
{
public $foo;
}
Now to use the annotations you would just need to do the following:
<?php
/**
* @MyCompany\Annotations\Foo(bar="test")
* @MyCompany\Annotations\Bar(foo="test")
*/
class User
{
}
Now we can write a script to get the annotations above:
<?php
$reflClass = new ReflectionClass('User');
$classAnnotations = $reader->getClassAnnotations($reflClass);
echo $classAnnotations['MyCompany\Annotations\Foo']->bar; // prints foo
echo $classAnnotations['MyCompany\Annotations\Foo']->foo; // prints bar
You have a complete API for retrieving annotation class instances from a class, property
or method docblock:
- getClassAnnotations(ReflectionClass $class)
- getClassAnnotation(ReflectionClass $class, $annotation)
- getPropertyAnnotations(ReflectionProperty $property)
- getPropertyAnnotation(ReflectionProperty $property, $annotation)
- getMethodAnnotations(ReflectionMethod $method)
- getMethodAnnotation(ReflectionMethod $method, $annotation)
Read the full documentation to learn more about how to use the Doctrine annotations library!
Posted in: Doctrine, PHP, Uncategorized
Tags: annotations, doctrine, php
Today I was reading an article about array dereferencing in PHP trunk. It is an awesome new feature added to PHP! Imagine you had some code like this:
<?php
class Foo
{
public function bar()
{
}
}
function func()
{
return new Foo();
}
Previous to this addition in PHP you had to do something like this:
<?php
$foo = func();
$foo->bar();
Now it is possible to just call bar() directly without having to set the return of func() to a variable temporarily:
<?php
func()->bar();
You can also now access arrays when they are the return of a method:
<?php
function foo()
{
return array(1, 2, 3);
}
echo foo()[2]; // prints 3
This greatly improves the syntax of PHP and I am very happy to see this committed!
Posted in: PHP, Uncategorized
Tags: php
MongoDB is a schema-less database so as your domain model changes in Doctrine, you’ll have newer documents with different fields than older documents. Since we don’t have a way to rename a field internally in MongoDB, yet, the only other option is to fetch all the documents and rename it in your application and update the document. This could take a really long time depending on how big your database is and will require downtime.
Doctrine provides ways for you to “eventually” migrate all your documents at application run-time. You have several options for working with database schema changes and this blog post will try and demonstrate them!
Renaming a Field
Imagine you have a document in your domain named Person and it looked like this:
<?php
/** @Document */
class Person
{
/** @Id */
public $id;
/** @String */
public $name;
}
Then imagine later you decide you want to rename $name to $fullName like this:
<?php
/** @Document */
class Person
{
/** @Id */
public $id;
/** @String */
public $fullName;
}
All documents from now on will be created with a fullName property but you’ll still have old documents with the name field. You can use the @AlsoLoad annotation here to also load another fields value in that property if it exists:
<?php
/** @Document */
class Person
{
/** @Id */
public $id;
/**
* @String
* @AlsoLoad("name")
*/
public $fullName;
}
Transforming Data
Another situation might be you want to load the name and fullName fields in to individual first and last name fields. We can handle this using the @AlsoLoad annotation on a method:
<?php
/** @Document */
class Person
{
/** @Id */
public $id;
/** @String */
public $firstName;
/** @String */
public $lastName;
/** @AlsoLoad({"name", "fullName"}) */
public function populateFirstAndLastName($fullName)
{
$e = explode(' ', $fullName);
$this->firstName = $e[0];
$this->lastName = $e[1];
}
}
So when a document has a field named name, or fullName it will execute the populateFirstAndLastName() method to handle the change when the document is loaded.
Moving Fields
You also have a few other options for dealing with changes in your model:
- @PostLoad – execute code after all fields have been loaded.
- @PrePersist – execute code before your document gets saved.
- @NotSaved – load values into fields without saving them again.
Imagine you have some address fields on a Person document:
<?php
/** @Document(collection="people") */
class Person
{
/** @Id */
public $id;
/** @String */
public $name;
/** @String */
public $street;
/** @String */
public $city;
}
Then later you want to store a persons address in another object as an embedded document:
<?php
/** @EmbeddedDocument */
class Address
{
/** @String */
public $street;
/** @String */
public $city;
public function __construct($street, $city)
{
$this->street = $street;
$this->city = $city;
}
}
/**
* @Document(collection="people")
* @HasLifecycleCallbacks
*/
class Person
{
/** @Id */
public $id;
/** @String */
public $name;
/** @NotSaved */
public $street;
/** @NotSaved */
public $city;
/** @EmbedOne(targetDocument="Address") */
public $address;
/** @PostLoad */
public function postLoad()
{
if ($this->street !== null || $this->city !== null)
{
$this->address = new Address($this->street, $this->city);
}
}
}
The above will change the document each time it is loaded. If you want to change it permanently in the database you can do it when the document is being updated:
<?php
/**
* @Document(collection="people")
* @HasLifecycleCallbacks
*/
class Person
{
// ...
/** @PreUpdate */
public function preUpdate()
{
if ($this->street !== null || $this->city !== null)
{
$this->address = new Address($this->street, $this->city);
}
}
}
Posted in: Uncategorized
Single Collection Inheritance in the Doctrine MongoDB ODM allows you to map multiple classes in an inheritance hierarchy to a single collection in MongoDB. An example might be in a CMS where you have several different content types like the base Node, Page and BlogPost which all extends an abstract ContentType class.
First define the ContentType class that is a @MappedSuperclass:
<?php
/**
* @MappedSuperclass
* @HasLifecycleCallbacks
*/
abstract class ContentType
{
/** @Id */
protected $id;
/** @Date */
protected $createdAt;
/** @Date */
protected $updatedAt;
public function getId()
{
return $this->id;
}
/** @PreUpdate */
public function preUpdate()
{
$this->updatedAt = new DateTime();
}
/** @PrePersist */
public function prePersist()
{
$this->createdAt = new DateTime();
$this->updatedAt = new DateTime();
}
}
Now we can define our base Node content type:
<?php
/**
* @Document(collection="pages")
* @InheritanceType("SINGLE_COLLECTION")
* @DiscriminatorField(fieldName="type")
* @DiscriminatorMap({
* "node"="Node",
* "page"="Page",
* "blog_post"="BlogPost"
* })
*/
class Node extends ContentType
{
/** @String */
protected $title;
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
}
This gives us our base content type node functionality that we can extend to create the Page document which adds a body field for the page:
<?php
/** @Document */
class Page extends Node
{
/** @String */
protected $body;
public function getBody()
{
return $this->body;
}
public function setBody($body)
{
$this->body = $body;
}
}
And the BlogPost document is a custom type of page that adds some additional fields related to blog posts like an excerpt and tags:
<?php
/** @Document */
class BlogPost extends Page
{
/** @Collection */
private $tags = array();
/** @String */
private $excerpt;
public function getExcerpt()
{
return $this->excerpt;
}
public function setExcerpt($excerpt)
{
$this->excerpt = $excerpt;
}
public function addTag($tag)
{
$this->tags[] = $tag;
}
public function removeTag($tag)
{
$key = array_search($tag, $this->tags);
if ($key !== false) {
unset($this->tags[$key]);
}
}
public function getTags()
{
return $this->tags;
}
}
You can easily add new content types by mapping a document class that extends the base Node. All your documents will be stored in a single collection and a discriminator field will be used to discriminate which class created each document.
Now we can use our document classes and create new instances and persist them. Here is an example where we create a new blog post:
<?php
$post = new BlogPost();
$post->setTitle('Test');
$post->setExcerpt('Testing');
$post->setBody('w00t');
$post->addTag('test');
$dm->persist($post);
$dm->flush();
The above would result in a document like the following in MongoDB:
Array
(
[_id] => 4c4f38978ead0ef23f000000
[createdAt] => MongoDate Object
(
[sec] => 1280260247
[usec] => 0
)
[updatedAt] => MongoDate Object
(
[sec] => 1280260247
[usec] => 0
)
[title] => Test
[body] => w00t
[tags] => Array
(
[0] => test
)
[excerpt] => Testing
[type] => blog_post
)
Posted in: Doctrine, MongoDB, PHP
Tags: doctrine2, mongodb, odm, php
In Doctrine 1 when you want to specify a reference you have two options, you can set the foreign key manually:
<?php
$user->setProfileId($profileId);
$user->save();
The problem here is that the profile_id is set in the object but the reference to a Profile instance is not set. The next option is to actually set the object reference:
<?php
$profile = Doctrine_Core::getTable('Profile')->find($profileId);
$user->setProfile($profile);
$user->save();
Here the reference is set properly but the downside to this approach is that it requires us to load the entire Profile object just to set the reference. It is silly! Thanks to the Doctrine2 ORM and MongoDB ODM you have the ability to retrieve a reference to an object without having to hit the database. Here is an example:
<?php
$profile = $em->getReference('Profile', $profileId);
$user->setProfile($profile);
$em->flush();
Or the same thing with the MongoDB ODM:
<?php
$profile = $dm->getReference('Profile', $profileId);
$user->setProfile($profile);
$dm->flush();
Posted in: Doctrine, MongoDB, PHP
Tags: doctrine2, mongodb, odm, orm
The PHP MongoDB extension provides a nice and convenient way to store files in chunks of data with the MongoDB GridFS. It uses two database collections, one to store the metadata for the file, and another to store the contents of the file. The contents are stored in chunks to avoid going over the maximum allowed size of a MongoDB document.
You can easily setup a Document that is stored using the MongoDB GridFS by using the @File annotation:
<?php
namespace Documents;
/** @Document(collection="files") */
class Image
{
/** @Id */
private $id;
/** @String */
private $name;
/** @File */
private $file;
private function getId()
{
return $id;
}
private function setName($name)
{
$this->name = $name;
}
private function getName()
{
return $this->name;
}
private function getFile()
{
return $this->file;
}
private function setFile($file)
{
$this->file = $file;
}
}
Notice the $file property with @File annotation, it tells the Document that it is is to be stored using the MongoGridFS and the MongoGridFSFile instance is placed in the $file property for you.
Now you can create a new Image setting the path to a file and persist it:
<?php
$image = new Image();
$image->setName('Test image');
$image->setFile('/path/to/image.png');
$dm->persist($image);
$dm->flush();
Then later you can retrieve that image and render it:
<?php
$image = $dm->createQuery('Documents\Image')
->field('name')
->equals('Test image')
->getSingleResult();
header('Content-type: image/png;');
echo $image->getFile()->getBytes();
You can of course make references to this Image document from another document.
Imagine you had a Profile document and you wanted every Profile to have a profile
image:
<?php
namespace Documents;
/** @Document(collection="profiles") */
class Profile
{
/** @Id */
private $id;
/** @String */
private $name;
/** @ReferenceOne(targetDocument="Documents\Image") */
private $image;
private function getId()
{
return $this->id;
}
private function getName()
{
return $this->name;
}
private function setName($name)
{
$this->name = $name;
}
private function getImage()
{
return $this->image;
}
private function setImage(Image $image)
{
$this->image = $image;
}
}
Now you can create a new Profile and give it an Image:
<?php
$image = new Image();
$image->setName('Test image');
$image->setFile('/path/to/image.png');
$profile = new Profile();
$profile->setName('Jonathan H. Wage');
$profile->setImage($image);
$dm->persist($profile);
$dm->flush();
If you want to query for the Profile and load the Image reference in a query
you can use:
<?php
$profile = $dm->createQuery('Profile')
->field('name')->equals('Jonathan H. Wage')
->getSingleResult();
$image = $profile->getImage();
header('Content-type: image/png;');
echo $image->getFile()->getBytes();
Posted in: Doctrine, MongoDB, PHP
Tags: doctrine, mongodb, odm, php
One of the greatest things about MongoDB is the fact that it is schema-less. It makes for a very flexible domain model persistence layer. For example it is possible to have multiple levels of embedded documents. A useful example might be where you have many profiles and each profile has many addresses. In the Doctrine MongoDB ODM mapping this is trivial.
First create your top level User document:
<?php
/** @Document(collection="users") */
class User
{
/** @Id */
private $id;
/** @String */
private $username;
/** @EmbedMany(targetDocument="Profile") */
private $profiles = array();
public function setUsername($username)
{
$this->username = $username;
}
public function addProfile(Profile $profile)
{
$this->profiles[] = $profile;
}
}
As you can see we embed another document class named Profile so lets define that as an embedded document:
<?php
/** @EmbeddedDocument */
class Profile
{
/** @String */
private $name;
/** @EmbedMany(targetDocument="Address") */
private $addresses = array();
public function setName($name)
{
$this->name = $name;
}
public function addAddress(Address $address)
{
$this->addresses[] = $address;
}
}
Finally, we’ve embedded a document in Profile named Address so lets define it:
<?php
/** @EmbeddedDocument */
class Address
{
/** @String */
private $number;
/** @String */
private $street;
/** @String */
private $city;
/** @String */
private $state;
/** @String */
private $zipcode;
public function setNumber($number)
{
$this->number = $number;
}
public function setStreet($street)
{
$this->street = $street;
}
public function setCity($city)
{
$this->city = $city;
}
public function setState($state)
{
$this->state = $state;
}
public function setZipcode($zipcode)
{
$this->zipcode = $zipcode;
}
}
Now you can start working with the PHP objects just like you would if no persistence layer was present at all and persist the objects transparently when you are ready to have the state of the objects managed by Doctrine:
<?php
$user = new User();
$user->setUsername('jwage');
$profile = new Profile();
$profile->setName('Profile #1');
$user->addProfile($profile);
$address = new Address();
$address->setNumber('6512');
$address->setStreet('Mercomatic');
$address->setCity('Nashville');
$address->setState('Tennessee');
$address->setZipcode('37209');
$profile->addAddress($address);
$profile = new Profile();
$profile->setName('Profile #2');
$user->addProfile($profile);
$address = new Address();
$address->setNumber('475');
$address->setStreet('Buckhead Ave');
$address->setCity('Atlanta');
$address->setState('Georgia');
$address->setZipcode('30303');
$profile->addAddress($address);
$dm->persist($user);
$dm->flush();
The above would result in an array being stored in MongoDB like the following:
Array
(
[_id] => MongoId Object
(
)
[username] => jwage
[profiles] => Array
(
[0] => Array
(
[name] => Profile #1
[addresses] => Array
(
[0] => Array
(
[number] => 6512
[street] => Mercomatic
[city] => Nashville
[state] => Tennessee
[zipcode] => 37209
)
)
)
[1] => Array
(
[name] => Profile #2
[addresses] => Array
(
[0] => Array
(
[number] => 475
[street] => Buckhead Ave
[city] => Atlanta
[state] => Georgia
[zipcode] => 30303
)
)
)
)
)
We can then later retrieve the documents from MongoDB and our object domain model will be reconstructed as you have mapped it:
<?php
$user = $dm->findOne('User', array('username' => 'jwage'));
You can see the complete working script for this blog post as a gist on github.
Posted in: Doctrine, MongoDB, PHP
Tags: doctrine, mongodb, odm, php
The Yahoo! YQL API has the ability to provide geo location information for IP addresses. Using this in PHP is dead simple. Here is a barebones example demonstrating how you can use PHP and YQL for this:
<?php
$ipAddress = '76.22.200.69';
$query = sprintf("select * from ip.location where ip='%s'", $ipAddress);
$queryUrl = "http://query.yahooapis.com/v1/public/yql?q=" . urlencode($query)."&format=json&env=".urlencode("store://datatables.org/alltableswithkeys");
$json = file_get_contents($queryUrl);
$data = json_decode($json);
print_r($data);
The above code would output the following:
stdClass Object
(
[query] => stdClass Object
(
[count] => 1
[created] => 2010-07-27T04:27:20Z
[lang] => en-US
[results] => stdClass Object
(
[Response] => stdClass Object
(
[Ip] => 76.22.200.69
[Status] => OK
[CountryCode] => US
[CountryName] => United States
[RegionCode] => 47
[RegionName] => Tennessee
[City] => Nashville
[ZipPostalCode] => 37205
[Latitude] => 36.1121
[Longitude] => -86.863
[Timezone] => 0
[Gmtoffset] => 0
[Dstoffset] => 0
)
)
)
)
I took this a step further and built a little abstraction layer on top of this functionality. Now, retrieving geo location information for IP addresses using the YQL API is easier than ever using the PHP YQL Geo Locator library. You can get the code on github. Continue reading to learn how to get started!
First, clone the git repository:
$ git clone git://github.com/jwage/php-yql-geo-locator.git
Now you need to setup your code to use the library:
<?php
use GeoLocator\Locator;
use GeoLocator\Location;
use GeoLocator\GoogleMapImage;
require 'php-yql-geo-locator/lib/GeoLocator/Location.php';
require 'php-yql-geo-locator/lib/GeoLocator/Locator.php';
require 'php-yql-geo-locator/lib/GeoLocator/GoogleMapImage.php';
After setting everything up you are ready to start working with geo locations
using the locator API:
- getGeoLocation($ip)
- getGoogleMapImageForIps(array $ips)
- getGoogleMapImageForIp($ip)
Here is an example using the getGeoLocation() method:
<?php
$geoLocation = $locator->getGeoLocation('76.22.200.69');
It returns an instance of GeoLocator\Location and has a simple public API for retrieving the geo location information for the IP address:
<?php
echo $geoLocation->getLatitude();
You can also export all the information to a PHP array using the toArray() method:
<?php
print_r($geoLocation->toArray());
It would result in an array that looks like this:
Array
(
[ip] => 76.22.200.69
[countryCode] => US
[countryName] => United States
[regionCode] => 47
[regionName] => Tennessee
[city] => Nashville
[zipPostalCode] => 37205
[latitude] => 36.1121
[longitude] => -86.863
[timezone] => 0
[gmtOffset] => 0
[dstOffset] => 0
)
Get a google map that plots multiple IP addresses:
<?php
$image = $locator->getGoogleMapImageForIps(array(
'76.22.200.69',
'74.125.65.106'
));
The above method returns an instance of GoogleMapImage and has the following API:
- setWidth($width)
- setHeight($height)
- setMaptype($maptype)
- setSensor($sensor)
- setZoom($zoom)
- addLocation(Location $location)
- getUrl()
- getHTMLImageTag()
- __toString()
Now you can just echo the $image to get the HTML image:
<?php
echo $image;
The above would result in an image tag that looks like the following:
<img src="http://maps.google.com/maps/api/staticmap?zoom=&size=550x550&maptype=roadmap&sensor=false&markers=color:blue|label:76.22.200.69|36.1121,-86.863&markers=color:blue|label:74.125.65.106|37.4192,-122.057" width="550" height="550" />
Posted in: PHP
Tags: php, yql