In a hasMany
relationship, what should happen with the children's record if you delete the parent record? I will show you three ways how to deal with that situation.
Here are our options:
First, the project setup and the problem itself.
In the project, we have two Models: Company and Contact. A contact belongs to a company.
database/migrations/xxx_create_companies_table.php:
Schema::create('companies', function (Blueprint $table) { $table->id(); $table->string('company_name'); $table->string('company_address'); $table->string('company_website'); $table->string('company_email'); $table->timestamps();});
database/migrations/xxx_create_contacts_table.php:
Schema::create('contacts', function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained(); $table->string('contact_name'); $table->string('contact_last_name'); $table->string('contact_phone_1'); $table->string('contact_phone_2'); $table->string('contact_email'); $table->string('contact_skype'); $table->string('contact_address'); $table->timestamps();});
To delete a company in the Controller, simply call the delete()
method on the Company
Model, which comes from the Route Model Binding.
use App\Models\Company;use Illuminate\Http\RedirectResponse; class ContactCompanyController extends Controller{ // ... public function destroy(Company $contactCompany): RedirectResponse { $contactCompany->delete(); return redirect()->back(); }}
When the delete method is executed without any additional checks, the Integrity constraint violation
error is shown.
Now, what are our options to solve it?
Let's validate if the company has children of contacts. If it does, do not allow deleting them. There are a few ways to do that, but the most straightforward is adding an if statement in the destroy()
method.
use App\Models\Company;use Illuminate\Http\RedirectResponse;use Illuminate\Validation\ValidationException; class ContactCompanyController extends Controller{ public function destroy(Company $company): RedirectResponse { if ($company->contacts()->exists()) { throw ValidationException::withMessages([ 'contacts_exists' => 'Cannot delete, company has contact records.', ]); } $company->delete(); return redirect()->back(); }}
Now, if you try to delete, you should see the validation message.
What if we know upfront that some company has contacts? Maybe then we just shouldn't show the delete button for that company?
In the Controller, we eagerly load the contacts.
use App\Models\Company;use Illuminate\Contracts\View\View; class ContactCompanyController extends Controller{ public function index(): View { $companies = Company::with('contacts')->get(); return view('companies.index', compact('companies')); } // ...}
And then, in the View, we do an if check to show a delete button.
// ...@if(empty($company->contacts()->count())) <form action="{{ route('companies.destroy', $company) }}" method="POST"> @csrf @method('DELETE') <x-danger-button> Delete </x-danger-button> </form>@endif// ...
Now, the delete button is only shown for records that don't have contacts.
But I wonder if this method is suitable.
First, you still need to validate that on the back-end because you cannot trust front-end validation at any time.
It also adds performance issues: in the Controller, you must load more relationships for all the records. But what if there are more records inside that company? Transactions, employees, documents, whatever, you would have to check all of them while loading the table.
And 99% of the users won't ever need to delete anything. So you are loading a lot of data for 1% of the customers. So, instead of the front-end validation, only doing the back-end validation is enough.
You can specify the onDelete()
action when creating foreign keys. By default in MySQL, the value is NO ACTION
, which acts like RESTRICT
, meaning it would show an SQL error when trying to delete the parent record.
Other possible values are CASCADE
and SET NULL
. The constrained migration looks like this:
database/migrations/xxx_create_contacts_table.php:
Schema::create('contacts', function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained(); $table->string('contact_name'); $table->string('contact_last_name'); $table->string('contact_phone_1'); $table->string('contact_phone_2'); $table->string('contact_email'); $table->string('contact_skype'); $table->string('contact_address'); $table->timestamps();});
Let's try both CASCADE
and SET NULL
in action.
database/migrations/xxx_create_contacts_table.php:
Schema::create('contacts', function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained()->onDelete('cascade'); $table->string('contact_name'); $table->string('contact_last_name'); $table->string('contact_phone_1'); $table->string('contact_phone_2'); $table->string('contact_email'); $table->string('contact_skype'); $table->string('contact_address'); $table->timestamps();});
With this rule, after deleting the company, all the contacts associated with it are also automatically deleted. There is no need to add any code for this in Laravel Controller.
Another option is SET NULL
. When setting a foreign key to null, it must first be set to nullable.
database/migrations/xxx_create_contacts_table.php:
Schema::create('contacts', function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->nullable()->constrained()->onDelete('SET NULL'); $table->string('contact_name'); $table->string('contact_last_name'); $table->string('contact_phone_1'); $table->string('contact_phone_2'); $table->string('contact_email'); $table->string('contact_skype'); $table->string('contact_address'); $table->timestamps();});
After deleting the company, all the records in the contacts
table that were associated with it were set to NULL.
Another popular way of dealing with all of that is to use SoftDeletes, which are easy to add in Laravel.
database/migrations/xxx_create_companies_table.php:
Schema::create('companies', function (Blueprint $table) { $table->id(); $table->string('company_name'); $table->string('company_address'); $table->string('company_website'); $table->string('company_email'); $table->timestamps(); $table->softDeletes(); });
database/migrations/xxx_create_contacts_table.php:
Schema::create('contacts', function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->nullable()->constrained()->onDelete('SET NULL'); $table->string('contact_name'); $table->string('contact_last_name'); $table->string('contact_phone_1'); $table->string('contact_phone_2'); $table->string('contact_email'); $table->string('contact_skype'); $table->string('contact_address'); $table->timestamps(); $table->softDeletes(); });
app/Models/Company.php:
use Illuminate\Database\Eloquent\SoftDeletes; class Company extends Model{ use HasFactory; use SoftDeletes; // ...}
app/Models/Contact.php:
use Illuminate\Database\Eloquent\SoftDeletes; class Contact extends Model{ use HasFactory; use SoftDeletes; // ...}
When we delete a company now, it gets deleted, but no changes are made in the contacts
table. We didn't tell DB to delete the companies
record from the database. Laravel only marks it as deleted, and child records are not triggered by it.
If you want to cascade the child records with the soft deletes, you can do that with a package like michaeldyrynda/laravel-cascade-soft-deletes.