Eager loading of related model (nested set) - activerecord

In Yii1 I could load parent (nested set) using eager loading. It was like this:
class Category extends CActiveRecord {
public function relations() {
return array(
'parent' => array(self::HAS_ONE, 'Category', '', 'on' => '(t.left between parent.left and parent.right) and parent.level + 1 = t.level', 'joinType'=>'left join'),
);
}
}
(relation to the same model, setting 2nd parameter as empty, and setting my own condition in on parameter).
How can I do the same in Yii2? (cause Yii2 needs key => value pair to initialize relation).

Try use like this:
public function getRelation()
{
return $this->hasOne(RelationTable::className(), ['id' => 'relation_id'])->onCondition(array|string)->andOnCondition(array|string);
}
or
public function getRelation()
{
return $this->hasOne(RelationTable::className(), ['id' => 'relation_id'])->andOnCondition(array|string);
}
More is here:
http://www.yiiframework.com/doc-2.0/guide-db-active-record.html#relational-data

Related

Model appends including entire relationship in query

Edit: I was able to see where the relations are being included in my response, but I still don't know why.
On my Customer model, I have:
protected $appends = [
'nps',
'left_feedback',
'full_name',
'url'
];
The accessors are as follows:
/**
* Accessor
*/
public function getNpsAttribute() {
if ($this->reviews->count() > 0) {
return $this->reviews->first()->nps;
} else {
return "n/a";
}
}
/**
* Accessor
*/
public function getLeftFeedbackAttribute() {
if ($this->reviews && $this->reviews->count() > 0 && $this->reviews->first()->feedback != null) {
return "Yes";
} else {
return "No";
}
}
/**
* Accessor
*/
public function getFullNameAttribute() {
return ucwords($this->first_name . ' ' . $this->last_name);
}
/**
* Accessor
*/
public function getUrlAttribute() {
$location = $this->location;
$company = $location->company;
$account_id = $company->account->id;
return route('customers.show', ['account_id' => $account_id, 'company' => $company, 'location' => $location, 'customer' => $this]);
}
So if I comment out the $appends property, I get the response I originally wanted with customer not returning all the relations in my response.
But I do want those appended fields on my Customer object. I don't understand why it would include all relations it's using in the response. I'm returning specific strings.
So is there a way to keep my $appends and not have all the relations it's using in the accessors from being included?
Original Question:
I am querying reviews which belongsTo a customer. I want to include the customer relation as part of the review, but I do not want to include the customer relations.
$reviews = $reviews->with(['customer' => function($query) {
$query->setEagerLoads([]);
$query->select('id', 'location_id', 'first_name', 'last_name');
}]);
$query->setEagerLoads([]); doesn't work in this case.
I've tried $query->without('location'); too, but it still gets included
And I should note I don't have the $with property on the model populated with anything.
Here is the Review model relation:
public function customer() {
return $this->belongsTo('App\Customer');
}
Here is the Customer model relation:
public function reviews() {
return $this->hasMany('App\Review');
}
// I dont want these to be included
public function location() {
return $this->belongsTo('App\Location');
}
public function reviewRequests() {
return $this->hasMany('App\ReviewRequest');
}
In the response, it will look something like:
'review' => [
'id'=> '1'
'customer => [
'somecol' => 'test',
'somecolagain' => 'test',
'relation' => [
'relation' => [
]
],
'relation' => [
'somecol' => 'sdffdssdf'
]
]
]
So a chain of relations ends up being loaded and I don't want them.
As you said in one comment on the main question, you are getting the relations due to the appended accessors.
Let me show you how it should be done (I am going to copy paste your code and simply edit some parts, but you can still copy paste my code and place it in yours and will work the same way but prevent adding the relations) and then let me explain why is this happening:
/**
* Accessor
*/
public function getNpsAttribute() {
if ($this->reviews()->count() > 0) {
return $this->reviews()->first()->nps;
} else {
return "n/a";
}
}
/**
* Accessor
*/
public function getLeftFeedbackAttribute() {
return $this->reviews()->count() > 0 &&
$this->reviews()->first()->feedback != null
? "Yes"
: "No";
}
/**
* Accessor
*/
public function getFullNameAttribute() {
return ucwords($this->first_name . ' ' . $this->last_name);
}
/**
* Accessor
*/
public function getUrlAttribute() {
$location = $this->location()->first();
$company = $location->company;
$account_id = $company->account->id;
return route('customers.show', ['account_id' => $account_id, 'company' => $company, 'location' => $location, 'customer' => $this]);
}
As you can see, I have changed any $this->relation to $this->relation()->first() or $this->relation->get().
If you access any Model's relation as $this->relation it will add it to the eager load (loaded) so it will really get the relation data and store it in the Model's data so next time you do $this->relation again it does not have to go to the DB and query again.
So, to prevent that, you have to access the relation as $this->relation(), that will return a query builder, then you can do ->count() or ->exists() or ->get() or ->first() or any other valid query builder method, but accessing the relation as query builder will prevent on getting the data and store it the model (I know doing ->get() or ->first() will get the data, but you are not directly getting it through the model, you are getting it through the query builder relation, that is different).
This way you will prevent on storing the data on the model, hence giving you problems.
You can also use API Resources, it is used to map a Model or Collection to a desired output.
One last thing, if you can use $this->relation()->exists() instead of $this->relation()->count() > 0 it will help on doing it faster, mostly any DB is faster on looking if data exists (count >= 1) than really counting all the entries it has, so it is faster + more performant on using exists.
Try :
$review->with(‘customer:id,location_id,first_name,last_name’)->get();
Or :
$review->withOnly(‘customer:id,location_id,first_name,last_name’)->get();

