rumspeed/laravel-notes
A Laravel package for attaching notes to Eloquent models. Add polymorphic notes with migrations, traits, and simple APIs so your app can record comments, reminders, or activity logs without building custom note storage.
composer require rumspeed/laravel-notes. On v1.3.0 this resolves against the Laravel 13 skeleton; if you're still on Laravel 10–12, pin ^1.2 instead of taking the latest.php artisan vendor:publish --tag="laravel-notes-config" and --tag="laravel-notes-migrations", then php artisan migrate. Publish migrations before writing application code so you can adjust column types/nullability in your own migration copy.config/notes.php (table name, Note model binding, author resolver) and the package's HasNotes trait. Those two files tell you the entire public surface; nearly everything else is a thin Eloquent wrapper.use Rumspeed\Notes\Concerns\HasNotes;
class Order extends Model
{
use HasNotes;
}
$order->notes()->create(['body' => 'Refund approved']), rendered with $order->notes()->latest()->with('author')->paginate(). That's the 80% path; everything else is customization.Service-layer writes, not controllers. The package gives you a relation, not a workflow. Put note creation in a dedicated action or service (AddNoteToOrder) so you can attach the author, a type/category, and any metadata in one place and reuse it from jobs, commands, and Livewire alike. This also gives you one chokepoint for authorization.
Author attribution via config. Rather than passing user_id on every call, set the author resolver in config/notes.php to something like fn () => auth()->id(). Explicitly pass an author for system-generated notes (queued jobs have no authenticated user) — otherwise they silently land as null.
Event-driven notes. The highest-leverage pattern is auto-noting state changes from model events or domain events:
protected static function booted(): void
{
static::updated(function (Order $order) {
if ($order->wasChanged('status')) {
$order->notes()->create([
'body' => "Status: {$order->getOriginal('status')} → {$order->status}",
'user_id' => auth()->id(),
]);
}
});
}
Do this from a listener rather than the model itself if the note text needs anything beyond the model's own state.
Querying and UI. Always ->with('author') when rendering a timeline — the author relation is the classic N+1 here. For long histories, paginate the relation, not the parent, and consider a latest() index on created_at in your published migration.
Forms and validation. Wrap the create call in a Form Request (body required, max length matching your column) and a policy check on the noteable model. The package intentionally doesn't ship authorization; if you skip it you'll end up with users writing notes onto records they can't see.
Testing. RefreshDatabase plus a factory for the Note model (check whether the package ships one — if not, write your own in database/factories). Assert on counts and content, not on the package's internal table shape.
composer why-not rumspeed/laravel-notes v1.3.0 before assuming it installs, and check whether illuminate/support in the package's composer.json widened or replaced the previous constraint. If it replaced it, v1.3.0 will not install on Laravel 11/12 and you must stay on ^1.2.composer update is sufficient. Only republish migrations if you want to diff them against your published copy — never --force over a migration you've already run in production.php artisan config:clear (and optimize:clear in CI) after editing config/notes.php, or you'll debug a resolver that isn't the one running.Relation::enforceMorphMap() in a service provider, do it before any notes exist — existing rows store FQCNs and will break resolution when you switch to aliases. Migrate the stored noteable_type values in the same deploy.->cascadeOnDelete() on the noteable index in your published migration; the package can't know your intent.user_id or type. If create() silently drops a field, check $fillable on the model (or your config-bound replacement) before assuming a package bug.$model->notes(). Any direct Note::query() in a report or admin panel is a cross-tenant leak waiting to happen — add a global scope if you query the model directly.How can I help you explore Laravel packages today?