Database schema & key types
One polymorphic table records who liked what. Its name comes from likes.table; the columns are fixed:
| Column | Type | Notes |
|---|---|---|
id | auto-increment | Always an auto-incrementing key, whatever key_type says. |
actor_type, actor_id | morph pair (unique key) | The model that gave the like. actor_id is typed by key_type. |
likeable_type, likeable_id | morph pair (unique key) | The model that received it. likeable_id is typed by key_type. |
type | string, default like | The reaction type. |
created_at, updated_at | timestamps | created_at drives the trending window. |
deleted_at | soft delete | Set on unlike; a re-like restores the row. |
A unique key, likes_actor_likeable_type_unique — prefixed with your table name when you change it — covers (actor_type, actor_id, likeable_type, likeable_id, type) — soft-deleted rows included — so the database itself keeps one row per actor, likeable and reaction type, and every verb and check hits that exact lookup. The published migration in short:
$name = LikesConfig::table(); // likes.table, read strictly
// Throws for an unrecognized value, so a typo in the host's config fails
// the migration instead of quietly building bigint columns.
$keyType = KeyType::fromConfig('likes.key_type');
Schema::create($name, function (Blueprint $table) use ($name, $keyType): void {
$table->id();
$table->morphKey('actor', $keyType, nullable: false); // actor_type + actor_id
$table->morphKey('likeable', $keyType, nullable: false); // likeable_type + likeable_id
$table->string('type')->default('like');
$table->timestamps();
$table->softDeletes();
// One row per actor + likeable + reaction type, soft-deleted rows included.
$table->unique(
['actor_type', 'actor_id', 'likeable_type', 'likeable_id', 'type'],
$name.'_actor_likeable_type_unique',
);
});morphKey() is a Blueprint macro from the Roundly package toolkit. The service provider registers it on boot, so it exists before php artisan migrate runs.
Key types
key_type types both morph pairs, so actors and likeables share one key type. Pick the one that matches your models:
| key_type | Morph columns | Use when your models… |
|---|---|---|
bigint | morphs() — unsigned big integer ids | use Laravel’s default auto-incrementing keys (the default) |
uuid | uuidMorphs() | use HasUuids |
ulid | ulidMorphs() | use HasUlids |
Set it before you publish and run the migration:
LIKES_KEY_TYPE=uuid # bigint (default) | uuid | uliduse Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Likes\Traits\GivesLikes;
use RoundlyConsulting\Likes\Traits\HasLikes;
class User extends Model
{
use GivesLikes, HasUuids;
}
class Post extends Model
{
use HasLikes, HasUuids;
}A blank value (LIKES_KEY_TYPE=) is not set and keeps bigint. Any other value throws InvalidConfigurationException when the migration runs, instead of silently building bigint columns.
Soft deletes
Unliking soft-deletes the row, and liking again restores it instead of inserting a duplicate — each actor keeps a single row per likeable and reaction type. Checks, counts, scopes and summaries all ignore soft-deleted rows.
Custom model & table
Extend the package model to add your own behaviour, then point the config at it:
namespace App\Models;
use RoundlyConsulting\Likes\Models\Like as BaseLike;
class Like extends BaseLike
{
// your own casts, relations or scopes
}// config/likes.php
'model' => App\Models\Like::class,The model reads likes.table unless your subclass sets $table explicitly — an explicit $table wins, as Eloquent intends. A configured class that isn’t the package model or a subclass of it throws InvalidConfigurationException naming the key — it is never silently replaced by the packaged model.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.