Working with global events for models, on Laravel - php

It's basically what I need: if any model row on my project is created, updated or removed, I need call a callback to work with this model row. I'll use that to create a changelog in other model, that will not trigger this global event to avoid looping.
I tried set it like that:
Model::creating($callback);
But it doesn't worked. Work only if I set it directly to all models that I have. It's bad because I need specific one-by-one. If I create a new one, I need specify it manually. Example:
User::creating($callback);
Company::creating($callback);
...
Any change will be logged on another model, called Log. It'll not trigger this $callback, because it'll register on table logs (managed by Log) each change in other models. Something like:
$user = new User;
$user->name = "John";
$user->age = 18;
$user->save();
$user->name = "John Doe";
$user->save();
$user->delete();
$user->restore();
$user->forceDelete();
I'll register something like:
id | object_type | object_id | event_type | object_data
.. App\User 1 created { name: John, age: 18 }
.. App\User 1 updated { name: John Doe }
.. App\User 1 trashed null
.. App\User 1 restored null
.. App\User 1 removed null

With Laravel 5.1
BaseTrait Trait:
trait BaseTrait
{
public static function bootBaseTrait()
{
static:creating(function($item) {
....
});
static:updating(function($item) {
....
});
}
// It binds the events in bootBaseTrait to any subclass
// Base run first, then run your subclass's events if you defined on the subclass
}
Base Model:
use XX\XX\BaseTrait;
use Illuminate\Database\Eloquent\Model;
class Base extends Model
{
use BaseTrait;
}
XXX Model:
use XX\XX\Base;
class XXX extends Base
{
}
How does this work?
You can see /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
protected static function bootTraits()
The method tries to call bootTraitName when you use a trait!

