mohmmadshabrani / laravel-select-except
Omit columns from Laravel Eloquent and query builder SELECT statements at the SQL level.
Package info
github.com/Mohmmadshabrani/laravel-select-except
pkg:composer/mohmmadshabrani/laravel-select-except
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
README
Omit columns from Laravel SELECT statements at the SQL level.
$hidden still fetches the data and only strips it when serializing. This package never selects those columns — the same idea as SQL SELECT * EXCEPT (...). Use it for passwords, tokens, and heavy text/json fields on index queries.
User::selectExcept('password', 'remember_token')->get(); Post::selectExcept('body')->paginate();
Works on every Eloquent query after install. No model changes required.
Install
composer require mohmmadshabrani/laravel-select-except
Laravel auto-discovers the service provider. Publish the config only if you want to change cache or error behavior:
php artisan vendor:publish --tag=select-except-config
Usage
One-off
Use anywhere you have an Eloquent builder — models, relations, eager loads, API index endpoints:
User::selectExcept('password', 'remember_token')->get(); User::selectExcept(['password', 'remember_token'])->first(); // Already selected columns are filtered, not replaced User::select('id', 'name', 'email', 'password') ->selectExcept('password') ->get(); // SELECT id, name, email // Joins stay unambiguous: columns are qualified (`users.email`) User::selectExcept('password') ->join('posts', 'posts.user_id', '=', 'users.id') ->get(); User::with([ 'posts' => fn ($posts) => $posts->selectExcept('body', 'html'), ])->get(); $user->posts()->selectExcept('body')->paginate();
Skip $hidden columns for read/API queries (do not do this on login — you still need password to verify the hash):
User::selectExceptHidden()->get();
Query builder
DB::table('users')->selectExcept('password')->get(); DB::table('users as u')->selectExcept('password')->get();
Default omissions on a model
For models with bulky columns, put the trait on the model and list columns once. List endpoints stay cheap; detail endpoints opt back in.
use Shabrani\SelectExcept\HasSelectExcept; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasSelectExcept; protected array $selectExcept = [ 'body', 'html', 'search_index', ]; }
Post::paginate(); // body/html/search_index not selected Post::withAllColumns()->find($id); // full row for an edit/show page Post::including('body')->find($id); // body back, html + search_index still out Post::including('body', 'html')->find($id);
Recommended: add the trait to your app base model so selectExcept(), withAllColumns(), and including() autocomplete everywhere, and subclasses can set $selectExcept when needed.
namespace App\Models; use Illuminate\Database\Eloquent\Model as BaseModel; use Shabrani\SelectExcept\HasSelectExcept; abstract class Model extends BaseModel { use HasSelectExcept; }
Typical API controller
class PostController { public function index() { return Post::query() ->latest() ->paginate(); } public function show(Post $post) { return Post::including('body', 'html')->findOrFail($post->id); } }
Combine with Laravel when
Post::query() ->when($request->boolean('lite'), fn ($query) => $query->selectExcept('body', 'html')) ->get();
Why not $hidden?
$hidden |
selectExcept() |
|
|---|---|---|
| Still selected from MySQL/Postgres | Yes | No |
Present on getAttributes() / toJson() internals |
Yes | No |
| Helps list-query performance (skips large TEXT/JSON) | No | Yes |
| Stops accidental logging of secrets loaded on the model | No | Yes |
Use both: $hidden for serialization safety, selectExcept() / $selectExcept so the database never sends the bytes.
Caching
Column listings are cached so selectExcept() does not describe the table on every query.
- Cache is forever by default (
ttlnull) - Flushed automatically after migrations (
MigrationsEnded,DatabaseRefreshed) - Connection + database + table are part of the key
php artisan select-except:clear
SELECT_EXCEPT_CACHE=true SELECT_EXCEPT_CACHE_STORE=redis SELECT_EXCEPT_CACHE_TTL=
Set ttl to seconds if you prefer a sliding refresh instead of forever.
Config
// config/select-except.php 'throw_when_empty' => true,
If an omit list would leave zero columns, the package throws EmptySelectionException instead of falling back to SELECT * (which would leak the columns you tried to hide). Set this to false to leave the query unchanged.
Notes
- Unknown column names are ignored.
- Raw expressions (
selectRaw,DB::raw) are never stripped. selectExcept('users.password')andselectExcept('password')are the same.- Do not omit the primary key if you still need
chunk(),cursor(), or route-model binding on the result. - Login/register queries that hash-check
passwordmust not omit it. $model->passwordafterselectExcept('password')isnull(or an exception if you enableModel::preventAccessingMissingAttributes()). That is expected: it was never selected.
Testing this package
composer install
composer test
Credits
The core idea — omit columns with array_diff against the table schema, instead of a hardcoded $columns list — comes from Stack Overflow:
- Razor (accepted answer) showed
scopeExclude()witharray_diff($this->columns, …). That works, but every model has to keep$columnsin sync with the table. - ManojKiran skipped
$columnsentirely:getColumnListing()plus a cachedgetTableColumns(). That is the approach this package packages up.
This package keeps that schema-based listing and adds qualified columns, query-builder support, migration-aware cache flushing, and $selectExcept only as an optional default omit list (not a full column inventory).
License
MIT