arcanedev/laravel-notes
A lightweight notes system for Laravel that lets you attach notes to any Eloquent model through a polymorphic relationship. Ships with a HasNotes trait, migration, configurable Note model, and a simple API for creating and retrieving notes on your records.
Install and publish everything first. The package ships a config file, a migration, and a service provider with auto-discovery; there is no facade you need to register manually.
composer require arcanedev/laravel-notes
php artisan vendor:publish --provider="Arcanedev\LaravelNotes\LaravelNotesServiceProvider"
# or target pieces individually
php artisan vendor:publish --provider="Arcanedev\LaravelNotes\LaravelNotesServiceProvider" --tag=config
php artisan vendor:publish --provider="Arcanedev\LaravelNotes\LaravelNotesServiceProvider" --tag=migrations
php artisan migrate
Look at these three files before writing any code:
config/notes.php — points at the Note model class and the author (user) model. On Laravel 8+ this default is typically App\User, so change it to App\Models\User if that is your namespace.notable_type/notable_id for the model being annotated, plus author columns and a content column). Read it; do not assume.vendor/arcanedev/laravel-notes/src/Traits/HasNotes.php — this is the entire public API surface on your models.Attach the trait to the model you want to annotate:
use Arcanedev\LaravelNotes\Traits\HasNotes;
class Order extends Model
{
use HasNotes;
}
First use case — an internal note on a record:
$order->createNote('Customer called about a duplicate charge; refund issued manually.');
$order->notes; // Collection of Note models
$order->notes()->latest()->get();
$order->notesCount(); // handy for badges in an index table
That is the whole loop: create a note, read the notes back. Everything else is wiring it into your UI and authorization layer.
One polymorphic table, many annotatable models. Because notes are stored with notable_type/notable_id, you can put HasNotes on Order, Customer, Invoice, and Ticket and get a single notes table you can query globally — useful for an admin-wide "recent notes" feed:
Note::with('notable', 'author')->latest()->paginate(50);
Author attribution. Pass the author explicitly in jobs, commands, and queued listeners — there is no authenticated user in those contexts:
$order->createNote($body, $request->user());
// queued job / artisan command
$order->createNote("Auto-flagged by reconciliation job.", $systemUser);
Rely on the auth-resolved default only inside HTTP controllers where auth()->user() is guaranteed.
Never expose note creation without a policy. The package has no authorization, validation, or rate limiting. Wrap it:
// StoreNoteRequest
public function rules(): array
{
return ['content' => ['required', 'string', 'max:5000']];
}
// NoteController@store
$this->authorize('createNote', $order);
$order->createNote($request->validated('content'), $request->user());
Eager-load to avoid N+1 in list views. A table of 50 orders each rendering a note count and latest note is the classic trap:
$orders = Order::withCount('notes')
->with(['notes' => fn ($q) => $q->latest()->limit(1)])
->paginate(25);
Custom note model when you outgrow the defaults. Extend the shipped model, add casts, scopes, or soft deletes, then swap it in config:
namespace App\Models;
use Arcanedev\LaravelNotes\Models\Note as BaseNote;
class Note extends BaseNote
{
protected $casts = ['content' => 'string', 'created_at' => 'immutable_datetime'];
public function scopeInternal($query)
{
return $query->whereNull('author_id');
}
}
// config/notes.php
'note' => App\Models\Note::class,
Use it for human notes, not audit trails. These are deliberate annotations typed by a person ("refund approved by finance"), not automatic change tracking. Pair it with spatie/laravel-activitylog or owen-it/laravel-auditing for field-level history rather than trying to make this package do both.
Render as a timeline component. A small Blade/Livewire partial that takes any HasNotes model and renders notes()->with('author')->latest()->paginate(20) works across every annotatable model — build it once.
Testing. Assertions are plain database/relationship assertions; there is no testing helper to learn:
$order->createNote('looks good', $user);
$this->assertDatabaseHas('notes', [
'notable_id' => $order->id,
'notable_type' => $order->getMorphClass(),
'content' => 'looks good',
]);
$this->assertCount(1, $order->fresh()->notes);
Create notes through the trait in factories rather than instantiating Note directly, so the polymorphic keys stay consistent.
App\User namespace. On Laravel 8+ this silently resolves to a non-existent class and author relations break. Fix config/notes.php immediately after publishing.Relation::enforceMorphMap(), existing notes.notable_type values are stored as full class names and will no longer resolve. Decide on a morph map before you write production notes, or write a data migration.Order leaves orphaned notes unless the published migration defines a foreign key with cascadeOnDelete (it generally cannot, because the relation is polymorphic). Add a deleting hook or scheduled cleanup: Note::whereDoesntHave('notable')->delete().{{ $note->content }}). If you need markdown rendering, pipe it through a sanitizer — never {!! !!} raw user input.Note::created() listeners are the cleanest extension point for notifications or search indexing.$model->notes is empty but rows exist, check the morph type first — it is almost always a namespace or morph-map mismatch. Note::first()->notable_type will tell you instantly.TEXT or VARCHAR in your published migration; enforce a max: rule in the form request either way so the UI truncation and DB limit agree.withCount('notes') per request over cached counters unless you have a clear invalidation story on note creation.How can I help you explore Laravel packages today?