maize-tech/laravel-markable
Add likes, bookmarks, favorites, reactions and more to Laravel models with a simple “markable” system. Includes install command, configurable user model and table prefix, and optional publishable migrations per mark type for quick setup.
Installation:
composer require maize-tech/laravel-markable
php artisan markable:install
App\Models\User).Publish Migrations:
Choose the mark types you need (e.g., bookmark, favorite, like, reaction) and publish their migrations:
php artisan vendor:publish --tag="markable-migration-like"
php artisan migrate
Apply to a Model:
Add the Markable trait and define allowed marks in your model:
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
class Course extends Model
{
use Markable;
protected static $marks = [
Like::class,
];
}
First Usage:
$course = Course::first();
$user = auth()->user();
Like::add($course, $user); // Add a like
Like::toggle($course, $user); // Toggle like
Like::has($course, $user); // Check if liked
Mark Management:
Mark::add(), Mark::remove(), or Mark::toggle() for CRUD operations.
Like::add($model, $user, ['metadata' => 'value']);
Like::remove($model, $user);
Mark::has($model, $user, $value) to verify if a mark exists.Mark::count($model, $value) to get the total count of a specific mark.Eloquent Relationships:
$course->likes; // Collection of Like models
$course->likers; // Collection of users who liked the course
Course::whereHasLike($user)->get(); // Courses liked by $user
Post::whereHasReaction($user, 'heart')->get(); // Posts with 'heart' reactions
Custom Marks:
class Bookmark extends Mark
{
public static function markableRelationName(): string
{
return 'bookmarkers';
}
}
Reaction):
'allowed_values' => [
'reaction' => ['heart', 'kissing_heart', '*'], // Wildcard allows any value
],
Metadata Handling:
Like::add($course, $user, ['topic' => 'Laravel', 'reason' => 'useful']);
$like = Like::where('markable_id', $course->id)->first();
$like->metadata; // ['topic' => 'Laravel', ...]
BackedEnum Support (v3.0+):
enum ReactionType: string { case Like = 'like'; }
config/markable.php:
'allowed_values' => [
'reaction' => ReactionType::class,
],
Reaction::add($post, $user, ReactionType::Like);
API Endpoints:
POST /posts/{post}/like):
Route::post('/posts/{post}/like', function (Post $post) {
Like::toggle($post, auth()->user());
return response()->json(['status' => 'success']);
});
Frontend Integration:
document.querySelector('.like-button').addEventListener('click', async () => {
const response = await fetch(`/posts/${postId}/like`, { method: 'POST' });
// Update UI based on response
});
Caching:
$count = Cache::remember("like_count_{$course->id}", now()->addHours(1), function () {
return Like::count($course);
});
Validation:
allowed_values in config:
use Maize\Markable\Exceptions\InvalidMarkValueException;
try {
Reaction::add($post, $user, 'invalid_value');
} catch (InvalidMarkValueException $e) {
// Handle invalid value
}
Testing:
public function test_like_toggle()
{
$course = Course::factory()->create();
$user = User::factory()->create();
Like::toggle($course, $user);
$this->assertTrue(Like::has($course, $user));
}
Migration Conflicts:
table_prefix in config/markable.php matches the published migrations.Wildcard Values:
'*' in allowed_values disables validation for that mark, allowing any value. Use sparingly.'allowed_values' => [
'reaction' => [ReactionType::class, '*'], // Allows enums + any string
],
Performance with Large Datasets:
whereHasLike() can be slow on tables with millions of records.user_id and markable_id columns in mark tables.User Model Assumption:
User model with an id column. Customize user_model in config if using a different auth system.Metadata Serialization:
json_encode()/json_decode() for complex objects.Dynamic Scopes:
whereHasLike() are generated dynamically. Override markRelationName() if the default pluralization doesn’t fit your naming convention.Mark Not Found:
$marks array.Invalid Mark Value:
add()/toggle() matches allowed_values in config.try {
Reaction::add($post, $user, 'invalid');
} catch (Exception $e) {
dd($e->getMessage());
}
Relationships Not Loading:
markableRelationName() and markRelationName() methods return correct relation names.dd($course->relations); // Check if 'likes' or 'likers' exist
Custom Mark Logic:
Mark class to add business logic (e.g., auto-remove old marks):
class Bookmark extends Mark
{
protected static function boot()
{
parent::boot();
static::created(function ($mark) {
// Add custom logic here
});
}
}
Observers:
Like::observe(LikeObserver::class);
class LikeObserver
{
public function created(Like $like)
{
// Notify user or update analytics
}
}
Policy Integration:
class LikePolicy
{
public function toggle(User $user, Course $course)
{
return $user->can('like_courses');
}
}
Register in AuthServiceProvider:
Gate::define('like_courses', function (User $user) {
return $user->isAdmin();
});
API Resources:
class LikeResource extends JsonResource
{
public function toArray($request)
{
return [
'user' => $this->user,
'
How can I help you explore Laravel packages today?