how to require attaching related resources upon creation of resources - Laravel Nova - php

I have a model called Tree that is supposed to be associated to 1..n Things. Things can be associated to 0..n things. In other words this is a many-to-many relationship, and a Thing must be chosen when a Tree is being created. My thing_tree migration looks like this (there's also a thing_thing pivot table but that's irrelevant):
public function up()
{
Schema::create('thing_tree', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->unsignedBigInteger('tree_id')->nullable();
$table->unsignedBigInteger('thing_id')->nullable();
$table->unique(['tree_id', 'thing_id']);
$table->foreign('tree_id')->references('id')->on('trees')->onDelete('cascade');
$table->foreign('thing_id')->references('id')->on('things')->onDelete('cascade');
});
}
My Tree model looks like this:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Tree extends Model
{
use HasFactory;
protected $guarded = [];
public function path(){
$path = '/trees/' . $this->id;
return $path;
}
public function associatedThings () {
return $this->belongsToMany(Thing::class);
}
}
The Thing model looks like this:
public function trees()
{
return $this->belongsToMany(Tree::class);
}
public function parentOf (){
return $this->belongsToMany(Thing::class, 'thing_thing', 'parent_id', 'child_id');
}
public function childOf(){
return $this->belongsToMany(Thing::class, 'thing_thing', 'child_id', 'parent_id');
}
Finally, the Tree Nova resource has these fields:
public function fields(Request $request)
{
return [
ID::make(__('ID'), 'id')->sortable(),
Text::make('name'),
ID::make('user_id')->hideWhenUpdating()->hideWhenCreating(),
Boolean::make('public'),
BelongsToMany::make('Things', 'associatedThings')
];
}
It should not be possible to create a Tree without an attached Thing, but the creation screen looks like this:
How do I require this in Nova?

This is not possible through nova's default features. Here is how I would go about it with the least effort (you Might want to create a custom field for that yourself) - or at least how I solved a similar issue in the past:
1. Add the nova checkboxes field to your project
2. Add the field to your nova ressource :
// create an array( id => name) of things
$options = Things::all()->groupBy('id')->map(fn($e) => $e->name)->toArray();
// ...
// add checkboxes to your $fields
Checkboxes::make('Things', 'things_checkboxes')->options($options)
3. Add a validator that requires the things_checkboxes to be not empty
4. Add an observer php artisan make:observer CheckboxObserver that will sync the model's relations with the given id-array through the checkboxes and then remove the checkboxes field from the object (as it will throw a column not found otherwise), so something like this:
public function saving($tree)
{
// Note: In my case I would use the checkbox_relations method of the HasCheckboxes trait and loop over all checkbox relations to perform the following and get the respective array keys and relation names
$available_ids = array_unique($tree['things_checkboxes']);
// Attach new ones, remove old ones (Relation name in my case comes from the HasCheckboxes Trait)
$tree->things->sync($available_ids);
// Unset Checkboxes as the Key doesn't exist as column in the Table
unset($tree['things_checkboxes']);
return true;
}
5. Add the same thing in reverse for the retreived method in your observer if you want to keep using the checkboxes to handle relations. Otherwise, add ->hideWhenUpdating() to your checkbox field
I added a trait for that to easily attach the relations through checkboxes to a model:
trait HasCheckboxRelations
{
/**
* Boot the trait
*
* #return void
*/
public static function bootHasCheckboxRelations()
{
static::observe(CheckboxObserver::class);
}
/**
* Defines which relations should be display as checkboxes instead of
* #return CheckboxRelation[]
*/
public static function checkbox_relations()
{
return [];
}
}
And checkbox_relations holds an array of instances of class CheckboxRelation which again holds informations about the key name, the relation name and so on.
public function __construct(string $relationName, string $relatedClass, string $fieldName, bool $hasOverrides = false, string $relationType = null, array $_fields = [])
Also, I added a method attachCheckboxRelationFields to the default nova resource which will be called on the $fields when the model uses the trait.
Now, I only have to add HasCheckboxRelations to a model, add the array of checkbox_relations and thats it - I have a belongsToMany relation on the nova resource through checkboxes. Of course you don't have the option to manage pivot fields anymore if you go for it this way - which might be why it was not done by the nova devs - but for simple belongsToMany relations I really like to work with the checkbox solution instead of the default attach-table. And for data with pivot fields you can still use the default way.
Also note that parts of the code where written on the fly so it might not work out of the box, but the overall idea should be delivered.
Hope it helped!

