9.6. Xyster_Orm_Relation

This class allows you to define relationships between your entities. An instance of this class holds the details of a given relationship. There are four different types.

One

Where one entity stores the primary key of another entity (one-to-one)

Many

Where one entity "owns" several; they all store its primary key (one-to-many)

Belongs

An entity "owned" by a many relationship (many-to-one)

Joined

When two entities can belong to more than one of each other (many-to-many)

9.6.1. One-to-one

Adding to the above person class to demonstrate, we'll add an extra field that refers to another person object.

<?php
class PersonMapper extends Xyster_Orm_Mapper
{
        // protected properties go here 

        public function init()
        {
            $this->_hasOne('nemesis', array('class'=>'Person', 'id'=>'nemesisId'));
        }
}

The init method is called when the mapper is constructed. Basically, it"s a place to keep the relationship definitions for an entity and any initializations you require. In this case, we've said that person has a one-to-one relationship with itself based on the nemesisId field. The relationship methods take two parameters: the name of the relationship and an array of options. See the Xyster_Orm_Relation API for further details on these options.

You now expose some extra features with a relationship defined.

<?php
$orm = Xyster_Orm::getInstance();
$luke = $orm->get('Person', 1);
$vader = $orm->get('Person', 2);
$luke->nemesis = $vader;
// or $luke->setNemesis($vader);
echo $luke->nemesis->name; // echoes 'Darth Vader'
$orm->commit();

Once commit is called, the ORM system assigns $luke's nemesisId with $vader's primary key. As a developer, you should only be working with objects and not worry about primary keys. See the following example in which two objects are related before they're even saved into the data store.

<?php
// create Han Solo
$han = new Person();
$han->name = 'Han Solo';
$han->affiliation = 'Rebel';
$han->alignment = 'good';
$han->gender = 'M';
$han->race = 'Human';
// create Jabba the Hutt
$jabba = new Person();
$jabba->name = 'Jabba Desilijic Tiure';
$jabba->affiliation = 'Criminal';
$jabba->alignment = 'evil';
$jabba->gender = 'M';
$jabba->race = 'Hutt';
// Jabba is Han's nemesis
$han->nemesis = $jabba;
// Jabba is too egocentric to have a nemesis, just save Han
$orm->persist($han);
$orm->commit();
// both Han and Jabba are saved

9.6.2. One-to-many

Here's another entity. The person entity should be able to be related to any number of these.

<?php
class Quote extends Xyster_Orm_Entity {}
class QuoteSet extends Xyster_Orm_Set { }
class QuoteMapper extends Xyster_Orm_Mapper 
{
        protected $_domain = 'myDb';
        protected $_lifetime = 0; // a quote shouldn't ever change
}

A person can have a list of quotes; a one-to-many relationship. In the person mapper, we can specify this relationship to the quote entity.

<?php
class personMapper extends Xyster_Orm_Mapper
{
        // protected $_domain, etc.

        public function init()
        {
            $this->_hasOne('nemesis', array('class'=>'Person', 'id'=>'nemesisId'))
                ->_hasMany('quotes');
        }
}

A property called quotes is now available on the entity, and it returns a QuoteSet. You probably also want to add a "belongs" relationship to the QuoteMapper class.

<?php
class QuoteMapper extends Xyster_Orm_Mapper
{
        // stuff declared above

        public function init()
        {
            $this->_belongsTo('person');
        }
}

A "belongs" relationship works the same way that a "one" relationship does, except it wires up the fact that quote objects should have the person object they relate to assigned to them when added to the person object's quote set. The following example shows how to add quote entities to the set.

<?php
$quote1 = new Quote();
$quote1->value = 'What is thy bidding, my master?';
$vader->quotes->add($quote1); 
// or $vader->getQuotes()->add($quote1);
$quote2 = new Quote();
$quote2->value = "Help me, Obi-won; you're my only hope!";
$leia->quotes->add($quote2);
$orm->commit();
// try out the 'belongs' relationship on quote
echo $quote2->person->name; // prints 'Princess Leia Organa'

Quotes can also be removed from the set; any removed entities will be deleted from the data store when the work unit is committed.

<?php
$quotes = $leia->getQuotes();
echo count($quotes); // prints 1
foreach( $quotes as $quote ) {
        if ( $quote->value == "Help me, Obi-won; you're my only hope!" ) {
                $leia->quotes->remove($quote);
        }
}
echo count($quotes); // prints 0
$orm->commit();

Here are some more quotes for us to play with.

<?php
$quotes = array();
$quotes['Han Solo'] = array( "She's fast enough for you, old man",
        "Yeah, but this time I've *got* the money." );
$quotes['Darth Vader'] = array( "I have you now!",
        "I find your lack of faith disturbing." );
$quotes['Luke Skywalker'] = array( "I'm not such a bad pilot myself",
        "I'm Luke Skywalker. I'm here to rescue you." );
$quotes['Princess Leia Organa'] = array( 
        "Why you stuck-up, half-witted, scruffy-looking nerf herder!",
        "Aren't you a little short for a stormtrooper?" );
foreach( $quotes as $name => $personQuotes ) {
        if ( $person = $orm->find('Person', array('name'=>$name)) ) {
                foreach( $personQuotes as $quoteText ) {
                        $quote = new Quote();
                        $quote->value = $quoteText;
                        $person->quotes->add($quote);
                }
        }
}
$orm->commit();

And now that they're all saved, let's go through all person entities in the data store and print their quotes.

<?php
$people = $orm->getAll('Person');
foreach( $people as $person ) {
        $quotes = $person->getQuotes();
        if ( count($quotes) ) {
                echo "Quotes for ".$person->name.":\n";
                foreach( $quotes as $quote ) {
                        echo "\t\"".$quote->value."\"\n";
                }
                echo "\n";
        }
}

9.6.2.1. Cascading

When an entity is deleted or has its primary key changed, other entities that depend on it could be orphaned if necessary constraints aren't defined in the data store. If the backend doesn't have the capability to manage these kind of constraints, a one-to-many relationship can have options set to enforce them. There are two options available to a one-to-many relationship for this purpose.

The onDelete option supports three values available as constants on the Xyster_Orm_Relation class. ACTION_SET_NULL will set the foreign key to null on all entities in the associated set. ACTION_CASCADE will let the backend take care of the cascade and just removes the related entities from the session. ACTION_REMOVE will delete every related entity in the set one at a time. ACTION_NONE does nothing; if the backend has constraints they are still enforced.

The 'onUpdate' option only supports one option in addition to ACTION_NONE. ACTION_CASCADE will set the foreign key in each entity in the set. Regardless of the setting, any constraints the backend has defined are enforced.

9.6.3. Joined (Many-to-many)

A joined relationship, also called many-to-many, uses a table to store the primary keys of two entities, relating them. Let's define another entity.

<?php
class Skill extends Xyster_Orm_Entity {}
class SkillSet extends Xyster_Orm_Set {}
class SkillMapper extends Xyster_Orm_Mapper
{
        protected $_domain = 'myDb';

        public function init()
        {
                $details = array("table"=>'person_skill', "class"=>'Person');
                $this->_hasJoined('people', $details);
        }
}

The joined relationship takes many arguments in the details array. The table is by default the declaring class and the joined class separated by an underscore. In this case, it would be "skill_person", but let's say the table is really called "person_skill". See the Xyster_Orm_Relation API for more information about parameters to this method. Let's also define this relationship in the person mapper.

<?php
class PersonMapper extends Xyster_Orm_Mapper
{
        // protected $_domain, etc.

        public function init()
        {
            $this->_hasOne('nemesis', array('class'=>'Person', 'id'=>'nemesisId'))
                ->_hasMany('quotes')
                ->_hasJoined('skills');
        }
}

Now that the relationships are defined, let's create some skill entities to use.

<?php
$skills = array("Piloting", "Gunning", "The Force", "Diplomacy", "Leadership");
foreach( $skills as $skillName ) {
        $skill = new Skill();
        $skill->name = $skillName;
        $orm->persist($skill);
}
$orm->commit();

And we can now relate these skills to people.

<?php
// load the skills
$piloting = $orm->find('Skill','Piloting');
$gunning = $orm->find('Skill','Gunning');
$theForce = $orm->find('Skill','The Force');
$diplomacy = $orm->find('Skill','Diplomacy');
$leadership = $orm->find('Skill','Leadership');
// and let's assign the skills
$luke->skills->addAll(Xyster_Collection::using(array($piloting, $gunning, $theForce)));
$vader->skills->addAll(Xyster_Collection::using(array($piloting, $gunning, $theForce, $leadership)));
$leia->skills->addAll(Xyster_Collection::using(array($diplomacy, $theForce, $leadership))); // debatable
$han->skills->addAll(Xyster_Collection::using(array($piloting, $gunning, $diplomacy)));
$orm->commit();

So now there will be 13 records in the person_skill table. 3 for Luke, 4 for Vader, 3 for Leia, and 3 for Han.