Yii ActiveRecord using with() not finding joined record - yii

I have two models: User and UserProfile
Inside the User model, I have defined the following relations:
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'userProfile' => array(self::HAS_ONE, 'UserProfile', 'user_id'),
);
}
In UserProfile, I have this relation defined:
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'user' => array(self::BELONGS_TO, 'User', 'user_id'),
);
}
Now when I run the following code in my controller:
$user = User::model()->with('userProfile')->findByPK($userId);
$userProfile = $user->userProfile;
print_r($userProfile);
The $userProfile variable is null. I've checked and double-checked the database and code, I've re-read the Yii documentation as well, and seems everything is the way it should be. But it just refuses to work!
Any idea what am I doing wrong?

Generally, you can't have this:
'userProfile' => array(self::HAS_ONE, 'UserProfile', 'user_id'),
and this:
'user' => array(self::BELONGS_TO, 'User', 'user_id'),
both use the user_id key unless both your tables have a user_id key as their primary key. More likely, what you're after is this, like you have:
'userProfile' => array(self::HAS_ONE, 'UserProfile', 'user_id'),
But what that equates to is the SQL statement:
user.id = userProfile.user_id
If that isn't what you want, then you'll need to adjust accordingly. One of the most helpful things for figuring this out is either turning on basic logging of your SQL statements or using the Yii debug toolbar Makes it much easier to see what SQL is being run vs. what you thought would be run.

Related

Can I transform the value of my primary key in a specific one-to-many relationship using Fluent API?

I'm trying to manually scaffold part of an existing database schema in EF Core 2.2. The database in question is part of a 3rd party ERP software, and I have zero control over the design of the database itself, so I'm trying to work with what I am given. So far it's going okay, however I am hitting a snag due to a questionable database design choice by the ERP vendor.
Consider the following one-to-many relationship I'm trying to build in my OnModelCreating() method within my DbContext (via Fluent API):
modelBuilder.Entity<SalesOrderHeader>(s => {
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.SalesOrderNumber)
.HasForeignKey(e => e.RelatedPO_SONumber);
});
This is almost what I need, however, in the WorkOrders table "RelatedPO_SONumber" is of type string, where "SalesOrderNumber" in SalesOrderHeaders table is of type int. Further, what's stored in "RelatedPO_SONumber" is a 0-padded string (always 8 characters long) of "SalesOrderNumber" (eg SalesOrderHeader.SalesOrderNumber = 1234567, WorkOrder.RelatedPO_SONumber = "01234567").
Here's what I tried and might help illustrate what I'm trying to do via format specifier (throws ArgumentException "The expression should represent a simple property access: 't => t.MyProperty'"):
modelBuilder.Entity<SalesOrderHeader>(s => {
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.SalesOrderNumber.ToString("D8"))
.HasForeignKey(e => e.RelatedPO_SONumber);
});
I tried an alternative approach which was to make another property in my SalesOrderHeader entity definition, and using that as the argument for HasPrincipalKey():
// within SalesOrderHeader
[NotMapped]
public string ZeroPaddedSONumber => SalesOrderNumber.ToString("D8");
// within DbContext OnModelCreating()
modelBuilder.Entity<SalesOrderHeader>(s => {
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.ZeroPaddedSONumber)
.HasForeignKey(e => e.RelatedPO_SONumber);
});
This resulted in:
"InvalidOperationException: No backing field could be found for property 'ZeroPaddedSONumber' of entity type 'SalesOrderHeader' and the property does not have a setter."
Is there a way to pass anything other than a strict property reference, but rather a transformed "version" of that property?"
Thanks in advance.
EDIT: Following a suggestion in a comment, I tried this:
modelBuilder.Entity<SalesOrderHeader>(s => {
s.Property(e => e.SalesOrderNumber)
.HasConversion(n => n.ToString("D8"), s => int.Parse(s));
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.SalesOrderNumber)
.HasForeignKey(e => e.RelatedPO_SONumber);
});
This doesn't seem to work either, I get the following exception (I get the same exception if I omit the conversion):
The types of the properties specified for the foreign key {'RelatedPO_SONumber'} on entity type 'WorkOrderHeader' do not match the types of the properties in the principal key {'SalesOrderNumber'} on entity type 'SalesOrderHeader'.