Laravel model function best prickets

im new in Laravel , I have an issue as below
I make in category model query to check is category is exist or not
as below
public function scopeIsExist($query ,$id)
{
return $query->where(['deleted' => 1, 'id' => $id])->orderBy('id', 'DESC')->first();
}
and my controller is
public function edit($id)
{
$dataView['category'] = Category::IsExist($id);
if(!$dataView['category'])
{
return view('layouts.error');
}else{
$dataView['title'] = 'name';
$dataView['allCategories'] = Category::Allcategories()->get();
return view('dashboard.category.edit')->with($dataView);
}
}
my problem is when I use method isEXIST if id not found it not redirect to error page but ween i remove ISEXIST AND replace it as below
$dataView['category'] = Category::where(['deleted' => 1, 'id' => $id])->orderBy('id', 'DESC')->first();
it work well .
can any one help me
That's because local scope should return an instance of \Illuminate\Database\Eloquent\Builder. You should remove the first() in the scope and put it in the controller.
Redefine your scope like so:
public function scopeIsExist($query ,$id)
{
return $query->where(['deleted' => 1, 'id' => $id])->orderBy('id', 'DESC');
}
In your controller edit method:
$dataView['category'] = Category::IsExist($id)->first();
You can have a look to the doc for local scopes https://laravel.com/docs/8.x/eloquent#local-scopes

Laravel / OctoberCMS frontend filter

I am using OctoberCMS and I have created a custom component. I am trying to create a frontend filter to filter Packages by the Tour they are assigned to.
This is what I have so far. The issue is that the code is looking for a tour field within the packages table rather than using the tour relationship. Does anyone have any ideas?
<?php namespace Jakefeeley\Sghsportingevents\Components;
use Cms\Classes\ComponentBase;
use JakeFeeley\SghSportingEvents\Models\Package;
use Illuminate\Support\Facades\Input;
class FilterPackages extends ComponentBase
{
public function componentDetails()
{
return [
'name' => 'Filter Packages',
'description' => 'Displays filters for packages'
];
}
public function onRun() {
$this->packages = $this->filterPackages();
}
protected function filterPackages() {
$tour = Input::get('tour');
$query = Package::all();
if($tour){
$query = Package::where('tour', '=', $tour)->get();
}
return $query;
}
public $packages;
}
I really appreciate any help you can provide.
Try to query the relationship when the filter input is provided.
This is one way to do it;
public $packages;
protected $tourCode;
public function init()
{
$this->tourCode = trim(post('tour', '')); // or input()
$this->packages = $this->loadPackages();
}
private function loadPackages()
{
$query = PackagesModel::query();
// Run your query only when the input 'tour' is present.
// This assumes the 'tours' db table has a column named 'code'
$query->when(!empty($this->tourCode), function ($q){
return $q->whereHas('tour', function ($qq) {
$qq->whereCode($this->tourCode);
});
});
return $query->get();
}
If you need to support pagination, sorting and any additional filters you can just add their properties like above. e.g;
protected $sortOrder;
public function defineProperties(): array
{
return [
'sortOrder' => [
'title' => 'Sort by',
'type' => 'dropdown',
'default' => 'id asc',
'options' => [...], // allowed sorting options
],
];
}
public function init()
{
$filters = (array) post();
$this->tourCode = isset($filters['tour']) ? trim($filters['tour']) : '';
$this->sortOrder = isset($filters['sortOrder']) ? $filters['sortOrder'] : $this->property('sortOrder');
$this->packages = $this->loadPackages();
}
If you have a more complex situation like ajax filter forms or dynamic partials then you can organize it in a way to load the records on demand vs on every request.e.g;
public function onRun()
{
$this->packages = $this->loadPackages();
}
public function onFilter()
{
if (request()->ajax()) {
try {
return [
"#target-container" => $this->renderPartial("#packages",
[
'packages' => $this->loadPackages()
]
),
];
} catch (Exception $ex) {
throw $ex;
}
}
return false;
}
// call component-name::onFilter from your partials..
You are looking for the whereHas method. You can find about here in the docs. I am not sure what your input is getting. This will also return a collection and not singular record. Use ->first() instead of ->get() if you are only expecting one result.
$package = Package::whereHas('tour', function ($query) {
$query->where('id', $tour);
})->get();

