Docs›Banco de dados›Global scopes
Banco de dados

Global scopes

addGlobalScope, withoutGlobalScopes.

Global scopes aplicam filtros automaticamente em todas as queries de um model — mecanismo nativo do Eloquent (addGlobalScope). O MAD usa isso por baixo dos panos em três traits prontos: soft delete (HasMadSoftDeletes), isolamento multi-tenant (BelongsToTenant) e isolamento por unidade/filial (BelongsToUnit).

Declarando

class Produto extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope('ativo', function (Builder $builder) {
            $builder->where('ativo', true);
        });
    }
}

Agora toda query de Produto já inclui WHERE ativo = 1 automaticamente:

$items = Produto::all();
// → SELECT * FROM produto WHERE ativo = 1

$items = Produto::where('valor', '>=', 100)->get();
// → SELECT * FROM produto WHERE valor >= 100 AND ativo = 1

Exemplo real do framework — soft delete

Mad\Database\Concerns\HasMadSoftDeletes registra o scope madSoftDelete, que só filtra quando o model tem coluna de deleção configurada:

public static function bootHasMadSoftDeletes(): void
{
    static::addGlobalScope('madSoftDelete', function ($builder) {
        $col = $builder->getModel()->getDeletedAtColumn();
        if ($col) {
            $builder->getQuery()->whereNull($col);
        }
    });
}

Exemplo real do framework — isolamento multi-tenant

Mad\Database\Concerns\BelongsToTenant registra o scope mad_tenant atrás de uma feature flag (mad.tenant.row_scope_enabled):

public static function bootBelongsToTenant(): void
{
    static::addGlobalScope('mad_tenant', function (Builder $builder): void {
        if (! config('mad.tenant.row_scope_enabled')) {
            return; // flag OFF => sem filtro
        }
        $tid = TenantContext::id();
        if ($tid === null) {
            return; // sem tenant corrente => sem filtro
        }
        $builder->where($builder->getModel()->getTable() . '.tenant_id', $tid);
    });
}

Repare no padrão: o scope sempre checa as próprias precondições (flag, contexto) antes de filtrar — nunca assume que o ambiente vai ter o que ele precisa.

O mesmo trait também stampa tenant_id no evento creating quando a linha não trouxer valor — leitura e escrita ficam no mesmo eixo.

Exemplo real do framework — isolamento por unidade (filial)

Mad\Database\Concerns\BelongsToUnit é o espelho do anterior, mas o eixo é a filial dentro do mesmo cliente: scope mad_unit filtrando unit_id por UnitContext::id(), mais o stamp no creating. A flag global é mad.general.multiunit ('1' = ON; default '0' = sem filtro):

// Flag POR MODEL (opcional): a específica vence quando setada ('1'/'0');
// unset/'' herda a global mad.general.multiunit.
class Document extends Model
{
    use BelongsToUnit;

    protected static function unitScopeFlagKey(): ?string
    {
        return 'mad.ged.multiunit';
    }
}

Isso permite ter o sistema inteiro per-unidade e um módulo nível-empresa (ou o inverso). BelongsToUnit e BelongsToTenant são combináveis: tenant = empresa, unit = filial dentro da empresa. Ambos valem só em models data-plane — control-plane (iam/log) nunca usa.

Desabilitar scope

// Desabilita um scope específico
$items = Produto::withoutGlobalScope('ativo')->get();

// Desabilita vários, por nome
$items = Produto::withoutGlobalScopes(['ativo', 'madSoftDelete'])->get();

// Desabilita TODOS os scopes
$items = Produto::withoutGlobalScopes()->get();
// Cenário comum: painel admin que precisa ver registros inativos
public function onListarTodos(): void
{
    $todos = Produto::withoutGlobalScope('ativo')
        ->orderBy('nome')
        ->get();
}

Scope vs filtro manual

CenárioAbordagem
Filtro de negócio (ativo, publicado)addGlobalScope — economiza repetição
Multi-tenant (tenant_id)addGlobalScope via BelongsToTenant — segurança em camada
Multi-unidade / filial (unit_id)addGlobalScope via BelongsToUnit — flag mad.general.multiunit
Filtro de UI (busca, categoria)where() manual — varia por página
Soft delete (deleted_at)HasMadSoftDeletes — já automático

API

MétodoDescrição
addGlobalScope($name, $closure)Adiciona scope ao model (estático, em booted())
withoutGlobalScope($name)Desabilita um scope nomeado
withoutGlobalScopes($names = null)Desabilita vários (array) ou todos (sem args)
getGlobalScopes()Lista scopes ativos do model

NUNCA fazer

Não confie em scope para autorização sensível. Qualquer scope pode ser desabilitado com withoutGlobalScopes() em outra parte do código. Para regras críticas (financeiro, dados sensíveis), valide TAMBÉM no controller.

// ❌ Quebra em CLI/job — session() pode não estar disponível
static::addGlobalScope('tenant', function (Builder $builder) {
    $builder->where('empresa_id', session('empresa_id')); // pode lançar em contexto sem session
});

// ✅ Verifica antes de aplicar (mesmo padrão do BelongsToTenant real)
static::addGlobalScope('tenant', function (Builder $builder) {
    if (!app()->bound('session')) {
        return;
    }
    $emp = session('empresa_id');
    if ($emp !== null) {
        $builder->where('empresa_id', $emp);
    }
});

Próximos