Model Relationships

A key piece of using ADM effectively is going to be knowing how to setup relationships between models and the database, and models with other models. This page will walk through the more common scenarios and how to deal with them.

Model to Database

This is super simple, but super important. Models are mapped to a persistance layer (likely a database) by defining what connection they use for storage and retrieval. If no connection is explicitly defined on a model, then it is assumed to use the Default connection from the config settings. The connection can be defined via a $model_connection data member.

class User extends \Cora\Model {
    // Specify that this model uses the connection named "Db2".
    public $model_connection = 'Db2';

    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ]
    ];
}

SOLID Principles

A quick sidenote for SOLID purists - the idea of having a separate mapper for Model => Connection was considered, but ultimately decided against. Being able to look at a model an infer something about what data store it came from and gets stored to is convenient, especially considering there's no direct tie to the persistence layer. More importantly, adding an additional mapping layer would add complexity without any benefit in functionality. Currently the relationship between objects looks like this: Model => Connection => Storage Medium.

Comparing that to Model => Mapper => Connection => Storage Medium just sub out "Model" for "Mapper" in the above bullet list and the functionality is the exact same. The only difference is there's no indication on the Model of how it's used.

[How to save a model to multiple mediums? Say I want to save the same model to both Database1 and Database2? If passed to different Repo, issues with related models?]

Model to Table

Normally a model will be mapped to a table/collection that is the same name as the model itself, but with an "s" added to the end. For example, a User model would by default be mapped to a "users" table. Then for models outside the base models directory, each piece of their namespace will have an underscore (see Default Table Names section). However, if you want to change what table a model gets mapped to, you can do so by defining a $model_table data member.

class User extends \Cora\Model {
    // Specify the name of the table we want to map this User model to.
    public $model_table = 'site_users';

    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ]
    ];
}

This is also a good opportunity to mention that if you are using a connection to something other than a database (say the connection adaptor saves to plain text files), then it's up to the creator of that adaptor to decide what to do with settings that don't map perfectly to non-database storage mediums.

Model to Table Rows

Normally there's a 1-to-1 relationship between a model and the number of rows in a table. If you call:

$users = $usersRepository->findAll();

On a Users repository, the result will be all the users in the table. If you wanted only "active" users you would have to normally pass in a partial query or else grab everything and then filter afterwards:

// Grab only "active" users
$users = $usersRepository->findAll(function($query) {
  return $query->where('type', 'Active');
})

// Grab all users, but then filter out non-active ones after the fact
$users = $usersRepository->findAll();
$users->filter(function($user) {
    return $user->type == 'Active';
});

This works fine, but if you have a specific subset you need to access often in your app, having to do this extra work every time can end up being cumbersome. ADM provides an answer to this in the form of a static method you can define called model_constraints.

Assuming we have a "User" model defined, below we define a subset ActiveUser model that extends it and only fetches users who's status is "Active'. This model will fetch data from the Users table, but there will no longer be a 1-to-1 relationship between the model and the number of rows on the table.

Model Constraints Example

<?php
class ActiveUser extends \Models\User {
    // Specify that we want to fetch data from the Users table
    public $model_table = 'users';

    // Define constraints on this model
    public static function model_constraints($query) 
    {
        $query->where('status', 'Active');
        return $query;
    }
}

Now if we grab all the models from an ActiveUsers repository, it will pull from the Users table but only give us the subset.

$activeUsers = $activeUsersRepository->findAll();

Model Attribute to Table Column

By default, an attribute on a model will map to a column of the same name in a database table. However, it will be common that you will want to change this behavior. Doing so is easy, just add a "field" definition to tell ADM which column to map that field to. In the example below we say to map the "name" attribute to a column named "full_name" in the database.

class User extends \Cora\Model {
    // Specify the name of the table we want to map this User model to.
    public $model_table = 'site_users';

    public $model_attributes = [
        'user_id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar',
            'field' => 'full_name'
        ]
    ];
}

It's important to note that as of this writing, the primary key column cannot utilize the "field" setting. In the case of the primary ID, it must match the name of the underlying table column. However, there is a way to achieve the same functionality.

