Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions migrations/Version20260513120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20260513120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Rebuild logging table to match the redesigned Log entity (auto-id PK, nullable user FK, datetime, entity reference, JSON changes, IP).';
}

public function up(Schema $schema): void
{
$this->addSql('DROP TABLE IF EXISTS logging');

$this->addSql('CREATE TABLE logging (
id INT AUTO_INCREMENT NOT NULL,
user_id INT DEFAULT NULL,
username VARCHAR(180) DEFAULT NULL,
date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
entity_class VARCHAR(255) NOT NULL,
entity_id VARCHAR(64) DEFAULT NULL,
action VARCHAR(16) NOT NULL,
changes JSON DEFAULT NULL,
ip_address VARCHAR(45) DEFAULT NULL,
INDEX idx_logging_date (date),
INDEX idx_logging_entity (entity_class, entity_id),
INDEX idx_logging_user (user_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');

$this->addSql('ALTER TABLE logging
ADD CONSTRAINT FK_logging_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE logging DROP FOREIGN KEY FK_logging_user');
$this->addSql('DROP TABLE IF EXISTS logging');

$this->addSql('CREATE TABLE logging (
user_id INT NOT NULL,
date TIME NOT NULL,
action VARCHAR(255) NOT NULL,
PRIMARY KEY(user_id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}

public function isTransactional(): bool
{
return false;
}
}
64 changes: 64 additions & 0 deletions src/Command/PurgeLogsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace App\Command;

use App\Repository\LogRepository;
use App\Repository\WorkflowLogRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
name: 'app:purge-logs',
description: 'Delete audit and workflow log entries older than a given number of days (run via daily cron).',
aliases: ['workflow:purge-logs'],
)]
class PurgeLogsCommand extends Command
{
public function __construct(
private readonly LogRepository $logRepository,
private readonly WorkflowLogRepository $workflowLogRepository,
) {
parent::__construct();
}

protected function configure(): void
{
$this->addOption('days', null, InputOption::VALUE_REQUIRED, 'Delete audit and workflow log entries older than this many days.', 90);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

if ('workflow:purge-logs' === $input->getFirstArgument()) {
$io->warning('The "workflow:purge-logs" command is deprecated and will be removed in a future release. Use "app:purge-logs" instead.');
}

$days = filter_var($input->getOption('days'), FILTER_VALIDATE_INT);

if (false === $days || $days <= 0) {
$io->error('--days must be a positive integer.');

return Command::FAILURE;
}

$before = new \DateTimeImmutable('-'.$days.' days');
$audit = $this->logRepository->purgeOlderThan($before);
$workflow = $this->workflowLogRepository->purgeOlderThan($before);

$io->success(sprintf(
'Deleted %d audit log and %d workflow log entries older than %d days.',
$audit,
$workflow,
$days,
));

return Command::SUCCESS;
}
}
50 changes: 0 additions & 50 deletions src/Command/PurgeWorkflowLogsCommand.php

This file was deleted.

12 changes: 12 additions & 0 deletions src/Entity/Enum/LogAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace App\Entity\Enum;

enum LogAction: string
{
case CREATE = 'create';
case UPDATE = 'update';
case DELETE = 'delete';
}
167 changes: 113 additions & 54 deletions src/Entity/Log.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,89 +4,148 @@

namespace App\Entity;

use App\Entity\Enum\LogAction;
use App\Repository\LogRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Entity(repositoryClass: LogRepository::class)]
#[ORM\Table(name: 'logging')]
#[ORM\Index(name: 'idx_logging_date', columns: ['date'])]
#[ORM\Index(name: 'idx_logging_entity', columns: ['entity_class', 'entity_id'])]
#[ORM\Index(name: 'idx_logging_user', columns: ['user_id'])]
class Log
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $user_id;
#[ORM\Column(type: 'time')]
private $date;
#[ORM\Column(type: 'string', length: 255)]
private $action;

/**
* Set user_id.
*
* @param int $userId
*
* @return Log
*/
public function setUserId($userId)
{
$this->user_id = $userId;
private ?int $id = null;

#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
private ?User $user = null;

#[ORM\Column(type: 'string', length: 180, nullable: true)]
private ?string $username = null;

#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $date;

#[ORM\Column(name: 'entity_class', type: 'string', length: 255)]
private string $entityClass;

#[ORM\Column(name: 'entity_id', type: 'string', length: 64, nullable: true)]
private ?string $entityId = null;

#[ORM\Column(type: 'string', length: 16, enumType: LogAction::class)]
private LogAction $action;

/** @var array<string, mixed>|null */
#[ORM\Column(type: 'json', nullable: true)]
private ?array $changes = null;

#[ORM\Column(name: 'ip_address', type: 'string', length: 45, nullable: true)]
private ?string $ipAddress = null;

public function getId(): ?int
{
return $this->id;
}

public function getUser(): ?User
{
return $this->user;
}

public function setUser(?User $user): self
{
$this->user = $user;

return $this;
}

/**
* Get user_id.
*
* @return int
*/
public function getUserId()
public function getUsername(): ?string
{
return $this->user_id;
return $this->username;
}

/**
* Set date.
*
* @param \DateTime $date
*
* @return Log
*/
public function setDate($date)
public function setUsername(?string $username): self
{
$this->date = $date;
$this->username = $username;

return $this;
}

/**
* Get date.
*
* @return \DateTime
*/
public function getDate()
public function getDate(): \DateTimeImmutable
{
return $this->date;
}

/**
* Set action.
*
* @param string $action
*
* @return Log
*/
public function setAction($action)
public function setDate(\DateTimeImmutable $date): self
{
$this->action = $action;
$this->date = $date;

return $this;
}

public function getEntityClass(): string
{
return $this->entityClass;
}

public function setEntityClass(string $entityClass): self
{
$this->entityClass = $entityClass;

return $this;
}

/**
* Get action.
*
* @return string
*/
public function getAction()
public function getEntityId(): ?string
{
return $this->entityId;
}

public function setEntityId(?string $entityId): self
{
$this->entityId = $entityId;

return $this;
}

public function getAction(): LogAction
{
return $this->action;
}

public function setAction(LogAction $action): self
{
$this->action = $action;

return $this;
}

/** @return array<string, mixed>|null */
public function getChanges(): ?array
{
return $this->changes;
}

/** @param array<string, mixed>|null $changes */
public function setChanges(?array $changes): self
{
$this->changes = $changes;

return $this;
}

public function getIpAddress(): ?string
{
return $this->ipAddress;
}

public function setIpAddress(?string $ipAddress): self
{
$this->ipAddress = $ipAddress;

return $this;
}
}
Loading