All Eloquent model events have the format eloquent.event:ClassName.
You could subscribe to the eloquent.* event.
Event::listen('eloquent.*', function($model) {});
Then you can check if the current model is a log model (in which case you'd return true straight out of it to avoid infinite recursion), otherwise, save the change to your log model.

Related

Laravel 8 Multiple Relationships for Factory

In Laravel 8 it is possible to quickly fill relationships with factories. However, I cannot figure out how to generate more than one relationship. How can I create a random or new relationship for each link using the new Laravel 8 syntax?
This factory syntax is only available in Laravel 8.
https://laravel.com/docs/8.x/database-testing#factory-relationships
Problem
Consider the following relationship:
Each link belongs to a website and a post.
Both websites and posts can have many links.
<?php
class Post extends Model
{
use HasFactory;
function links()
{
return $this->hasMany(Link::class);
}
}
class Website extends Model
{
use HasFactory;
function links()
{
return $this->hasMany(Link::class);
}
}
class Link extends Model
{
use HasFactory;
function post()
{
return $this->belongsTo(Post::class);
}
function website()
{
return $this->belongsTo(Website::class);
}
}
What I tried/want
What I tried below will only generate one model for all the links. How can I create a random or new relationship for each link using the new Laravel 8 syntax?
Link::factory()->count(3)->forPost()->forWebsite()->make()
=> Illuminate\Database\Eloquent\Collection {#4354
all: [
App\Models\Link {#4366
post_id: 1,
website_id: 1,
},
App\Models\Link {#4395
post_id: 1, // return a different ID
website_id: 1,
},
App\Models\Link {#4370
post_id: 1, // return a different ID
website_id: 1, // return a different ID
},
],
}
Just add this to your LinkFactory:
public function definition()
{
return [
'post_id' => function () {
return Post::factory()->create()->id;
},
.....
];
}
And now you can create new Post for each new Link:
Link::factory()->count(3)->create();//Create 3 links with 3 new posts
or attach new Links to existing Post:
Link::factory()->count(3)->create(['post_id' => Post::first()->id]); //create 3 links and 0 new posts
In Laravel 9, you can use this macro:
// database/factoryMacros.php
<?php
namespace Database\Support;
use Illuminate\Database\Eloquent\Factories\BelongsToRelationship;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
/** #param Factory|Model $factory */
Factory::macro('hasParent', function (mixed $factory, string $relationship = null): self {
return $this
->state(function () use ($factory, $relationship): array {
$belongsTo = new BelongsToRelationship(
factory: $factory,
relationship: $relationship ?? guessBelongsToMethodName($factory),
);
return $belongsTo
->recycle(recycle: $this->recycle)
->attributesFor(model: $this->newModel());
});
});
Factory::macro('hasChildren', fn (...$arguments): self => $this->has(...$arguments));
Factory::macro('hasChild', fn (...$arguments): self => $this->has(...$arguments));
/** #param Factory|Model $factory */
function guessBelongsToMethodName(mixed $factory): string
{
$modelName = is_subclass_of($factory, Factory::class)
? $factory->modelName()
: $factory::class;
return Str::camel(class_basename($modelName));
}
Usage
Use the method hasParent($factory) instead of for($factory):
// Creates 3 Link, 3 Post, 3 Website
Link::factory()
->count(3)
->hasParent(Post::factory())
->hasParent(Website::factory())
->make();
You can also use hasChildren($factory) or hasChild($factory) instead of has for name consistency:
// Creates 3 Post, 3 Link
Post::factory()
->count(3)
->hasChild(Link::factory())
->make();
The syntax of the macros is the same as for and has.
You can explicitly define the relationship name, pass complex factory chains, pass a concrete model, and use it with recycle, for example.
Installation
Add the file to your composer.json:
{
...
"autoload": {
"files": [
"database/factoryMacros.php"
]
}
}
Run a composer dump-autoload to reload the composer file.
Alternatively, you can register the macro as a service or load it as a mixin.
PS: I intend to create a library for this in the future.
Tests
/**
* Use "DatabaseEloquentFactoryTest.php" as base:
* https://github.com/laravel/framework/blob/de42f9987e01bfde50ea4a86becc237d9c8c5c03/tests/Database/DatabaseEloquentFactoryTest.php
*/
class FactoryMacrosTest extends TestCase
{
function test_belongs_to_relationship()
{
$posts = FactoryTestPostFactory::times(3)
->hasParent(FactoryTestUserFactory::new(['name' => 'Taylor Otwell']), 'user')
->create();
$this->assertCount(3, $posts->filter(function ($post) {
return $post->user->name === 'Taylor Otwell';
}));
$this->assertCount(3, FactoryTestUser::all());
$this->assertCount(3, FactoryTestPost::all());
}
}
TL;DR;
In Laravel 9, it is not possible to achieve this. The for() uses a single model for all instances.
There's a PR to fix this behavior, but the PR was closed, and I'm not sure it will ever be implemented:
https://github.com/laravel/framework/pull/44279
The laravel magic factory method for allows you to populate the database with one record from the foreign table. See link to documentation https://laravel.com/docs/8.x/database-testing#belongs-to-relationships
In your case, using forPost() and forWebsite() will allow you to populate the database with one id from the Post table and the Website table.
If you want to use different IDs use this syntax instead
Link::factory()->count(3)->make()
Had a similar problem and was only able to get it working when I attached within the afterCreating() on a single factory. This allows me to create/store the id of each model and then attach to the Link model
I'm choosing to start with WebsiteFactory but you can also start with PostFactory since those are the "highest parent" models. If you try to make a Link without the website_id and the post_id I believe you will get a error asking for both.
class WebsiteFactory extends Factory
{
public function definition(){...}
public function configure()
{
return $this->afterCreating( function (Website $website){
// the website model is created, hence after-creating
// attach Website to a new Post
$post = Post::factory()->hasAttached($website)->create();
// create new links to attach to both
$links = Link::factory()->for($website)->for($post)->count(3)->create();
});
You can leave PostFactory and LinkFactory as simple definition blocks (or add other stuff if you wanted). Now when you create a new Website via factory, it will create a new post and 3 new links. For example, you can now run
php artisan tinker
$w = Website::factory()->create(); // one website-one post-3 links
$ws = Website::factory()->count(5)->create(); // 5 website-5 post- 15 links
Check out the Factory Callbacks here (9.x docs, but they are in 8.x too):
https://laravel.com/docs/9.x/database-testing#factory-callbacks
\App\Models\Category::factory(10)
->has(Product::factory()->count(10), 'products')
->create();
It would be better if you play around with code. You will understand better.
$user = User::factory()
->has(Post::factory()->count(3), 'posts')
->create();
The above code will create three post for a single user. It will insert three post row and a user row. On the other hand the code below, seems three post will be inserted for user with name Jessica Aercher, that is it won't insert a user.
$posts = Post::factory()
->count(3)
->for(User::factory()->state([
'name' => 'Jessica Archer',
]))
->create();

Laravel Eloquent Model change table name on runtime in a related instance

I am new to Laravel and also asked the question on Laracast without any success so far.
Here is my problem: I have a database layout something like this:
Table: categoryA_products
Table: categoryB_products
Table: categoryC_products
and per default the Laravel user table:
Table: user
I have create a two Laravel Eloquent models:
Product:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
// protected $table = '';
public function users()
{
return $this->belongsTo( User::class );
}
}
User:
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function products()
{
return $this->hasMany( Product::class );
}
}
As each product has a different table name I would normally create 1 model for each table but as they are all similar I would like to define the model table name at runtime.
I know I can do this with "$product->setTable()" but as I use the "newRelatedInstance" class from Laravel (hasMany and belongsTo) I cannot initiate the product class and set the table.
Is there a workaround for this?
Yes, I am aware that I could create a category table and link the products to each category but this is a fictional database model. There is a reason for this approach and I can explain it more in detail if needed. That said it make sense for this sample but I cannot use it for the live database.
I have a working solution with a model for each "category" but this is very messy.
Any help would be appreciated.
Since you're unable to load the relations, you could try referencing and re-initializing them like:
$relations = $product->getEagerLoads();
$attributes = $product->getOriginal();
table_name = 'categoryA_products'; // or categoryB_products or categoryC_products
$product->newQuery()
->newModelInstance($attributes)
->setTable($table_name)
->setEagerLoads($relations)
->...

Laravel 5 Eloquent relationship: can't modify/overwrite relationship table property

I am using Laravel 5's belongsToMany method to define related tables using an intermediary pivot table. My application is using the eloquent models Tour and TourCategory. In the Tour Model I have:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tour extends Model
{
public function cats(){
return $this->belongsToMany('App\TourCategory', 'tour_cat_assignments', 'tour_id', 'cat_id');
}
}
In my controller I am retrieving all the data from the tour table along with the associated category data using Laravel's with method:
$tours = Tour::with('cats')->get();
That all works fine. The problem is that I don't want the category data in its current raw form, I need to first rearrange it. However I cannot overwrite the cats property without unsetting it first:
public function serveTourData(){
$tours = Tour::with('sections', 'cats')->get();
foreach($tours as $tour){
unset($tour->cats); // If I unset first, then it respects the new value. Why do I need to do this?
$tour->cats = "SOME NEW VALUE";
}
Log::info($tours);
}
Can someone explain the logic behind this please?
To override relations on some model, you can use:
public function serveTourData(){
$tours = Tour::with('sections', 'cats')->get();
foreach($tours as $tour){
$tour->setRelation('cats', "SOME NEW VALUE");
}
Log::info($tours);
}
For laravel 5.4 - setRelation
Of course if you are using laravel >= 5.6, you can unset relations by unsetRelation

How to soft delete related records when soft deleting a parent record in Laravel?

I have this invoices table that which has the following structure
id | name | amount | deleted_at
2 iMac 1500 | NULL
and a payments table with the following structure
id | invoice_id | amount | deleted_at
2 2 1000 | NULL
Invoice Model
class Invoice extends Model {
use SoftDeletes;
}
here's the code to delete the invoice
public function cance(Request $request,$id)
{
$record = Invoice::findOrFail($id);
$record->delete();
return response()->json([
'success' => 'OK',
]);
}
Payments model
class Payment extends Model {
use SoftDeletes;
}
The softDelete on Invoice table works perfectly but its related records (payments) still exists.How do I delete them using softDelete?
Eloquent doesn't provide automated deletion of related objects, therefore you'll need to write some code yourself. Luckily, it's pretty simple.
Eloquent models fire different events in different stages of model's life-cycle like creating, created, deleting, deleted etc. - you can read more about it here: http://laravel.com/docs/5.1/eloquent#events. What you need is a listener that will run when deleted event is fired - this listener should then delete all related objects.
You can register model listeners in your model's boot() method. The listener should iterate through all payments for the invoice being deleted and should delete them one by one. Bulk delete won't work here as it would execute SQL query directly bypassing model events.
This will do the trick:
class MyModel extends Model {
protected static function boot() {
parent::boot();
static::deleted(function ($invoice) {
$invoice->payments()->delete();
});
}
}
You can go one of 2 ways with this.
The simplest way would be to override Eloquents delete() method and include the related models as well e.g.:
public function delete()
{
$this->payments()->delete();
return parent::delete();
}
The above method should work just find but it seems a little bit dirty and I'd say it's not the preferred method within the community.
The cleaner way (IMO) would be to tap into Eloquents events e.g.:
public static function boot()
{
parent::boot();
static::deleting(function($invoice) {
$invoice->payments()->delete();
});
}
Either (but not both) of the above methods would go in your Invoice model.
Also, I'm assuming that you have your relationships set up in your model, however, I'm not sure if you allow multiple payments for one invoice. Either way you might need to change the payments() in the examples to whatever you've named the relationship in your invoice model.
Hope this helps!
I know you asked this question a long time ago but I found this package to be very simple and straightforward.
Or you can use this package it's useful too.
Remember to install the right version depending on your laravel version.
You must install it via composer:
composer require askedio/laravel5-soft-cascade ^version
In second package:
composer require iatstuti/laravel-cascade-soft-deletes
Register the service provider in your config/app.php.
you can read the docs on the GitHub page.
If you delete a record this package recognizes all of its children and soft-delete them as well.
If you have another relationship in your child model use its trait in that model as well. its so much easier than doing it manually.
The second package has the benefit of deleting grandchildren of the model. in some cases, I say its a better approach.
If the relationship of your database does not go any further than only one layer, then you could simply use Laravel events to handle your soft-deletes within the Model boot() method as follow:
<?php
//...
protected static boot() {
parent::boot();
static::deleting(function($invoice) {
$invoice->payments()->delete();
});
}
If, however, your structure goes deeper than only one layer, you will have to tweak that piece of code.
Let's say for example you don't want to remove the payments of an invoice but rather the whole payment history of a given user.
<?php
// ...
class Invoice extends Model
{
// ...
/**
* Holds the methods names of Eloquent Relations
* to fall on delete cascade or on restoring
*
* #var array
*/
protected static $relations_to_cascade = ['payments'];
protected static boot()
{
parent::boot();
static::deleting(function($resource) {
foreach (static::$relations_to_cascade as $relation) {
foreach ($resource->{$relation}()->get() as $item) {
$item->delete();
}
}
});
static::restoring(function($resource) {
foreach (static::$relations_to_cascade as $relation) {
foreach ($resource->{$relation}()->get() as $item) {
$item->withTrashed()->restore();
}
}
});
}
public function payments()
{
return $this->hasMany(Payment::class);
}
}
<?php
// ...
class User extends Model
{
// ...
/**
* Holds the methods names of Eloquent Relations
* to fall on delete cascade or on restoring
*
* #var array
*/
protected static $relations_to_cascade = ['invoices'];
protected static boot()
{
parent::boot();
static::deleting(function($resource) {
foreach (static::$relations_to_cascade as $relation) {
foreach ($resource->{$relation}()->get() as $item) {
$item->delete();
}
}
});
static::restoring(function($resource) {
foreach (static::$relations_to_cascade as $relation) {
foreach ($resource->{$relation}()->get() as $item) {
$item->withTrashed()->restore();
}
}
});
}
public function invoices()
{
return $this->hasMany(Invoice::class);
}
}
This paradigm ensures Laravel to follow the rabbit hole no matter how deep it goes.
You can use Model Observers as well:
php artisan make:Observer InvoiceOberser --model=Invoice
It will create a new file in /app/Observers/InvoiceObserver.php with the following methods:
created
updated
deleted
restored
forceDeleted
You just need to update the deleted method to this:
public function deleted(Invoice $invoice)
{
$invoice->payments()->delete();
}
And finally in /app/Providers/EventServiceProvider.php add this lines:
// On the top
use App\Models\Invoice;
use App\Observers\InvoiceObserver;
// On boot method
Invoice::observe(InvoiceObserver::class);

Laravel 5 mutators only work when I create a record and not when I update a record

Hi I have created a mutator to only store digits on my phone numbers. Here is my code in my Profile Model.
public function setPhoneAttribute($phone)
{
$this->attributes['phone'] = preg_replace("/[^0-9]/","",$phone);
}
This works when I create a new record, but if I update the record it does not work. My question is how do I execute the Mutator on both create and update?
Here is how I update and create in my controller:
namespace App\Http\Controllers;
use App\Http\Requests;
use App\Http\Requests\ProfileRequest;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Auth;
use App\Profile;
class ProfileController extends Controller {
public function create(ProfileRequest $request)
{
// Check if the user does not have a profile yet
if(!Auth::user()->profile()->first()){
// Save to database
$saveToDatabase = Auth::user()->profile()->create($request->all());
return $saveToDatabase;
}
}
public function update(Profile $profile, ProfileRequest $request)
{
// Save to database
$saveToDatabase = Auth::user()->profile()->update($request->all());
return $saveToDatabase;
}
}
Here's what's happening:
Auth::user()->profile()->create($request->all()) calls the create method on your relationship (HasOneOrMany). This method then creates a new instance of the related model. This is important because obviously attribute mutators are only used when the record is created through the model.
However the relationship object doesn't have any update method. (It also wouldn't make sense to have one...). So what's happening instead is, when you do Auth::user()->profile()->update($request->all()). The update call get's proxied off to a query builder instance (that matches the relationship). This results in something like this being executed:
UPDATE profiles SET foo = 'bar' WHERE [relationship conditions]
It doesn't use the model at all. Therefore the mutator doesn't work.
Instead you have to call the update method on the actual related model. You can access it by just calling the relation as a property like this:
$saveToDatabase = Auth::user()->profile->update($request->all());
// ^^
// no parentheses
If the Profile model is injected correctly you actually might also just use that though:
public function update(Profile $profile, ProfileRequest $request)
{
// Save to database
$saveToDatabase = $profile->update($request->all());
return $saveToDatabase;
}
Using this code instead of your code
$saveToDatabase = Auth::user()->profile->update($request->all());

Categories