Laravel save updates another timestamp column - laravel

I am having an issue when updating a column in a model with 2 timestamp column, where if you update one column, it also updates the other column for some reason, not only that, it is always -8 hours. Code snippet below.
$punchIn = $user->punches()
->whereNotNull('start')
->whereNull('end')
->firstWhere('schedule_id', $schedule->id);
if($punchIn) {
// something is happening here during saving. it also updates the start column for some reason.
// this thing substracts 8 hours from the end column and updates the start column with it.
// I have been debugging this for 3 days now.
$punchIn->end = Carbon::now();
$punchIn->save(); // I also tried the other way to update, but still same thing.
$punchIn->refresh(); // this has the new value of start and end.
}
This is the migration file:
Schema::create('user_punches', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('schedule_id')->constrained('schedules')->cascadeOnDelete();
$table->timestamp('start')->nullable(); // overrides
$table->timestamp('end')->nullable(); // overrides
$table->timestamps();
});
And this is the user eloquent relationship:
class User extends Authenticatable implements MustVerifyEmail {
use HasApiTokens, HasFactory, Notifiable, HasRoles;
public function punches()
{
return $this->hasMany(UserPunch::class);
}
}
UserPunch model:
class UserPunch extends Model {
use HasFactory;
protected $fillable = [
'schedule_id',
'start',
'end',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
I have been on this issue for days now. I have tried other similar cases here, like problem could be with Mysql's timestamp if not null, but still the issue is being persistent.
Anybody can point me out to the right direction? I would really appreciate any explanation on this one. Right now, I am stuck to this. My last option is to use datetime instead of timestamp but that would mean I would trace back the long codes and all relationships which I am trying to avoid.

Holy moly. I figured it out.
So apparently, it is set to update into CURRENT_TIMESTAMP by default. And the migration file that I have was already the solution for it, but was overwritten when I restored from a dump file coming from production database that had the issue.
Answers to these 2 questions here, really gave me enlightenment of this issue. I will paste it here for reference purposes in case other people stumbled upon this.
Laravel 6 - Updating the model timestamps
Laravel Timestamp Being Updated Without Explicit Call To Do So
Lesson learned, when dealing with timestamps, always make them nullable

Related

Laravel multiple relation in a same table

In my project, users can like & comment on feeds & forums. So, there is a contribution page where the user can see where he has provided his input (like or comment) sorted by created_at time.
There may be another feature in future like feed & forum where user can also provide like & comment.
In my contribution page, I want to list data like this -
You have commented on user_2's feed feed_title at created_at - comment
You have liked user_2's feed feed_title at created_at
You have commented on user_3's forum forum_title at created_at - comment
You have liked user_3's forum forum_title at created_at
But I am stuck in database design. So far I am trying this -
Schema::create('contributions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuid('user_id');
$table->uuid('contribution_id'); // id of my like/comment
$table->string('contribution_type'); // feed_like/feed_comment/forum_like/forum_comment
$table->uuid('target_id'); // id of feed/forum where I provided comment or like
$table->timestamps();
});
But it will cause a query loop when I retrieve the data. So, is there any better approach to what I am trying to get?
You are probably looking for Polymorphic Relationships.
That enables you to simplify the relationship by providing an ID of the related model and a naming of the related model instead.
A sample migration would look like this, using the morph method as inspiration (since you're using UUID's):
Schema::create('contributions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuid('user_id');
$table->uuid('contributable_id');
$table->string('contributable_type');
$table->timestamps();
});
This should enable you to do something like:
class Contribution extends Model {
public function contributable() {
return $this->morphTo();
}
}
class User extends Model
{
/**
* Get the user's contributions.
*/
public function contributions()
{
return $this->morphToMany(Contribution::class, 'contributable');
}
}
You should be able to retrieve the users contributions that way and defining the action based on the morphed instance type.

Laravel store data thru model

I dont know how else to label the title. Anyone have ideas go ahead and make a suggested edit.
I have a series of Models for the database in my application.
I can currently add a global scope to the model and have the model automatically add a "where" clause on my queries to the database on a key:value pair. This is working great.
class Customers extends Model
{
protected $table = 'customers';
protected static function boot() {
parent::boot();
static::addGlobalScope('companyRecordID', function (Builder $builder) {
$builder->where('companyRecordID', Auth::guard('user')->user()->companyRecordID);
});
}
}
I am having troubles trying to identify if this can be done; Id like to be able to store the "companyRecordID" from the Auth::guard('user')->user()->companyRecordID automatically when a database record is created. Similar to created_at and updated_at are created automatically without requiring code from the controller to define.
Can someone direct me to what I should be looking for. Ive spent a few hours trying to google key word pairs to find an answer with no avail.
If you are using Models to create the records you can listen for the creating event for the Model and then add any additional fields you may need. You can create a listener for the creating event on Customers:
Customers::creating(function ($customer) {
if (auth('user')->user()) {
$customer->companyRecordID = auth('user')->user()->companyRecordID;
}
});
You can throw that in a Service Provider's boot method or your Model's boot method. If you throw it in the Model's boot method you may want to adjust to using static:: instead of Customers::.
I believe you got your answer but,
On your model please use protected $fillable
protected $fillable = ['','',''];
protected $table = 'customers';
also, you can use Relationships to optimize your codes.

Laravel - why is a Model's relations not being loaded?

I have a Laravel 5.3 site and I think maybe I have some weird things going on due to some actions happening in API controllers and some happening in the regular controllers.
Or maybe an issue where at some times I am dealing with a Model, and sometimes with an Eloquent Collection.
The issue is, I am trying to retrieve relations on a Model and am getting null.
For instance, I have course Model that relates to week Model.
In course Model I get week items as
public function weeks()
{
return $this->hasMany(Week::class, 'course_id');
}
In backend, these relations get sent in this way:
$course->load('weeks')
All is good.
But when course item gets deleted and I try and take action in the week controller as
static::deleting(function($course) {
$course->weeks->delete();
});
$course->weeks is null. At that time I see in the database that all is good and this course items does indeed have a week item related, but $course shows 0 relations.
So something odd is happening where $course->webinars is not grabbing the week items related.
Is there something that I am fundamentally doing wrong? Maybe it is because in the models I have these sorts of statements:
protected $table = 'Week';
Are these preventing the relations from being pulled? I always thought that is I had some function in a model that returns relations that those relations would always be available when I use syntax $course->weeks.
Ideas?
Thanks again,
You can simply setup migrations to automatically delete from weeks if you delete a course, provided you have foreign key relationship.
If you have a column course_id in weeks table then add this into your migration
$table->foreign('course_id')
->references('id')->on('courses')
->onDelete('cascade')
I think you can use Observers. In your AppServiceProvider, first register the observer.
public function boot()
{
Course::observe(CourseObserver::class);
}
Now, add an Observer class.
class CourseObserver
{
public function deleting(Course $course)
{
$course->weeks()->delete();
}
}

Laravel fills attributes in model in migration even though they are not fillable

I have two entities:
Person, which has workspace_id column
PersonRevision, which does not have workspace_id column
In a migration I have this:
$persons = Person::all();
foreach ($persons as $person) {
$revision = new PersonRevision;
$revision->fill($person->getAttributes());
$revision->save();
}
PersonRevision does not have workspace_id in fillable property. But for some reason I got an error: column "workspace_id" of relation "person_revisions" does not exist.
Besides, if I run the migration second time (by just typing php artisan migrate second time) it works fine.
What could be the reason? I know I can manually list attributes which need to be filled, or use array_except but this is inconvenient and does not answer why that happens.
Could you check what the actual sql statement is that's executed? To me it seems like you have a database which does not contain workspace_id yet, but your model does expect it to be there. Even when not filling the workspace_id the query generated from your eloquent model will probably try to update it with the value null, which it can't do if it's not there.
Because of issues like this it's best not to use eloquent models in database migrations, instead use raw sql.
Laravel SeedCommand handle() function runs with Model::ungarded() which forces all props to be fillable. When unguarded is true it auto fills regardless of fillable array.
public function isFillable($key)
{
if (static::$unguarded) {
return true;
}
[...]
}
Simply add this atop your seeder file Model::reguard();

