Laravel Relationship availability after save() - php

I'm building a simple timetracking App using Laravel 5 where I have two Laravel models, one called "User" and one called "Week"
The relationships are pretty simple:
Week.php:
public function user()
{
return $this->belongsTo('App\User');
}
User.php:
function weeks()
{
return $this->hasMany('App\Week');
}
Now, the User.php file also has a simple helper / novelty function called "getCurrentWeek()" that, as the name suggests, returns the current week
function getCurrentWeek()
{
$possibleWeek = $this->weeks->sortByDesc('starts')->all();
if(count($possibleWeek) != 0)
{
return $possibleWeek[0];
}
else
{
return null;
}
}
My problem is: If I create the very first week for a user and attach / relate it to the user like so:
$week = new Week;
//Omitted: Setting so attributes here ...
$week->user_id = $user->id;
$weeks->save()
$user->weeks()->save($week);
And then call the $user->getCurrentWeek(); method, that method returns null, although a new week has been created, is related to the user and has been saved to the database. In my mind, the expected behaviour would be for getCurrentWeek() to return the newly created week.
What am I misunderstanding about Eloquent here / doing just plain wrong?

Relationship attributes are lazy loaded the first time they are accessed. Once loaded, they are not automatically refreshed with records that are added or removed from the relationship.
Since your getCurrentWeek() function uses the relationship attribute, this code will work:
$week = new Week;
// setup week ...
$user->weeks()->save($week);
// $user->weeks attribute has not been accessed yet, so it will be loaded
// by the first access inside the getCurrentWeek method
dd($user->getCurrentWeek());
But, this code will not work:
// accessing $user->weeks lazy loads the relationship
echo count($user->weeks);
// relate a new record
$week = new Week;
// setup week ...
$user->weeks()->save($week);
// $user->weeks inside the method will not contain the newly related record,
// as it has already been lazy loaded from the access above.
dd($user->getCurrentWeek());
You can either modify your getCurrentWeek() method to use the relationship method ($this->weeks()) instead of the attribute ($this->weeks), which will always hit the database, or you can reload the relationship (using the load() method) after adding or removing records.
Change the getCurrentWeek() method to use the relationship method weeks() (updated method provided by #Bjorn)
function getCurrentWeek()
{
return $this->weeks()->orderBy('starts', 'desc')->first();
}
Or, refresh the relationship using the load() method:
// accessing $user->weeks lazy loads the relationship
echo count($user->weeks);
// relate a new record
$week = new Week;
// setup week ...
$user->weeks()->save($week);
// reload the weeks relationship attribute
$user->load('weeks');
// this will now work since $user->weeks was reloaded by the load() method
dd($user->getCurrentWeek());

Just to add to #patricus answer...
After save/create/update, you can also empty all the cached relation data like so:
$user->setRelations([]);
Or selectively:
$user->setRelation('weeks',[]);
After that, data will be lazy loaded again only when needed:
$user->weeks
That way you can continue using lazy loading.

$user->weeks()->save($week); is not needed because you manually attached the week to the user by using $week->user_id = $user->id; and saving it.
You could actually rewrite the whole function to:
function getCurrentWeek()
{
return $this->weeks->sortByDesc('starts')->first();
}
Or
function getCurrentWeek()
{
return $this->weeks()->orderBy('starts', 'desc')->first();
}
Edit:
I made a little proof of concept and it works fine like this:
App\User.php
function weeks()
{
return $this->hasMany('App\Week');
}
function getCurrentWeek()
{
return $this->weeks->sortByDesc('starts')->first();
}
App\Week.php
public function user()
{
return $this->belongsTo('App\User');
}
routes/web.php or App/Http/routes.php
Route::get('poc', function () {
$user = App\User::find(1);
$week = new App\Week;
$week->user_id = $user->id;
$week->starts = Carbon\Carbon::now();
$week->save();
return $user->getCurrentWeek();
});
Result:
{
id: 1,
user_id: "1",
starts: "2016-09-15 21:42:19",
created_at: "2016-09-15 21:42:19",
updated_at: "2016-09-15 21:42:19"
}

Related

Problem with implicit Enum Binding on route in laravel

I have this route
Route::get('/post/{post:uuid}', [\App\Http\Controllers\PostController::class, 'showPost']);
And it works, if the user inputs an inexisting uuid, the app responses a 404 error, but now I want to add one more condition by using enums on route.
I have an enum called PostStateEnum.php
<?php
namespace Modules\Muse\Enum;
use App\Http\Traits\EnumTrait;
enum PostStateEnum: string
{
use EnumTrait;
case DRAFT = 'draft';
case WAITING_APPROVAL = 'waiting_approval';
case APPROVED = 'approved';
case REJECTED = 'rejected';
case PUBLISHED = 'published';
case UNPUBLISHED = 'unpublished';
}
I want to add a condition in the route: if the $post->state is PostStateEnum::PUBLISHED I want to go to the 'showPost' in my PostController
Currently, I'm handle that logic on my controller
public function showPost(Post $post)
{
if ($post->state == PostStateEnum::PUBLISHED)
{
dump($post);
} else {
return abort(404);
}
}
According to the laravel 9 docs I understand is that I need to create another enum with only one state to be able to validate that from the route, is that correct?
Is possible? Or my way is better?
I think you are confusing what enums in the route can bring. It is not about what is already saved, but more to use it as a filter / input. Imagine you want to have a route, that show posts based on status.
Route::get('posts/{PostStateEnum}');
In your controller you would be able to filter based on that.
public function index(PostStateEnum $enum) {
if ($enum ==PostStateEnum::PUBLISHED) {
// query filter published
} else if ($enum ==PostStateEnum::UNPUBLISHED) {
// query filter unpublished
}
}
Your enum is not from the input, but from the model, therefor what you are doing is actually the correct aproach. If not done, remember to cast your enum.
class Post extends Model {
protected $casts = [
'status' => PostStateEnum::class,
];
}
As a more general code improvement tip, doing if else, like you did in your example is non optimal for readability, you can in these cases, reverse the if logic and do an early return approach.
public function showPost(Post $post)
{
if ($post->state !== PostStateEnum::PUBLISHED)
{
return abort(404);
}
return $post;
}

Why model/variable keeps its value?

I have problem with my code in Laravel Framework. What I am trying to do is sell method inside model. The problem is that $variable keeps its value untill next code execution. How I should make it so it's gonna work like I want to?
/// Model method.
public function Sell()
{
$this->UserData->increment('gold', ($this->ItemData->price / 100) * 80);
$this->delete();
return null;
}
///in controller
$user = \Auth::user();
$user_item = UserItems::find(10);
$user_item->Sell();
return $user_item->ItemData->name; /// Returns full data about model even if I deleted it/sold. After next refresh, returns null/error.I want it to return null since beginning.
Even if you have deleted a record from a database by $this->delete(), $user_item variable still holds a reference to the filled model until the script ends or you destroy the variable.
It seems that you want to return a result of the sell() function. In your case it would be
///in controller
$user = \Auth::user();
$user_item = UserItems::find(10);
$user_item->Sell();
return $user_item->Sell();
I don't know what you are trying to do but below code will solve your issue -
/// Model method.
public function Sell()
{
$this->UserData->increment('gold', ($this->ItemData->price / 100) * 80);
$this->delete();
$this->ItemData = $this->ItemData->fresh()
return null;
}
///in controller
$user = \Auth::user();
$user_item = UserItems::find(10);
$user_item->Sell();
return $user_item->ItemData->name; /// Returns full data about model even if I deleted it/sold. After next refresh, returns null/error.I want it to return null since beginning.

Save attribute of model in database in PHP in OctoberCMS

Hi I have problem when i tried to save attribute of model to database. I write in OctoberCMS and i have this function:
public function findActualNewsletter()
{
$actualNewsletter = Newsletter::where('status_id', '=', NewsletterStatus::getSentNowStatus())->first();
if (!$actualNewsletter) {
$actualNewsletter = Newsletter::where('send_at', '<=', date('Y-m-d'))->where('status_id', NewsletterStatus::getUnsentStatus())->first();
$actualNewsletter->status_id = NewsletterStatus::getSentNowStatus();
dd($actualNewsletter);
}
return $actualNewsletter;
}
getSentNowStatus()=2;
getUnsentStatus()=1;
dd($actualNewsletter) in my if statement show that status_id = 2 But in database i still have 1. I used this function in afterSave() so i dont need:
$actualNewsletter->status_id = NewsletterStatus::getSentNowStatus();
$actualNewsletter->save();
becosue i have error then i use save in save.
Of course i filled table $fillable =['status_id']. And now i dont know why its not save in database when it go to my if. Maybe someone see my mistake?
If you are trying to modify the model based on some custom logic and then save it, the best place to put it is in the beforeSave() method of the model. To access the current model being saved, just use $this. Below is an example of the beforeSave() method being used to modify the attributes of a model before it gets saved to the database:
public function beforeSave() {
$user = BackendAuth::getUser();
$this->backend_user_id = $user->id;
// Handle archiving
if ($this->is_archived && !$this->archived_at) {
$this->archived_at = Carbon\Carbon::now()->toDateTimeString();
}
// Handle publishing
if ($this->is_published && !$this->published_at) {
$this->published_at = Carbon\Carbon::now()->toDateTimeString();
}
// Handle unarchiving
if ($this->archived_at && !$this->is_archived) {
$this->archived_at = null;
}
// Handle unpublishing, only allowed when no responses have been recorded against the form
if ($this->published_at && !$this->is_published) {
if (is_null($this->responses) || $this->responses->isEmpty()) {
$this->published_at = null;
}
}
}
You don't have to run $this->save() or anything like that. Simply modifying the model's attributes in the beforeSave() method will accomplish what you desire.

Laravel detect if there is a new item in an array

I want to implement a system in my project that "alerts" users when there is a new comment on one of their posts.
I currently query all comments on the posts from the logged in user and put everything in an array and send it to my view.
Now my goal is to make an alert icon or something when there is a new item in this array. It doesn't have to be live with ajax just on page load is already good :)
So I've made a function in my UsersController where I get the comments here's my code
public function getProfileNotifications()
{
$uid = Auth::user()->id;
$projects = User::find($uid)->projects;
//comments
if (!empty($projects)) {
foreach ($projects as $project) {
$comments_collection[] = $project->comments;
}
}
if (!empty($comments_collection)) {
$comments = array_collapse($comments_collection);
foreach($comments as $com)
{
if ($com->from_user != Auth::user()->id) {
$ofdate = $com->created_at;
$commentdate = date("d M", strtotime($ofdate));
$comarr[] = array(
'date' => $ofdate,
$commentdate,User::find($com->from_user)->name,
User::find($com->from_user)->email,
Project::find($com->on_projects)->title,
$com->on_projects,
$com->body,
Project::find($com->on_projects)->file_name,
User::find($com->from_user)->file_name
);
}
}
} else {
$comarr = "";
}
}
Is there a way I can check on page load if there are new items in the array? Like keep a count and then do a new count and subtract the previous count from the new one?
Is this even a good way to apprach this?
Many thanks in advance! Any help is appreciated.
EDIT
so I added a field unread to my table and I try to count the number of unreads in my comments array like this:
$uid = Auth::user()->id;
$projects = User::find($uid)->projects;
//comments
if (!empty($projects)) {
foreach ($projects as $project) {
$comments_collection[] = $project->comments;
}
}
$unreads = $comments_collection->where('unread', 1);
dd($unreads->count());
But i get this error:
Call to a member function where() on array
Anyone any idea how I can fix this?
The "standard" way of doing this is to track whether the comment owner has "read" the comment. You can do that fairly easily by adding a "unread" (or something equivalent) flag.
When you build your models, you should define all their relationships so that stuff like this becomes relatively easy.
If you do not have relationships, you need to define something like the following:
In User
public function projects()
{
return $this->hasMany('App\Models\Project');
}
In Project
public function comments()
{
return $this->hasMany('App\Models\Comment');
}
Once you hav ethose relationshipt, you can do the following. Add filtering as you see fit.
$count = $user->projects()
->comments()
->where('unread', true)
->count();
This is then the number you display to the user. When they perform an action you think means they've acknowledged the comment, you dispatch an asynchronous request to mark the comment as read. A REST-ish way to do this might look something like the following:
Javascript, using JQuery:
jQuery.ajax( '/users/{userId}/projects/{projectId}/comments/{commentId}', {
method: 'patch'
dataType: 'json',
data: {
'unread': false
}
})
PHP, in patch method:
$comment = Comment::find($commentId);
$comment->update($patchData);
Keep in mind you can use Laravel's RESTful Resource Controllers to provide this behavior.
try this
$unreads = $project->comments()->where('unread', 1);
dd($unreads->count());
EDIT
My be Has Many Through relation will fit your needs
User.php
public function comments()
{
return $this->hasManyTrough('App\Project', 'App\Comment');
}
Project.php
public function comments()
{
return $this->hasMany('App\Comment');
}
then you can access comments from user directly
$user->comments()->where('unread', 1)->count();
or I recommend you define hasUnreadComments method in User
public function hasUnreadComments()
{
$return (bool) $this->comments()->where('unread', 1)->count();
}
P.S.
$uid = Auth::user()->id;
$projects = User::find($uid)->projects;
this code is horrible, this way much better
$projects = Auth::user()->projects;

Laravel relationship count()

I want to get a total user transaction (specific user) with relationship.
I've done it but i'm curious is my way is good approach.
//User Model
public function Transaction()
{
return $this->hasMany(Transaction::class);
}
//Merchant Model
public function Transaction()
{
return $this->hasMany(Transaction::class);
}
public function countTransaction()
{
return $this->hasOne(Transaction::class)
->where('user_id', Request::get('user_id'))
->groupBy('merchant_id');
}
public function getCountTransactionAttribute()
{
if ($this->relationLoaded('countTransaction'))
$this->load('countTransaction');
$related = $this->getRelation('countTransaction');
return ($related) ? (int)$related->total_transaction : 0;
}
//controller
$merchant = Merchant::with('countTransaction')->get();
What make me curious is part inside countTransaction. I put where where('user_id', Request::get('user_id')) directly inside the model.
is it good approach or any other way to get specific way?
expected result:
"merchant:"{
"name": "example"
"username" : "example"
"transactions": {
"count_transactions: "4" //4 came from a specific user.
}
}
I need to get the merchant data with the transaction count for specific user. This query is based on logged in user. so when a user access merchant page, they can see their transaction count for that merchant.
Thanks.
You really want to keep request data outside of your models (instead opting to pass it in). I'm also a little confused about why you have both a 'hasOne' for transactions, and a 'hasMany' for transactions within the merchant model.
I would probably approach the problem more like the below (untested, but along these lines). Again I'm not fully sure I understand what you need, but along these lines
// Merchant Model
public function transactions()
{
return $this->hasMany(Transaction::class);
}
public function countTransactionsByUser($userId)
{
return $this
->transactions()
->where('user_id', $userId)
->get()
->pluck('total_transaction')
->sum();
}
// Controller
$userId = request()->get('user_id');
// ::all() or however you want to reduce
// down the Merchant collection
//
$merchants = Merchant::all()->map(function($item, $key) {
$_item = $item->getAttributes();
$_item['transactions'] = [
'count_transactions' => $item->countTransactionsByUser($userId);
];
return $_item;
});
// Single total
// Find merchant 2, and then get the total transactions
// for user 2
//
$singleTotal = Merchant::find(2)
->countTransactionsByUser($userId);

Categories