Using the above as an example, our model ID is defined as "user_id" because presumably that's what the ID column on the database table is called. But let's say we wanted to be able to just type $user->id in our code. We can do this by defining can alias for the ID column:

class User extends \Cora\Model {
    // Specify the name of the table we want to map this User model to.
    public $model_table = 'site_users';

    public $model_attributes = [
        'user_id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar',
            'field' => 'full_name'
        ]
    ];

    // This let's us type $user->id instead of $user->user_id.
    protected $id_name = 'user_id';
}

Dynamic field mapping (new as of v2.6)

Another way you can change which the mapping from database fields to model attribute is by passing in a LoadMap when fetching the models. Note: This will override any mappings hard-coded into the model definitions.

// Create a LoadMap
$loadMap = new \Cora\Adm\LoadMap([
  'full_name' => 'name'
]);

// Fetch users and pass in the LoadMap
// If you don't want to apply constraints, just return the query
$users = $repo->findAll(function($query) {
  return $query;
}, false, $loadMap);

One-to-One via Relation Table

So this is a relationship setup I generally don't recommend be used, but for completeness sake I added it in at some point. The idea is you want a pure one-to-one relationship between one model and another. Let's say you have a forum where users can chat and each user has the option to upload a custom profile picture for themselves. That picture will be stored a separate table from the main users table, and each pic will be unique to the user that uploaded it. One way to setup that relation would be:


class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'pic' => [
            'model' => 'Avatar',
            'usesRefTable' => true,
            'relName' = 'userAvatar'
        ]
    ];

    public function __construct($name = null)
    {   parent::__construct();
        $this->name = $name;
    }
}

class Avatar extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'file' => [
            'type' => 'blob'
        ],
        'owner' => [
            'model' => 'User',
            'usesRefTable' => true,
            'relName' = 'userAvatar'
        ]
    ];

    public function __construct($file = null)
    {   parent::__construct();
        $this->file = $file;
    }
}

The "usesRefTable" descriptor is necessary because normally a single related model would be stored as a column on the same table and this forces the use of a 3rd relation table. The "relName" descriptor is to ensure both sides of the relationship use the same relation table.

Note that relation tables by nature support many-to-many relationships, so what we're doing here is artificially limiting that capability. Also, relation tables necessitate the need for JOINs on the data fetched, so there's some level of performance cost. What I'd probably recommend using instead is a one-to-many by column setup where you just limit the relationship to a one-to-one by convention within your codebase.

One-to-One via Abstract

For another pure one-to-one solution, you can use abstract model constraints. This yet again is a solution I would advise against, but is included here for completeness' sake. For a better description of how Abstract relationships work, please see the "One-to-many by Abstract" section.


class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'pic' => [
            'model' => 'Avatar'
        ]
    ];

    public function __construct($name = null)
    {   parent::__construct();
        $this->name = $name;
    }
}

class Avatar extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'file' => [
            'type' => 'blob'
        ],
        'owner' => [
            'model' => 'User',
            'using' => '_getOwner'
        ]
    ];

    public function __construct($file = null)
    {   parent::__construct();
        $this->file = $file;
    }

    protected function _getOwner($query) {
        $query->where('pic', $this->id);
        return $query;
    }
}

One-to-Many via Column

Let's say you have some Users and some Roles (admin, moderator, registered user, etc). Your Users are stored in a table named "users" and your list of Roles are stored in a table named "roles". You want to be able to assign a role to a user. You can do that by simply defining a "model" descriptor on an attribute and specifying to which type of model it's related.


class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'role' => [
            'model' => 'Role'
        ]
    ];

    public function __construct($name = null)
    {   parent::__construct();
        $this->name = $name;
    }
}

class Role extends \Cora\Model {
    // Set collection access key
    public $model_collection_offset = 'name';

    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ]
    ];

    public function __construct($name = null)
    {   parent::__construct();
        $this->name = $name;
    }
}

What this does is map the "role" column on the users table to a specific role in the roles table. To assign a role to a user, you would then either assign the person a pre-existing role or else create a new one.


//////////////////////////////////////
//  Assign an existing role to a user
//////////////////////////////////////

// Let's create a new user Bob
$user = new \Models\User('Bob');

