Find a model's siblings using a relation - php

This is the table for the model:
CREATE TABLE IF NOT EXISTS `SomeModel` (
`id` int NOT NULL AUTO_INCREMENT,
`parent_id` int NOT NULL
)
My goal is to be able to query a model with its siblings using:
SomeModel::model()->with('siblings')->findByPk($id);
Here is my current attempt at the relation:
public function relations()
{
return array(
'siblings' => array(self::HAS_MANY, 'SomeModel', array('parent_id'=>'parent_id')),
);
}
The problem is that I can't find a way to create a condition so that the model itself isn't returned along with it's siblings in the $model->siblings array.
Any thoughts would be great.
Thanks!

Change your relation to this:
'siblings'=>array(self::HAS_MANY, 'Address', array('parent_id'=>'parent_id'),'condition'=>'siblings.id!=t.id')
Edit: Some explanation, in the documentation for relation(), we can specify extra options for the join that takes place and these additional options:
Additional options may be specified as name-value pairs in the rest array elements.
Plus the default alias for the table is t hence use t.id.
Edit: from the comments:
Implementing lazy loading, the way you want it, will be tough to accomplish(I don't know how, not sure if possible either), however i can suggest making the current code better, by
using named scopes, use a scope when you are doing eager loading, and add the condition siblings.id!=t.id in the scope:
// add this function to your model, and remove the condition from the relation
public function scopes(){
return array(
'eagerSibs'=>array(
'condition'=>'siblings.id!=t.id',
),
);
}
Do eager loading with scope:
SomeModel::model()->eagerSibs()->with('siblings')->findByPk($id);
This will remove the error with lazy loading $model->siblings
Although the error of lazy loading will be removed you will still be getting the current record, to counter that you can add and use a function of the model which will load the related records without the current one, but ofcourse you won't be using $model->siblings, and instead have something like: $model->getLazySibs();
public function getLazySibs(){
$sibs=$this->siblings;
foreach ($sibs as $asib){
if ($asib->id != $this->id)
$lazySibs[]=$asib;
}
return $lazySibs;
}

Related

Getting the rows created before the selected one

I was wondering about the best way to get the count of all the rows created before the selected one. Right now I have defined an accessor that looks like this:
// In the model
public function getPositionAttribute() {
return self::where([
// Some other condition
['created_at', '<', $this->created_at->toDateTimeString()]
])->count();
}
// In the code
$model->position
It works correctly, but I'm worried about 2 things:
Is it a bad practice to call self on the model? Looks somehow off to me.
When called in a foreach this obviously generates a query for each element which is far from optimal. Is there any way to refactor this so that it can be eager loaded in a single query?
Bonus: I have totally discarded the idea of keeping a column with some kind of index because that initially sounded impossible to maintain, eg. when a record is deleted all the others should somehow shift position. Should I reconsider it? Is there a better way?
Pretty sure that using self here is the "best practice" because that is how that keyword was designed to be used.
In regards to refactoring, i personally can't think of optimizing the query as is but instead you could create a function that preloads all the position then use it normally. Assuming your model has a unique key 'id' and you are passing in a collection of model then, you can try something like this:
public static function populateOrderPositions($modelCollection){
// Optimize this query to include your "other condition"
$idCollection = Model::orderBy('created_at') // this will make it in the order of creation
->pluck('id'); // this will only retrieve the id field
// This array will contain an array with the model object ids as key and a numeric position (starts at 0)
$positionArr = $idCollection->flip()->all();
// Then just load all the position into the object and return it.
return $modelCollection->map(function($modelObj) use ($positionArr){
return $modelObj->position = $positionArr[$modelObj->id] + 1; // +1 because array key starts at 0
};
}
You would also need to adjust your attribute code to use the loaded attribute instead of ignoring the loaded attribute like so:
public function getPositionAttribute() {
return $this->attributes['position'] ?? self::where([
// Some other condition
['created_at', '<', $this->created_at->toDateTimeString()]
])->count();
}
With these changes, you can then prepopulate the position then use it afterward without the need to query the database.
These code are untested as i don't know how your model and query will be structured and is more of an example. Also you would need to compare the performance vs your original code.

Laravel Eloquent - Do not run relationship query if column value is NULL or 0

I have various relationships within my Eloquent Models that look like this:
public function main_image()
{
return $this->hasOne(Media::class, 'id', 'main_image_id');
}
However, it will run a SQL query if the main_image_id is null or 0, so I end up with a number of queries like this:
select * from `media` where `media`.`id` is null and `media`.`id` is not null limit 1
Which obviously will not return anything, but still wastes resources. Is there any way of automatically checking for that?
Currently what I do is have a method, like hasMainImage(), that checks that main_image_id is not null and not 0, but a lot of the current system already uses the relationships, and I was wondering if I can add the check to the relationship method itself?
I have tried adding a check to it and return null if the column has no real value, but I've got an Exception that it has to return a Relation object. Or if I'm trying to Eager load it, I receive following error:
Call to a member function addEagerConstraints() on null
public function main_image()
{
if (!$this->main_image_id) {
return null;
}
return $this->hasOne('App\Modules\Media\Models\Media', 'id', 'main_image_id');
}
Thanks for your help!
EDIT:
A perhaps more clear example:
$page = Page::find(1);
var_dump($page->main_image); // This will run a query as shown above that is guaranteed to return nothing
// Since at this point system knows that $page->main_image_id is 0 or null, I would like to use that to not run the query and automatically set $page->main_image to null
You declare your relations in wrong order, Eloquent documentation is clear about this.
The entity that does no make sense by itself (without its "parent" you may say) should incorporate "belongsTo" relation (as documentation says inverse).
For demonstration lets say you have User model and you have many details about that user so you create Detail model.
Now User model should have detail relation:
public function detail() {
return $this->hasOne('App\Detail', 'user_id', 'id'); //you can see the difference here from your code sample, you have 2nd and 3rd parameter reversed
}
And Detail model should have user relation:
public function user()
{
return $this->belongsTo('App\User');
}

Ordered dropDownList using relations?

I have some forms in Yii using the following to get lists of data from related tables in the form of a drop down:
dropDownList(CHtml::listData(Company::model()->findAll(array('order' => 'company ASC'))));
This works, but that means for every drop down list (which theres a lot of) I'm putting this array('order' => 'company ASC' in every one.
Is this the best way to do it? Is there not a way to get this data using the model relations(), and specifying the order within the relation?
I believe that the correct way to do this is by using scopes.
You can define any number of scopes that order the result set and use them like so:
Company::model()->scopeName()->findAll();
If your application always requires companies to be fetched in a sorted order, you can even define a default scope in your model class:
public function defaultScope() {
return array('order' => 'company ASC');
}
This will result in every call to Company::model()->findAll(); returning sorted results.
I usually add an opts() methods to each model that could be used as source for a dropdown:
class Company extends CActiveRecord
{
// ...
public static opts()
{
$opts = array();
foreach(self::model()->findAll(array('order'=>'name ASC')) as $model)
$opts[$model->id] = $model->name;
return $opts;
}
It's used like this
echo $form->dropDownList($user, 'company_id', Company::opts());
If you need the same options several times on a page, you could even "cache" the result in a private static class variable or use DAO to fetch the list data in a more efficient way.

CakePHP: Containable behaviour doesn't work with find() even if contain() is called in beforeFind

My Problem: Linked to my Employees table I've got an Address table containing a virtual field called full_name (I guess you can imagine by yourself what it does). I added the Containable Behaviour and this function
function beforeFind() {
$this->contain('Address.full_name');
}
to my Employees model, so that I don't have to call $this->contain(..) in every single controller action (I need the full_name field in pretty every action). BUT id doesn't work then if the controller action does just a $this->Employee->find('all') (or read(..). Contrary, it works if
The controller action uses $this->paginate(); instead
$this->Employee->contain('Address.full_name'); gets called before the $this->Employee->find('all'); call. I can't imagine the cause for this because after this explicit contain(..) call, contain(..) gets called again by the Model callback function beforeFind(), as a debug proofed which I inserted into the cake/libs/models/behaviours/containable.php:contain() function *cough*.
As far as I recall, a contain() statement only works once, for the query operation immediately following it. Subsequent queries will require their own contain() statement e.g.
$this->Employee->contain('Address.full_name');
$this->Employee->find('all'); //first find
// returns all of Employee + Address.full_name
$this->Employee->find('all'); //second find
// returns all of Employee + all of Address + all of any other associated tables.
I don't recommend using contain() in beforeFind() as it is intended to modify specific returns. It soon becomes second nature to use it before each query where you will then have fine control of the data returned.
If you have a widespread requirement for a limited return, you can set that up in the associations on the model.
1) Your Model beforeFind() should accept a param $queryData and return true if the find() should be executed, or false if it should abort beforeFind. Generally the beforeFind($queryData) method will modify the $queryData array and return it.
function beforeFind($queryData) {
// Modify $queryData here if you want
return $queryData
}
2) Trying to maintain a persistant $contain is a bit strange. Containable obviously contains assocations so that extra/additional information is fetched. Your virtual field should be returned in a normal find() operation. If you want to restrict the fields which are returned you should define those either in the Model or in the Model association
var $belongsTo = array(
'Employee' => array(
'className' => 'Employee',
'fields' => array('Employee.id', 'Employee.full_name'),
'conditions' => array()
)
);
Containable will rebind associations quickly for you, but you should instead define default fields/conditions through the Model association ($belongsTo, $hasMany, $hasOne etc)
Another alternative is to actually create Model methods which reflect the data you are trying to fetch, a very basic example:
function activeEmployees() {
$contain = array('Address');
$conditions array('Employee.started' => '2010-09-01');
$this->find('all', array('conditions' => $conditions, 'contain' => $contain));
}
And then call these convienience methods from the Controller just like you would a find
$this->Employee->activeEmployees();
You could also choose to pass a param to Employee::activeEmployees(); which is an array of additional $conditions or $contain options which are merged with the standard options.

Using Multiple Tables in a Zend Model and returning a combined result set

Hi This is either a very specific or very generic quetion - I'm not sure, and I'm new to the Zend framework / oo generally. Please be patient if this is a stupid Q...
Anyway, I want to create a model which does something like:
Read all the itmes from a table 'gifts' into a row set
for each row in the table, read from a second table which shows how many have been bought, the append this as another "field" in the returned row
return the row set, with the number bought included.
Most of the simple Zend examples seem to only use one table in a model, but my reading seems to suggest that I should do most of the work there, rather than in the controller. If this is too generic a question, any example of a model that works with 2 tables and returns an array would be great!
thanks for your help in advance!
I assume second tables is something like "gift_order" or something.
In this case you need to specify tables relationships beetween "gift" and and "gift_order" via foreign keys and describe it in table class.
It will look like this
class GiftOrder extends Zend_Db_Table_Abstract
{
/** Table name */
protected $_name = 'gif_order';
protected $_referenceMap = array(
"Fileset" =>array(
"columns" => array("gifId"),
"refTableClass" => "Gift",
"refColumns" => array("id")
));
........................
You need to specify foreigh key constraint while create table with SQL
ALTER TABLE `gift_order`
ADD CONSTRAINT `order_to_gift` FOREIGN KEY (`giftId`) REFERENCES `gift` (`id`) ON DELETE CASCADE;
If this is something you looking for you could find more on this at this link link http://framework.zend.com/manual/en/zend.db.table.relationships.html
With such solution you will be able to loop gifts and get their orders without any complex SQL's
$rowSetGifts = $this->findGifts();
while($rowSetGifts->next()){
$gift = $rowSetGifts->current();
$orders = $gift->findGiftOrder();//This is magick methods, this is the same $gift->findDependentRowset('GiftOrder');
//Now you can do something with orders - count($orders), loop them or edit
}
I would recommend creating a function in your gifts model class that returns what you want. It would probably look something like:
public function getGiftWithAdditionalField($giftId) {
$select = $this->getAdapter()->select()
->from(array('g' => 'gifts'))
->joinLeft(array('table2' => 't2'), 'g.gift_id = t2.gift_id', array('field' => 'field'))
->where('g.gift_id = ?', $giftId);
return $this->getAdapter->fetchAll($select);
}
You can check out the Zend Framework Docs on Joins for more info.

Categories