fix: never persist a half-built user on first login

This commit is contained in:
Björn Fromme
2026-08-19 12:30:42 +02:00
parent 0c667d6b69
commit 2601e46ec4
4 changed files with 218 additions and 26 deletions
+25 -15
View File
@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Logger;
use App\Entity\LogEntry;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Types\Types;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\LogRecord;
@@ -13,30 +13,40 @@ use Monolog\LogRecord;
/**
* Monolog handler that persists log entries to the database.
*
* Writes log records to the LogEntry entity, replacing message placeholders
* Writes log records to the log_entry table, replacing message placeholders
* with context values. Includes channel, context, and extra data for auditing.
* Only processes INFO level and above to avoid storing debug messages.
*
* The write goes through the DBAL connection rather than the EntityManager on purpose. A log record
* can be emitted at any point, including halfway through somebody else's unit of work, and an
* EntityManager flush would write out whatever that unit of work has pending so far — which is how
* a half-built User once reached the database with a NULL password. Staying out of the UnitOfWork
* also keeps logging alive after a failed flush has closed the EntityManager, which is exactly when
* there is something worth logging.
*
* Reads still go through the LogEntry entity and its repository.
*/
class DatabaseHandler extends AbstractProcessingHandler
{
public function __construct(private readonly EntityManagerInterface $entityManager)
public function __construct(private readonly Connection $connection)
{
parent::__construct(Level::Info);
}
protected function write(LogRecord $record): void
{
$message = $this->replacePlaceHolder($record);
$logEntry = new LogEntry($record->channel, $message);
$logEntry
->setContext($record->context)
->setExtra($record->extra)
->setErrorCode($record->extra['error_code'] ?? null)
;
$this->entityManager->persist($logEntry);
$this->entityManager->flush();
$this->connection->insert('log_entry', [
'channel' => $record->channel,
'message' => $this->replacePlaceHolder($record),
'context' => $record->context,
'extra' => $record->extra,
'error_code' => $record->extra['error_code'] ?? null,
'created_at' => new \DateTimeImmutable('now'),
], [
'context' => Types::JSON,
'extra' => Types::JSON,
'created_at' => Types::DATETIME_IMMUTABLE,
]);
}
private function replacePlaceHolder(LogRecord $record): string