../courses.php
CRM
CRM101
90
Building A Small CRM Workflow
Connect contacts, notes, tasks, and follow-ups into one simple CRM process.
With nine lectures' worth of features in one file — contacts, status, notes, tasks, search, reports — where would you actually start looking if a newly added contact showed up in the report count but not in the search results?
Start at the boundary between the two, not at the top of the file. The report count and the search results read from the exact same
$contacts array, so if one shows a contact and the other doesn't, the difference has to be in how each one filters that array, not in the data itself. Checking the search's specific condition — does it check the right field, does it use
!== false correctly — against that one contact directly is a far faster path to the actual bug than rereading all nine lectures' worth of code from the top.
Every lecture in this course added exactly one capability to the same CRM: reading contacts, adding them, tracking status, recording notes, tracking tasks, being honest about storage limits, searching, naming a real permissions gap, and reporting numbers. This lesson assembles all of it into one finished file.
Nothing in this finished version is unfamiliar.
file_get_contents,
json_decode and
json_encode,
$_POST and
$_GET,
array_filter and
count,
stripos — every one of these already appeared in an earlier lecture, doing the exact same job it does here.
Debugging a multi-feature PHP file works the same way it did for the multi-part JavaScript widget in an earlier course: check boundaries between features, not the whole file at once — does the data going into a feature match what that feature actually expects.
This lesson is also where this course is honest, one final time, about what it deliberately left out: real login and permissions enforcement, named directly in lecture 8, and safe simultaneous multi-user writing, named directly in lecture 6. Both are real, solved problems — they're solved by the larger Jit-CRM course, with a real database and real authentication, not by anything built here.
This lesson's code is the complete, working CRM. Reading it should feel like recognizing nine pieces you already understand individually, now sitting together in one file.
The finished simple CRM
<?php
$contactsFile = __DIR__ . '/contacts.json';
$backupFile = __DIR__ . '/contacts_backup.json';
copy($contactsFile, $backupFile);
$contacts = json_decode(file_get_contents($contactsFile), true);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['new_contact'])) {
$contacts[] = [
'name' => $_POST['name'],
'email' => $_POST['email'],
'status' => $_POST['status'],
'notes' => [],
'tasks' => []
];
}
if (isset($_POST['new_task'])) {
$index = $_POST['contact_index'];
$contacts[$index]['tasks'][] = [
'text' => $_POST['task_text'],
'done' => false
];
}
if (isset($_POST['complete_task'])) {
$index = $_POST['contact_index'];
$taskIndex = $_POST['task_index'];
$contacts[$index]['tasks'][$taskIndex]['done'] = true;
}
file_put_contents($contactsFile, json_encode($contacts));
}
$searchTerm = isset($_GET['search']) ? $_GET['search'] : '';
$results = array_filter($contacts, function ($c) use ($searchTerm) {
return $searchTerm === '' || stripos($c['name'], $searchTerm) !== false;
});
$totalLeads = count(array_filter($contacts, function ($c) {
return $c['status'] === 'lead';
}));
?>
<div>Total leads: <?php echo $totalLeads; ?></div>
<form method="GET">
<input name="search" value="<?php echo $searchTerm; ?>">
<button type="submit">Search</button>
</form>
<?php foreach ($results as $i => $contact) { ?>
<h3><?php echo $contact['name']; ?> (<?php echo $contact['status']; ?>)</h3>
<?php foreach ($contact['notes'] as $note) { ?>
<p><?php echo $note['date']; ?> — <?php echo $note['text']; ?></p>
<?php } ?>
<?php foreach ($contact['tasks'] as $task) { ?>
<p><?php echo $task['done'] ? '[done] ' : '[ ] '; ?><?php echo $task['text']; ?></p>
<?php } ?>
<?php } ?>
Backup, then read — lecture 6 — The very first lines copy the file before anything else happens, exactly the habit lecture 6 introduced, run unconditionally on every page load.
Three named POST actions — lectures 2, 4, and 5 — Adding a contact, adding a task, and completing a task each check their own
isset() condition, the same multi-action pattern lecture 5 introduced for telling several possible form submissions apart in one block.
Search and report — lectures 7 and 9 — Both reuse
array_filter() exactly as introduced in lecture 3, just with different conditions plugged in for different purposes.
Notes and tasks, nested per contact — lectures 4 and 5 — The two inner
foreach loops inside the outer contact loop are unchanged from how each lecture originally introduced them.
What's deliberately still missing — No login, no real permission check before any action, no protection against two simultaneous writes — exactly the two honest gaps named in lectures 6 and 8, still present here on purpose, not fixed by simply combining everything.
A finished feature here is composition, the same as it was for the JavaScript widget
Every individual piece of this file already existed in an earlier lecture — what's new is the order they're combined in and the fact that they now all depend on the same shared
$contacts array correctly.
Checking the boundary between two features beats rereading the whole file
A contact appearing in the report but not in search means the bug lives specifically in the search condition, not anywhere else in this file — narrowing to that one boundary is far faster than reading all nine lectures' logic from the top down.
This file's two named gaps aren't oversights — they're the honest reason the larger course exists
A version of this lesson that quietly pretended these two limits were solved would contradict everything lectures 6 and 8 specifically built toward — naming them again here, at the very end, is what makes the next, larger CRM course's reason for existing concrete rather than just implied.
Each function's narrow job is what let nine capabilities fit in one readable file
array_filter() only filters,
json_encode() only converts — none of the individual pieces had to grow more complicated as more lectures were added, they just got combined and reused.
This whole CRM is now a template, not just a finished example
The exact same shape — read JSON, handle a few named POST actions, filter for search and reports, loop and display — applies to a simple inventory tracker or a simple booking list, with mostly the specific field names being what would actually change.
Guessing at a cross-feature bug by rereading everything — avoid
// reads all 80 lines from top to bottom hoping to spot it
Checking the specific boundary directly — use this
var_dump($contacts[count($contacts) - 1]);
// then separately check whether THAT exact contact
// passes the search condition on its own
Paste your finished CRM file into ChatGPT and ask: "If a newly added contact appeared in one feature here but not another, which two or three specific lines would most quickly narrow down why?" It's good at suggesting the right boundary, though actually adding a test contact and checking it directly beats guessing from the explanation alone.
Assemble the complete CRM from this lesson's code, using your own
contacts.json with a few starting contacts already in place.
Add a new contact, add a task to it, mark that task done, and search for it — confirming every feature from earlier lectures still works together correctly.
Deliberately break one piece — remove the
!== false from the search condition — and use the boundary-checking approach from this lesson to find it.
Write down, from memory, which lecture in this course each piece of the finished file actually came from.
Explain, out loud or in writing, exactly what would need to be added for this CRM to be genuinely safe for more than one person to use, naming both gaps from lectures 6 and 8 specifically.
Rereading an entire multi-feature file top to bottom looking for a bug, instead of checking the specific boundary between the two features that disagree.
Assuming combining nine working lectures' worth of code automatically produces something safe for multiple simultaneous users, without addressing the two gaps named honestly in lectures 6 and 8.
Treating this finished file as something to memorize line by line, rather than as a structure — read, branch by action, filter, display — that will reappear in a different shape in the next real project.
Believing a small, simple CRM like this one is somehow less "real" than a large one, instead of recognizing it as a genuinely complete, working system within its own honestly stated limits.
Forgetting which earlier lecture a specific piece of this finished file came from, making it harder to revisit and understand that piece more deeply if needed later.
Read a multi-feature PHP and JSON file and trace exactly how data moves from request to storage to display, debug it by checking the boundary between two disagreeing features, and state plainly what this simple CRM can and cannot safely be used for.
Login - Jit4All
Login
Welcome back to Jit4All