Twitter Updates for 2010-07-28

Powered by Twitter Tools

Posted in: Uncategorized

Inheritance and Mapped Super Classes in Doctrine

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: , , ,

Setting Entity/Document references without hitting the database

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: , , ,

Storing Files with MongoDB GridFS

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: , , ,

Twitter Updates for 2010-07-27

Powered by Twitter Tools

Posted in: Uncategorized

Multiple levels of Embedded Documents in MongoDB

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: , , ,

Using YQL to get geo location information for an IP address

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: ,

Executing SQL after loading your data fixtures in symfony 1.4

Sometimes you may need to execute some SQL after your data fixtures are loaded in symfony 1.4 if you need to do something that is specific to your RDBMS that is not supported by Doctrine or Propel. Thankfully symfony 1.4 has many well placed events which allow you to hook in to core functionality and execute your own code when certain actions are performed.

Here is an example where you execute some manually SQL statements after the doctrine:data-load task:

<?php

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    // ...
    $this->dispatcher->connect('command.post_command', array(
      $this, 'listenToCommandPostCommandEvent'
    ));
  }

  public function listenToCommandPostCommandEvent(sfEvent $event)
  {
    $invoker = $event->getSubject();
    if ($invoker instanceof sfDoctrineDataLoadTask)
    {
      $conn = Doctrine_Manager::connection();
      $conn->exec(// ...);
    }
  }
}

Symfony 1.4 has many events that you can use to customize things when developing your project. Read more about events in the 1.4 documentation.

Posted in: symfony 1.4

Tags: , , ,

Doctrine MongoDB Object Document Mapper in Symfony2

MongoDB

The MongoDB Object Document Mapper is much like the Doctrine2 ORM in the way it works and architecture. You only deal with plain PHP objects and they are persisted transparently without imposing on your domain model.

You can read more about the Doctrine MongoDB Object Document Mapper on the projects documentation.

Configuration

To get started working with Doctrine and the MongoDB Object Document Mapper you just need to enable it:

# config/config.yml
doctrine_odm.mongodb: ~

The above YAML is the most simple example and uses all of the default values provided, if you need to customize more you can specify the complete configuration:

---
# config/config.yml
doctrine_odm.mongodb:
  server: mongodb
  options:
    connect: true
  metadata_cache_driver: array # array, apc, xcache, memcache

If you wish to use memcache to cache your metadata and you need to configure the Memcache instance you can do the following:

---
# config/config.yml
doctrine_odm.mongodb:
  server: mongodb
  options:
    connect: true
  metadata_cache_driver:
    type: memcache
    class: Doctrine\Common\Cache\MemcacheCache
    host: localhost
    port: 11211
    instance_class: Memcache

Multiple Connections

If you need multiple connections and document managers you can use the following syntax:

---
doctrine_odm.mongodb:
  default_connection: conn2
  default_document_manager: dm2
  metadata_cache_driver: apc
  connections:
    conn1:
      server: mongodb
      options:
        connect: true
    conn2:
      server: mongodb
      options:
        connect: true
  document_managers:
    dm1:
      connection: conn1
      metadata_cache_driver: xcache
    dm2:
      connection: conn2

Now you can retrieve the configured services connection services:

<?php

$conn1 = $container->getService('doctrine.odm.mongodb.conn1_connection');
$conn2 = $container->getService('doctrine.odm.mongodb.conn2_connection');

And you can also retrieve the configured document manager services which utilize the above connection services:

<?php

$dm1 = $container->getService('doctrine.odm.mongodb.dm1_connection');
$dm2 = $container->getService('doctrine.odm.mongodb.dm1_connection');

XML

You can specify the same configuration via XML if you prefer that. Here are the same examples from above in XML.

Simple Single Connection:

<?xml version="1.0" ?>

<container xmlns="http://www.symfony-project.org/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:doctrine="http://www.symfony-project.org/schema/dic/doctrine/odm/mongodb"
    xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd
                        http://www.symfony-project.org/schema/dic/doctrine/odm/mongodb http://www.symfony-project.org/schema/dic/doctrine/odm/mongodb/mongodb-1.0.xsd">

    <doctrine:mongodb server="mongodb://localhost:27017">
        <metadata_cache_driver type="memcache">
            <class>Doctrine\Common\Cache\MemcacheCache</class>
            <host>localhost</host>
            <port>11211</port>
            <instance_class>Memcache</instance_class>
        </metadata_cache_driver>
        <options>
            <connect>true</connect>
        </options>
    </doctrine:mongodb>
</container>

Multiple Connections:

<?xml version="1.0" ?>

<container xmlns="http://www.symfony-project.org/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:doctrine="http://www.symfony-project.org/schema/dic/doctrine/odm/mongodb"
    xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd
                        http://www.symfony-project.org/schema/dic/doctrine/odm/mongodb http://www.symfony-project.org/schema/dic/doctrine/odm/mongodb/mongodb-1.0.xsd">

    <doctrine:mongodb
            metadata_cache_driver="apc"
            default_document_manager="dm2"
            default_connection="dm2"
            proxy_namespace="Proxies"
            auto_generate_proxy_classes="true"
        >
        <doctrine:connections>
            <doctrine:connection id="conn1" server="mongodb://localhost:27017">
                <options>
                    <connect>true</connect>
                </options>
            </doctrine:connection>
            <doctrine:connection id="conn2" server="mongodb://localhost:27017">
                <options>
                    <connect>true</connect>
                </options>
            </doctrine:connection>
        </doctrine:connections>
        <doctrine:document_managers>
            <doctrine:document_manager id="dm1" server="mongodb://localhost:27017" metadata_cache_driver="xcache" connection="conn1" />
            <doctrine:document_manager id="dm2" server="mongodb://localhost:27017" connection="conn2" />
        </doctrine:document_managers>
    </doctrine:mongodb>
</container>

Writing Document Classes

You can start writing document classes just how you normally would write some PHP classes. The only difference is that you must map the classes to the MongoDB ODM. You can provide the mapping information via xml, yaml or annotations. In this example, for simplicity and ease of reading we will use annotations.

First, lets write a simple User class:

<?php

// src/Application/HelloBundle/Document/User.php

namespace Application\HelloBundle\Document;

class User
{
    protected $id;
    protected $name;

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

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}

This class can be used independent from any persistence layer as it is a regular PHP class and does not have any dependencies. Now we need to annotate the class so Doctrine can read the annotated mapping information from the doc blocks:

<?php

// ...

/** @Document(collection="users") */
class User
{
    /** @Id */
    protected $id;

    /** @String */
    protected $name;

    // ...
}

Using Documents

Now that you have a PHP class that has been mapped properly you can begin working with instances of that document persisting to and retrieving from MongoDB.

From your controllers you can access the DocumentManager instances from the container:

<?php

class UserController extends Controller
{
    public function createAction()
    {
        $user = new User();
        $user->setName('Jonathan H. Wage');

        $dm = $this->container->getService('doctrine.odm.mongodb.document_manager');
        $dm->persist($user);
        $dm->flush();

        // ...
    }
}

Later you can retrieve the persisted document by its id:

<?php

class UserController extends Controller
{
    public function editAction($id)
    {
        $dm = $this->container->getService('doctrine.odm.mongodb.document_manager');
        $user = $dm->find('HelloBundle:User', $id);

        // ...
    }
}

Posted in: Doctrine, MongoDB, Symfony2

Tags: , , ,