Solved thread

This post is marked as solved. If you think the information contained on this thread must be part of the official documentation, please contribute submitting a pull request to its repository.

Model with relationships

Hi, I´m currently trying to use Phalcon for a backend service framework for apps. I know PHP quite well, and i've heard a lot good things about Phalcon and decided to give it a try. I like it very much, although i have stumbled across a problem i cannot seem to solve, not even by spending the last couple of hours on google (Normally that does the trick).

Anyways, i´m trying to define a relationship betwen two Models - A user table, and a country table. Each user has a country, but not the other way around. I started by simply defining a column in the user table as a foreign key to the country table. I used the hasOne method in the initializing method on the Users model, but this resulted in warning when doing a $user->country->name.

So i re-RTFM, reworked the tables and the model, added a relational table between the user and the country table. My current table design is (A little truncated - i left some non important fields out):

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=2 ;

CREATE TABLE IF NOT EXISTS `countries` (
  `code` varchar(2) COLLATE utf8_unicode_ci NOT NULL,
  `name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE IF NOT EXISTS `users_countries` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `users_id` int(10) NOT NULL,
  `countries_id` varchar(2) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  KEY `countries_id` (`countries_id`),
  KEY `users_id` (`users_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=5 ;
ALTER TABLE `users_countries`
  ADD CONSTRAINT `users_countries_ibfk_4` FOREIGN KEY (`countries_id`) REFERENCES `countries` (`code`) ON DELETE CASCADE ON   UPDATE CASCADE,
  ADD CONSTRAINT `users_countries_ibfk_3` FOREIGN KEY (`users_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

And my PHP Models:

class Users extends \Phalcon\Mvc\Model {
  // Properties here - all public.
  public function initialize() {}
    $this->hasOne('id', 'UserCountries', 'users_id', array('foreignKey' => TRUE));
    $this->hasOne('id', 'UserPictures', 'users_id');
  }
}

class UsersCountries extends \Phalcon\Mvc\Model {
  // Properties here - all public.
  public function initialize() {
    $this->belongsTo('users_id', 'Users', 'id');
    $this->belongsTo('countries_id', 'Countries', 'id');
  }
}

class Countries extends \Phalcon\Mvc\Model { }

So the idea is that the user know about the country, but the country doesn´t know anything about the user.

Now when i create a user, i do

$user = new Users;
$user->username = "kaa";
$user->password = "test";
$user->save();

The user gets created alright, but i do not have a clue about how to create the relation to country the proper way. I could do:

$country = new UsersCountries;
$country->users_id = $user->id;
$country->countries_id = 'DK';
$country->save();

which does store the relation in the DB, but i cant find any way to access the relation from the user object. When i try by

$user->usersCountries

i get the exception "Uncaught exception 'Phalcon\Mvc\Model\Exception' with message 'Model 'UserCountries' could not be loaded".

So it seems to me, that im kinda missing the point of using these relationships between models in Phalcon. Anyone know how to work with these relationship s the right way / know a place i can read about them?

Besides that, i find Phalcon very nice to work with, so good work devs :-)

Best regards.

edited May '14

I've updated your post with better syntax highlighting. The help section here: http://forum.phalconphp.com/help/markdown explains how, or you can just "Edit" your post to see what I did.

Also, can you double-check your table declaration? You're declaring countries twice, not declaring anything like userCountries, and you have nothing about ids.

Yea that looks better, sry for the inconvenience, but thanks.!

edited May '14

I edited my previous post with some more comments. No worries about the inconvenience - we don't yet have a good way of communicating proper code formatting.

edited May '14

That was a copy / paste error - i have edited the post and added the right table dump, thanks again..

If each user has 1 country, you should get rid of the users_countries table, and just add a column to the users table. users_countries is totally superfluous. You can then map a user directly to the country.

I do agree it is superfluous, although one could argue it adds another layer of abstraction. Still though, superfluous or not, I still dont understand why it does not work? And how to get relationships like this to work at all. The project im working on (and later projects) will require table design where a relation table like the one above is necessary, so i kinda need to get it working.

I dont feel i can find enough information about this in the manuel. Do you know of any examples where a relationship actually works as intended?

Unless I'm misunderstanding something (which is entirely possible), that intermediate table serves technically no purpose. There is a one-to-one relationship between the users and users_countries table. The users_countries table doesn't add anything. Proper nomalization technique would say to combine those two. In short (again, unless I'm misunderstanding something), that's just poor database design.

Anyway, enough about that. You've set up your relationship with the name "userCountries", but the class & table name is "usersCountries" - you're missing the "s" when you're setting up your relation.

edited May '14

Embarrassing i totally overlooked that. However, except for the exception not being thrown, (of course), it still fails with Access to undefined property error when i try to access the $user->usersCountries property.

It´s like the Users model does not know that a country relation has been created for the user.

Just to experiment a little, i have tried to delete the users_countries relation table, and just adding a column on the users table, mapping directly to the countries table (like you suggested). This way i create a user by

$user = new Users;
$user->username = "kaa";
$user->password = "test";
$user->country = "DK";
$user->save();

Now if my understanding is not totally off (which it might well be), accessing $user->country should return to me a Countries model object, which looks like:

class Countries extends \Phalcon\Mvc\Model {
   public $code;
   public $name;
}

and therefore doing $user->country->name should return, in this case, "Denmark". Sadly it is the same error as before mentioned - Access to undefined property error.

I dont know why, but i´m having a hard time wrapping my head around relationship between models in Phalcon. I mean, I could easily enough just code my way out of it, but this will just result in (alot of) redundant code.

Try removing the "foreignKey" option in your users class. It's not necessary unless you want cascading updates/deletes, and I've noticed it causes a lot of problems for people.

As for your "little experiment" - your problem there is you have a relationship with the same name as a property - "country". If you rename the appropriate user column to country_code and update your code accordingly, it should work. You can also set up an alias for a relationship if you prefer.

edited May '14

Hmm doing as you suggested, changing the column to country_code, and updating the code still gives me an Access to undefined property error, when i try to access $user->country.

So in the model Users my initializing method is

public function initialize() {
    $this->hasOne('country', 'Countries', 'code');
}

But this does not make much sense to me. With the docs in mind

The first parameter indicates the field of the local model used in the relationship; the second indicates the name of the referenced model and the third the field name in the referenced model

I understand (and correct me if i'm wrong) that the first parameter of the hasOne() method, which in the example above is country, should be the name of the local property, which after the change is named country_code.

So i dont see why the hasOne() shouldn't be:

public function initialize() {
    $this->hasOne('country_code', 'Countries', 'code');
}


50.1k
Accepted
answer

It should be. You're just referencing the relationship wrong. Part of this may be confusion with plurality - which is why all my models and tables are all singular named (ie: "country" instead of "countries" since each user only has one country).

Relationships are referenced by the related model name, so assuming your last snippet is the one you're using, you can refer to the related Country model with:

$user->Countries

However, you can also set up an alias for the relationship like so:

public function initialize() {
    $this->hasOne('country_code', 'Countries', 'code',['alias'=>'country']);
}

which would allow you to use:

$user->country
edited May '14

Ahh finally i got it! And got it working too!

The second parameter of hasOne (which by restudying the manuel, perhaps in this scenario should be a belongsTo?), is the property name you use on the model object when accessing the relationship.

The manual says

By accessing an attribute with the same name as the relationship will retrieve all its related record(s).

which is true and all, but in my case it was just a little confusing. I don't know, properly just me :-)

Anyways, quasipickle, thanks so much for your time, patience and effort to help me understand, much appreciated!

So i guess i "accept" your last answer, right?

Actually, it's the third parameter is the property name. The second parameter is the model name.

You should "accept" whichever comment in the thread was the closest to answering your question. The point of accepting an answer is to help others who might be having similar problems - but yes, I think my last response was probably the most accurately helpful to you.