How to use existing data from the database in Codeception FactoryMuffin?

I'm trying to set up easy test data in my Acceptance tests:
public function shouldUseAFakeAccountHolder(AcceptanceTester $I) {
$I->have(AccountHolder::class);
// ...
}
I've copied the example code from the Codeception documentation and modified it with my entity names (as well as fixing the bugs).
<?php
public function _beforeSuite()
{
$factory = $this->getModule('DataFactory');
// let us get EntityManager from Doctrine
$em = $this->getModule('Doctrine2')->_getEntityManager();
$factory->_define(AccountHolder::class, [
'firstName' => Faker::firstName(),
// Comment out one of the below 'accountRole' lines before running:
// get existing data from the database
'accountRole' => $em->getRepository(AccountRole::class)->find(1),
// create a new row in the database
'accountRole' => 'entity|' . AccountRole::class,
]);
}
The relationship using existing data 'accountRole' => $em->getRepository(AccountRole::class)->find(1) always fails:
[Doctrine\ORM\ORMInvalidArgumentException] A new entity was found through the relationship 'HMRX\CoreBundle\Entity\AccountHolder#accountRole' that was not configured to cascade persist operations for entity: HMRX\CoreBundle\Entity\AccountRole#0000000062481e3f000000009cd58cbd. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'HMRX\CoreBundle\Entity\AccountRole#__toString()' to get a clue.
If I tell it to create a new entry in the related table 'accountRole' => 'entity|' . AccountRole::class, it works, but then it adds rows to the table when it should be using an existing row. All the role types are known beforehand, and a new random role type makes no sense because there's nothing in the code it could match to. Creating a duplicate role works, but again it makes so sense to have a separate role type for each user since roles should be shared by users.
I've had this error before in Unit tests, not Acceptance tests, when not using Faker / FactoryMuffin, and it's been to do with accessing each entity of the relationship with a different instance of EntityManager. As soon as I got both parts using the same instance, it worked. I don't see how to override the native behaviour here though.
It works (at least in Codeception 4.x) by using a callback for the existing relation:
<?php
public function _beforeSuite()
{
$factory = $this->getModule('DataFactory');
$em = $this->getModule('Doctrine2')->_getEntityManager();
$factory->_define(AccountHolder::class, [
'firstName' => Faker::firstName(),
'accountRole' => function($entity) use ($em) {
$em->getReference(AccountRole::class)->find(1);
},
]);
}
I've found it here: https://github.com/Codeception/Codeception/issues/5134#issuecomment-417453633

Zend Authentication 'Zend\Authentication\Adapter\DbTable' gives error while authenticating