// Grab pre-existing list of roles
$roles = $this->rolesRepository->findAll();

// Assign a role to Bob
$user->role = $roles->admin;

// Save
$usersRepository->save($user);

//////////////////////////////////////
//  Create a new role for a user
//////////////////////////////////////

// Let's create a new user Bob
$user = new \Models\User('Bob');

// Create a new role for Bob
$user->role = new \Models\Role('CoolDude');

// Save
$usersRepository->save($user);

After doing this, if you pull up Bob in your database you should see an ID number corresponding to the role he was assigned in his "role" column.

This is considered a one-to-many relationship because while a user would only have a single Role, a Role (say "admin") could have many users who belong to it. Let's modify our solution to have a reference on the Roles side of the relationship to all the Users who have that Role:


class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'role' => [
            'model' => 'Role'
        ]
    ];

    public function __construct($name = null)
    {   parent::__construct();
        $this->name = $name;
    }
}

class Role extends \Cora\Model {
    // Set collection access key
    public $model_collection_offset = 'name';

    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'users' => [
            'models' => 'User',
            'via' => 'role'
        ]
    ];

    public function __construct($name = null)
    {   parent::__construct();
        $this->name = $name;
    }
}

A User has a single Role, but a Role can have multiple Users. Calling $user->role will return a single Role model (if one has been assigned), while calling $role->users will return a collection that may have any number of Users within it.

The "via" descriptor tells ADM which column on the Users table to look at when fetching users. Let's say the "admin" role has an ID number of 3 in the roles table. "via" tells ADM to look at the users table via the "role" column and find all the users who have a value of 3 for their role. What you're saying is "grab me all the related users via this column".

For one more example, let's view a User and a Comment model. This time the plural side will be on the Users model (a user can have many comments) while the singular side is with the comment (a comment can only have one owner who made it).


class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'comments' => [
            'models' => 'Comment',
            'via' => 'madeBy'
        ]
    ];

    public function __construct($name = null)
    {   parent::__construct();
        $this->name = $name;
    }
}

class Comment extends \Cora\App\Model {
    public $model_attributes = [ 
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'madeBy' => [
            'model' => 'User'
        ],
        'timestamp' => [
            'type' => 'datetime'
        ],
        'text' => [
            'type' => 'text'
        ]
    ];

    public function __construct($madeBy = null, $text = null)
    {
        $this->madeBy = $madeBy;
        $this->text = $text;
    }
}

One-to-Many via Relation Table

Below is an example of a one-to-many relationship between a User (a user can have many Articles they've written) and and some Articles (an article can only have a single author).


class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'articles' => [
            'models' => 'Article',
            'relName' = 'userArticle'
        ]
    ];
}

class Article extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'author' => [
            'model' => 'User',
            'usesRefTable' => true,
            'relName' = 'userArticle'
        ]
    ];
}

As discussed in previous sections, you have to force the singular side of the relationship to use a reference table. Then defining the relationship name is so that both sides of the relationship use the same table. Another option is explicitly defining the relation table name using "relTable", but see the many-to-many via relation table section for more info on that.

One-to-Many via Abstract

Most relationships between models tend to be explicit. A User has X role as defined by their role ID. A Topic has X comments, where each comment is related to a topic by the topic's ID. Model X is related to model Y via a relation table that says ID A on table X is related to ID B on table Y. All these are explicit connections.

However, there very well might be situations where the relationship is somewhat abstract and not explicitly defined. An example might be getting users from the same city:

$user = $usersRepository->find(1); // Grab user #1
foreach ($user->usersFromSameCity as $person) {
    echo $person->name."<br>";
}

Could you keep a giant relation table that maps every user to every other user in the same city? Possibly. Would it be a rediculous solution? Yes. Yes, it would.

A better solution is to use a method to define some sort of custom relationship. This is done via the "using" descriptor on an attribute. The value should be the name of the method that will define the relationship.

