Open-source project
maize-tech/laravel-markable avatar
maize-tech/laravel-markable

laravel-markable: likes, bookmarks, favourites and reactions for Laravel

Integrate likes, bookmarks, favorites, reactions and custom made marks into your application

760 stars29 forksPHPMIT

At a glance

What is it?
laravel-markable is a Laravel package that turns any Eloquent model into something a user can like, bookmark, favourite, or react to, with a trait on the model and a static API on each mark class. It ships a config file, four separate migrations, custom metadata, and a documented way to add your own mark types.
Who is it for?
laravel-markable fits a Laravel team that has several kinds of user to model interaction, likes, saves, favourites, and reactions, and that wants one implementation of that pattern with a consistent API rather than three bespoke ones. It does not fit an application that wants all of those in one table, because the package deliberately uses a separate table per mark type for query performance.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 38 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One pattern for every kind of mark

laravel-markable integrates likes, bookmarks, favorites, reactions, and custom made marks into your application, and the package description says it in one line: it allows you to easily add the markable feature to your application, as for example likes, bookmarks, favorites and so on.

The idea is that a mark is a relationship between a user and a model, and that different products want different names for the same relationship. A course can be liked, a recipe can be saved, an article can be reacted to with an emoji, and the code that stores and queries those relationships is the same shape each time. The package gives you that shape and the four mark types it ships with, plus a way to define your own.

The implementation is a trait on your model and a static API on the mark classes, so a feature reads as a one liner at the call site.

``` php use App\Models\Course; use Maize\Markable\Models\Like;

$course = Course::firstOrFail(); $user = auth()->user();

Like::add($course, $user); // marks the course liked for the given user

Like::remove($course, $user); // unmarks the course liked for the given user

Like::toggle($course, $user); // toggles the course like for the given user

Like::has($course, $user); // returns whether the given user has marked as liked the course or not

Like::count($course); // returns the amount of like marks for the given course ```

Five static methods, add, remove, toggle, has, and count, and each of them takes the model and the user. The count method takes only the model, since it is a question about the model rather than about a user.

The project is MIT licensed, published on Packagist as maize-tech/laravel-markable, and the last push was on 2026-09-15. The releases are 3.0.0 from 2026-04-15, 2.3.2 from 2025-07-19, and 2.3.1 from 2025-03-28, so the 3.0 line is the current one and is a major version.

Install, publish the migrations you need, migrate

The installation is a Composer requirement followed by an Artisan command, and the second step matters because the package ships four separate migrations and you almost certainly do not want all of them.

bash
composer require maize-tech/laravel-markable
bash
php artisan markable:install

The install command automatically publishes the config file. Then the README says to publish only the migrations you need, and gives one command per mark type.

bash
php artisan vendor:publish --tag="markable-migration-bookmark"
bash
php artisan vendor:publish --tag="markable-migration-favorite"
bash
php artisan vendor:publish --tag="markable-migration-like"
bash
php artisan vendor:publish --tag="markable-migration-reaction"

And then the migration itself.

bash
php artisan migrate

The published config file is a standard Laravel config array with three keys. The user model key names the fully qualified class name of the user model, and the table prefix key sets the prefix for all the mark tables, defaulting to markable_. The allowed values key lists the allowed values for each mark type, and if a specific mark should not accept any values you avoid adding it to the list, with the array key matching the mark class name in lower case. The reaction entry is present with an empty array in the example, which is how you say that a reaction takes a value from a defined set.

The table prefix matters for the custom mark workflow later, because the migration for a new mark type has to be named with that prefix, and the class name of the mark has to match it.

The trait, the marks array, and a model that opts in

To use the package you add the Markable trait to the model that should have marks, and then you define the list of possible marks for that model in the marks array, with the list of mark class namespaces.

``` php <?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model; use Maize\Markable\Markable; use Maize\Markable\Models\Like;

class Course extends Model { use Markable;

protected $fillable = [ 'title', 'description', ];

protected static $marks = [ Like::class, ]; } ```

Two things to notice. The trait is what brings the relationship methods onto the model, and the marks array is the per-model allow list, which means a Course can be liked but not reacted to while a Recipe can be both. That is a useful property in a product where different content types support different interactions, and it is why the list lives on the model rather than in the config.

The mark class namespacing in the array is what the package resolves the mark classes through, and it is also what the config key matching refers to, the allowed values key matching the mark class name in lower case.

The trait name and the array are the entire integration surface. There is no service to register, no event to listen for, and no controller to write, and the calling code is the static API shown earlier.

A table per mark type, on purpose

The custom mark section explains the schema decision, and it is the one design choice in the package that is worth arguing with.

The package allows you to define custom marks, and the first thing you need to do is create a migration which defines the new mark model. The reason, as the README puts it, is that the package works with separate tables for each mark in order to increase the performances when executing related queries.

The migration table name should contain the prefix defined in the table prefix attribute under the config file, and the default prefix is set to markable_.

``` php return new class extends Migration { public function up() { Schema::create('markable_bookmarks', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnUpdate()->cascadeOnDelete(); $table->morphs('markable'); $table->string('value')->nullable(); $table->json('metadata')->nullable(); $table->timestamps(); }); } } ```

