- Full Name: Phan Thị Thùy Nhung
- Student ID: 23020006
MY TODO is a web-based task management system developed as a course project.
The application helps users organize their tasks efficiently by categories, priorities, and due dates.
The system provides a simple and user-friendly interface while maintaining essential task management features commonly used in real-world applications.
- User authentication (login system – planned for future extension)
- Create, update, and delete tasks
- Categorize tasks (system and custom categories)
- Set task priorities and due dates
- Filter tasks by category and priority
- Warning section for overdue and due-today tasks
- HTML5
- CSS3
- Vanilla JavaScript
- PHP (Laravel Framework)
- MySQL
- Git & GitHub
- Composer
- PHP (>= 8.0)
- Composer
- MySQL
git clone https://github.com/pttnhung/my-todo.git
cd my-todo
composer installcp .env.example .env
php artisan key:generate
php artisan migratephp artisan serveAccess the application at:
http://127.0.0.1:8000
This project implements several optimizations to reduce database load and improve response time.
Cache store configuration (.env):
CACHE_STORE=databaseCreate cache table (only once):
php artisan cache:table
php artisan migrateWhy caching?
GET /api/tasksandGET /api/categoriesare frequently requested by the frontend.- Without caching, the app would query the database repeatedly for the same data.
- With caching, the app returns cached results until they expire or data changes.
Because CACHE_STORE=database does not support cache tags, the project uses a version-based invalidation approach:
-
Each user has a cache version key:
tasks:v:{userId}categories:v:{userId}
-
Cached list keys include the current version:
tasks:user:{userId}:v:{version}:qs:{hash}categories:user:{userId}:v:{version}
-
When data changes (create/update/delete), the version increments → old cached lists become unused automatically.
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rule;
private function catVersionKey(int $userId): string
{
return "categories:v:$userId";
}
private function bumpCatVersion(int $userId): void
{
Cache::increment($this->catVersionKey($userId));
}
public function index(Request $request)
{
$userId = $request->user()->id;
$v = Cache::get($this->catVersionKey($userId), 1);
$cacheKey = "categories:user:$userId:v:$v";
$data = Cache::remember($cacheKey, now()->addMinutes(10), function () use ($userId) {
return Category::query()
->where(function ($q) use ($userId) {
$q->where('user_id', $userId)
->orWhere(function ($q2) {
$q2->where('user_id', 1)->where('is_system', 1);
});
})
->orderBy('is_system', 'desc')
->orderBy('sort_order')
->orderBy('created_at')
->get();
});
return response()
->json($data)
->header('Cache-Control', 'private, max-age=600');
}
// After create/update/delete:
$this->bumpCatVersion($userId);use Illuminate\Support\Facades\Cache;
private function taskVersionKey(int $userId): string
{
return "tasks:v:$userId";
}
private function bumpTaskVersion(int $userId): void
{
Cache::increment($this->taskVersionKey($userId));
}
public function index(Request $request)
{
$userId = $request->user()->id;
$qs = http_build_query($request->query());
$qsHash = md5($qs);
$v = Cache::get($this->taskVersionKey($userId), 1);
$cacheKey = "tasks:user:$userId:v:$v:qs:$qsHash";
$data = Cache::remember($cacheKey, now()->addSeconds(60), function () use ($userId) {
return Task::query()
->with('category') // prevents N+1 queries
->where('user_id', $userId)
->orderBy('due')
->orderBy('created_at', 'desc')
->get();
});
return response()
->json($data)
->header('Cache-Control', 'private, max-age=60');
}
// After create/update/delete:
$this->bumpTaskVersion($userId);To avoid querying category once per task (N+1), tasks are loaded with related category using:
Task::query()->with('category')->where('user_id', $userId)->get();This reduces unnecessary database queries and improves scalability.
Each user cannot create duplicate category names. The project enforces this rule via validation:
$data = $request->validate([
'name' => [
'required', 'string', 'max:100',
Rule::unique('categories', 'name')->where(fn ($q) => $q->where('user_id', $userId)),
],
]);To reduce database bottlenecks and speed up common filters/sorting, the project adds composite indexes on frequently queried columns.
Migration example:
Schema::table('tasks', function (Blueprint $table) {
$table->index(['user_id', 'created_at']);
$table->index(['user_id', 'due']);
$table->index(['user_id', 'status']);
$table->index(['user_id', 'category_id']);
});
Schema::table('categories', function (Blueprint $table) {
$table->index(['user_id', 'sort_order']);
$table->index(['user_id', 'name']);
$table->index(['user_id', 'is_system']);
});Verify indexes in MySQL:
SHOW INDEX FROM tasks;
SHOW INDEX FROM categories;- Default system categories are predefined and cannot be deleted.
- User authentication (login/register) is included in the project scope but may not be fully implemented yet.
- All API endpoints are served under
/api/*. - This project is developed for educational purposes as part of a university course.
This project is created for academic use only.





