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

Laravel Saveable Laravel Package

ritechoice23/laravel-saveable

A Laravel package that adds saveable behavior to Eloquent models, letting users bookmark, favorite, or save any model through simple polymorphic relationships and an easy-to-use trait.

View on GitHub
Deep Wiki
Context7

Getting Started

ritechoice23/laravel-saveable is a small trait-based package for adding "save / bookmark / favourite" behaviour to Eloquent models via a polymorphic relationship. Version 1.0.1 is a patch release on top of 1.0.0 — no breaking changes are signalled by the release notes, so you can pin ^1.0.1 and treat the 1.0.0 → 1.0.1 diff as bug-fix only.

  1. Require the package:
    composer require ritechoice23/laravel-saveable
    
  2. Publish and run the migration (the package ships the saves pivot table):
    php artisan vendor:publish --tag="saveable-migrations"
    php artisan migrate
    
    Publish the config too if the tag exists — check vendor/ritechoice23/laravel-saveable/config for table name and model overrides before running migrate.
  3. Read first, in this order: README.md (trait names and method signatures), the published migration (column names and indexes), then the 1.0.0...1.0.1 diff on GitHub. Because the release note is only a changelog link, the diff is the authoritative record of what changed between the versions you might already have installed.
  4. Add the traits. Typically one trait goes on the "actor" (usually User) and one on the "target" (any model that can be saved):
    // app/Models/User.php
    use Ritechoice23\Saveable\Traits\HasSaves;
    
    The exact trait names are the one thing to confirm from the README before writing code — get them from src/Traits rather than guessing.
  5. First use case: a bookmark button on a Post. From the controller, $user->save($post) / $user->unsave($post) (or toggle) and $user->hasSaved($post) for the button state. That loop — toggle, check, list — is 90% of real usage; get it working end-to-end before touching counters or scopes.

Implementation Patterns

Trait placement is symmetric. Put the "saver" trait on User and the "saveable" trait on every model that can be bookmarked (Post, Product, Video). Users routinely add only the User side and then wonder why $post->savers is undefined.

Controller + route model binding is the cleanest integration:

Route::post('/posts/{post}/save', [SaveController::class, 'store'])->middleware('auth');
Route::delete('/posts/{post}/save', [SaveController::class, 'destroy'])->middleware('auth');
public function store(Post $post)
{
    auth()->user()->save($post);      // idempotent — confirm in README
    return back();
}

Authorise before mutating (a policy on save), otherwise any authenticated user can save any record — including ones they can't view.

Lists and feeds. Build the "my saved items" page from the relationship with eager loading, not from the target model:

$saved = auth()->user()->saves()
    ->with('saveable')
    ->latest()
    ->paginate(24);

If the package exposes a savedPosts()-style typed accessor, prefer it — it avoids filtering a mixed polymorphic collection in PHP, which is where most N+1s appear.

Counters on cards. Use withCount on the index query rather than per-row queries:

Post::withCount('savers')->paginate();

and cache the result if the page is hot — aggregate counts on a polymorphic pivot are not cheap at scale.

API / Inertia responses. Expose two booleans computed once per collection, using a single query for the whole page (fetch the set of saved IDs for the current user, then in_array in the resource) instead of hasSaved() inside a loop.

Toggling in the UI. Prefer the toggle endpoint plus optimistic UI over separate save/unsave buttons; it halves the route surface and removes the "double-save" race when a user double-clicks.

Testing. Use factories for both sides and assert idempotency explicitly:

$user->save($post);
$user->save($post);
$this->assertDatabaseCount('saves', 1);

That single test catches the most common production bug with this class of package.

Gotchas and Tips

  • Morph maps break silently. If you call Relation::enforceMorphMap(), the saveable_type column stores the alias, not the FQCN. Renaming App\Models\Post is then safe, but changing an alias orphans rows. Decide before you ship, not after.
  • Duplicate rows. The pivot almost certainly has a unique index on (user_id, saveable_type, saveable_id); if the package's save() doesn't check first, wrap it in firstOrCreate yourself or catch the QueryException. Verify against the published migration — if there is no unique index, add one in your own migration before going live.
  • Soft deletes. Deleting a Post with SoftDeletes leaves the save row intact; ->with('saveable') then returns null and your Blade/resource will throw on $save->saveable->title. Filter with ->whereHas('saveable') or add a whereNull('deleted_at') scope.
  • Hard deletes and FK constraints. If the migration declares a cascading foreign key on user_id but a plain morph pair for the saveable side, deleting a Post leaves dangling rows. Add a model deleting observer to purge saves, or a scheduled cleanup.
  • Table name collisions. saves is generic; if you already have a saves table (game saves, drafts), publish the config and rename before migrating. Renaming after data exists is painful.
  • Patch-level upgrade. Moving 1.0.0 → 1.0.1 should be a composer update ritechoice23/laravel-saveable with no code changes. Because the release note is only a changelog link, actually open the compare diff and scan src/ and any migration changes — a migration edit in a patch release is the one thing that can bite you, since published migrations won't re-run.
  • Debugging "nothing saves". Check, in order: trait present on both models, migration run (php artisan migrate:status), the row actually in the DB (DB::table('saves')->latest('id')->first()), then the relationship name — saveable_type must match what Eloquent resolves.
  • Extension points. The published config is the supported override surface (custom pivot model, table name, morph key). If you need behaviour beyond it — events, notifications on save, save-count caching — extend the package's model or wrap the trait in your own service rather than editing vendor/.
  • Don't put heavy logic in the trait. Keep notification/analytics fan-out in a SavedItem event listener or an application service. Traits are hard to test in isolation and harder to replace if the package is abandoned; keeping your domain code outside it makes an eventual swap a one-day job.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package