Cache relation laravel with orderby - php

I would like to know how can I cache laravel relation with orderby in controller ?
Currently I have this in my controller :
MyController.php
$replies = $message->replies()->orderBy('position', 'asc')->get();
MessageEntity.php
/* Message has many Replies */
public function replies() {
return \Cache::remember('messages_' . $this->id . '_replies', $this->expiration, function () {
return $this->hasMany('App\Model\Entities\ReplyEntity', 'fk_message_id', 'id')->get();
});
}
But I have this error :
BadMethodCallException
Method Illuminate\Database\Eloquent\Collection::orderBy does not exist.

since you are using get() in your relation method, the method wont return the relation but will return the list from db, and that would make sense, because you don not want to cache the relation, but you want to cache the data:
you may add orderBy clause to your query Builder in the function before using 'get()'
public function replies() {
return \Cache::remember('messages_' . $this->id . '_replies', $this->expiration, function () {
return $this->hasMany('App\Model\Entities\ReplyEntity', 'fk_message_id', 'id')->orderBy('position', 'asc')->get();
});
}
please not if your order by columns or direction are not always the same, you should include the order direction and columns in the key of you cache
as a personal opinion, the relation method should represent the relation only, without get and cache, so you can use it somewhere else.
the cache should be in the repository of the model ('MessagesRepository')