I used a simple instruction from
http://framework.zend.com/manual/2.0/en/modules/zend.authentication.adapter.dbtable.html#advanced-usage-by-example for Zend Authentication.
Here is my code:
$adapter = $sm->get('adapter');
$authAdapter = new DbTable($adapter);
$authAdapter -> setTableName('users')->setIdentityColumn('username')->setCredentialColumn('password');
$authAdapter -> setIdentity('admin')-> setCredential('password');
$authAdapter -> authenticate();
The above code generates error as follows:
The supplied parameters to DbTable failed to produce a valid sql statement, please check table and column names for validity.
I know if makes sense to use ZF-Commons and ZF-Users module and not reinvent the wheel... but being relatively new to ZF2 I want to try it myself.
The answer's right there in the error message, Zend\Authentication\Adapter\DbTable expects more than just an adapter as a parameter, it also needs a table name, and the name of the identifier and credential columns...
$authAdapter = new DbTable($dbAdapter,
'tableName',
'identifierColumnName',
'credentialColumnName'
);
This info is covered in the docs, which is always a good starting point -> http://zf2.readthedocs.org/en/release-2.1.4/modules/zend.authentication.adapter.dbtable.html
I know this is an old one and probably you have the answer now. Even though, I will put my answer here for further users. I myself faced lots of similar issues where the tutorials expect us to have some preconditions in place.
This error occur because you are not completely right in the statement to get the DB adapter. You need to call a service management instance with an string defined in your locals or globals configurations. For example:
I have this factory defined in my global.php file:
return array(
'service_manager' => array(
'factories' => array(
'Zend\Db\Adapter\Adapter'
=> 'Zend\Db\Adapter\AdapterServiceFactory',
),
),
'db' => array(
'driver' => 'Pdo',
'driver_options' => array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
),
),
);
In my local.php I have the credentials and information to reach my DB server as follow:
return array(
'db' => array(
'username' => 'valid_dbusername',
'password' => 'valid_dbpass',
'dsn' => 'mysql:dbname=valid_dbname;host=valid_dbserver',
),
);
This is enough to define my service calle 'Zend\Db\Adapter\Adapter'. Then, to instantiate my Db adapter I have the following lines inside any function in my controller:
if (!$this->adapter) {
$sm = $this->getServiceLocator();
$this->adapter = $sm->get('Zend\Db\Adapter\Adapter');
}
It is important to note adapter here is a class variable. So, it must be defined inside my controller class like:
public $adapter;
This steps are enough to you get an DB adapter. I am assuming you dont have a factory called 'adapter'. This should make your example work.

Kohana 3.3 ORM - how to find all ancestors in a tree (self-referencing table)

How to find all the ancestors (not only direct parent) of a record with the following model:
class Model_Category extends ORM {
protected $_belongs_to = array(
'parent' => array('model' => 'Category', 'foreign_key' => 'category_id'),
);
protected $_has_many = array(
'children' => array('model' => 'Category', 'foreign_key' => 'category_id'),
);
$category->parent->find() is only giving the direct parent and even when trying to fetch the parent of the parent with a recursive function it throws Kohana_Exception: Method find() cannot be called on loaded objects.
This occurs so natural to me that I guess there must an easy way to do it - I'm not sure if it's me or the lack of documentation on ORM relations - halp!
You should be able to do this with a recursive function or a while loop
$current = $category;
while ($current->parent->loaded())
{
//save $current
$current = $category->parent;
}
Use Nested Sets. For example, https://github.com/evopix/orm-mptt. It has special methods like parents(), children(), siblings() etc. Of course, this requires modifications in your DB table.
Ok, turns out the related models actually ARE included, cause if you do
$category->parent->parent->name it will be the name of the grand-parent of $category.
My problem was that I was printing $category->parent->as_array() and expecting to see the parent relations in the (multidimensional) array which simply appears not to be the case with kohana.

Attaching a count variable to an object

I need some help in showing a count of posts of each user in a CGridView.
For example:
I have a User and Post model.
I can access the posts of a user through $user->posts
Is there something I can add to have $user->num_posts
You should simply use a stat relation, e.g. in your User model :
public function relations()
{
return array(
// ...
'posts' => array(self::HAS_MANY, 'Post', 'user_id'),
'num_posts' => array(self::STAT, 'Post', 'user_id'),
// ...
);
}
http://www.yiiframework.com/doc/guide/1.1/fr/database.arr#statistical-query
EDIT :
You should also use eager loading to build your dataprovider, e.g. :
$criteria=new CDbCriteria;
$criteria->with('num_posts');
About SQL queries, this should use only 3 queries :
one query for gridview page count,
one query to get users models,
one query to get users num_posts.
Just take a look at Yii logs to be sure.
You can use getter in your Post model.
Something like this:
public static function getpostscount($id)
{
$total=0;
$provider=Post::model()->findall('user_id=:user_id',array(':user_id'=>$id));
foreach ($provider as $data)
{
if ($data->userid==$id)//userid name of column with user id in post model
{$total++;
}
}
return $total;
}
Then in your view just pass user->id to getter and it will return you count of posts for this user. Something like this:
echo "User have".Post::model()->getpostscount($user->id);//$user->id is id of user for wich we will look count of posts
Regards.