class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'city' => [
            'type' => 'varchar'
        ],
        'usersFromSameCity' => [
            'models' => 'User',
            'using' = '_similarUsers'
        ]
    ];

    protected function _similarUsers($query) {
        // (This query will be executed against the Users table)

        // Specify that we only want Users who are from the same city
        $query->where('city', $this->city);

        // Exclude the current user from the results.
        $query->where('id', $this->id, '<>');

        // Now that our custom restrictions have been defined, return the query.
        return $query;
    }
}

Many-to-Many via Relation Table

Default Table Names, Same Database

This case uses ADM's default settings and would likely only be useful to someone that is generating their database from the models. We're also saying that both these models are located in the same database for this example.

For this example let's pretend we're operating a collaborative blogging site that let's multiple users author the same article together. This is a many-to-many relationship in that a User can have multiple articles they've written and an Article can have multiple Users that helped write it.


class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'articles' => [
            'models' => 'Article',
            'relName' => 'authorArticle'
        ]
    ];
}

class Article extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'authors' => [
            'models' => 'User',
            'relName' => 'authorArticle'
        ]
    ];
}

One quick thing of note is the "relName" (relationship name) field. Why did we need this? The answer in short is that we needed that because we want both sides of the relationship to use the same relation table. If we edit an Article's authors, we want that change to also be reflected in the list of Articles a user has written if the data gets pulled from the User side of the relationhip. If we didn't put this in then the User side of the relationship would pull from a table named

ref_users__articles__articles

and the Article side would have pulled from a relation table named

ref_users__authors__article

See the section of this document related to Default Table Names for more info.

Another option to solve the above problem is we could have explicitly set the full name of the relation table with the "relTable" setting. You can see that in action in this section by looking at the "Custom Table Names" use case.

[Picture of Tables]

Default Table Names, Different Database

Let's use the same collaborative blogging site example from above, only this time let's have the Articles live in a different database from the Users. This setup is almost identical, the exception being you have to tell ADM which database to use for the relation table. This is done by specifying one side of the relationship as the "passive" side. ADM will then know not to use this "passive" side when looking for the relation table.

The Article model is defined as using connection 'DB2'. Because no $model_connection attribute is defined on the User model, it will use the default connection as defined in your app's database config file.


class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'articles' => [
            'models' => 'Article'
        ]
    ];
}

class Article extends \Cora\Model {
    public $model_connection = 'DB2';
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'authors' => [
            'models' => 'User',
            'passive' => true
        ]
    ];
}

It's worth noting that we didn't need the "relName" setting this time. Because we're forcing the relationship to be controlled by one side, ADM knows to use the same relation table for both sides.

[Picture of Tables]

Custom Table Names, Pre-Existing Database

For many of you reading this, you'll either already have a database schema setup that you need to adhere to, or you'll just want to setup the database yourself without any sort of auto-generation or consideration of models. In these cases you'll need the flexibility to map the model to the database as it already exists. Here we show how to tell ADM which table to use for the relations, and also which columns in that table have the IDs for each side of the relationship.

Notice that "relThis" and "relThat" swap depending on which side of the relationship you're looking at.


class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'articles' => [
            'models' => 'Article',
            'relTable' => 'users_articles',
            'relThis' => 'user_id',
            'relThat' => 'article_id'
        ]
    ];
}

class Article extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'authors' => [
            'models' => 'User',
            'relTable' => 'users_articles',
            'relThis' => 'article_id',
            'relThat' => 'user_id'
        ]
    ];
}

[Picture of Tables]

Special Case, Same Type

Let's talk about the special case where a model has a relationship with other models of the same type and you're using a relation table. For instance, a User might have some relationship to other Users that you need to represent.

In this example we'll say a user has "friends" who are other users.


class User extends \Cora\App\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'friends' => [
            'models' => 'User',
            'relTable' => 'ref_users_friends',
            'relThis' => 'user_id',
            'relThat' => 'friend_id'
        ]
    ];
}

// In this edge case there is no 2nd model. The User is getting related to other users.

Default Table Names

The default reference table name is "ref_" followed by the namespace of the first model with plural "s" on the end, then a double underscore, then the name of the relationship, then a double underscore, and the namespace of the second model (likewise plural). Each piece of a namespace is separated by a single underscore "_".

For example:

Relating a User model to a number of Users\Comment models where the relationvia relation table would default to "ref_users__users_comments".