In a Laravel application, settings are often conveniently represented by dedicated Settings classes. They provide typed access, defaults, casts, and a clear application-facing API.
But sometimes the requirement is different: an admin page only needs to show what is actually stored in the settings table.
If several groups share that table and the page is read-only, loading every Settings class can be unnecessary work.
In that case, a direct query may be the most precise read model for that screen rather than a shortcut around the application.
The Problem Appears When the Screen Does Not Need Settings Behavior
Imagine a system with several groups of settings: profile, notifications, mail, integrations, and others.
Each group has its own Settings class, while the values are physically stored in one table.
If an admin page only needs a list of groups, names, and stored values, it does not necessarily need to instantiate every class just to flatten those objects back into a table.
$rows = DB::table('settings') ->select([ 'group', 'name', 'payload', 'locked', ]) ->orderBy('group') ->orderBy('name') ->get();This query answers exactly the question the page is asking: which rows currently exist in storage.
It does not try to reproduce settings behavior, apply defaults, or pretend that the page is editing those values.
Distinguish Stored State From Resolved Values
A Settings class may return a value after casts, defaults, or other package behavior has been applied.
A direct table query shows what is actually stored.
Those are different questions. Application code usually wants the resolved value. A diagnostic or inspection page may instead need the stored record itself.
Application code: "What value should the application use?" Read-only admin page: "What is actually stored right now?"Several Groups in One Table Suggest a Simpler Read Path
When all groups are stored in one table, the schema itself already provides a useful projection for an overview page.
The page can group rows by group, order by name, and display them without knowing the PHP class for every group.
This is especially useful when new groups may appear later: the page sees the actual stored rows and does not need a separate registry of every Settings class.
$settings = DB::table('settings') ->select([ 'group', 'name', 'payload', 'locked', ]) ->orderBy('group') ->orderBy('name') ->get() ->groupBy('group');The Key Boundary Is Truly Read-Only Behavior
A direct query becomes dangerous when the same code also starts writing settings.
That can bypass casts, validation, caching, events, or other guarantees provided by the package.
The boundary is simple: direct reads can be appropriate for diagnostics or inspection when intentional; writes should continue through the official settings mechanism.
// Read-only inspection: acceptable.$rows = DB::table('settings') ->orderBy('group') ->orderBy('name') ->get(); // Write path: use the Settings class.$settings = app(NotificationSettings::class);$settings->email_enabled = false;$settings->save();Do Not Instantiate Objects Just Because They Exist
An abstraction is useful when the page needs the behavior it provides.
If the screen only needs four columns from one table, instantiating many objects may add no value.
This does not make Settings classes bad. It means this particular screen has a different job.
This Is Not Only About Performance
Yes, a direct query may be cheaper than loading many classes. But performance is not the main argument.
The stronger reason is matching the query to the task.
A page that inspects stored settings should read stored settings. If one table is enough, extra object construction may only hide the simplicity of the requirement.
When Direct Table Reads Are the Wrong Choice
There are several cases where the Settings classes remain the better boundary:
- the page needs already transformed values;
- it needs defaults that are not stored in the table;
- it depends on group-specific behavior;
- it edits settings;
- the package provides important caching or other guarantees;
- the storage format is an implementation detail the page should not know.
A Direct Query Does Not Mean Dumping Raw Data Into the UI
Even in read-only mode, the page may still need a presentation layer: group labels, value formatting, sensitive-value masking, or a clear display of the locked state.
Direct table access simplifies retrieval, but it does not remove presentation responsibilities.
Treat Sensitive Settings Separately
A settings table may contain values that should not be displayed in plain form even to an administrator.
Read-only does not mean "show everything." Before rendering, decide which values may be shown, which must be masked, and which must not be returned at all.
This boundary is more important than the convenience of a direct query.
What to Test
1. The page shows all required groups from the table.2. A new settings row appears without registering another class in the page.3. The page does not mutate data.4. Sensitive values are masked or hidden.5. Ordering by group and name is stable.6. A missing row is not silently replaced with an unexpected default.7. locked is displayed as state, not modified by the page.8. The write path still uses Settings classes.For a Read-Only Page, the Simplest Read Path Can Be the Most Accurate
Settings classes remain a good boundary for code that consumes or changes settings.
But an internal page that only needs to display the actual state of one table has a different requirement.
In that case, a direct query can be simpler, clearer, and less coupled to the list of concrete classes.
The important part is not to extend this read exception into writes. Reading stored state directly can be appropriate. Changing settings should still go through the mechanism responsible for their rules.
Not every read needs the full behavior of the model when the screen only needs the actual stored state.



