Starting to play with the Doctrine OrientDB ODM

Since I am actively playing around with it, I wanted to share some snippets to use the Doctrine OrientDB ODM in your PHP applications.

Prelude

In the last few weeks I’ve started working, for fun and profit, to a personal project, nothing really exciting as of now.

The thing is, since I wanted to get back on some cool piece of software, I decided to go for OrientDB for the persistence and a mini-framework a-la Symfony2 as foundation for the PHP application – I actually considered NodeJS first, but I need a prototype in 2 months so…

Point being, I’d like to share with you my basic approach to the OrientDB ODM.

The container

Given I’ve been inspired to Symfony2, instantiating the main ODM classes happens in the DIC:

container.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
services:
  orientdb.binding.parameters:
    class: Doctrine\OrientDB\Binding\BindingParameters
    arguments:
      host:     127.0.0.1
      port:     2480
      username: admin
      password: admin
      database: DBNAME
  orientdb.binding:
    class: Doctrine\OrientDB\Binding\HttpBinding
    arguments:
      parameters: @orientdb.binding.parameters
  odm:
    class: Doctrine\ODM\OrientDB\Manager
    arguments:
      mapper: @odm.mapper
      binding: @orientdb.binding
  odm.mapper:
    class: Doctrine\ODM\OrientDB\Mapper
    arguments:
      documentProxyDirectory: %base-dir%/tmp/
      annotationReader: @odm.annotation-reader
    calls:
      - [setDocumentDirectories, [ %base-dir%/src/PROJECT/Entity/ : "PROJECT\Entity" ] ]
  odm.annotation-reader:
    class: Doctrine\ODM\OrientDB\Mapper\Annotations\Reader
    arguments:
      cacheReader: @cache.array
  cache.array:
    class: Doctrine\Common\Cache\ArrayCache

parameters:
  base-dir: /Users/odino/Sites/PROJECT

As you see, you need:

Autoloading

The autoloading is straightforward, thanks to the PSR-0; the only thing that you should keep in mind is that you will need to specify a separate autoloader for proxy classes, since they can be generated wherever you want (ideally, in a temporary folder, since they should be removed every time you deploy):

autoload.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

require_once __DIR__.'/../vendor/symfony/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php';

use Symfony\Component\ClassLoader\UniversalClassLoader;

$loader = new UniversalClassLoader();

$loader->registerNamespaces(array(
    'Symfony'                       => __DIR__.'/../vendor/symfony/symfony/src',
    'Doctrine\Common'               => __DIR__.'/../vendor/doctrine/common/lib',
    'Doctrine\OrientDB'             => __DIR__.'/../vendor/doctrine/orientdb-odm/src',
    'Doctrine\ODM\OrientDB'         => __DIR__.'/../vendor/doctrine/orientdb-odm/src',
    'Doctrine\OrientDB\Proxy'       => __DIR__.'/../tmp',
));

$loader->register();

You should set the autoloader for Doctrine\OrientDB\Proxy accordingly to the argument documentProxyDirectory of the odm.mapper service.

Entities

Following what we specified in the container.yml, entities should be located in %base-dir%/src/PROJECT/Entity/ and follow the namespace PROJECT\Entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php

namespace PROJECT\Entity;

use Doctrine\ODM\OrientDB\Mapper\Annotations as ODM;

/**
* @ODM\Document(class="user")
*/
class User
{
    /**
     * @ODM\Property(name="@rid", type="string")
     */
    protected $rid;

    /**
     * @ODM\Property(type="string")
     */
    protected $email;

    /**
     * @ODM\Property(type="string", notnull="false")
     */
    protected $nick;

    /**
     * @ODM\Property(type="linklist")
     */
    protected $addresses;

    /**
     * Returns the nickname of the user, or his email if he has no nick set.
     * 
     * @return string
     */
    public function getNick()
    {
        return $this->nick ?: $this->getEmail();
    }

    public function setNick($nick)
    {
        $this->nick = $nick;
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }

    public function getAddresses()
    {
        return $this->addresses;
    }

    public function setAddresses($adresses)
    {
        $this->addresses = $addresses;
    }

    public function getRid()
    {
        return $this->rid;
    }

    public function setRid($rid)
    {
        $this->rid = $rid;
    }
}