It is because of you are retrieving a collection and the orderBy() is a query method.
You will need to change your entity method to
/* Message has many Replies */
public function replies() {
return \Cache::remember('messages_' . $this->id . '_replies', $this->expiration, function () {
return $this->hasMany('App\Model\Entities\ReplyEntity', 'fk_message_id', 'id')->orderBy('position', 'asc')->get();
});
}
and in the controller just get it with
$replies = $message->replies();
Or use an alternative method for collection sortBy( https://laravel.com/docs/7.x/collections#method-sortby )

Related

the relation is not appending to the model in laravel

i want to append a relation to my model in laravel i know its possible with resource but i need it to be appened to model so here its like below what i do :
protected $appends = ['accommodation_rooms'];
public function getAccommodationRoomsAttribute(){
return $this->accommodationRooms();
}
and my relation is :
public function accommodationRooms()
{
return $this->Hasmany(AccommodationRoom::class);
}
but when i run my api it returns null but when i call the relation it has the relation and it has no problem . any idea what i am doing wrong ??
EDIT
$data = Accommodation::with('city', 'accommodationFacilities', 'gallery')
->where('is_deleted', 0)->Paginate(env('PAGINATE_NUMBER'));
return $data;
return $this->accommodationRooms() will return an instance of query builder. That is probably why it was showing empty.
Change it to
public function getAccommodationRoomsAttribute(){
return $this->accommodationRooms;
}
Call it without ()
public function getAccommodationRoomsAttribute(){
return $this->accommodation_rooms;
}
And update your relation to:
public function accommodationRooms()
{
return $this->hasMany(AccommodationRoom::class);
}
And make sure you have accommodation_id column in your accommodation_rooms table

Laravel 5.7: New Accessor works great but is not recognisable in Repository Class (unknown column) despite of adding $appends to Model

So I have a class Order extends Model.
I created an Accessor called requiresApproval that returns true or false.
public function getRequiresApprovalAttribute(): bool
{
if ($some_physical_column_from_db === 'does not matter') {
return true;
}
return false;
}
When I have my Order model and I call $order->requiresApproval I get my boolean value. Everything works great.
However I need this accessor to appear on the list of attributes because I want to use it in my repository class in where condition within query.
So based on the official documentation, I added:
protected $appends = [
'requires_approval',
];
but when I dd() my Order, this attribute is not on the list of attributes (while $appends property indicates the accessor is there).
Long story short:
When in my repository I call:
public function getOrdersEligibleToBeSendToOptions(): Collection
{
$columns = [
'*',
];
return $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->where('requiresApproval', '=', true) // this fails
// ->where('requires_approval', '=', true) // this fails as well
->get($columns);
}
I get:
What am I doing wrong? How can I use my accessor within repository class?
OK, this works, but the reason I don't like this solution is the fact that just half of the conditions are on the DB layer, the rest is by filtering what's already fetched.
If the query is going to return (let's say) thousand of records and filter returns just a few of them I personally see this as a huge waste of DB resource.
public function getOrdersEligibleToBeSendToOptions(): Collection
{
$columns = [
'*',
];
$results = $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->get($columns);
return $results->filter(function ($value, $key) {
return $value->requiresApproval === false;
});
}
Eloquent queries work on the database fields, but you can use your accessor after fetching a colletion from the database like this.
Here is some good article about this:
https://medium.com/#bvipul/laravel-accessors-and-mutators-learn-how-to-use-them-29a1e843ce85
return $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->get($columns)
->filter(function ($row) {
return $row->requires_approval === true;
});
The model virtual attributes cannot be used within queries. Perhaps a better approach would be to create a scope to enforce this constraint on a query:
class Order extends Model
{
public function scopeRequiresApproval($query)
{
return $query->where('some_column', '>', 100);
}
}
Then
return $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->requiresApproval()
->get($columns);

Cache Eloquent Relationship query

How can I cache this Eloquent query:
dd($user->roles);
Because above will somehow trigger the $user->roles() query I assume.
I have tried with this:
public function roles() {
return \Cache::remember('user_' . $this->id . '_roles', 10, function() {
return $this->hasMany('App\Role');
});
}
But it does not work, because it has to return a array, not eloquent query.
Any suggestions?
Here is my approach:
public function bookmarks(): HasMany
{
return $this->hasMany(Bookmark::class);
}
protected function getBookmarksCacheKey(): string
{
return sprintf('user-%d-bookmarks', $this->id);
}
public function clearBookmarksCache(): bool
{
return Cache::forget($this->getBookmarksCacheKey());
}
public function getBookmarksAttribute(): Collection
{
if ($this->relationLoaded('bookmarks')) {
return $this->getRelationValue('bookmarks');
}
$bookmarks = Cache::rememberForever($this->getBookmarksCacheKey(), function () {
return $this->getRelationValue('bookmarks');
});
$this->setRelation('bookmarks', $bookmarks);
return $bookmarks;
}
You can't store a relationship in the cache. You need to cache the actual data retrieved from the database. So you'll have something like this:
public function roles()
{
return \Cache::remember('user_' . $this->id . '_roles', 10, function()
{
return $this->hasMany('App\Role')->get()->toArray();
});
}
And now you have to access it as a method, not a property, because it's not returning a relation anymore (and Eloquent would throw an exception):
$user->roles();
Now you should get an array as you want.
If you want to cache user together with its roles you can do it this way:
$user = User::find(1);
$user->load('roles');
Cache::put('users_'.$user->id, $user, 10);
I don't know why, but you need to use load here instead of with. If you used with you would get error that you cannot cache PDO instance.

laravel 4 how to order and join by eloquent

Im new in Laravel 4, and right now im coding for small project, i use laravel as framework to build my website, but my code i always wonder it's optimize or not because in my model i just wrote:
Category Model
public function parents()
{
return $this->belongsTo('Category', 'cat_father');
}
public function children()
{
return $this->hasMany('Category', 'cat_father');
}
}
Post Model:
<?php
class Post extends BaseModel{
public $table = "post";
protected $primaryKey = 'idpost';
public function Category()
{
return $this->belongsTo('Category', 'cat_id');
}
}
because i didn't know how to join 2 tables in laravel 4, i have a condition is find all post from my categories, which it hadn't belong to category name "Reunion", but i didn't know how to do that, therefore i wrote 2 lines code for that purpose (im not sure wrote code in controller is best way but i didn't know how to call method from Model to controller and get return value)
My method from controller for select all post, it hasn't belong to category name "Reunion"
public function getAllPostView()
{
$getCat = Category::where('cat_name','=', 'Reunion')->firstOrFail();
$post = Post::where('cat_id', '!=', $getCat->idcategory)->get();
return View::make('layouts.post')->with('post',$post);
}
My question, my code is optimize when i wrote it in controller? and how to wrote it in model and get parameter for passing it to controller and use it to view.
second question is how to order "POST" because some cases post need to be ordered from new to old
This is how you do it:
$exclude = 'Reunion';
$posts = Post::select('posts.*')->join('categories', function ($j) use ($exclude) {
$j->on('posts.cat_id', '=', 'categories.idcategory')
->where('categories.name', '<>', $exclude);
})->get();
could just use simple joins
public function getAllPostView()
{
$getCat = Category::where('cat_name','=', 'Reunion')
->join('post','post.cat_id', '!=','Category.idcategory')->get();
return View::make('layouts.post')->with('post',$post);
}
Look out for same field names in both the tables if so can use select
$getCat = Category::select('Category.idcategory as cat_id','Category.cat_id as pos_id','many other fields')
// 'as cat_id' not required for unique field names
->join('post','post.cat_id', '!=','Category.idcategory')
->where('cat_name','=', 'Reunion')
->get();

