Learning a debugger is frustrating because the work is invisible. You’re stepping through execution paths you can’t see, inspecting variables whose values may change, and setting breakpoints on lines that may or may not get hit. The feedback loop between action and understanding is abstract. Students set a breakpoint, step through some code, and still don’t know if they’re any closer to finding the problem.
When I built my Xdebug course for CraftQuest, I wanted to solve this. What if debugging progress wasn’t invisible? What if fixing a bug in code made something happen on screen that was immediate, satisfying, impossible to ignore?
So I built a plugin called Buggy that spawns animated bugs in the Craft CMS Control Panel. These visual bugs represent actual software bugs in the code that students need to find and fix using Xdebug. Fix the defect, and the bugs disappear.

When you first install Buggy, the page reloads and bugs start appearing one by one, crawling across your control panel until the screen is swarming with them. It’s unsettling in a way that’s hard to ignore. They keep moving, and there are enough of them to feel like an infestation. You want them gone.
So you follow the video course. You set a breakpoint, step through the code, and find the defect in the getSwarm() method. You fix it, reload the page, and watch the swarm of bugs shrink. Not gone, but noticeably smaller. That’s your feedback: visible proof that what you just did actually worked.
Two more bugs remain in the plugin code. You follow the course, debug each one using step debugging with Xdebug, and another wave of insects disappear from the screen. After the third fix, you reload the control panel and it’s clean. No bugs, no congratulations message. Just the control panel interface, the way it should be.

A bug-free control panel means you’ve finished the exercises in the course. No quiz score, no congratulations message, no “you passed” modal. Just the clean interface you earned, and the skills to find the next bug in your own code.
The Implementation
If you want to build something similar, the key challenge is running two versions of the same functionality: one that ties bug counts to actual code defects, and one that allows manual control after students have fixed everything.
I solved this with a helper class that acts as a service dispatcher:
public function getService(): BugService|BuggyService
{
if (Buggy::$plugin->getSettings()->automaticBugSpawning) {
return Buggy::$plugin->buggyService;
}
return Buggy::$plugin->bugService;
}
Controllers and template variables call this method without knowing which mode is active. When automatic mode is enabled, BugggyService runs internal checks against the codebase and returns a bug count based on how many defects remain. When automatic mode is off, BugService lets instructors spawn bugs manually for demonstrations or additional practice.
The pattern is simple, but it keeps the teaching logic cleanly separated from the rest of the plugin.


