laravel-todo maintained by charleslightjarvis
Laravel Todo
Attach todo lists to any Eloquent model in your Laravel application.
The Problem
In a Laravel application, multiple entities (User, Project, Team, Invoice...) often need their own todo list. Without a package, you have to:
- Create a separate table for each entity type (
user_todos,project_todos,team_todos...) - Duplicate models, scopes, and relationships
- Maintain the same logic across multiple places
- No unified API to interact with todos
The Solution
laravel-todo lets you attach todos to any Eloquent model with a single trait. One table, one logic, every model.
- ✅ Attach todos to
User,Project,Team,Invoice— anything - ✅ Fluent API via Trait and Facade
- ✅ Built-in scopes:
pending,completed,overdue,highPriority,dueToday - ✅ Polymorphic creator support (tracks who created the todo)
- ✅ Zero duplication
Features
- ✅ Polymorphic relationship – attach todos to any model (
User,Project,Team, etc.) - ✅ Fluent API via Facade or Trait
- ✅ Built-in scopes:
pending(),completed(),overdue(),highPriority(),dueToday() - ✅ Status management:
pending,in_progress,completed,cancelled - ✅ Priority levels:
low,medium,high - ✅ Tracks who created each todo (polymorphic
creatorrelation) - ✅ Zero UI – backend only, integrate however you want
Requirements
- PHP 8.2 or higher
- Laravel 11.0 or higher
Installation
Install the package via Composer:
composer require charleslightjarvis/laravel-todo
Publish the migration file:
php artisan vendor:publish --tag="todo-migrations"
Run the migrations:
php artisan migrate
Publish the configuration file (optional):
php artisan vendor:publish --tag="todo-config"
Configuration
The config file config/todo.php allows you to customize:
return [
'prune_after_days' => 30,
'models' => [
'todo' => CharlesLightjarvis\Todo\Models\Todo::class,
],
'todo_morph_key' => 'todoable_id',
];
Usage
1. Add the trait to your model
Add HasTodos to any Eloquent model you want to attach todos to:
use CharlesLightjarvis\Todo\Traits\HasTodos;
class User extends Model
{
use HasTodos;
}
class Project extends Model
{
use HasTodos;
}
2. Creating todos via the Trait
Use the todos() relation directly on any model that uses HasTodos:
$user = User::find(1);
// Create a todo on the model
$todo = $user->todos()->create([
'title' => 'Buy groceries',
'priority' => 'high',
'due_at' => now()->addDays(2),
]);
// Create with addTodo() — optionally assign a creator
$todo = $user->addTodo([
'title' => 'Finish the report',
'priority' => 'medium',
], $creator);
3. Querying todos via the Trait
All built-in scopes are available directly on the relation:
$user->todos()->pending()->get();
$user->todos()->inProgress()->get();
$user->todos()->completed()->get();
$user->todos()->cancelled()->get();
$user->todos()->highPriority()->get();
$user->todos()->overdue()->get();
$user->todos()->dueToday()->get();
// Scopes can be chained
$user->todos()->pending()->highPriority()->get();
4. Creating and querying todos via the Facade
The Todo facade provides a model-agnostic API — useful when you do not have a direct reference to the owning model instance:
use CharlesLightjarvis\Todo\Facades\Todo;
// Create a todo for any model
$todo = Todo::createFor($user, [
'title' => 'Fix navigation bug',
'priority' => 'high',
'due_at' => now()->addWeek(),
]);
// Scope queries to a specific model
Todo::for($user)->pending()->get();
Todo::for($user)->highPriority()->get();
Todo::for($user)->overdue()->get();
// Count
Todo::for($user)->count();
Todo::for($user)->pending()->count();
Todo::for($user)->completed()->count();
5. Completing and cancelling todos
Both methods are ownership-aware — they silently return false if the todo does not belong to the model:
// Complete a todo
$user->completeTodo($todo);
// After completion
$todo->refresh();
$todo->status->value; // 'completed'
$todo->completed_at; // Carbon timestamp
// Cancel a todo
$user->cancelTodo($todo);
$todo->refresh();
$todo->status->value; // 'cancelled'
// Another user cannot complete a todo they don't own
$otherUser->completeTodo($todo); // returns false, status unchanged
6. Accessing todo relations
// All todos attached to the model
$user->todos;
// All todos created by the model (via the creator relation)
$user->createdTodos;
7. Tracking who created a todo
// Via addTodo — pass the creator as the second argument
$todo = $project->addTodo(['title' => 'Review PR'], auth()->user());
// Or set creator fields manually
$todo = $project->todos()->create([
'title' => 'Review PR',
'creator_type' => $user->getMorphClass(),
'creator_id' => $user->id,
]);
// Resolve the creator
$todo->creator; // returns the creator model
Available Scopes
| Scope | Description |
|---|---|
pending() |
Status = pending |
inProgress() |
Status = in_progress |
completed() |
Status = completed |
cancelled() |
Status = cancelled |
overdue() |
Not completed + due_at in the past |
highPriority() |
Priority = high |
dueToday() |
due_at is today |
Enums
The package provides two enums for type safety:
use CharlesLightjarvis\Todo\Enums\TodoStatusEnum;
use CharlesLightjarvis\Todo\Enums\TodoPriorityEnum;
// Status values
TodoStatusEnum::PENDING->value; // 'pending'
TodoStatusEnum::IN_PROGRESS->value; // 'in_progress'
TodoStatusEnum::COMPLETED->value; // 'completed'
TodoStatusEnum::CANCELLED->value; // 'cancelled'
// Priority values
TodoPriorityEnum::LOW->value; // 'low'
TodoPriorityEnum::MEDIUM->value; // 'medium'
TodoPriorityEnum::HIGH->value; // 'high'
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Contributions are welcome! Please see CONTRIBUTING for details.
Security
If you discover any security-related issues, please email charlestagne55@gmail.com instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.