My project is a multi-tenant, single database system for design Projects. Company tenants usually service external Companies, but I am also trying to allow for Company tenants who do not have client Companies (they only have internal/in-house Projects). I have been attempting to utilize a single Companies table, using a can_parent bool field to determine if they can add client Companies and a company_id field to indicate which company is the parent. I’m trying to be efficient, but I have a bad feeling I’m going to keep running into ownership issues with my current design.
Schema::create('companies', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->unsignedBigInteger('company_id')->nullable();
$table->boolean('can_parent')->default(1);
$table->boolean('active')->default(1);
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
$table->softDeletes();
$table->foreign('company_id')
->references('id')
->on('companies')
->onDelete('cascade');
});
I have a Company model and a Companies model (I’m adding to the Company booted method to pull in the client companies and allow the parent company to add/edit them. Works okay.
<?php
namespace App\Models;
...
class Company extends Model
{
protected $table = 'companies';
protected $fillable = [
'name',
'can_parent',
'company_id',
];
// company people
public function clients(): HasMany
{
return $this->hasMany(Client::class);
}
public function projects(): HasMany
{
return $this->hasMany(Project::class);
}
public function companies(): HasMany
{
return $this->hasMany(Companies::class);
}
protected static function booted(): void
{
static::addGlobalScope('owned', function ($query) {
$query->where('id', '=', auth()->user()->profile->company_id)
->orwhere('company_id', '=', auth()->user()->profile->company_id);
});
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Companies extends Model
{
protected $table = 'companies';
protected $fillable = [
'name',
'parent',
];
public function company(): BelongsTo
{
return $this->belongsTo(Company::class, 'company_id', 'id');
}
public function client(): BelongsToMany
{
return $this->belongsToMany(Client::class);
}
protected static function booted()
{
static::addGlobalScope('owned', function ($query) {
$query->where('id', '=', auth()->user()->profile->company_id)
->orWhere('company_id', '=', auth()->user()->profile->company_id);
});
}
}
Feels a little hacky, I’m pretty sure there’s a better Laravel way to make these models work that I’ll come upon, but I am successfully adding tenant Companies and their external Companies without error. Now, here’s where I am encountering the issue that is making me rethink my design. I have a Projects model which are owned by the external Companies via company_id, but need to be accessed/controlled by the tenant Company (the parent of the external Company). It doesn’t feel very efficient in the current design, and adding to the booted query currently disables me from editing the projects (since the company join returns the ‘id’ field for both the projects and the companies, the edit query is ambiguous).
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Str;
class Project extends Model
{
protected $fillable = [
'name',
'company_id',
'status',
'published'
];
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
protected static function booted(): void
{
static::creating(function (Project $project) {
static::addGlobalScope('owned', function ($query) {
$company = Company::find(auth()->user()->profile->company_id);
if ($company->can_parent) {
$query->join('companies', 'companies.id', '=', 'projects.company_id')
->selectRaw('projects.id, projects.name, companies.id AS whatever')
->where('companies.company_id', '=', $company->id);
} else {
$query->where('company_id', '=', $company->id);
}
});
}
}
I could add an additional field to the Projects table and call it companies_id, storing the tenant parent Company with each record, but that seems inefficient. That said, the current query above is no model of efficiency, either. Most queries will build-on/run-through the Company & Projects tables, so I’m sweating how I am currently going about this. Really would appreciate any advice on how I could improve my design.