DEV Community

Cover image for Interactive debugging with Symfony Console
Woody Gilk
Woody Gilk

Posted on

Interactive debugging with Symfony Console

One of the most important systems we have at RoundingWell is the Evaluation Engine, or "evaluations" as we generally refer to it. This system is responsible for processing customer-specific configured event listeners, which allows us to provide a highly unique user experience for each organization. Needless to say, this system is incredibly important to our application and being able to know exactly what it did, or will do, for a given event is critical.

Today I was working on transitioning one of our customers from some legacy events to newer events and needed to be sure that one of the steps was processing the event correctly. We have a lot of logging around the evaluations, and I knew the information I needed was in the logs. But the problem with having a lot of logging is that, well, there are a lot of logs. Plus, I only needed to run the first part of each trigger to verify that the migration worked.

I thought to myself, "Wouldn't it be clever if we could have an Xdebug-like step debugger for the evaluation engine? I know that Symfony Console has all the functionality I need for this..."

And that was exactly what I did. Because our application is fully dependency injected, I was able to create a new class to wrap the StepFactory, which is responsible for reading the configuration and creating the "steps" that make up the evaluation.

use RoundingWell\Framework\DebugVar;
use RuntimeException;
use Symfony\Component\Console\Style\SymfonyStyle;

readonly class StepFactoryWithConsoleConfirmation implements StepFactory
{
    public function __construct(
        private StepFactory $stepFactory,
        private SymfonyStyle $style,
        private bool $confirmCreatedStep = true,
        private bool $showCreatedStep = true,
        private bool $showStepDefinition = false,
    ) {
    }

    public function create(object $subject, StepDefinition $definition): Step
    {
        if ($this->showStepDefinition) {
            $debug = new DebugVar($definition->parameters);

            $this->style->info(
                message: <<<TEXT
                Next step is $definition->name with parameters:

                $debug
                TEXT,
            );
        }

        $step = $this->stepFactory->create($subject, $definition);

        if ($this->showCreatedStep) {
            $debug = new DebugVar($step);

            $this->style->info(
                message: <<<TEXT
                Step $definition->name created as:

                $debug
                TEXT,
            );
        }

        if ($this->confirmCreatedStep && ! $this->style->confirm(question: "Continue with evaluation?")) {
            throw new RuntimeException(
                message: "Evaluation aborted at step {$definition->name}",
            );
        }

        return $step;
    }
}
Enter fullscreen mode Exit fullscreen mode

And, with a little bit of container manipulation in my console command, we have an interactive evaluation debugger:

use DI\Container;
use RoundingWell\Common\Command\CommandBus;
use RoundingWell\Common\Command\CommandBusComposed;
use RoundingWell\Common\Command\Middleware\LoggingMiddleware;
use RoundingWell\Evaluation\Command\EvaluateEvent;
use RoundingWell\Evaluation\Command\EvaluateEventHandler;
use RoundingWell\Evaluation\Step\StepFactory;
use RoundingWell\Evaluation\Step\StepFactoryWithConsoleConfirmation;
use Symfony\Component\Console\Style\SymfonyStyle;

readonly class EvaluationDebug
{
    public function __construct(
        private Container $container,
    ) {
    }

    public function __invoke(
        SymfonyStyle $symfonyStyle,
        string $eventType,
        string $eventId,
        string|null $evaluationId = null,
    ): void {
        // The command bus MUST ONLY log executed commands.
        $commandBus = new CommandBusComposed(
            $this->container->get(LoggingMiddleware::class),
        );

        // The step factory MUST be wrapped to step through the evaluation.
        $stepFactory = new StepFactoryWithConsoleConfirmation(
            stepFactory: $this->container->get(StepFactory::class),
            style: $symfonyStyle,
        );

        $this->container->set(CommandBus::class, $commandBus);
        $this->container->set(StepFactory::class, $stepFactory);

        $command = new EvaluateEvent(
            eventClass: $eventType,
            eventId: $eventId,
            evaluationId: $evaluationId,
        );

        $this->container->get(EvaluateEventHandler::class)->handle($command);

        $symfonyStyle->success('Evaluation complete');
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it for today!

Top comments (2)

Collapse
 
altesack profile image
Marat Latypov

Hi! Fine approach!
But want to ask, why do you use readonly class, if you have no way overwrite class properties?
You know, it's not bad, actually. It just looks like overusing for me.
Or do you have such code conventions in your app?
I'm just curious

Collapse
 
woodygilk profile image
Woody Gilk

It is part of our code style, to always use readonly unless a class is intended to be modified. And very few classes are.