Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Admin Listing Laravel Package

savannabits/admin-listing

savannabits/admin-listing is a Laravel package for quickly building admin listing pages. Declare columns, search, filters, sorting, and row actions in one configurable class, then get paginated, Blade-rendered tables out of the box. Ideal for CRUD backends and internal tools.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, pin, and prove it boots before you write any listing code.

composer require savannabits/admin-listing
git diff composer.lock   # commit this delta as its own commit

This is a v7.0.1 line last released 2020-05-15. The 7 overwhelmingly likely tracks Laravel 7, not the package's own major. So your very first task is not coding — it is opening the package's composer.json and reading the php and illuminate/* constraints:

cat vendor/savannabits/admin-listing/composer.json
php artisan about   # confirm your Laravel/PHP baseline

If you are on Laravel 8+, a package of this vintage typically fails at container-boot time (service provider referencing a removed contract, a Blade directive that no longer exists) rather than install time. Prove it boots on a scratch branch with php artisan serve and a single admin route before committing to it.

Read the source in this order — the source is the documentation. With 0 stars and no linked docs, there is no contract other than the code:

  1. The service provider (src/*ServiceProvider.php) — what it binds, whether it auto-discovers, which publish tags it registers, and whether it merges a config.
  2. The base listing class — this is the class you subclass. Its constructor, the query()/columns()/actions()-style hooks, and the property names are your entire API surface. Skim it top to bottom once; it's small.
  3. The Blade views — because the rendered markup is what your admin layout must accommodate. Check whether pagination is Bootstrap 4 or Tailwind, and whether any JS (jQuery?) is assumed.

Publish everything publishable immediately.

php artisan vendor:publish --provider="Savannabits\AdminListing\AdminListingServiceProvider"

Publish config and views even if you don't customize them yet. Committing them turns future package upgrades into a reviewable diff instead of a black box — which matters far more here than usual, given you'll likely be maintaining this code yourself eventually.

First use case: one read-only listing, zero row actions. Pick User — it exercises search, sort, and date formatting without any relationship or authorization complexity. Define columns for name, email, created_at; mark name and email searchable; mark created_at sortable; wire it into an index() action inside your existing admin middleware group. That single screen validates routing, layout integration, pagination rendering, and search/sort round-tripping in about an hour. Do not start with delete flows or custom column renderers.

Implementation Patterns

One listing class per model, resolved from a thin controller. The controller should do three things: resolve the listing, hand it the request, return the view. Every column, filter, and action decision lives in the listing class so it's reusable across a full CRUD screen and a dashboard widget, and testable without HTTP.

// app/Admin/Listings/UserListing.php
class UserListing extends BaseListing
{
    protected function query()
    {
        return User::query()->with(['team', 'roles']);
    }
}
// Controller — thin on purpose
public function index(UserListing $listing)
{
    return view('admin.users.index', [
        'listing' => $listing->forRequest(request()),
    ]);
}

Eager-load in the query hook, never in the controller. Relationship columns and row actions are exactly where admin listings fall over at scale. Putting with([...]) in the listing's query hook means every consumer — full index, dashboard widget, export — inherits the fix.

Compose a shared abstract base listing. Define App\Admin\Listings\BaseListing extending the package class, with the columns and actions every screen needs (id, created_at formatted consistently, a standard delete action). Then per-model listings extend that. This is the single highest-leverage pattern with this package: it stops you re-declaring date formatting and delete semantics fifteen times, and it gives you one place to change when someone asks for a different timestamp format.

Whitelist both searchable and sortable columns explicitly. Map the public request key to a real DB column and reject anything not in the map. Never let a request parameter flow into orderBy() or a LIKE clause directly.

protected array $sortable = [
    'name'       => 'users.name',
    'email'      => 'users.email',
    'created_at' => 'users.created_at',
];

// in the query hook or a sort hook
$key = request('sort');
if ($key && isset($this->sortable[$key])) {
    $query->orderBy($this->sortable[$key], request('direction') === 'desc' ? 'desc' : 'asc');
}

Clamp the per-page value to an allowed set. If your listing accepts per_page from the request, restrict it to [10, 25, 50, 100] inside the listing class. An unbounded per_page is the cheapest way for someone to accidentally run a SELECT * over your largest table.

Custom column rendering via Blade partials, not string concatenation. Status badges, linked foreign keys, truncated text — render a small partial from the column definition. It keeps escaping decisions visible in the template layer and makes the column reusable across listings.

// column definition
['key' => 'status', 'view' => 'admin.columns.status-badge']
{{-- resources/views/admin/columns/status-badge.blade.php --}}
<span class="badge badge-{{ $row->status === 'active' ? 'success' : 'secondary' }}">
    {{ $row->status }}
</span>

Row actions belong to policies, not the listing. Render edit/delete buttons conditionally with @can, but re-check authorization in the destroy controller. A listing that hides a button is a UI affordance, not a security boundary. For delete, always a POST form with @method('DELETE') and @csrf — never a GET link, which is both a CSRF hole and a prefetcher/crawler hazard.

@can('delete', $row)
<form method="POST" action="{{ route('admin.users.destroy', $row) }}" class="d-inline">
    @csrf @method('DELETE')
    <button class="btn btn-sm btn-danger" onclick="return confirm('Delete?')">Delete</button>
</form>
@endcan

Test at the route level. A feature test hitting the admin index route — asserting a known row appears, that a search term narrows the set, and that an unauthorized user gets 403 — catches more real regressions than unit-testing the listing class in isolation. Add one test per listing; they're cheap and they catch sort-whitelist regressions immediately.

Gotchas and Tips

Assume the package is effectively unmaintained — decide the exit path now. Last release 2020, zero stars, zero dependents, no upgrade guides, no changelog beyond "Dependencies Updated." Pin the exact version, treat the lockfile as your rollback, and answer the question before you build: if you hit a bug upstream won't fix, do you vendor the source into app/Admin/Listing/ or migrate to Filament / Backpack / Orchid? Discovering this constraint during an urgent admin feature is the expensive version of the same decision. Honestly: for anything beyond a prototype, the maintained alternative wins on TCO.

Read the transitive dependency diff, not the headline version. "Dependencies Updated" can mask a shared library moving a minor version — exactly the class of change that breaks quietly at runtime rather than at install. composer update --dry-run savannabits/admin-listing and read the full package list.

Publish config before config:cache runs in a deploy. If your pipeline caches config and your listing settings exist only in the package's merged defaults, later edits to a config value will appear to do nothing. Diff your published config against the package default on every version bump.

N+1 sneaks in through columns, not just relationships. A column that renders $row->team->name without team eager-loaded is one query per row per page. Load the index route with DB::listen and count queries on a 50-row page before shipping — not after someone complains the admin panel is slow.

Watch for layout collisions. A 2020-era Laravel package very likely assumes Bootstrap 4 pagination markup and may ship jQuery-dependent confirmation JS. If your admin shell is Tailwind or Livewire, plan to override the published pagination view and any action-confirmation partials rather than fight the defaults. Check this in the first hour — it changes your estimate.

Sorting and LIKE '%term%' search on unindexed columns is a silent performance cliff. Search and sort are server-side by design, which means they hit the DB on every keystroke-triggered request. Confirm the columns you expose are indexed, and prefer prefix search (LIKE 'term%', index-friendly) over infix (LIKE '%term%', full scan) where the UX allows it.

Debug the builder, not the HTTP response. When a listing returns the wrong rows, instrument the query hook with ->toSql() plus getBindings() and log the request parameters the listing reads. Most "the package is broken" reports resolve to a filter combined with an unexpected default scope — a soft-delete global scope, a tenancy scope, or a stale cached config.

Extension points, in order of preference — stop as high as you can:

  1. Override the query hook
  2. Override column/action definitions
  3. Publish and edit the package views
  4. Subclass the base class
  5. Vendor the source into app/
  6. Fork

Each step down the list multiplies your upgrade cost. Given there likely are no future upgrades, steps 5–6 are more defensible here than with a maintained package — but only make that call deliberately, in writing, with an owner attached.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment