<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
if ($tenantId = auth()->user()?->tenant_id) {
$builder->where($model->getTable() . '.tenant_id', $tenantId);
}
}
}
<?php
namespace App\Models;
use App\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected static function booted(): void
{
// Apply tenant scope to all queries
static::addGlobalScope(new TenantScope());
// Automatically set tenant_id on creation
static::creating(function ($post) {
if (!$post->tenant_id && auth()->user()) {
$post->tenant_id = auth()->user()->tenant_id;
}
});
}
// Remove global scope when needed
public function scopeAllTenants($query)
{
return $query->withoutGlobalScope(TenantScope::class);
}
}
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// Create view for post statistics
DB::statement("
CREATE VIEW post_stats AS
SELECT
posts.id,
posts.title,
posts.user_id,
COUNT(DISTINCT comments.id) as comments_count,
COUNT(DISTINCT likes.id) as likes_count,
AVG(ratings.score) as avg_rating
FROM posts
LEFT JOIN comments ON posts.id = comments.post_id
LEFT JOIN likes ON posts.id = likes.post_id
LEFT JOIN ratings ON posts.id = ratings.post_id
GROUP BY posts.id, posts.title, posts.user_id
");
}
public function down(): void
{
DB::statement('DROP VIEW IF EXISTS post_stats');
}
};
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PostStat extends Model
{
protected $table = 'post_stats'; // Reference the view
public $timestamps = false; // Views don't have timestamps
protected $casts = [
'avg_rating' => 'float',
];
// Read-only model for view
public function save(array $options = [])
{
throw new \Exception('Cannot save to view');
}
public function delete()
{
throw new \Exception('Cannot delete from view');
}
// Relationships
public function post()
{
return $this->belongsTo(Post::class);
}
}
Combining global scopes with database views creates powerful data access patterns for multi-tenancy and security. Global scopes automatically filter all queries for a model—perfect for tenant isolation or active record filtering. I implement the Scope interface with an apply() method. Database views present filtered/joined data as virtual tables. Laravel models can query views like regular tables. For complex reporting, views aggregate data efficiently. Materialized views cache expensive computations. The combination enables row-level security—users only see their data. I use views for legacy database integration, exposing normalized structures. This architecture separates data access concerns from business logic.