Automatically deleting related rows in Laravel (Eloquent ORM)

When I delete a row using this syntax:
$user->delete();
Is there a way to attach a callback of sorts, so that it would e.g. do this automatically:
$this->photo()->delete();
Preferably inside the model-class.
I believe this is a perfect use-case for Eloquent events (http://laravel.com/docs/eloquent#model-events). You can use the "deleting" event to do the cleanup:
class User extends Eloquent
{
public function photos()
{
return $this->has_many('Photo');
}
// this is a recommended way to declare event handlers
public static function boot() {
parent::boot();
static::deleting(function($user) { // before delete() method call this
$user->photos()->delete();
// do the rest of the cleanup...
});
}
}
You should probably also put the whole thing inside a transaction, to ensure the referential integrity..
You can actually set this up in your migrations:
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Source: http://laravel.com/docs/5.1/migrations#foreign-key-constraints
You may also specify the desired action for the "on delete" and "on
update" properties of the constraint:
$table->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');
Note: This answer was written for Laravel 3. Thus might or might not works well in more recent version of Laravel.
You can delete all related photos before actually deleting the user.
<?php
class User extends Eloquent
{
public function photos()
{
return $this->has_many('Photo');
}
public function delete()
{
// delete all related photos
$this->photos()->delete();
// as suggested by Dirk in comment,
// it's an uglier alternative, but faster
// Photo::where("user_id", $this->id)->delete()
// delete the user
return parent::delete();
}
}
Hope it helps.
Relation in User model:
public function photos()
{
return $this->hasMany('Photo');
}
Delete record and related:
$user = User::find($id);
// delete related
$user->photos()->delete();
$user->delete();
There are 3 approaches to solving this:
1. Using Eloquent Events On Model Boot (ref: https://laravel.com/docs/5.7/eloquent#events)
class User extends Eloquent
{
public static function boot() {
parent::boot();
static::deleting(function($user) {
$user->photos()->delete();
});
}
}
2. Using Eloquent Event Observers (ref: https://laravel.com/docs/5.7/eloquent#observers)
In your AppServiceProvider, register the observer like so:
public function boot()
{
User::observe(UserObserver::class);
}
Next, add an Observer class like so:
class UserObserver
{
public function deleting(User $user)
{
$user->photos()->delete();
}
}
3. Using Foreign Key Constraints (ref: https://laravel.com/docs/5.7/migrations#foreign-key-constraints)
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
As of Laravel 5.2, the documentation states that these kinds of event handlers should be registered in the AppServiceProvider:
<?php
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
User::deleting(function ($user) {
$user->photos()->delete();
});
}
I even suppose to move them to separate classes instead of closures for better application structure.
It is better if you override the delete method for this. That way, you can incorporate DB transactions within the delete method itself. If you use the event way, you will have to cover your call of delete method with a DB transaction every time you call it.
In your User model.
public function delete()
{
\DB::beginTransaction();
$this
->photo()
->delete()
;
$result = parent::delete();
\DB::commit();
return $result;
}
To elaborate on the selected answer, if your relationships also have child relationships that must be deleted, you have to retrieve all child relationship records first, then call the delete() method so their delete events are fired properly as well.
You can do this easily with higher order messages.
class User extends Eloquent
{
/**
* The "booting" method of the model.
*
* #return void
*/
public static function boot() {
parent::boot();
static::deleting(function($user) {
$user->photos()->get()->each->delete();
});
}
}
You can also improve performance by querying only the relationships ID column:
class User extends Eloquent
{
/**
* The "booting" method of the model.
*
* #return void
*/
public static function boot() {
parent::boot();
static::deleting(function($user) {
$user->photos()->get(['id'])->each->delete();
});
}
}
Using Constrained()
After Laravel 7, new foreignId() and constrained() methods are available for defining relationship constraint in database. OnDelete() method can be used on these methods to automatically delete related records.
Old style
$table->unsignedBigInterer('user_id');
$table->foreign('user_id')
->references('id')
->on('users')
->onDelete('cascade');
New style
$table->foreignId('user_id')
->constrained()
->onDelete('cascade');
I would iterate through the collection detaching everything before deleting the object itself.
here's an example:
try {
$user = User::findOrFail($id);
if ($user->has('photos')) {
foreach ($user->photos as $photo) {
$user->photos()->detach($photo);
}
}
$user->delete();
return 'User deleted';
} catch (Exception $e) {
dd($e);
}
I know it is not automatic but it is very simple.
Another simple approach would be to provide the model with a method. Like this:
public function detach(){
try {
if ($this->has('photos')) {
foreach ($this->photos as $photo) {
$this->photos()->detach($photo);
}
}
} catch (Exception $e) {
dd($e);
}
}
Then you can simply call this where you need:
$user->detach();
$user->delete();
Add delete function on model that you want to delete
Define relations of models
for example in this instance:
/**
* #return bool|null
*/
public function delete(): ?bool
{
$this->profile()->delete();
$this->userInterests()->delete();
$this->userActivities()->delete();
$this->lastLocation()->delete();
return parent::delete();
}
And relations in user model are:
public function profile()
{
return $this->hasOne(Profile::class, 'user_id', 'id');
}
public function userInterests()
{
return $this->hasMany(userInterest::class, 'user_id', 'id');
}
public function userActivities()
{
return $this->hasMany(userActivity::class, 'user_id', 'id');
}
public function lastLocation()
{
return $this->hasOne(LastLocation::class, 'user_id', 'id');
}
This way worked for me on Laravel 8:
public static function boot() {
parent::boot();
static::deleted(function($item){
$item->deleted_by = \Auth::id(); // to know who delete item, you can delete this row
$item->save(); // to know who delete item, you can delete this row
foreach ($item->photos as $photo){
$photo->delete();
}
});
}
public function photos()
{
return $this->hasMany('App\Models\Photos');
}
Note: deleting in this syntax $user->photos()->delete(); not worked for me...
In my case it was pretty simple because my database tables are InnoDB with foreign keys with Cascade on Delete.
So in this case if your photos table contains a foreign key reference for the user than all you have to do is to delete the hotel and the cleanup will be done by the Data Base, the data base will delete all the photos records from the data base.
Here are the perfect solutions.
# model
public function order_item_properties()
{
return $this->hasMany(OrderItemProperty::class, 'order_id', 'id');
}
public function order_variations()
{
return $this->hasMany(OrderItemVariation::class, 'order_id', 'id');
}
# controller
$order_item = OrderItem::find($request->order_id);
$order_item->order_item_properties()->delete();
$order_item->order_variations()->delete();
$order_item->delete();
return response()->json([
'message' => 'Deleted',
]);
Or you can do this if you wanted, just another option:
try {
DB::connection()->pdo->beginTransaction();
$photos = Photo::where('user_id', '=', $user_id)->delete(); // Delete all photos for user
$user = Geofence::where('id', '=', $user_id)->delete(); // Delete users
DB::connection()->pdo->commit();
}catch(\Laravel\Database\Exception $e) {
DB::connection()->pdo->rollBack();
Log::exception($e);
}
Note if you are not using the default laravel db connection then you need to do the following:
DB::connection('connection_name')->pdo->beginTransaction();
DB::connection('connection_name')->pdo->commit();
DB::connection('connection_name')->pdo->rollBack();
It’s better to use onDelete cascade when defining your model’s migration. This takes care of deleting the model’s relations for you:
e.g.
$table->foreign(’user_id’)
->references(’id’)->on(’users’)
->onDelete(’cascade’);
If you happen to find yourself thinking about how to delete a model and its relations to a level greater than 3 or 4 nested relations, then you should consider redefining your model's relationships.
$table->foreignId('user_id')->constrained('user')->cascadeOnDelete();
or
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
yeah, but as #supersan stated upper in a comment, if you delete() on a QueryBuilder, the model event will not be fired, because we are not loading the model itself, then calling delete() on that model.
The events are fired only if we use the delete function on a Model Instance.
So, this beeing said:
if user->hasMany(post)
and if post->hasMany(tags)
in order to delete the post tags when deleting the user, we would have to iterate over $user->posts and calling $post->delete()
foreach($user->posts as $post) { $post->delete(); } -> this will fire the deleting event on Post
VS
$user->posts()->delete() -> this will not fire the deleting event on post because we do not actually load the Post Model (we only run a SQL like: DELETE * from posts where user_id = $user->id and thus, the Post model is not even loaded)
You can use this method as an alternative.
What will happen is that we take all the tables associated with the users table and delete the related data using looping
$tables = DB::select("
SELECT
TABLE_NAME,
COLUMN_NAME,
CONSTRAINT_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE REFERENCED_TABLE_NAME = 'users'
");
foreach($tables as $table){
$table_name = $table->TABLE_NAME;
$column_name = $table->COLUMN_NAME;
DB::delete("delete from $table_name where $column_name = ?", [$id]);
}

Categories