Laravel Map DB Column Names Using Proper Convention to Actual DB Column Names in Model

We're building a portal to replace part of an existing application as step one, but the DB schema holds to absolutely no conventions. Aside from the lack of any constraints, indexes, etc the names of columns are not descriptive and not snake-cased.
Is it possible to map DB table column names so that the portal uses proper descriptive and snake-cased column names like first_name but writes to the actual database column first to at least have the portal be a first step towards cleaning up the tech debt?
For example, similar to how the table name (Model::table) can be set if the table name doesn't follow convention:
Example
private $columns = [
// convention => actual
'first_name' => 'first',
'last_name' => 'last',
'mobile_phone' => 'phone',
'home_phone' => 'otherPhone', // seriously!?
];
I've looked through Model and the HasAttributes trait, but I'm still hoping that this might exist, or someone has found a way to do this as a temporary solution.
You can create a parent class for all your models:
abstract class Model extends \Illuminate\Database\Eloquent\Model {
protected $columns = [];
public function attributesToArray()
{
$attributes = parent::attributesToArray();
foreach ($this->columns as $convention => $actual) {
if (array_key_exists($actual, $attributes)) {
$attributes[$convention] = $attributes[$actual];
unset($attributes[$actual]);
}
}
return $attributes;
}
public function getAttribute($key)
{
if (array_key_exists($key, $this->columns)) {
$key = $this->columns[$key];
}
return parent::getAttributeValue($key);
}
public function setAttribute($key, $value)
{
if (array_key_exists($key, $this->columns)) {
$key = $this->columns[$key];
}
return parent::setAttribute($key, $value);
}
}
Then override $columns in your models:
protected $columns = [
'first_name' => 'first',
'last_name' => 'last',
'mobile_phone' => 'phone',
'home_phone' => 'otherPhone',
];
The proper way is to use accessors and mutators.
Defining An Accessor
public function getFirstNameAttribute() {
return $this->first;
}
Then, you can access the value by $model->first_name.
Defining A Mutator
public function setFirstNameAttribute($value) {
$this->attributes['first'] = $value;
}
Then, you can mutate the value for example:
$model->first_name = 'first_name';
$model->save();

Create Relationship inside the create function

I have a model that has a one to many relationship to the versions of the description.
In my Controller
$tag = Tags::create([
'name' => $request->get('name'),
'user_id' => \Auth::id(),
]);
$tag->update([
'content' => $request->get('description')
]);
In my Model:
public function setContentAttribute(string $value)
{
$this->versions()->create([
'user_id' => \Auth::id(),
'value' => $value
]);
}
So I can't put content directly as an attribute in the create method because there is no Model right now.
But is it possible to overwrite the create Method?
When I try to overwrite something like this in my Model it will do an infinity loop
public static function create($attr) {
return parent::create($attr);
}
So my question is if it is possible to have something like this:
$tag = Tags::create([
'name' => $request->get('name'),
'user_id' => \Auth::id(),
'content' => $request->get('content')
]);
and in the Model:
public static function create($attr) {
$value = $attr['content'];
$attr['content'] = null;
$object = parent::create($attr);
$object->content = $value;
$object->save();
return $object;
}
Update
I didn't overwrite the create method but called it customCreate. So there is no infinity loop anymore and I can pass all variables to the customCreate function that handles the relationships for me.
Solution
After reading the changes from 5.3 to 5.4 it turns out that the create method was moved so you don't have to call parent::create() anymore.
The final solution is:
public static function create($attr) {
$content = $attr['content'];
unset($attr['content']);
$element = static::query()->create($attr);
$element->content = $content;
$element->save();
return $element;
}
I don't see why not and you could probably implement a more general approach? Eg. checking if set{property}Attribute() method exists, if it does - use it to assign a value, if it doesn't - use mass assigning.
Something like:
public static function create($attr) {
$indirect = collect($attr)->filter(function($value, $property) {
return method_exists(self::class, 'set' . camel_case($property) . 'Attribute');
});
$entity = parent::create(array_diff_key($attr, $indirect->toArray()));
$indirect->each(function($value, $property) use ($entity) {
$entity->{$property} = $value;
});
$entity->save();
return $entity;
}
I haven't really tested it but it should work. I use something like this in one of my Symfony apps.

Resources