The columns tell you what the package can do. The foreign id to the user with cascading updates and deletes means a deleted user takes their marks with them, and the polymorphic columns mean one table can point at any model type, which is what makes a single marks table per type work for every model. The value column holds the mark value, for a reaction, and the metadata column is a JSON blob for the custom metadata feature.

The performance argument is real. A single polymorphic table for every mark type means a query for the count of likes has to filter on the type, and a table per type means it does not. The cost is a schema per interaction type, and a migration per type, which is why the README tells you to publish only the migrations you need.

For an application with two mark types, that trade is easy. For an application that lets users define their own mark types at runtime, it is not, and that is a case this schema does not cover.

Custom marks, and two relation names that are easy to mix up

Once you have the migration, you create a class that extends the abstract Mark class, and the README explains the two methods you can implement on it, which are easy to confuse because their names differ by one word.

The markableRelationName method is used to retrieve the users who marked a given model entity with the mark entity as pivot. The markRelationName method is used to retrieve the list of marks of a given model entity, and by default the relation name is the plural name of the mark class name.

``` php <?php

namespace App\Models;

use Maize\Markable\Mark;

class Bookmark extends Mark { public static function markableRelationName(): string { return 'bookmarkers'; } /** * The override is useless in this case, as I am returning the default * relation name which is the plural name of the mark class name (bookmarks, indeed) */ public static function markRelationName(): string { return 'bookmarks'; } } ```

The example makes the distinction concrete. The first method returns bookmarkers, which is the people who did the marking, and the second returns bookmarks, which is the marks themselves, and the comment in the code points out that the second override is pointless because it returns the default.

That comment is worth reading as documentation of the intended usage. The markRelationName override exists for the case where the default plural is wrong, usually because the class name is singular and awkward, and in the common case you implement only the first method and leave the second alone.

The custom metadata feature is the other half of the extension story. If needed, you may also add custom metadata when assigning a mark, and the metadata is passed as a third argument to add and to toggle, as an array of key and value pairs.

Values, metadata, and where the package stands

The last section of the usage documentation is about working with mark values, where a mark such as a reaction carries a value, and the excerpt available here stops before the details. What the configuration already tells you is enough to understand the shape of it. The allowed values key lists the values each mark type may take, and a mark type that takes no values is simply left out of that list, and the value column in the migration is nullable, so a like has no value and a reaction does.

The metadata column is the escape hatch, and it is the same mechanism as the custom metadata argument, stored rather than filtered. That is a useful distinction: a value is constrained and queryable, metadata is free form and not.

The repository is a conventional Laravel package. There is a src directory with the code, a config directory, a database directory for the migrations, a tests directory, a composer manifest, a PHPUnit configuration, and a PHPStan configuration, which is the static analyser the project uses, along with a changelog, a licence file, and the artwork for the social card.

The continuous integration setup has two workflows visible in the badges. One runs the tests, and the other is named for fixing PHP code style issues, which is the automated style fixer run as a workflow rather than as a local step. That combination, a test workflow and a style workflow, is the standard arrangement for a package that wants contributors to be able to open a pull request without setting anything up.

The version history is short and moves in a sensible way, with 2.3.1 in March 2025, 2.3.2 in July, and the 3.0.0 major in April 2026, and the project is not archived with a push as recent as 2026-09-15, so it is maintained.

Editorial conclusion

laravel-markable fits a Laravel team that has several kinds of user to model interaction, likes, saves, favourites, and reactions, and that wants one implementation of that pattern with a consistent API rather than three bespoke ones. It does not fit an application that wants all of those in one table, because the package deliberately uses a separate table per mark type for query performance. Verify first that your Laravel version is compatible with the 3.0 release, which is a major version, that you publish only the migrations for the marks you actually use, since the install command publishes all four, and that the relation names your templates expect match the defaults, which are the plural of the mark class name.

Frequently asked questions

How do I add likes to a Laravel model with laravel-markable?

Add the Markable trait to the model and list the mark classes you want in the marks array on that model. After that the static methods on the mark class handle the rest, with add, remove, toggle, has, and count.

How do I install laravel-markable?

Require it with Composer, run the install Artisan command, which publishes the config file, then publish only the migrations you need with the vendor publish command using the tag for the mark type, and finally run the migration. The package ships separate migrations for bookmark, favorite, like, and reaction.

Why does laravel-markable use a separate table for each mark type?

Because the package works with separate tables for each mark in order to increase performance when executing related queries, so a count of likes does not have to filter on the mark type. The cost is one migration per mark type, and the table name has to carry the prefix configured in the config file, which defaults to markable_.

How do I add my own mark type to laravel-markable?

Create a migration for the new table, naming it with the configured table prefix, and then create a class extending the abstract Mark class. You implement the markableRelationName method for the users who made the mark, and you can override the markRelationName method for the marks themselves, which by default is the plural of the class name.

Can a mark carry extra data in laravel-markable?

Yes. You can add custom metadata when assigning a mark by passing an array as the third argument to add or toggle, and the migration for the mark includes a nullable JSON metadata column to store it. A separate value column exists for marks that take a value, such as a reaction.

Official sources

  1. Issues
  2. License: MIT
  3. maize-tech/laravel-markable on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/maize-tech-laravel-markable.svg)](https://hysenlabs.com/projects/maize-tech-laravel-markable)