As you see, mapping an entity is pretty easy: the first annotation is at class level, to define which OrientDB classes are mapped by the entity, then for every property that you want to be persisted / hydrated, you define another annotation and public getters / setters; if you want the property to be public, you dont need getters / setters.

The property-level annotation has 3 parameters:

What about controllers?

You can access the ODM from within controllers of your application by just using the container:

PROJECT/Controller/User.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace PROJECT\Controller;

use Project\Entity\User;

class UserController
{
  public function somethingAction()
  {
      $user       = new User();
      $manager    = $this->getService('odm');

      $manager->...
  }
}

Repositories

At this point, after boostrapping the environment and creating your first entity, you might want to play with the repository in your controllers, to manipulate and retrieve collections:

PROJECT/Controller/User.php
1
2
3
4
<?php

$manager      = $this->getService('odm');
$userRepository = $manager->getRepository('PROJECT\Entity\User')

then, with the repository, you can start retrieving objects:

Using the repository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

// find all users
$userRepository->findAll();

// find one user given its RID
$userRepository->find($rid);

// find all users with the nick "overlord"
$userRepository->findByNick("overlord");

// find the user with the nick "TheOnlyOverlord"
$userRepository->findOneByNick("TheOnlyOverlord");

// find jack's wife
$jack  = $userRepository->findOneByName("Jack");
$wifey = $userRepository->findOneBySpouse($jack); // spouse is an attribute of type "link"

and it’s not over, since you can, of course, add custom repository classes.

Custom repositories must be located in the entity’s folder and follow the naming convention EntitytheymapRepository: for our User entity, we would need to create a UserRepository class in %base-dir%/src/PROJECT/Entity/:

PROJECT\Entity\UserRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace PROJECT\Entity;

use Doctrine\ODM\OrientDB\Repository;

class UserRepository extends Repository
{
    /**
     * Retrieves a random user.
     * 
     * @return \PROJECT\Entity\User
     */
    public function findRandomUser()
    {
        return array_rand($this->findAll());
    }
}

so then you can call your new methods over repositories:

Using custom repositories
1
2
3
<?php

$manager->getRepository('PROJECT\Entity\User')->findRandomUser();

Can I haz raw queries?

Entities and repositories are good, but what about adding some SQL+2 to the mix?

That’s very easy, thanks to the query builder that’s packed with the ODM:

Example queries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

use Doctrine\OrientDB\Query\Query;

// instantiate a query object
$query = new Query();

// simple SELECT
$query->from(array('user'))->where('nick = ?', $nick);

// throwing some spice into the mix
$query->orWhere('attribute = ?', $attribute)
    ->orWhere('(this IS NULL OR that IS NOT NULL)')
    ->limit(10)
    ->orderBy(...);

// SELECTing a single record
$query->from(array($rid));

// SELECTing two records
$query->from(array($rid1, $rid2));

When you manipulate the $query object you are basically creating an SQL query with an object-oriented fluent interface; to eventually execute the query, just pass the object to the Manager:

Executing a query
1
2
3
4
5
6
<?php

$query = new Query();
$query->from(array('user'))->where('gender = ?', "male");

$males = $manager->execute($query);

Point being, how do you save data?

Since persistence is not already handled by the Manager, you will need to use raw queries for now:

Saving data
1
2
3
4
5
6
7
8
9
10
<?php

$user = array(
  'name' => 'Jack'
);

$query = new Query();
$query->insert()->into('user')->fields(array_keys($user))->values($user);

$manager->execute($query);

From the trenches

We’ve been very active since a couple months, and we’ve actually been able to roll out some major bugfixes and improvements (more than 10 in the last few weeks):

I would not advise you to install one of the old tags, or even the last one, which brings the namespace changes for the incubation in the Doctrine organization, but to install it directly from master via composer:

1
"doctrine/orientdb-odm": "dev-master",

as we are constantly doing bugfixes and so on (I would day you would get an update – at least – every week).

That is it, now start playing around!

Notes
  1. Be aware that if you are retrieving a property which is NULL in the DB and you don’t declare it as NULLable, an exception will be thrown (and there is an issue to improve the exception message https://github.com/doctrine/orientdb-odm/issues/152)
  2. OrientDB’s QL is called SQL+, as it looks like SQL but has some major improvements, as it’s very developer-friendly
comments powered by Disqus