I have the following tables:
User:
userID
...
Lesson:
lessonID
...
Users_Lessons_Status (which acts as a pivot table and holds other information):
userID references User.userID
lessonID references Lessons.lessonID
latestSectionID
percentComplete
What I want to do is, for each user, for each lesson, there should be a row in the pivot table that tells how much the user has completed in that lesson and what their latest section ID was. That is, there should be a unique pair with userID and lessonID (primary keys?).
I have set up my models like so:
<?php
class User extends Eloquent implements UserInterface, RemindableInterface {
...
public function lessonStatuses()
{
return $this->belongsToMany('Lesson', 'users_lessons_status', 'lessonID', 'userID')->withPivot('latestSectionID', 'percentComplete');
}
}
<?
class Lesson extends Eloquent {
protected $table = 'lessons';
protected $primaryKey = 'lessonID';
public function userStatuses()
{
return $this->belongsToMany('User', 'users_lessons_status', 'userID', 'lessonID');
}
}
?>
My current route looks like this:
Route::post('dbm/users/setLatestSectionID', function() {
if(Auth::check()) {
$user = User::find(Input::get('userID'));
$lesson = Lesson::find(Input::get('lessonID'));
$us = $user->lessonStatuses();
$us->attach($lesson->lessonID,
["latestSectionID" => Input::get('latestSectionID'), "percentComplete" => Input::get('percentComplete')] );
}
});
This works, however, it creates a new row every time I update it for the same userID and lessonID, so the pair is no longer unique. Which methods should I use for this purpose? I tried both save(), attach() and push() in the documentation but I'm not sure which one to use here.
Edit: to clarify, the resulting table should look something like this:
id|userID|lessonID|latestSectionID|percentComplete
1 1 1 X Y
2 1 2
3 1 3
4 2 1
5 3 1
6 3 2
....
Edit 2: Fixed the User->belongsToMany() method and added the withPivot call.
It seems like a bug, nevertheless you can do this:
...->sync([$id], false); // detaching set to false, so it will only insert new rows, skip existing and won't detach anything
edit:
As said in comment - it will not work for you, as you want to set pivot data.
So basically there is no method to do this at the moment, but something like this should do:
// belongsToMany.php
public function attachOrUpdate($id, array $attributes = array(), $touch = true)
{
if ($id instanceof Model) $id = $id->getKey();
if ( ! $this->allRelatedIds()->contains($id)) // getRelatedIds() in prior to v5.4
{
return $this->attach($id, $attributes, $touch);
}
else if ( ! empty($attributes))
{
return $this->updateExistingPivot($id, $attributes, $touch);
}
}
I'm gonna test it and if it passes, send a pull request to 4.1
I faced this recently and fixed it in this way:
Use updateExistingPivot first and check the result , if result is 1 it means there were row with the same userID and lessonID and it's been updated successfully, otherwise, if result is 0 it means there were no rows with this userID and lessonID, so you can attach it in order to create new row
$update_result = $us->updateExistingPivot($lesson->lessonID,
["latestSectionID" => Input::get('latestSectionID'), "percentComplete" => Input::get('percentComplete')] );
if($update_result == 0) {
$us->attach($lesson->lessonID,
["latestSectionID" => Input::get('latestSectionID'), "percentComplete" => Input::get('percentComplete')] );
}
You should be using updateExistingPivot().
Update your code to use
$us->updateExistingPivot($lesson->lessonID,
["latestSectionID" => Input::get('latestSectionID'), "percentComplete" => Input::get('percentComplete')], true );
The last parameter will update the timestamps for all related models. If not, you can remove or set to false.
If you want to attach only when a record doesn't exist, you could do something like this...
Route::post('dbm/users/setLatestSectionID', function() {
if(Auth::check()) {
$user = User::find(Input::get('userID'));
$lesson = [
"latestSectionID" => Input::get('latestSectionID'),
"percentComplete" => Input::get('percentComplete')
];
$num_lessons = $user->lessonStatuses()->where('id', Input::get('lessonID'))->count();
if($num_lessons == 0) {
$user->attach($lesson->lessonID, $lesson);
} else {
$user->updateExistingPivot($lesson->lessonID, $lesson);
}
}
});
Related
This $checkLike = $user->places->contains($placeId); is meant to check if the user has liked the sight with placeId, however, it keep returning false even though there's a record inside the database.
I have 3 tables like this:
Table: Users
Columns: id, username, password
Table: Places
Columns: id, place_id
Table: Place_User
Columns: id, user_id, place_id
and currently I have 1 record in each table like this:
Users Place_User Places
6, Admin, Admin
1, 6, 15
15, b4b8adfb46dfbabdf864b
So I assumed $checkLike = $user->places->contains($placeId); would return true when there's already a record. Sadly that's not the case.
My models:
class User extends \Eloquent implements Authenticatable
{
use AuthenticableTrait;
public function places(){
return $this->belongsToMany('App\Place');
}
}
And
class Place extends Model
{
public function user(){
return $this->belongsToMany('App\User');
}
}
The ->contains method would only work if your $placeId is set to the id of a place, and not the place_id of App\Place.
Take the following as an example:
$placeId = 15;
$user->places->contains($placeId); // true
However, the following would return false because it's referencing to place_id attribute:
$placeId = 'b4b8adfb46dfbabdf864b';
$user->places->contains($placeId); // false
To achieve the above, you have to pass a callable and compare the right attribute:
$placeId = 'b4b8adfb46dfbabdf864b';
$user->places->contains(function ($place) use ($placeId) {
return $place->place_id == $placeId;
});
Laravel 5.7. I have 2 Eloquent models: Owner, Cat.
Owner model:
public function cats()
{
return $this->belongsToMany('App\Cat')->withPivot('borrowed');
}
Cat model:
public function owners()
{
return $this->belongsToMany('App\Owner')->withPivot('borrowed');
}
The cat_owner pivot table has these fields:
id | cat_id | owner_id | borrowed
---------------------------------
1 | 3 | 2 | 1
I want my API to return a list of all cats, and if the logged-in user has borrowed this cat, I want the borrowed field to be set to true. This is what I have so far:
Controller:
public function index()
{
return CatResource::collection(Cat::all());
}
CatResource:
public function toArray()
{
$data = ['id' => $this->id, 'borrowed' => false];
$owner = auth()->user();
$ownerCat = $owner->cats()->where('cat_id', $this->id)->first();
if ($ownerCat) {
$data['borrowed'] = $ownerCat->pivot->borrowed == 1 ? true : false;
}
return $data;
}
This works, but it seems wasteful to request the $owner for every cat, e.g. if there's 5000 cats in the database. Is there a more efficient way to do this? I can think of 2 possible ways:
Pass the $owner to the CatResource (requires overriding existing collection resource).
Get this information in the controller first, before passing to the CatResource.
I prefer the second way, but can't see how to do it.
Try Conditional Relationship.
public function toArray($request)
{
return [
'id' => $this->id,
'borrowed' => false,
'borrowed' => $this->whenPivotLoaded('cat_owner', function () {
return $this->owner_id === auth()->id() && $this->pivot->borrowed == 1 ? true : false;
})
];
}
then call return CatResource::collection(Cat::with('owners')->get());
You are right, this does a lot of extra loading. I think you are running into the limitation that you can't know which record form cat_owner you want until you know both the records you're using from the cat and owner table.
For anyone still interested, my solution would be to make a resource that gives you just the pivot values
Since the following returns a collection you canNOT go to the pivot table on it:
/*
* returns a collection
*/
$data['borrowed'] = $this->owners
/*
* So this doesNOT work. Since you can’t get the pivot
* data on a collection, only on a single record
*/
$data['borrowed'] = $this->owners->pivot
You should receive the collection and then you can read the pivot data in the Resource of the owner Record. If this resource is only for the pivot data I would call it something like attributes.
create a new resourse for the attributes, something like:
class CatOwnerAttributeResource extends JsonResource
{
public function toArray($request)
{
return [
'borrowed' => $this->pivot->borrowed,
];
}
}
Then receive the collection like so:
$data = ['id' => $this->id, 'borrowed' => false];
/*
* Get the collection of attributes and grab the first (and only) record.
* NOTE: the filtering is done in the collection, not in the DBM. If there
* is a possibility that the collection of owners who own this cat gets really
* big this is not the way to go!
*/
if ($attributes =
CatOwnerAttributeResource::collection(
$this->owner
->where(‘id’ = $auth->user()->id)
->first()
) {
$data[‘borrowed’] = $attributes->borrowed == 1 ? true : false;
}
return $data;
Couldn’t run this code so please point errors out if you try it and it gives you any, I will adjust.
my knowledge on mysql is very basic and now im facing a "complex" (for me) query in which im stuck so thank you in advance if someone could give me some light on this.
I have three tables:
Orders
id | name | comments | ...
OrderLines
id | name | sDate | eDate | comments | ...
OrderLinesStats
id | lineID | date | status | ...
Every day OrderLinesStats is updated via a cron job and gets a new record with actual date, status and other fields so the highest id is the actual data.
Im trying to get that last stats row with a relation in yii2 as follows:
in OrdersLines model:
public function getLastOrdersLinesStats()
{
return $this->hasMany(OrdersLinesStats::className(), ['lineID' => 'id'])
->orderBy(['id'=>SORT_DESC])
->groupBy('lineID');
}
OrdersModel:
public function getOrdersLines()
{
return $this
->hasMany(OrdersLines::className(), ['orderID' => 'id'])
->orderBy(['typeID' => SORT_ASC, 'name' => SORT_ASC])
->with(['lastOrdersLinesStats']);
}
But when I debug the query looks like this:
SELECT * FROM `ordersLinesStats` WHERE `lineID` IN (1873, 1872, 1884, 1883, 1870, 1874, 1876, 1880, 1871, 1877, 1881, 1882, 1885, 1886, 1869, 1875, 1878) GROUP BY `lineID` ORDER BY `id` DESC
and doesnt give me the last stats record for each line... in fact, it gives me the oldest one. Seems that im missing something but i cant find it.
Thanks again
All you need to do is change the getLastOrdersLinesStats() to be as follows:
public function getLastOrdersLinesStats()
{
return $this->hasMany(OrdersLinesStats::className(), ['lineID' => 'id'])
->orderBy(['id'=>SORT_DESC])
->one();
}
This basically returns the last OrderLinesStats row that you want for each Order
You can access this as follows:
if you have an object called myOrder for example
then you can access the row you want as myOrder->lastOrderLinesStats
In OrdersModel add getLastOrderLineStat() method that uses via() junction:
public function getLastOrderLineStat()
{
return $this->hasOne(OrdersLinesStats::className(), ['lineID' => 'id'])
->orderBy(['id'=>SORT_DESC])
->groupBy('lineID')
->via('ordersLines');
}
If $model is an OrdersModel instance, you obtain the last stat row using:
$model->lastOrderLineStat
I am just answering this to be thorough and hopefully help other's who stumble upon this page.
I recommend always including both, hasOne and hasMany. This way, you can pop the top record, or retrieve all of them.
/**
* #return \yii\db\ActiveQuery
*/
public function getUserPlan()
{
return $this->hasOne(UserPlan::className(), ['user_id' => 'id'])
->orderBy(['id' => SORT_DESC])
->one();
}
/**
* #return \yii\db\ActiveQuery
*/
public function getUserPlans()
{
return $this->hasMany(UserPlan::className(), ['user_id' => 'id'])
->orderBy(['id' => SORT_DESC])
->all();
}
hasMany will return an array of ActiveQuery Objects, where hasOne will return just an ActiveQuery Object by itself.
You use them like so (example in UserController on User model):
$model = $this->findOne($id);
or
$model = User::findOne($id);
or
$model = User::find()->where(['id' => $id])->one();
Then grab the relations like so:
$plan = $model->userPlan
or
$plans = $model->userPlans
For userPlan:
$planId = $plan->id;
Handling userPlans:
foreach($plans as $plan) {
$plan->id;
}
After creating model, when I try to get his attributes, i get only fields in database that are filled.
----------------------------------------------
DB: | id | shopID | name | bottleID | capacity |
----------------------------------------------
| 1 | 8 | Cola | 3 | |
----------------------------------------------
In this case I need capacity attribute too, as empty string
public function getDrinkData(Request $request)
{
$drink = Drink::where('shopId', $request->session()->get('shopId'))->first();
if($drink) {
$drink = $drink->attributesToArray();
}
else {
$drink = Drink::firstOrNew(['shopId' => $request->session()->get('shopId')]);
$drink = $drink->attributesToArray(); // i want to get even empty fields
}
return view('shop.drink')->(['drink' => $drink])
}
But for later usage (in view) I need to have all attributes, including empty ones. I know that this code works as it should, but I don't know how to change it to detect all attributes.
The model attributes are populated by reading the data from the database. When using firstOrNew() and a record doesn't exist, it makes a new instance of the model object without reading from the database, so the only attributes it has will be the ones manually assigned. Additionally, since there is no record in the database yet, you can't just re-read the database to get the missing data.
In this case, you can use Schema::getColumnListing($tableName) to get an array of all the columns in the table. With that information, you can create a base array that has all the column names as keys, and null for all the values, and then merge in the values of your Drink object.
public function getDrinkData(Request $request)
{
// firstOrNew will query using attributes, so no need for two queries
$drink = Drink::firstOrNew(['shopId' => $request->session()->get('shopId')]);
// if an existing record was found
if($drink->exists) {
$drink = $drink->attributesToArray();
}
// otherwise a new model instance was instantiated
else {
// get the column names for the table
$columns = Schema::getColumnListing($drink->getTable());
// create array where column names are keys, and values are null
$columns = array_fill_keys($columns, null);
// merge the populated values into the base array
$drink = array_merge($columns, $drink->attributesToArray());
}
return view('shop.drink')->(['drink' => $drink])
}
If you were using firstOrCreate(), then a new record is created when one doesn't exist. Since there a record in the database now, you could simply re-read the record from the database to populated all of the model attributes. For example:
public function getDrinkData(Request $request)
{
$drink = Drink::firstOrCreate(['shopId' => $request->session()->get('shopId')]);
// If it was just created, refresh the model to get all the attributes.
if ($drink->wasRecentlyCreated) {
$drink = $drink->fresh();
}
return view('shop.drink')->(['drink' => $drink->attributesToArray()])
}
What if you were to explicitly declare all the fields you want back.
An example of something I do that is a bit more basic as I'm not using where clauses and just getting all from Request object. I still think this could be helpful to someone out there.
public function getDrinkData(Request $request)
{
// This will only give back the columns/attributes that have data.
// NULL values will be omitted.
//$drink = $request->all();
// However by declaring all the attributes I want I can get back
// columns even if the value is null. Additional filtering can be
// added on if you still want/need to massage the data.
$drink = $request->all([
'id',
'shopID',
'name',
'bottleID',
'capacity'
]);
//...
return view('shop.drink')->(['drink' => $drink])
}
You should add a $fillable array on your eloquent model
protected $fillable = ['name', 'email', 'password'];
make sure to put the name of all the fields you need or you can use "*" to select all.
If you already have that, you can use the ->toArray() method that will get all attributes including the empty ones.
I'm using the same thing for my Post model and it works great with all fields including empty ones.
My model looks like this:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ["*"];
public function comments()
{
return $this->hasMany(Comment::class);
}
}
Do you need to set the $drink variable again after checking the $drink variable?
you can check the following code?
public function getDrinkData(Request $request)
{
$drink = Drink::where('shopId', $request->session()->get('shopId'))->first();
if(!count($drink)>0) {
$drink = Drink::firstOrNew(['shopId' => $request->session()->get('shopId')]);
}
return view('shop.drink')->(['drink' => $drink]); or
return view('shop.drink',compact('drink'));
}
hope it will help. :) :)
You can hack by overriding attributesToArray method
class Drink extends Model
{
public function attributesToArray()
{
$arr = parent::attributesToArray();
if ( !array_key_exist('capacity', $arr) ) {
$arr['capacity'] = '';
}
return $arr;
}
}
or toArray method
class Drink extends Model
{
public function toArray()
{
$arr = parent::toArray();
if ( !array_key_exist('capacity', $arr) ) {
$arr['capacity'] = '';
}
return $arr;
}
}
then
$dirnk->toArray(); // here capacity will be presented even it null in db
I've been searching the documents everywhere and I can't figure this one out. Suppose I have set up a model with a hasMany relationship and the inverse like this:
class MasterAccount extends Model {
//master_account_id exists as a foreign key in sub_account table
//and references primary key in master_account table as
//defined in the migrations I set up for this
public function SubAccounts()
{
return $this->hasMany('App\SubAccount');
}
}
And I've made sure that some sub accounts have values matching the master account's primary id in the master_account_id column.
I've tested the relationship by dd()ing values in my controller like this:
public function edit(MasterAccount $masterAcct)
{
dd($masterAccount->subAccounts);
}
And this does successfully return a collection of all the related models. However, I cannot figure out how I would update a single attribute for every model that belongsTo MasterAccount -- shouldn't there be a way to cascade like this? I hacked it to work by doing this:
<?php namespace App\Http\Controllers;
use App\SubAccount;
public function update(MasterAccountRequest $request, MasterAccount $masterAccount)
{
//current request id
$id = $masterAccount->id;
//cascade global value to related Accounts
if ($request->some_value == 1)
{
//look more into this... ARGH!!!
SubAccount::where('master_account_id', '=', $id)->update(['this_value' => 1]);
}
}
And this works but I just KNOW there is some "Eloquent," way to do this.....right?
According to the documentation this should work:
<?php namespace App\Http\Controllers;
use App\SubAccount;
public function update(MasterAccountRequest $request, MasterAccount $masterAccount)
{
//current request id
$id = $masterAccount->id;
//cascade global value to related Accounts
if ($request->some_value == 1)
{
//look more into this... Wooooot!!!
$masterAccount->SubAccounts()->update(['this_value' => 1]);
}
}