In the world of PHP development, CRUD (Create, Read, Update, Delete) applications are the “Hello World” of mastering a new framework. However, a real-world application requires more than just listing database rows. It needs robust searching, intelligent filtering, pagination, and secure state management.
Today, we are going to dissect a fully functional Todo application built using the Doppar Framework. We will walk through the codebase step-by-step, exploring how Doppar handles Modern PHP attributes, Route Model Binding, dynamic query building, and template rendering.
By the end of this guide, you will understand how to implement a feature-rich CRUD system with a clean, maintainable architecture.
Database Migration
Every great application starts with a solid database schema. In Doppar, we use migrations to define the structure of our tables. This ensures version control for our database.
We start by running the CLI command:
php pool make:model Todo --m
This creates both the Model and the Migration file. Let’s look at the migration logic:
use Phaseolies\Support\Facades\Schema;
use Phaseolies\Database\Migration\Blueprint;
public function up(): void
{
Schema::create('todo', function (Blueprint $table) {
$table->id();
$table->string('todo', 100);
$table->unsignedBigInteger('user_id');
$table->boolean('status')->default(false);
$table->timestamps();
});
}
Now run the migration
php pool migrate
The Model: Logic and Relationships
The Model in Doppar isn’t just a data container; it’s the logic layer that interacts with the database. Let’s examine App\Models\Todo.php
Mass Assignment Protection
protected $creatable = ["todo", "user_id", "status"];
Doppar uses the $creatable property to prevent Mass Assignment vulnerabilities. This acts as a whitelist. Only these fields can be filled via Todo::create([]) or $todo->update([]). This prevents malicious users from injecting fields like is_admin or id into the request.
Defining Relationships
The bindTo method explicitly connects the Todo to the User. This allows us to later fetch the creator of a Todo easily (e.g., $todo->user->name).
public function user()
{
$this->bindTo(User::class, 'id', 'user_id');
}
The Magic of doppar query binding: __byStatus
This is a standout feature in the provided code:
public function __byStatus(Builder $builder, ?bool $status)
{
return $builder->whereStatus($status);
}
This magic method acts as a Query Binding. When the controller calls $q->byStatus($val), the framework looks for __byStatus. This keeps our controller code clean. Instead of writing raw where clauses in the controller, we encapsulate that logic inside the Model.
Full Todo Model
<?php
namespace App\Models;
use Phaseolies\Database\Entity\Builder;
use Phaseolies\Database\Entity\Model;
class Todo extends Model
{
protected $creatable = ["todo", "user_id", "status"];
public function user()
{
$this->bindTo(User::class, 'id', 'user_id');
}
public function __byStatus(Builder $builder, ?bool $status)
{
return $builder->whereStatus($status);
}
}
The Controller
The TodoController is where the magic happens. It utilizes PHP 8 Attributes extensively, making the code declarative and easy to read.
create controller
php pool make:controller Todo/TodoController
And this is full version of controller
<?php
namespace App\Http\Controllers\Todo;
use Phaseolies\Utilities\Attributes\Route;
use Phaseolies\Utilities\Attributes\Mapper;
use Phaseolies\Http\Request;
use App\Models\Todo;
use App\Http\Controllers\Controller;
use Phaseolies\Utilities\Attributes\Model;
#[Mapper(prefix: 'todo', middleware: ['auth'])]
class TodoController extends Controller
{
#[Route(uri: '/', name: 'todo.index')]
public function index()
{
$todos = Todo::embed('user:name')
->if(
request()->status !== 'all' && request()->status !== null,
fn($q) => $q->byStatus(request()->status)
)
->search([
'todo',
'user.name'
], request()->search)
->newest('id')
->paginate(5);
return view('todo.index', compact('todos'));
}
#[Route(uri: 'create', name: 'todo.create')]
public function create()
{
return view('todo.create');
}
#[Route(uri: 'store', name: 'todo.store', methods: ['POST'])]
public function store(Request $request)
{
$request->sanitize([
'todo' => 'required|min:3|max:30'
]);
Todo::create([
'todo' => $request->todo,
'status' => (int) $request->status,
'user_id' => auth()->id()
]);
return redirect()->route('todo.index')->withSuccess('Todo created successfully');
}
#[Route(uri: 'edit/{todo}', name: 'todo.edit')]
public function edit(#[Model(exception: true)] Todo $todo)
{
return view('todo.edit', compact('todo'));
}
#[Route(uri: 'update/{todo}', name: 'todo.update', methods: ['PATCH'])]
public function update(#[Model(exception: true)] Todo $todo, Request $request)
{
$request->sanitize([
'todo' => 'required|min:3|max:30'
]);
$todo->update([
'todo' => $request->todo,
'status' => (int) $request->status
]);
return redirect()->route('todo.index')->withSuccess('Todo updated successfully');
}
#[Route(uri: 'delete/{todo}', name: 'todo.delete', methods: ['DELETE'])]
public function delete(#[Model(exception: true)] Todo $todo)
{
$todo->delete();
return redirect()->route('todo.index')->withSuccess('Todo deleted successfully');
}
}
Route Model Binding
Look at the update and delete methods:
public function delete(#[Model(exception: true)] Todo $todo)
Instead of accepting an $id and finding the Todo manually (e.g., $todo = Todo::find($id)), we use the #[Model] attribute. Doppar automatically finds the Todo based on the ID in the URL. If it’s not found, the exception: true flag triggers a 404 error page. This reduces boilerplate code significantly.
The Frontend: Odo & Bootstrap
Layout
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>#yield('title')</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Box Icon -->
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<meta name="csrf-token" content="[[ csrf_token() }}">
<style>
*,.sidebar-menu ul{padding:0}body{color:var(--text-primary);font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg-secondary);overflow-x:hidden}.sidebar-menu ul,.submenu{list-style:none}.card,.dropdown-menu,.search-box input:focus,.sidebar,.toggle-collapse,.top-nav{background:var(--bg-primary)}.sidebar,.toggle-collapse,.top-nav{background:var(--bg-secondary)}.toast,.top-nav{backdrop-filter:blur(10px)}:root{--bg-primary:#ffffff;--bg-secondary:#F0F4F9;--bg-tertiary:#e9ecef;--text-primary:#1a1d29;--text-secondary:#6c757d;--text-muted:#adb5bd;--border-color:#c5c5c5;--shadow:rgba(0, 0, 0, 0.08);--shadow-lg:rgba(0, 0, 0, 0.12);--primary:#6366f1;--primary-hover:#4f46e5;--primary-light:#eef2ff;--success:#10b981;--danger:#ef4444;--warning:#f59e0b;--info:#3b82f6;--sidebar-width:280px;--sidebar-collapsed:80px;--header-height:70px}*{margin:0;box-sizing:border-box}.sidebar{width:var(--sidebar-width);height:100vh;position:fixed;left:0;top:0;z-index:1100;display:flex;flex-direction:column}.search-box i{top:50%;transform:translateY(-50%)}.sidebar.collapsed{width:var(--sidebar-collapsed)}.sidebar-header{height:var(--header-height);padding:0 1.5rem;display:flex;align-items:center;justify-content:space-between;position:relative}.logo{transition:opacity .3s}.sidebar.collapsed .logo{opacity:0;pointer-events:none}.sidebar.collapsed .chevron,.sidebar.collapsed .menu-text,.sidebar.collapsed .sidebar-company-title,.sidebar.collapsed .sidebar-header::after,.submenu{display:none}.dropdown-menu,.toast{box-shadow:0 10px 40px var(--shadow-lg)}.dropdown-item:hover,.sidebar-menu li a:hover,.sidebar-menu li.active a,.theme-toggle:hover{color:var(--primary)}.toggle-collapse{position:relative;cursor:pointer;border:none;border-radius:8px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;transition:.3s;order:1}.sidebar-menu{flex:1;padding:1rem;overflow-y:auto}.sidebar-menu::-webkit-scrollbar{width:6px}.sidebar-menu::-webkit-scrollbar-track{background:0 0}.sidebar-menu::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:3px}.sidebar-menu li a{display:flex;align-items:center;padding:.475rem 1rem;color:var(--text-secondary);text-decoration:none;transition:.2s;font-weight:500;font-size:.95rem;position:relative;overflow:hidden}.sidebar-menu li a i{width:24px;margin-right:12px;font-size:1.1rem;transition:transform .2s}.sidebar.collapsed .sidebar-menu li a{justify-content:center;padding:.875rem}.sidebar.collapsed .sidebar-menu li a i{margin-right:0}.submenu{margin:.5rem 0 0 2.5rem;padding:.5rem 0;animation:.3s slideDown}@keyframes slideDown{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.sidebar-menu li.has-submenu.active>.submenu{display:block}.submenu li{margin-bottom:.25rem}.submenu li a{padding:.625rem 1rem;font-size:.875rem;margin-left:0}.chevron{margin-left:auto;font-size:.75rem;transition:transform .3s}.sidebar-menu li.has-submenu.active>a .chevron{transform:rotate(180deg)}.main-content{margin-left:var(--sidebar-width);transition:margin-left .3s cubic-bezier(.4, 0, .2, 1);min-height:100vh}.main-content.collapsed{margin-left:var(--sidebar-collapsed)}.top-nav{height:var(--header-height);position:sticky;top:0;z-index:1000}.profile-dropdown,.search-box input,.theme-toggle{background:var(--bg-secondary);color:var(--text-primary)}.nav-container{height:100%;align-items:center;display:flex;padding:0 2rem}.mobile-menu-btn,.theme-toggle{width:40px;height:40px;cursor:pointer}.nav-actions,.theme-toggle{align-items:center;display:flex}.nav-actions{gap:1rem;margin-left:auto}.dropdown-item,.profile-dropdown{transition:.2s;display:flex;gap:.75rem}.theme-toggle{border-radius:12px;border:none;transition:.2s;justify-content:center}.theme-toggle:hover{transform:scale(1.05)}.profile-dropdown{align-items:center;padding:.5rem 1rem;border-radius:12px;cursor:pointer;text-decoration:none}.profile-dropdown:hover{background:var(--primary-light);transform:translateY(-2px);box-shadow:0 4px 12px var(--shadow)}.profile-info{display:flex;flex-direction:column;align-items:flex-start}.profile-name{font-weight:600;font-size:.875rem;line-height:1.2}.profile-role{font-size:.75rem;color:var(--text-muted)}.dropdown-menu{border:1px solid var(--border-color);border-radius:12px;padding:.5rem;margin-top:.5rem}.dropdown-item{padding:.75rem 1rem;border-radius:8px;color:var(--text-primary);align-items:center}.dropdown-item i{width:20px}.content-wrapper{padding:2rem}.toast-body,.toast-header{padding:1rem 1.25rem}.toast{border:none;border-radius:12px}.toast-header{border-bottom:none;border-radius:12px 12px 0 0}@media (max-width:576px){.search-box{display:none}}.card{border:1px solid var(--border-color);border-radius:16px;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.08),0 0 1px rgba(0,0,0,.1);padding:10px}.btn-dark{background:var(--primary);border-color:var(--primary);border-radius:12px;padding:.625rem 1.5rem;font-weight:500;transition:.2s}.btn-dark:hover{background:var(--primary-hover);border-color:var(--primary-hover);transform:translateY(-2px);box-shadow:0 4px 12px rgba(99,102,241,.3)}.mobile-menu-btn{display:none;border-radius:12px;background:var(--bg-secondary);border:none;color:var(--text-primary)}@media (max-width:992px){.sidebar{transform:translateX(-100%)}.sidebar.show{transform:translateX(0)}.main-content{margin-left:0}.nav-container{padding:0 1rem}.profile-info{display:none}.mobile-menu-btn{display:flex;align-items:center;justify-content:center}}.sidebar-company-title{font-size:30px;padding:6px;font-weight:700;margin-left:0;order:2;flex:1;text-align:center}.sidebar-header::after{content:'';width:60px;order:3}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<div class="layout-menu-toggle toggle-collapse" id="toggle-collapse">
<i class="bx bx-menu bx-sm"></i>
</div>
<div class="sidebar-company-title">
doppar
</div>
</div>
<div class="sidebar-menu">
<ul>
<li class="[[ Request::is('/home') ? 'active' : '' ]]">
<a href="[[ route('dashboard') ]]">
<i class='bx bxs-dashboard'></i>
<span class="menu-text">Dashboard</span>
</a>
</li>
<li class="[[ Request::is('/todo') ? 'active' : '' ]]">
<a href="[[ route('todo.index') ]]">
<i class='bx bxs-dashboard'></i>
<span class="menu-text">Todo</span>
</a>
</li>
</ul>
</div>
</div>
<div class="main-content">
<nav class="top-nav">
<div class="nav-container">
<div class="nav-actions">
<button class="theme-toggle" id="themeToggle">
<i class='bx bx-moon'></i>
</button>
<div class="dropdown">
<a href="#" class="profile-dropdown dropdown-toggle" data-bs-toggle="dropdown">
<div class="profile-info">
<span class="profile-name">[[ Auth::user()?->name ]]</span>
</div>
</a>
<div class="dropdown-menu dropdown-menu-end">
<a href="[[ route('profile') ]]" class="dropdown-item">
<i class='bx bx-user-circle'></i> Profile
</a>
<div class="dropdown-divider"></div>
<form action="[[ route('logout') ]]" method="POST">
@csrf
<button type="submit" class="dropdown-item">
<i class='bx bx-log-out'></i> Logout
</button>
</form>
</div>
</div>
</div>
</div>
</nav>
<div class="content-wrapper">
#if (session()->has('success'))
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
<div class="toast show" role="alert">
<div class="toast-header bg-success text-white">
<strong class="me-auto">Success!</strong>
<small class="text-white">Just now</small>
<button type="button" class="btn-close btn-close-white"
data-bs-dismiss="toast"></button>
</div>
<div class="toast-body bg-light">
<div class="d-flex align-items-center">
<i class='bx bxs-badge-check text-success me-2 fs-4'></i>
<span>[[ session()->pull('success') ]]</span>
</div>
</div>
</div>
</div>
#endif
#if (session()->has('error'))
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
<div class="toast show" role="alert">
<div class="toast-header bg-danger text-white">
<strong class="me-auto">Error!</strong>
<small class="text-white">Just now</small>
<button type="button" class="btn-close btn-close-white"
data-bs-dismiss="toast"></button>
</div>
<div class="toast-body bg-light">
<div class="d-flex align-items-center">
<i class="bx bxs-error-circle text-danger me-2 fs-4"></i>
<span>[[ session()->pull('error') ]]</span>
</div>
</div>
</div>
</div>
#endif
#yield('content')
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Mobile Toggle
const mobileToggle = document.querySelector('.mobile-menu-btn');
const sidebar = document.querySelector('.sidebar');
if (mobileToggle) {
mobileToggle.addEventListener('click', function() {
sidebar.classList.toggle('show');
});
}
// Sidebar Collapse Toggle
const toggleCollapse = document.querySelector('.toggle-collapse');
toggleCollapse.addEventListener('click', function() {
sidebar.classList.toggle('collapsed');
document.querySelector('.main-content').classList.toggle('collapsed');
const icon = this.querySelector('i');
if (sidebar.classList.contains('collapsed')) {
icon.classList.replace('bx-chevron-left', 'bx-chevron-right');
} else {
icon.classList.replace('bx-chevron-right', 'bx-chevron-left');
}
});
// Submenu Toggle
document.querySelectorAll('.sidebar-menu li.has-submenu > a').forEach(function(menuLink) {
menuLink.addEventListener('click', function(e) {
if (sidebar.classList.contains('collapsed')) return;
e.preventDefault();
const parentLi = this.parentElement;
const wasActive = parentLi.classList.contains('active');
document.querySelectorAll('.sidebar-menu li.has-submenu').forEach(function(
item) {
if (item !== parentLi) {
item.classList.remove('active');
}
});
parentLi.classList.toggle('active', !wasActive);
});
});
// Theme Toggle
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
const icon = themeToggle.querySelector('i');
// Load saved theme
const savedTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
themeToggle.addEventListener('click', function() {
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
});
function updateThemeIcon(theme) {
if (theme === 'dark') {
icon.classList.replace('bx-moon', 'bx-sun');
const style = document.createElement('style');
style.id = 'dark-mode-filter';
style.textContent =
'html,iframe,img,video{ filter: invert(1) hue-rotate(180deg); opacity: 1; }';
document.head.appendChild(style);
} else {
icon.classList.replace('bx-sun', 'bx-moon');
const darkStyle = document.getElementById('dark-mode-filter');
if (darkStyle) {
darkStyle.remove();
}
}
}
// Apply filter on initial load if dark mode
if (savedTheme === 'dark') {
const style = document.createElement('style');
style.id = 'dark-mode-filter';
style.textContent = 'html,iframe,img,video{ filter: invert(1) hue-rotate(180deg); opacity: 1; }';
document.head.appendChild(style);
}
// Auto-hide toasts
setTimeout(function() {
const toasts = document.querySelectorAll('.toast');
toasts.forEach(toast => {
const bsToast = new bootstrap.Toast(toast);
bsToast.hide();
});
}, 5000);
});
</script>
</body>
</html>
index.odo.php
#extends('layouts.app')
#section('title')
Todos
#endsection
#section('content')
<h2 class="az-content-title">Todos</h2>
<div class="d-flex justify-content-between align-items-center mb-3">
<form action="#" method="GET" class="mr-3">
<div class="input-group">
<input type="text" name="search" class="form-control" placeholder="Search users..."
value="[[ request('search') ]]" style="height: 38px;">
<div class="input-group-append">
<select name="status" class="form-control">
<option value="all" selected>All</option>
<option value="1">Active</option>
<option value="0">Inactive</option>
</select>
</div>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">
<svg width="20" height="20" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.442 12.442a1 1 0 011.415 0l3.85 3.85a1 1 0 01-1.414 1.415l-3.85-3.85a1 1 0 010-1.415z"
clip-rule="evenodd" />
<path fill-rule="evenodd"
d="M8.5 14a5.5 5.5 0 100-11 5.5 5.5 0 000 11zM15 8.5a6.5 6.5 0 11-13 0 6.5 6.5 0 0113 0z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</form>
<div>
<a href="[[ route('todo.create') ]]" class="btn btn-outline-primary btn-rounded btn-block">
Create New Todo
</a>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive mt-3">
<table class="table table-striped mg-b-5">
<thead>
<tr>
<th>ID</th>
<th>Todo</th>
<th>Created By</th>
<th>Created At</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
#foreach ($todos['data'] ?? [] as $todo)
<tr>
<th scope="row">[[ $todo->id ]]</th>
<td>[[ $todo->todo ]]</td>
<td>[[ $todo->user?->name ]]</td>
<td>[[ \Carbon\Carbon::parse($todo->created_at)->diffForHumans() ]]</td>
<td>[[ $todo->status ]]</td>
<td>
<a href="[[ route('todo.edit', $todo->id) ]]">
<svg width="40" height="20" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg" transform="rotate(0 0 0)">
<path
d="M20.75 18.5V8.67794L19.25 10.1779V18.5C19.25 18.9142 18.9142 19.25 18.5 19.25H5.5C5.08579 19.25 4.75 18.9142 4.75 18.5V5.5C4.75 5.08579 5.08579 4.75 5.5 4.75H18.314L19.2936 3.77034C19.3842 3.67974 19.4806 3.59848 19.5816 3.52657C19.2607 3.35027 18.8921 3.25 18.5 3.25H5.5C4.25736 3.25 3.25 4.25736 3.25 5.5V18.5C3.25 19.7426 4.25736 20.75 5.5 20.75H18.5C19.7426 20.75 20.75 19.7426 20.75 18.5Z"
fill="#7ea30f" />
<path
d="M20.4838 6.51868C20.7767 6.22578 20.7767 5.75091 20.4838 5.45802C20.1909 5.16512 19.7161 5.16512 19.4232 5.45802L11.9298 12.9514L8.57686 9.59849C8.28396 9.3056 7.80909 9.3056 7.5162 9.5985C7.22331 9.89139 7.22331 10.3663 7.5162 10.6592L11.3995 14.5424C11.6924 14.8353 12.1672 14.8353 12.4601 14.5424L20.4838 6.51868Z"
fill="#7ea30f" />
</svg>
</a>
<form method="POST" action="[[ route('todo.delete', $todo->id) ]]" class="d-inline">
#csrf
#method('DELETE')
<button type="submit" class="btn btn-link p-0 border-0"
onclick="return confirm('Are you sure you want to delete this item?');">
<svg width="40" height="20" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M7.99902 4.25C7.99902 3.00736 9.00638 2 10.249 2H13.749C14.9917 2 15.999 3.00736 15.999 4.25V5H18.498C19.7407 5 20.748 6.00736 20.748 7.25C20.748 8.28958 20.043 9.16449 19.085 9.42267C18.8979 9.4731 18.7011 9.5 18.498 9.5H5.5C5.29694 9.5 5.10016 9.4731 4.91303 9.42267C3.95503 9.16449 3.25 8.28958 3.25 7.25C3.25 6.00736 4.25736 5 5.5 5H7.99902V4.25ZM14.499 5V4.25C14.499 3.83579 14.1632 3.5 13.749 3.5H10.249C9.83481 3.5 9.49902 3.83579 9.49902 4.25V5H14.499Z"
fill="#dc3545" />
<path
d="M4.97514 10.4578L5.54076 19.8848C5.61205 21.0729 6.59642 22 7.78672 22H16.2113C17.4016 22 18.386 21.0729 18.4573 19.8848L19.0229 10.4578C18.8521 10.4856 18.6767 10.5 18.498 10.5H5.5C5.32131 10.5 5.146 10.4856 4.97514 10.4578ZM10.774 13.4339L10.9982 17.9905C11.0185 18.4042 10.6996 18.7561 10.2859 18.7764C9.8722 18.7968 9.52032 18.4779 9.49997 18.0642L9.27581 13.5076C9.25546 13.0938 9.57434 12.742 9.98805 12.7216C10.4018 12.7013 10.7536 13.0201 10.774 13.4339ZM14.0101 12.7216C14.4238 12.742 14.7427 13.0938 14.7223 13.5076L14.4982 18.0642C14.4778 18.4779 14.1259 18.7968 13.7122 18.7764C13.2985 18.7561 12.9796 18.4042 13 17.9905L13.2241 13.4339C13.2445 13.0201 13.5964 12.7013 14.0101 12.7216Z"
fill="#dc3545" />
</svg>
</button>
</form>
</td>
</tr>
#endforeach
</tbody>
</table>
[[! paginator($todos)->linkWithJumps() !]]
</div>
</div>
</div>
#endsection
create.odo.php
#extends('layouts.app')
#section('title')
Create Todo
#endsection
#section('content')
<h2 class="az-content-title fw-bold">Create Todo</h2>
<div class="card mt-3">
<div class="card-body">
<form action="[[ route('todo.store') ]]" method="post">
#csrf
<div class="form-group">
<label>Todo:</label>
<input type="text" name="todo" class="form-control" required>
</div>
<div class="form-group mt-3">
<label></label>
<input type="checkbox" name="status" value="1"> Status
</div>
<div class="form-group">
<button class="btn btn-success btn-submit mt-3" type="submit">Submit</button>
</div>
</form>
</div>
</div>
#endsection
edit.odo.php
#extends('layouts.app')
#section('title')
Update Todo
#endsection
#section('content')
<h2 class="az-content-title fw-bold">Update Todo</h2>
<div class="card mt-3">
<div class="card-body">
<form action="[[ route('todo.update', $todo->id) ]]" method="post">
#csrf
#method('patch')
<div class="form-group">
<label>Todo:</label>
<input type="text" name="todo" class="form-control" value="[[ $todo->todo ]]">
</div>
<div class="form-group mt-3">
<label></label>
<input type="checkbox" name="status" value="1" [[ $todo->status === 1 ? 'checked' : '' ]]> Status
</div>
<div class="form-group">
<button class="btn btn-success btn-submit mt-3" type="submit">Submit</button>
</div>
</form>
</div>
</div>
#endsection
By following this pattern, you can scale this simple Todo app into a full-blown Project Management system. You have the primitives for searching, filtering, relating users to data, and managing CRUD operations securely.

Top comments (0)