alternative
https://github.com/Benjacho/belongs-to-many-field-nova
BelongsToManyField::make('Role Label', 'roles', 'App\Nova\Role'),

Related

I'm getting an error when trying to save a new model with 2 fields referencing different ids in the same table

I'm new to laravel, and I've picked up the basic workflow of creating, updating and deleting database entries using migrations, models and controllers. But now I'm trying to do the same with a subscriptions table that has a subscriberId and a followeeId in it. Both of these fields reference different ids of the same table (users). This kind of task seem to require some finetuning. And I'm stuck.
Here's my code with some comments.
Subscriptions Table
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('subscriberId');
$table->unsignedBigInteger('followeeId');
$table->foreign('subscriberId')->references('id')->on('users');
$table->foreign('followeeId')->references('id')->on('users');
});
Previously, I've used another approach to foreign ids, namely the one with the $table->foreignId('user_id')->constrained() pattern, but in this particular case I need to make sure that the two foreign ids reference different users, so I went for a more verbose option.
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
Here I've added the second parameter. This seems to work.
Subscription Model
class Subscription extends Model
{
use HasFactory;
protected $fillable = [
'subscriberId',
'followeeId'
];
public function subscriberId()
{
return $this->belongsTo(User::class, 'id', 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'id', 'followeeId');
}
}
Here I pass additional parameters, too, although in this case I'm not so sure if these are the correct ones. But this is my best guess. If I'm not mistaken, the second parameter of the belongsTo relation is inferred from the model that is being passed in, not the model of the parent class as is the case with the hasMany relation. So in this case that would be 'id' of the users table, which would be the default here anyway, but I need the third parameter, so I explicitly state the second parameter as well. Again, I'm not sure about this combination, but that's what I was able to make of the docs. I've also used other combinations of additional parameters, and even tried getting rid of these two public functions altogether, but that won't work either.
Now, here's the controller. If I do this:
$user->subscriptions()->get();
I do get the subscriptions I want. But if I do this instead:
$user->subscriptions()->create([
'subscriberId' => 1,
'followeeId' => 2
]);
I get the 500 error. I've also tried another approach:
$newSub = new Subscription;
$newSub->subscriberId = 1;
$newSub->followeeId = 2;
$newSub->save();
return $newSub;
But still no success. I still get the 500 error when I try to save()
Please help me out.
Solution
I should have used
public $timestamps = false
in the Subscription model, and I also misunderstood the docs. The correct combo is
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
and
Subscription Model
public function subscriberId()
{
return $this->belongsTo(User::class, 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'followeeId');
}

Extending the Spatie Role Model but use a different database table

What I need to do is extend all of the functionality of the Spatie permissions package Role model, but use a different table for the derived model.
Right now I have a model SubscriptionPackage that I want to emulate the behavior of a Role such that it can be assigned permissions and in turn this model can be assigned to users. But I wanna keep the Role model intact too.
I have tried extending Yes, but when I create a new SubscriptionPackage, the new record is created in the roles tables instead of subscription_packages table despite specifying the table in my derived Model. As shown below
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Models\Permission; // This extends from Spatie\Permission\Models\Permission
use Spatie\Permission\Models\Role as SpatieRole;
class SubscriptionPackage extends SpatieRole
{
//
protected $guarded = ['id'];
protected $table = 'subscription_packages';
/**
* The permissions that belong to the package.
*/
public function packagePermissions()
{
return $this->belongsToMany(Permission::class);
}
}
With the code above I expect when I create a new SubscriptionPackage, the record should be inserted into the subscription_packages table but in this case it goes to the roles table.
Any pointers on how to go about this will be highly appreciated.
If you have a look at the Role source code you will this inside the __construct method:
public function __construct(array $attributes = [])
{
$attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard');
parent::__construct($attributes);
$this->setTable(config('permission.table_names.roles')); // <-- HERE IS THE PROBLEM!
}
So, if you want that your SubscriptionPackage to write its records in the right table you have to override this behaviour like this:
public function __construct(array $attributes = [])
{
parent::__construct($attributes)
$this->setTable('your_table_name'); // <-- HERE THE SOLUTION!
}
I don't think you can. Spatie already have 5 tables and fetched data from those only. But still if you want to make the change you have make the changes with table and column name in the model

Polymorphic relationshoip in Laravel

