dive-be / laravel-wishlist
Manage your users' wishes in a Laravel app
Installs: 8 493
Dependents: 0
Suggesters: 0
Security: 0
Stars: 5
Watchers: 0
Forks: 2
Open Issues: 0
Requires
- php: ~8.3
- illuminate/auth: ^11.0
- illuminate/console: ^11.0
- illuminate/contracts: ^11.0
- illuminate/cookie: ^11.0
- illuminate/database: ^11.0
- illuminate/events: ^11.0
- illuminate/http: ^11.0
- illuminate/support: ^11.0
Requires (Dev)
- larastan/larastan: ^2.0
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0
- phpunit/phpunit: ^11.0
README
What problem does this package solve?
This package provides solutions to common problems when managing a user's wishlist. It also allows for seamless transitions between storage drivers depending on a user's authentication state.
Installation
You can install the package via composer:
composer require dive-be/laravel-wishlist
You can publish the config file and migrations with:
php artisan wishlist:install
This is the contents of the published config file:
return [ /** * The authentication guard to use when using the `eloquent` or `upgrade` drivers. */ 'auth_guard' => config('auth.defaults.guard'), 'cookie' => [ /** * You may choose to scope the cookies to a particular subdomain. * Especially useful when serving multiple apps. * The default (no scoping) will suffice for most apps, though. */ 'domain' => env('WISHLIST_COOKIE_DOMAIN', env('SESSION_DOMAIN')), /** * Time-to-Live in minutes. Default 43200 (1 month). */ 'max_age' => env('WISHLIST_COOKIE_MAX_AGE', 43_200), /** * You may customize the name of the cookie. Completely optional. */ 'name' => env('WISHLIST_COOKIE_NAME', 'wishlist'), ], /** * Supported drivers: * - "array" (only available during the current request lifecycle) * - "cookie" (persists the user's wishes as a serialized string inside a cookie) * - "eloquent" (persists the users' wishes to the `wishes` table) * - "upgrade" (uses the cookie driver if a user is not authenticated, otherwise uses the eloquent driver) */ 'driver' => env('WISHLIST_DRIVER', Dive\Wishlist\WishlistManager::ELOQUENT), 'eloquent' => [ /** * The model that should be used with this driver. * It must be, or extend the base Wish model. */ 'model' => Dive\Wishlist\Models\Wish::class, /** * You may choose to provide a context for the saved wishes in the database. * Particularly useful when serving multiple apps. The default will suffice for most apps, though. */ 'scope' => 'default', /** * The user model of your application. For most apps, the default will suffice. * However, if you wish, you may customize that here. This is used for the Wish model's * user relationship so you can display the owner of the wish in e.g. Laravel Nova. */ 'user' => config('auth.providers.users.model'), ], ];
Usage
Configuration
First things first, you should take a look at the published configuration file and adjust the values to your needs. If you need additional information about the available options, read on.
Drivers
Array ⏳
This driver is meant for use in testing. Its contents are ephemeral and should not be used in production.
See Testing section below for additional information regarding unit tests.
Cookie 🍪
The user's wishlist will be persisted client-side as a stringified JSON. You should make use of Laravel's cookie encryption (enabled by default) or any user will be able to crash your application (because there is no validation) when the cookie values are tampered with. The internal structure of your code base will be leaked partially as well if you do not make use of relation morph maps.
Note: make sure the name of the cookie you wish to use (pun intended) is not excluded for encryption in the
\App\Http\Middleware\EncryptCookies::$excluded
array.
Eloquent 🧑🎨
The user's wishlist will be persisted server-side in the wishes
table.
You can only use this driver inside routes that are protected by the auth
middleware as it requires an instance of an authenticated user.
Note: do not forget to publish and run the migrations if you opt to use this driver.
Upgrade 🚀
> This < is the driver that makes this package shine.
- When a guest (an unauthenticated user) is browsing your app, the Cookie driver will be used to persist the wishlist.
- When the user is authenticated, the Eloquent driver will be used.
🤌 Best of all? You don't have to change anything in your code.
But hold on, there is more! When the user logs in, the wishes can be carried over to the database automatically! 🙀 See Migrating wishes section below for additional information.
Preparing Wishable models
Next, you must make the Eloquent models that can be wished for Wishable
i.e. implement the contract and use the CanBeWished
trait.
An example:
use Dive\Wishlist\Contracts\Wishable; use Dive\Wishlist\Models\Concerns\CanBeWished; class Product extends Model implements Wishable { use CanBeWished; }
Preparing User model (optional)
For convenience, this package also provides an InteractsWithWishlist
trait which you can use in your User
model to interact with the wishlist.
use Dive\Wishlist\Models\Concerns\InteractsWithWishlist; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { use InteractsWithWishlist; }
You can now do the following:
$user = auth()->user(); $user->wish(Product::first()); // adds the product to the wishlist $user->unwish(Product::first()); // removes the product from the wishlist $user->wishes(); // retrieves all of the user's wishes
Resolving the wishlist
This package provides every possible way to resolve a Wishlist
instance out of the IoC container. We've got you covered!
Facade
use Dive\Wishlist\Facades\Wishlist; Wishlist::all();
or using the alias (particularly helpful in Blade views)
use Wishlist; Wishlist::all();
Helper
wishlist()->all();
Dependency injection
use Dive\Wishlist\Contracts\Wishlist; public function index(Wishlist $wishlist) { return view('wishlist.index')->with('wishes', $wishlist->all()); }
Service Location
app('wishlist')->all();
Capabilities 💪
You can refer to the Wishlist contract for an exhaustive list.
Adding a wish
$wish = Wishlist::add($product);
You will receive an instance of Dive\Wishlist\Wish
.
Retrieving all wishes
$wishes = Wishlist::all();
You will receive an instance of Dive\Wishlist\WishCollection<Wish>
.
Refer to the class definition for all convenience methods.
Finding a wish
$wish = Wishlist::find($product);
Eager loading wish relations
When there is only a single type of Wishable
(and you know for sure there won't be any other), you may omit the morph types entirely:
// ✅ Collection only contains Wishables of type "product" $wishes = Wishlist::all()->load(['productable', 'variant']);
In other cases, you must provide a type-relation map:
$wishes = Wishlist::all()->load([ Product::class => ['productable', 'variant'], Sample::class => 'purveyor', ]); // ✅ Collection contains multiple wishables
A LogicException
will be thrown in case of ambiguity:
// 🚫 Collection contains 2 types of Wishable: Product & Sample $wishes = Wishlist::all()->load(['productable', 'variant']);
Retrieving total count
$count = Wishlist::count();
Existence checks
$isWished = Wishlist::has($product);
Emptiness checks
Wishlist::isEmpty(); Wishlist::isNotEmpty();
Purging
$amountOfWishesRemoved = Wishlist::purge();
Wish removal
You can remove a wish either by its id
public function destroy(int $id) { Wishlist::remove($id); }
or by its underlying Wishable
public function destroy(Product $product) { Wishlist::remove($product); }
Migrating wishes 🚚
While using the Upgrade driver, you may want to carry over the user's cookie wishlist to the database. This is not enabled by default, but you have 2 options to opt into this behavior:
Event listener
Listen to the Login
event in EventServiceProvider
:
class EventServiceProvider extends ServiceProvider { protected $listen = [ \Illuminate\Auth\Events\Login::class => [ \Dive\Wishlist\Listeners\MigrateWishes::class, ], ]; }
Middleware
Add the MigrateWishes
middleware to your application's middleware stack:
class Kernel extends HttpKernel { protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Dive\Wishlist\Middleware\MigrateWishes::class, // 👈 // omitted for brevity ], ]; }
Note: make sure to place the middleware after
EncryptCookies
Route model binding
Wouldn't it be nice to have a wish automatically resolved from a route parameter?
Well, say no more! 👇
use Dive\Wishlist\Wish; Route::delete('wishlist/{wish}/delete', function (Wish $wish) { $wish->delete(); return redirect()->to('dashboard'); });
- As this is not an Eloquent model, you can still use this syntax even if you only use the Cookie driver! 🎉
- Just like Eloquent models, a
ModelNotFoundException
will be thrown if the requested wish cannot be found.
Note: you cannot make the
wish
parameter a child of a parent parameter because it does not make sense. An exception will be thrown if you attempt to do so.
Retrieving a particular user's wishlist 👱🏻♂️
You may want to modify a user's wishlist in e.g. an Artisan command where no auth context is available.
For these cases, you can invoke the forUser
method with a User
instance to retrieve a wishlist scoped to that user:
class ClearWishlistCommand extends Command { public function handle() { $user = User::first(); Wishlist::forUser($user)->purge(); }
Note: this is only available for the Eloquent driver
Extending the wishlist 👣
If the default drivers do not fulfill your needs, you may extend the WishlistManager
with your own custom drivers:
Wishlist::extend('redis', function () { return new RedisWishlist(); });
Testing 🔎
This package offers a fake implementation of the Wishlist
contract so you can make assertions in your unit tests and make sure you ship that bug-free code 💪.
use App\Http\Livewire\HeartButton; use Database\Factories\ProductFactory; use Dive\Wishlist\Facades\Wishlist; use function Pest\Livewire\livewire; test('A wish is added when the visitor clicks on the heart icon', function () { Wishlist::fake(); livewire(HeartButton::class)->call('wish', ProductFactory::new()->create()->getKey()); expect(Wishlist::count())->toBe(1); });
Testing (package)
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security related issues, please email oss@dive.be instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.