Counting page views with Laravel

I want to implement page view counter in my app. What I've done so far is using this method :
public function showpost($titleslug) {
$post = Post::where('titleslug','=',$titleslug)->firstOrFail();
$viewed = Session::get('viewed_post', []);
if (!in_array($post->id, $viewed)) {
$post->increment('views');
Session::push('viewed_post', $post->id);
}
return view('posts/show', compact('post', $post));
}
I retrieve the popular posts list like this :
$popular_posts = Post::orderBy('views', 'desc')->take(10)->get();
However, I'd like to know if there are any better ways to do this ? And with my current method, can I get a list of most viewed posts in the past 24 hours ? That's all and thanks!
As quoted in # milo526's comment, you can record all hits to your pages in a unique way instead of an increment. With this you have many possibilities to search for access information, including the listing of the posts sorted by most viewed.
Create a table to save your view records:
Schema::create("posts_views", function(Blueprint $table)
{
$table->engine = "InnoDB";
$table->increments("id");
$table->increments("id_post");
$table->string("titleslug");
$table->string("url");
$table->string("session_id");
$table->string("user_id");
$table->string("ip");
$table->string("agent");
$table->timestamps();
});
Then, create the corresponding model:
<?php namespace App\Models;
class PostsViews extends \Eloquent {
protected $table = 'posts_views';
public static function createViewLog($post) {
$postsViews= new PostsViews();
$postsViews->id_post = $post->id;
$postsViews->titleslug = $post->titleslug;
$postsViews->url = \Request::url();
$postsViews->session_id = \Request::getSession()->getId();
$postsViews->user_id = \Auth::user()->id;
$postsViews->ip = \Request::getClientIp();
$postsViews->agent = \Request::header('User-Agent');
$postsViews->save();
}
}
Finally, your method:
public function showpost($titleslug)
{
$post = PostsViews::where('titleslug', '=' ,$titleslug)->firstOrFail();
PostsViews::createViewLog($post);
//Rest of method...
}
To search the most viewed posts in the last 24 hours:
$posts = Posts::join("posts_views", "posts_views.id_post", "=", "posts.id")
->where("created_at", ">=", date("Y-m-d H:i:s", strtotime('-24 hours', time())))
->groupBy("posts.id")
->orderBy(DB::raw('COUNT(posts.id)', 'desc'))
->get(array(DB::raw('COUNT(posts.id) as total_views'), 'posts.*'));
Note that in PostsViews, you have data that can help further filter your listing, such as the session id, in case you do not want to consider hits from the same session.
You may need to adapt some aspects of this solution to your final code.
2020 Update (2)/ With Eloquent Relationships for Laravel 6
If you don't want to add a package to your application. I have developed the following solution based on "Jean Marcos" and "Learner" contribution to the question and my own research.
All credit goes to "Jean Marcos" and "Learner", I felt like I should do the same as Learner and update the code in a way the would be beneficial to others.
First of all, make sure you have a sessions table in the database. Otherwise, follow the steps in Laravel documentations to do so: HTTP Session
Make sure that the sessions are stored in the table. If not, make sure to change the SESSION_DRIVER variable at the .env set to 'database' not 'file' and do composer dump-autoload afterwards.
After that, you are all set to go. You can start by running the following console command:
php artisan make:model PostView -m
This will generate both the model and migration files.
Inside of the migration file put the following Schema. Be cautious with the columns names. For example, my posts table have the "slug" column title name instead of the "titleslug" which was mentioned in the question.
Schema::create('post_views', function (Blueprint $table) {
$table->increments("id");
$table->unsignedInteger("post_id");
$table->string("titleslug");
$table->string("url");
$table->string("session_id");
$table->unsignedInteger('user_id')->nullable();
$table->string("ip");
$table->string("agent");
$table->timestamps();
});
Then put the following code inside the PostView model file.
<?php
namespace App;
use App\Post;
use Illuminate\Database\Eloquent\Model;
class PostView extends Model
{
public function postView()
{
return $this->belongsTo(Post::class);
}
public static function createViewLog($post) {
$postViews= new PostView();
$postViews->post_id = $post->id;
$postViews->slug = $post->slug;
$postViews->url = request()->url();
$postViews->session_id = request()->getSession()->getId();
$postViews->user_id = (auth()->check())?auth()->id():null;
$postViews->ip = request()->ip();
$postViews->agent = request()->header('User-Agent');
$postViews->save();
}
}
Now inside the Post model write the following code. This to create the relation between the posts table and the post_views table.
use App\PostView;
public function postView()
{
return $this->hasMany(PostView::class);
}
In the same Post model you should put the following code. If the user is not logged in the the code will test the IP match. Otherwise, it will test both the session ID and the user ID as each user might have multiple sessions.
public function showPost()
{
if(auth()->id()==null){
return $this->postView()
->where('ip', '=', request()->ip())->exists();
}
return $this->postView()
->where(function($postViewsQuery) { $postViewsQuery
->where('session_id', '=', request()->getSession()->getId())
->orWhere('user_id', '=', (auth()->check()));})->exists();
}
You are ready now to run the migration.
php artisan migrate
When the user ask for the post. The following function should be targeted inside the PostController file:
use App\PostView;
public function show(Post $post)
{
//Some bits from the following query ("category", "user") are made for my own application, but I felt like leaving it for inspiration.
$post = Post::with('category', 'user')->withCount('favorites')->find($post->id);
if($post->showPost()){// this will test if the user viwed the post or not
return $post;
}
$post->increment('views');//I have a separate column for views in the post table. This will increment the views column in the posts table.
PostView::createViewLog($post);
return $post;
}
As I have a separate column for views in the post table. To search the most viewed posts in the last 24 hours you write this code in the controller. Remove paginate if you don't have pagination:
public function mostViwedPosts()
{
return Posts::with('user')->where('created_at','>=', now()->subdays(1))->orderBy('views', 'desc')->latest()->paginate(5);
}
I hope this would help/save someones time.
2020 Update
First of all, thanks a lot "Jean Marcos" for his awesome answer. All credit goes to him, I am just pasting a slightly modified answer combining my knowledge of Laravel as well.
Create a table to save your view records and name it with snake_case plural: post_views
Schema::create("post_views", function(Blueprint $table)
{
$table->engine = "InnoDB";//this is basically optional as you are not using foreign key relationship so you could go with MyISAM as well
$table->increments("id");
//please note to use integer NOT increments as "Jean Marcos' answer" because it will throw error "Incorrect table definition; there can be only one auto column and it must be defined as a key" when running migration.
$table->unsignedInteger("post_id");//note that the Laravel way of defining foreign keys is "table-singular-name_id", so it's preferable to use that
$table->string("titleslug");
$table->string("url");
$table->string("session_id");
$table->unsignedInteger('user_id')->nullable();//here note to make it nullable if your page is accessible publically as well not only by logged in users. Also its more appropriate to have "unsignedInteger" type instead of "string" type as mentioned in Jean Marcos' answer because user_id will save same data as id field of users table which in most cases will be an auto incremented id.
$table->string("ip");
$table->string("agent");
$table->timestamps();
});
Then, create the corresponding model. Please note to create "PascalCase" model name and singular form of the table so it should be like: PostView
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class PostView extends Model
{
public static function createViewLog($post) {
$postViews= new PostView();
$postViews->listing_id = $post->id;
$postViews->url = \Request::url();
$postViews->session_id = \Request::getSession()->getId();
$postViews->user_id = (\Auth::check())?\Auth::id():null; //this check will either put the user id or null, no need to use \Auth()->user()->id as we have an inbuild function to get auth id
$postViews->ip = \Request::getClientIp();
$postViews->agent = \Request::header('User-Agent');
$postViews->save();//please note to save it at lease, very important
}
}
Then run the migration to generate this table
php artisan migrate
Finally, your method:
public function showpost($titleslug)
{
$post = PostView::where('titleslug', '=' ,$titleslug)->firstOrFail();
\App\PostView::createViewLog($post);//or add `use App\PostView;` in beginning of the file in order to use only `PostView` here
//Rest of method...
}
To search the most viewed posts in the last 24 hours:
$posts = Posts::join("post_views", "post_views.id_post", "=", "posts.id")
->where("created_at", ">=", date("Y-m-d H:i:s", strtotime('-24 hours', time())))
->groupBy("posts.id")
->orderBy(DB::raw('COUNT(posts.id)'), 'desc')//here its very minute mistake of a paranthesis in Jean Marcos' answer, which results ASC ordering instead of DESC so be careful with this line
->get([DB::raw('COUNT(posts.id) as total_views'), 'posts.*']);
Note that in PostView, you have data that can help further filter your listing, such as the session id, in case you do not want to consider hits from the same session.
You may need to adapt some aspects of this solution to your final code.
So those were few modifications I wanted to point out, also you might want to put an additional column client_internet_ip in which you can store \Request::ip() which can be used as a filter as well if required.
I hope it helps
Eloquent Viewable package can be used for this purpose. It provides more flexible ways to do stuff like this(counting page views).
Note:The Eloquent Viewable package requires PHP 7+ and Laravel 5.5+.
Make Model viewable:
Just add the Viewable trait to the model definition like:
use Illuminate\Database\Eloquent\Model;
use CyrildeWit\EloquentViewable\Viewable;
class Post extends Model
{
use Viewable;
// ...
}
Then in the controller:
public function show(Post $post)
{
$post->addView();
return view('blog.post', compact('post'));
}
After that you can do stuff like this:(see package installation guide for more details)
// Get the total number of views
$post->getViews();
// Get the total number of views since the given date
$post->getViews(Period::since(Carbon::parse('2014-02-23 00:00:00')));
// Get the total number of views between the given date range
$post->getViews(Period::create(Carbon::parse('2014-00-00 00:00:00'), Carbon::parse('2016-00-00 00:00:00')));
// Get the total number of views in the past 6 weeks (from today)
$post->getViews(Period::pastWeeks(6));
// Get the total number of views in the past 2 hours (from now)
$post->getViews(Period::subHours(2));
// Store a new view in the database
$post->addView();
Implements same kind of idea as in the accepted answer, but provides more features and flexibilities.
First of all thanks to user33192 for sharing the eloquent viewable. Just want to make it clearer for others after looking at the docs. Look at the docs to install the package.
Do this in your Post Model:
use Illuminate\Database\Eloquent\Model;
use CyrildeWit\EloquentViewable\InteractsWithViews;
use CyrildeWit\EloquentViewable\Viewable;
class Post extends Model implements Viewable
{
use InteractsWithViews;
// ...
}
In your posts controller, use the record method to save a view;
public function show($slug)
{
$post = Post::where('slug',$slug)->first();
views($post)->record();
return view('posts.show',compact('post'));
}
In your views you can return the views (mine is posts.show) as you want. Check the document for more. I will just the total views of a post.
<button class="btn btn-primary">
{{ views($post)->count() }} <i class="fa fa-eye"></i>
</button>

Resources