I'm trying to understand polymorphic relationship in Laravel. I know how it works in principle, but the choice of wording in Laravel is not intuitive in this part. Given the exanple,
namespace App;
use Illuminate\Database\Eloquent\Model;
class Like extends Model
{
/**
* Get all of the owning likeable models.
*/
public function likeable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* Get all of the product's likes.
*/
public function likes()
{
return $this->morphMany('App\Like', 'likeable');
}
}
class Comment extends Model
{
/**
* Get all of the comment's likes.
*/
public function likes()
{
return $this->morphMany('App\Like', 'likeable');
}
}
How do yo put in plain English sentence morphTo for instance? It is "belongsto"? and morphmany, hasMany? going further,
$post = App\Post::find(1);
foreach ($post->likes as $like) {
//
}
$likeable = $like->likeable;
morphToMany and morphByMany
How do you describe in plain english?
A polymorphic relationship means an object can have a relationship to more than one type of object. This is determined by two fields in the database rather the typical one foreign key field you would normally see.
Using the code you included in your question any type of object extending the Model class can have a relationship with a Like object. So you could have Comments and Posts that can have Likes associated to them. In your likes table you may have rows where 'likable_type' = 'post' and 'likable_id' = 1 or 'likable_type' = 'comment' and 'likable_id' = 4 for example.

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 Removing Pivot data in many to many relationship

Not sure if I set this up correctly. In Laravel I'm creating two models with a many-to-may relationship
The models are Item and Tags. Each one contains a belongsTo to the other.
When I run a query like so:
Item::with('tags')->get();
It returns the collection of items, with each item containing a tags collection. However the each tag in the collection also contains pivot data which I don't need. Here it is in json format:
[{
"id":"49",
"slug":"test",
"order":"0","tags":[
{"id":"3","name":"Blah","pivot":{"item_id":"49","tag_id":"3"}},
{"id":"13","name":"Moo","pivot":{"item_id":"49","tag_id":"13"}}
]
}]
Is there anyway to prevent this data from getting at
you can just add the name of the field in the hidden part in your model like this:
protected $hidden = ['pivot'];
that's it , it works fine with me.
You have asked and you shall receive your answer. But first a few words to sum up the comment section. I personally don't know why you would want / need to do this. I understand if you want to hide it from the output but not selecting it from the DB really has no real benefit. Sure, less data will be transferred and the DB server has a tiny tiny bit less work to do, but you won't notice that in any way.
However it is possible. It's not very pretty though, since you have to override the belongsToMany class.
First, the new relation class:
class BelongsToManyPivotless extends BelongsToMany {
/**
* Hydrate the pivot table relationship on the models.
*
* #param array $models
* #return void
*/
protected function hydratePivotRelation(array $models)
{
// do nothing
}
/**
* Get the pivot columns for the relation.
*
* #return array
*/
protected function getAliasedPivotColumns()
{
return array();
}
}
As you can see this class is overriding two methods. hydratePivotRelation would normally create the pivot model and fill it with data. getAliasedPivotColumns would return an array of all columns to select from the pivot table.
Now we need to get this integrated into our model. I suggest you use a BaseModel class for this but it also works in the model directly.
class BaseModel extends Eloquent {
public function belongsToManyPivotless($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null){
if (is_null($relation))
{
$relation = $this->getBelongsToManyCaller();
}
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$otherKey = $otherKey ?: $instance->getForeignKey();
if (is_null($table))
{
$table = $this->joiningTable($related);
}
$query = $instance->newQuery();
return new BelongsToManyPivotless($query, $this, $table, $foreignKey, $otherKey, $relation);
}
}
I edited the comments out for brevity but otherwise the method is just like belongsToMany from Illuminate\Database\Eloquent\Model. Of course except the relation class that gets created. Here we use our own BelongsToManyPivotless.
And finally, this is how you use it:
class Item extends BaseModel {
public function tags(){
return $this->belongsToManyPivotless('Tag');
}
}
If you want to remove pivot data then you can use as protected $hidden = ['pivot']; #Amine_Dev suggested, so i have used it but it was not working for me,
but the problem really was that i was using it in wrong model so i want to give more detail in it that where to use it, so you guys don't struggle with the problem which i have struggled.
So if you are fetching the data as :
Item::with('tags')->get();
then you have to assign pivot to hidden array like below
But keep in mind that you have to define it in Tag model not in Item model
class Tag extends Model {
protected $hidden = ['pivot'];
}
Two possible ways to do this
1. using makeHidden method on resulting model
$items = Item::with('tags')->get();
return $items->makeHidden(['pivot_col1', 'pivot_col2']...)
2. using array_column function of PHP
$items = Item::with('tags')->get()->toArray();
return array_column($items, 'tags');

Categories