escolalms/bookmarks_notes
Laravel package that lets users bookmark content and attach personal notes to it. Ships the models and backend logic for creating bookmarks and notes, making it easy to add save-for-later and annotation features to any Laravel application.
composer require escolalms/bookmarks-notes
Verify the exact Composer name in the package's
composer.jsonbefore requiring — EscolaLMS repos are often named with dashes (Bookmarks-Notes) while the Composer package uses underscores or vice versa. Also confirm the PHP/Laravel constraint matches your app; the last tagged release is Feb 2024, so a Laravel 11/12 app may need--ignore-platform-reqsor a fork.
Then:
php artisan vendor:publish --provider="EscolaLms\BookmarksNotes\Providers\BookmarksNotesServiceProvider"
php artisan migrate
php artisan route:list --path=bookmark
Read these first, in order:
config/bookmarks_notes.php — table names, route prefix, middleware/guard, and (usually) the list of models that are allowed to be bookmarked/annotated.routes/ (or the route registration inside the service provider) — this is the real API contract; EscolaLMS packages register under /api/v1/... with the auth / Sanctum guard by default.src/Models + database/migrations — confirms whether it's polymorphic (bookmarkable_type/bookmarkable_id) or a fixed FK, and whether notes carry a free-text content or structured metadata.src/Contracts and src/Repositories — EscolaLMS binds interfaces in the provider; this is your extension seam.First use case: a learner marks a course topic as a bookmark and attaches a private note. Exercise it end-to-end with php artisan tinker (resolve the repository contract, create for User::first()) before wiring the HTTP layer, then hit POST /api/v1/bookmarks with a Sanctum token. If that round-trips and GET returns only the caller's rows, you understand the package.
Register a morph map before creating any data. If the package is polymorphic (it almost certainly is), stored *_type values are raw FQCNs by default. Add to AppServiceProvider::boot():
Relation::enforceMorphMap([
'course' => \EscolaLms\Courses\Models\Course::class,
'topic' => \EscolaLms\TopicTypeGift\Models\TopicGift::class,
]);
Do this before the first bookmark exists, or write a data migration to rewrite existing rows. Skipping it means refactoring a model namespace later silently orphans every bookmark.
Resolve through contracts, don't new models. EscolaLMS packages bind Contracts\BookmarkContract / Contracts\NoteContract to repository implementations in the service provider. Inject those in your own controllers/listeners so you can decorate them:
public function __construct(private BookmarkContract $bookmarks) {}
public function index(Request $request)
{
return BookmarkResource::collection(
$this->bookmarks->forUser($request->user(), (int) $request->input('per_page', 15))
);
}
Wrap creation in a domain service, not the controller. Bookmarking triggers events (see below), possibly notifications, and needs idempotency. A thin BookmarkService in your app that calls the contract + dispatches your own side effects keeps you decoupled from the package's HTTP layer, which you may want to bypass entirely and expose your own endpoints.
Hook the events for anything cross-cutting. Look for src/Events (e.g. BookmarkCreated, NoteCreated, NoteUpdated). Register listeners in your EventServiceProvider (or bootstrap/app.php on L11) to feed activity logs, analytics, or the EscolaLMS notifications package. Do not add logic by editing the package — listen.
Admin vs. user surface. EscolaLMS convention: user endpoints scope to auth()->user(), admin endpoints behind spatie/laravel-permission with GuardEnum::GUARD_API. If admin routes return 403 for your admin token, the permission seeder wasn't run — php artisan db:seed --class=BookmarksNotesPermissionSeeder (check database/seeders). Grant permissions to the admin role explicitly.
Pair with the content packages. The natural composition is Courses or TopicType* + this package: render a "Save" / "Add note" affordance per topic, then merge the bookmark state into the course tree response so the frontend doesn't need a second round-trip. Cache the per-user bookmark set keyed by user_id if you render it on every topic in a long course, and bust it on the *Created/*Deleted events.
Frontend consumption: stick to the package's own response shapes (JsonResource collections) and pagination params (per_page, page) — they match the rest of the EscolaLMS API and the official JS SDK. Don't reshape to your own envelope; you lose Swagger parity.
firstOrCreate semantics and catch QueryException on the composite key; if there's no unique index on (user_id, *_type, *_id), add one in your own migration.deleting observer on annotatable models that purges via the contract, or a scheduled notes:prune command.show/update/destroy on notes check ownership rather than just resolving by ID. In packages with little adoption this is the most likely real bug — write your own feature test asserting a 403 for cross-user access and keep it in your suite.auth:api. Check before assuming a custom guard works; if it doesn't, override the route registration by disabling the package's routes (config flag or provider subclass) and re-declaring them in your routes/api.php.@OA\. If the routes don't appear in your generated docs, the path isn't in config/l5-swagger.php annotations.paths — add vendor/escolalms/bookmarks-notes/src.vendor/bin/phpunit --filter BookmarksNotes), and be prepared to fork into app/ if a Laravel major bump breaks it. The migrations and models are small enough that inlining them is a realistic fallback — that's often cheaper than fighting a stale dependency.php artisan route:list --path=bookmark → config('bookmarks_notes') → provider in bootstrap/providers.php → migration ran (Schema::hasTable) → morph map → permission seeder. Nine times out of ten a "not working" report lands on step 3 or 5.note_meta table keyed to the note ID, or add a nullable json column via your own migration and cast it in a subclass. Keeps upstream diffs mergeable if a release ever lands.How can I help you explore Laravel packages today?