Skip to content

Instantly share code, notes, and snippets.

@francislavoie
Last active April 28, 2024 08:10
Show Gist options
  • Save francislavoie/3bbb0711ceddaea5651d288212daeded to your computer and use it in GitHub Desktop.
Save francislavoie/3bbb0711ceddaea5651d288212daeded to your computer and use it in GitHub Desktop.
A Symfony OutputInterface decorator that inserts timestamps on every line

A Symfony OutputInterface decorator that inserts timestamps on every line

I'm kinda proud of this, but it took me way too long to come up with the right solution 😅 I also couldn't find anyone on the internet who wrote something like this, so I'm sharing it in case others find it useful.

This can be used to wrap an $output in any CLI command, it will intercept all the newlines being printed out and insert the current time at the start of every line.

Easier said than done, because if we just naively insert dates after each newline, then the timestamp of a line would be the time from the previous message, not the current line being printed. So to correct that, we need to keep track of whether the previous write inserted a final newline, and if so prepend a date; and we need to skip adding a date on the last newline of that write.

Usage:

In a Symfony command:

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $io = new SymfonyStyle($input, new TimestampOutput($output));
        $io->section("This is a header, which will have dates at the start of each line");

        return self::SUCCESS;
    }

Example output:

[2024-03-26 02:53:10] 
[2024-03-26 02:53:10] This is a header, which will have dates at the start of each line
[2024-03-26 02:53:10] -----------------------------------------------------------------
[2024-03-26 02:53:10]

Implementation:

A file named TimestampOutput.php:

<?php

namespace Framework\Console\Output;

use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Traversable;
use const PHP_EOL;

class TimestampOutput implements OutputInterface
{
    private bool $lastWasNewline = true;

    public function __construct(
        private readonly OutputInterface $output,
        private readonly string          $format = '[Y-m-d H:i:s] ',
        private readonly string          $eol = PHP_EOL,
    ) {}

    public function write(iterable|string $messages, bool $newline = false, int $options = 0)
    {
        // Normalize the messages to an array
        if (!is_iterable($messages)) {
            $messages = [$messages];
        }
        if ($messages instanceof Traversable) {
            $messages = iterator_to_array($messages);
        }

        // Add newline to the last message
        if ($newline) {
            $messages[array_key_last($messages)] = $messages[array_key_last($messages)] . $this->eol;
        }

        // Write the timestamp if the last message ended with a newline (or is the first message)
        if ($this->lastWasNewline) {
            $this->output->write(date($this->format), false, $options);
        }

        // Check if the current message ends with a newline, so we can insert the newline
        // for the next message if we know it will start on a new line
        $this->lastWasNewline = str_ends_with($messages[array_key_last($messages)], $this->eol);

        // Add the timestamps after each newline in the message,
        // _except_ for the last newline, because we want to let
        // the next write to add its own with the correct time
        // if it will start on a new line.
        foreach ($messages as $i => $message) {
            // Count the number of newlines in the message
            $newlineCount = substr_count($message, $this->eol);

            // If this is the last message in this write, skip the last newline
            if ($i === count($messages) - 1) {
                $newlineCount--;
            }

            // If there are no newlines, skip this message
            if ($newlineCount < 1) {
                continue;
            }

            // Perform the replacement, with a count limit to skip the last newline
            $messages[$i] = preg_replace(
                "/{$this->eol}/",
                $this->eol . date($this->format),
                $message,
                $newlineCount
            );
        }

        // Finally, write the messages
        $this->output->write($messages, false, $options);
    }

    public function writeln(iterable|string $messages, int $options = 0)
    {
        $this->write($messages, true, $options);
    }

    public function setVerbosity(int $level)
    {
        $this->output->setVerbosity($level);
    }

    public function getVerbosity(): int
    {
        return $this->output->getVerbosity();
    }

    public function isQuiet(): bool
    {
        return $this->output->isQuiet();
    }

    public function isVerbose(): bool
    {
        return $this->output->isVerbose();
    }

    public function isVeryVerbose(): bool
    {
        return $this->output->isVeryVerbose();
    }

    public function isDebug(): bool
    {
        return $this->output->isDebug();
    }

    public function setDecorated(bool $decorated)
    {
        $this->output->setDecorated($decorated);
    }

    public function isDecorated(): bool
    {
        return $this->output->isDecorated();
    }

    public function setFormatter(OutputFormatterInterface $formatter)
    {
        return $this->output->setFormatter($formatter);
    }

    public function getFormatter(): OutputFormatterInterface
    {
        return $this->output->getFormatter();
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment