jpfuentes2 / php-activerecord

ActiveRecord implementation for PHP
http://www.phpactiverecord.org/
Other
1.32k stars 443 forks source link

How to nest has_many through relationships? #523

Open IllyaMoskvin opened 8 years ago

IllyaMoskvin commented 8 years ago

This is not so much a feature request, but rather, more of a question about best practices with the current version of PHP AR. Let's say that I have three entity types, which are connected in sequence through xref relationship tables:

entity_category = id | title | ...
entity_entry = id | description | ...
entity_object = id | accession | ...
xref_category_entry = id | category_id | entry_id
xref_entry_object = id | entry_id | object_id

There are two has_many through relationships here. In plain-speak, a category has multiple entries, an entry can belong to multiple categories, an entry features multiple objects, and an object can be featured in multiple entries.

I've set up my models. Everything works great. Leaving out the lengthy fields, I can do, for instance:

$category = Category::find( 10, array(
    'include' => array('entries' => array('objects') )
) );

$result = $category->to_json( array(
    'only' => array('id', 'title'),
    'include' => array('entries' => array(
        'only' => array('id','description'),
        'include' => array('objects' => array(
            'only' => array('id', 'accession')
        ))
    ))
));

...and get the following:

{
    id: 10,
    title: "Gabra",
    entries: [
        {
            id: 12,
            description: "I am an entry with only one object.",
            objects: [
                {
                    id: 12,
                    accession: "2014.220"
                }
            ]
        },
        {
            id: 13,
            description: "I am an entry with two objects.",
            objects: [
                {
                    id: 12,
                    accession: "2014.220"
                },
                {
                    id: 13,
                    accession: "2014.217"
                }
            ]
        }
    ]
}

However, what if I'd like the option to receive this JSON instead? Note that duplicate objects are automatically removed:

{
    id: 10,
    title: "Gabra",
    objects: [
        {
            id: 12,
            accession: "2014.220"
        },
        {
            id: 13,
            accession: "2014.217"
        }
    ]
}

Is there any easy way to set up my models to make serialization painless? For instance:

$result = $category->to_json( array(
    'only' => array('id', 'title'),
    'include' => array('objects' => array(
        'only' => array('id', 'accession')
    ))
));

Likewise, $category->entries returns an array of entries associated with that category. How should I configure my models so that $category->objects would return an array of objects associated with the entries that are associated with that category, with duplicates removed?

Apologies for the long, round-about post. I couldn't think of a better way to explain my situation without giving real-world examples. I'm also not sure if this question has been asked before; I searched, but had no luck. If there are previous discussions on this topic, please do link me to them. If you'd like me to share my models, please ask, but I think the setup is pretty intuitive given the tables involved. I'll keep working on this problem in the meantime. Thank you for your time.

IllyaMoskvin commented 8 years ago

This seems a bit insane to me, but I imagine that a nested has_many through relationship might be defined as follows:

class Category extends ActiveRecord\Model {

    static $table_name = 'entity_categories';
    static $primary_key = 'id';

    static $has_many = array(

        // This works currently
        array( 'entries', 'through' => 'category_entry_links' ),

        // This should(?) work
        array( 'entry_object_links', 'through' => array('entries', 'through' => 'category_entry_links') ),
        array( 'objects', 'through' => array('entry_object_links', 'through' => array('entries', 'through' => 'category_entry_links') ) ),

    );

}

Alternatively, I tried defining static $delegate = array( array('objects', 'to' => 'entries') ); for kicks, since $entry->objects would be a valid call, but no luck: I got an Call to member function __get() error.

koenpunt commented 8 years ago

I'm afraid such a relation is currently not possible. What I usually do in cases like this, is adding a get_association method to the model. In your case get_objects and write the join myself;

Object::find('all', [
  'join' => 'JOINS ... ON ...',
  'conditions' => ['category_id' => $this->id]
]);

This is just of the top of my head so it could be that I made a mistake