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).
BelongsToUniteBelongsToTenantsã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ário | Abordagem |
|---|---|
| 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étodo | Descriçã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
- Soft delete — withTrashed, restore.
- Filtros de query — como filtros manuais são montados.
- Callbacks — eventos do ciclo de vida do model.
- Models Eloquent — API base do model.