jobmetric/laravel-reaction
jobmetric/laravel-reaction is a Laravel package that adds a flexible reaction system to Eloquent models, letting users like, dislike, or react to posts and other records through configurable reaction types and simple polymorphic integration.
Install and publish
composer require <vendor>/laravel-reaction:^3.0
php artisan vendor:publish --tag="reaction-config"
php artisan vendor:publish --tag="reaction-migrations"
php artisan migrate
Verify the published service provider name before running anything: 3.0.0 renames the provider to match reaction, so if you ever registered the old provider manually in bootstrap/providers.php or config/app.php, remove it and re-run php artisan config:clear / composer dump-autoload.
Wire up both sides of the relationship
use Vendor\Reaction\Concerns\HasReaction;
class Post extends Model
{
use HasReaction; // the "reactable" side
}
use Vendor\Reaction\Concerns\CanReact;
class User extends Model
{
use CanReact; // the "reactor" side
}
First use case — react to a post
$post->react($user); // create a reaction
$post->reactions()->count(); // read raw rows
$post->isReactedBy($user); // existence check
$post->unreact($user); // remove
Exact method names on HasReaction changed in 3.0.0 — open src/Concerns/HasReaction.php first and confirm the signatures before writing them into controllers. That trait is the single most important file in the package; Reaction model, the migration, and the config file are the next three.
Files to read in this order
config/reaction.php — table name, prune_days, headers, and reaction type/source definitions.src/Concerns/HasReaction.php and src/Concerns/CanReact.php — the public API surface.src/Models/Reaction.php — fillable fields, casts, relationships.reactable_id / reactable_type columns and indexes.src/Events/* — hook notifications and counters here, not in controllers.Idempotent toggle endpoint. Wrap react/unreact in a single action with a unique constraint behind it:
public function toggle(Request $request, Post $post)
{
$request->user()->hasReactedTo($post)
? $request->user()->unreactTo($post)
: $request->user()->reactTo($post);
return back();
}
Use firstOrCreate semantics rather than a raw create() so double-submits don't 500 on the unique index.
Reaction types. The LikeType enums were removed in 3.0.0, so type/variant handling is now your responsibility. Two sane options:
reactTo($post, type: ReactionKind::Heart->value) and validate it against your own enum.config/reaction.php and validate with Rule::in(config('reaction.types')), so ops can change it without a deploy.Either way, don't rebuild the removed enum — the package intentionally stopped owning that vocabulary.
Counting without N+1. Aggregate counts in the query, not per-model loops:
Post::withCount('reactions')->paginate();
For high-traffic feeds, denormalize into a reactions_count column updated from the package's reaction-created / reaction-deleted events, and read the column on index pages.
Events as the integration seam. 3.0.0 adds a redesigned event set. Listen for them to send notifications, bump counters, or write audit rows — this keeps your controllers package-agnostic and survives a future major bump better than trait call sites do. Register listeners in EventServiceProvider and assert the event class names against src/Events/ since they were all renamed in this release.
Custom reactor/reactable sources. The new InvalidReactionSourceException is thrown when a model isn't a valid source for a reaction. That's your guardrail, not a bug: register/allow the intended models (via the config and the CanReact / HasReaction traits) and catch the exception at your HTTP boundary to convert it into a 422 rather than a 500.
Pruning old reactions. prune_days in config plus the package's prune command (verify the signature in the README) means reactions older than N days get deleted. Schedule it:
Schedule::command('reaction:prune')->daily();
This is the pattern for engagement data you don't want growing forever — but it is destructive, so set it deliberately (or null/0 to disable) per environment.
Localized labels. The new language line plus hasTranslation on the service provider means reaction type labels can be returned translated. Publish translations (--tag="reaction-translations") and resolve labels through the translator instead of hardcoding strings in Blade.
API responses and headers. The new headers config key controls headers emitted on package responses. If you front this with a SPA or a different origin, check that key before debugging CORS by hand.
Testing. A reaction factory ships in 3.0.0 — use it in feature tests instead of hand-building rows, and note the factory changed field names alongside the model rename:
$reaction = Reaction::factory()->for($post, 'reactable')->create();
3.0.0 is a rename-everything release — treat it as a hard cutover. HasLike → HasReaction, Like → Reaction, the package itself is now laravel-reaction, the service provider was renamed, and the LikeType enums were deleted. Grep your codebase for Like, likeable, and the old namespace before bumping; nothing here is aliased for backward compatibility.
The migration renames columns, not just tables. likeable_id / likeable_type became reactable_*, and the table name changed. Re-publishing migrations is not an upgrade path — for existing data you need a hand-written migration that renames the table and columns and preserves rows, then a rollback plan. Test on a database copy; the polymorphic type column also holds fully-qualified class names, so update any stored values if you rename your own models in the same release.
Config cache bites after publishing. New keys (prune_days, headers, table name) won't appear until php artisan config:clear. In CI, a stale cached config from a prior install is the most common cause of "table not found" after the rename.
Morph maps and the renamed type column. If you use Relation::enforceMorphMap(), the reactable_type values must match your morph aliases — otherwise HasReaction lookups silently return empty collections rather than erroring. Set the morph map before running seeders.
Duplicate reactions. The package gives you the relation, not the uniqueness policy. Add a composite unique index on (reactor_id, reactable_id, reactable_type) in a follow-up migration, and handle the resulting QueryException (or use the package's toggle helper if one exists). Without it, concurrent taps will produce duplicate rows and inflated counts.
Pruning is irreversible and timezone-sensitive. prune_days compares against a timestamp column; if your app writes UTC but your DB session timezone differs, you can prune a day too early or too late. Confirm both before enabling the schedule in production.
EnumMacros replaced the shipped enums. If you were calling enum helpers (EnumToArray etc.) from the old enums directory, rewrite those calls to the macro equivalents or just inline the array — the helper no longer lives where it did.
Old event listeners fail silently. Laravel only errors on unresolvable event classes at dispatch, and only if you're strict about it. All event files were renamed; audit EventServiceProvider::$listen and any Event::listen calls, and run one reaction end-to-end with logging on to confirm listeners actually fire.
InvalidReactionSourceException is usually a trait problem. Nine times out of ten it means the target model is missing HasReaction (or the reactor is missing CanReact), not that the config is wrong. Check trait composition first.
Check the core-package bump. 3.0.0 pins the underlying package core to 1.20. If another dependency in your app pins an older major of that same core, you'll get an unsatisfiable constraint — resolve it before starting the rename work, since it can force you onto a different PHP or Laravel minor.
Upgrade order that works: bump the dependency → mechanical class/trait/namespace rename → data-migration for the table and columns → config republish and diff → event listener audit → run the factory-based tests → enable pruning last. Doing the data migration last means every failed step is reversible with a git checkout.
How can I help you explore Laravel packages today?