<?php
//----------------------------------------------------------------------
// src/Logging/Tools.php
//----------------------------------------------------------------------
namespace App\AppRemix\Logging;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Security\Core\Security;
use App\Entity\Access;
use App\Entity\APIRest\AccessAPI;
use App\AppRemix\Entity\Client;
use App\AppRemix\Entity\Devis;
use App\AppRemix\Entity\Invoice;
use App\AppRemix\Entity\Log;
use App\AppRemix\Entity\Mission;
use App\AppRemix\Entity\Payment;
use App\AppRemix\Entity\Society;
use App\Services\Common\TextTools;
use App\Services\LogTools;
class Tools
{
public function __construct(ManagerRegistry $doctrine, TranslatorInterface $translator, Security $security,
LogTools $logTools, TextTools $textTools)
{
$this->em = $doctrine->getManager();
$this->translator = $translator;
$this->security = $security;
$this->logTools = $logTools;
$this->textTools = $textTools;
}
// No Persist, No Flush
// Returns the $log object
public function smartLog($args)
{
// Get the arguments
if (array_key_exists("action", $args)) $action = $args["action"];
else $action = null;
// Action is mandatory, all the rest isn't
if ($action === null)
return null;
if (array_key_exists("object_id", $args)) $objectId = $args["object_id"];
else $objectId = null;
if (array_key_exists("object_entity", $args)) $objectEntity = $args["object_entity"];
else $objectEntity = null;
if (array_key_exists("object_display", $args)) $objectDisplay = $this->textTools->cleanTextWithoutHtmlentities($args["object_display"]);
else $objectDisplay = null;
if (array_key_exists("mission_id", $args)) $missionId = $args["mission_id"];
else $missionId = null;
if (array_key_exists("mission_display", $args)) $missionDisplay = $this->textTools->cleanTextWithoutHtmlentities($args["mission_display"]);
else $missionDisplay = null;
if (array_key_exists("client_id", $args)) $clientId = $args["client_id"];
else $clientId = null;
if (array_key_exists("client_display", $args)) $clientDisplay = $this->textTools->cleanTextWithoutHtmlentities($args["client_display"]);
else $clientDisplay = null;
if (array_key_exists("old_value", $args)) $oldValue = $this->textTools->cleanTextWithoutHtmlentities($args["old_value"]);
else $oldValue = null;
if (array_key_exists("new_value", $args)) $newValue = $this->textTools->cleanTextWithoutHtmlentities($args["new_value"]);
else $newValue = null;
if (array_key_exists("info", $args)) $info = $args["info"];
else $info = null;
if (array_key_exists("society", $args)) $society = $args["society"];
else $society = null;
// Handle AccessApi
if (array_key_exists("access_api", $args))
{
$accessApi = $args['access_api'];
}
else
{
$accessApi = $this->getCurrentUser();
}
// Handle logging for AccessClient users
// At this point $access holds a valid not null object
// However, this object can be either an Access or an AccessClient
if ($accessApi instanceof AccessAPI)
{
$this->logTools->ploopLog("[AppRemix][Logging][smartLog] accessApi is AccessAPI ".$accessApi->displayForLog());
}
else
{
$this->logTools->ploopLog("[AppRemix][Logging][smartLog] accessApi is an alien ".$accessApi->displayForLog());
}
$log = new Log();
$log->setAction($action);
$log->setObjectId($objectId);
$log->setObjectEntity($objectEntity);
$log->setObjectDisplay($objectDisplay);
$log->setMissionId($missionId);
$log->setMissionDisplay($missionDisplay);
$log->setClientId($clientId);
$log->setClientDisplay($clientDisplay);
$log->setOldValue($oldValue);
$log->setNewValue($newValue);
$log->setInfo($info);
if ($society !== null)
{
$log->setSocietyId($society->getId());
$log->setSocietyName($society->getName());
}
if ($accessApi !== null)
{
$log->setAccessApiId($accessApi->getId());
$log->setAccessApiUsername($accessApi->getUsername());
}
// Translations
if (!empty($log->getObjectEntity()))
{
$translation = $this->translator->trans($log->getObjectEntity(), array(), 'rekapp_remix');
$log->setObjectEntityTranslation($translation);
}
$translation = $this->translator->trans($log->getAction(), array(), 'rekapp_remix');
$log->setActionTranslation($translation);
return $log;
}
// Always return an object of type AccessAPI
public function getCurrentUser()
{
$emptyAccess = $this->em->getRepository(Access::class)->findOneByEmail(Access::EMPTY_ACCESS);
// Get the Access|AccessAPI|AccessClient object returned by the security
$access = $this->security->getUser();
if (empty($access))
{
// This occurs when the code is execuded by a Command (CRON Task for example)
// In this case there is no firewall, and thus no Access|AccessAPI
$access = $emptyAccess;
}
else
{
// The firewall has been bypassed => We have an user provider,
// either Access or AccessAPI
// or an AccessClient :)
if (!($access instanceof Access) && !($access instanceof AccessAPI) && !($access instanceof AccessClient))
{
// This should not happen, but hey, life is weird, who knows
// Even VSCode proved to be worse than Atom ;)
$access = $emptyAccess;
}
}
if ($access instanceof AccessAPI)
{
// Bingo
return $access;
}
// This sould not happen
return $emptyAccess;
}
public function handleAddressChanges($args, $before, $after)
{
$args["old_value"] = null;
$args["new_value"] = null;
if ($before === null && $after === null)
{
return $args;
}
if ($before !== null && $after !== null)
{
if ($before->differsInValue($after))
{
$args["old_value"] = $before->display();
$args["new_value"] = $after->display();
return $args;
}
}
else
{
// before or after is null, but not both
if ($before !== null) $args["old_value"] = $before->display();
if ($after !== null) $args["new_value"] = $after->display();
return $args;
}
return $args;
}
public function handleBasicChanges($args, $before, $after)
{
$args["old_value"] = $before;
$args["new_value"] = $after;
return $args;
}
/**
* Compares Doctrine JSON column values as plain strings and records the diff when they differ.
*
* @param array<string, mixed> $args
*
* @return array<string, mixed>
*/
public function handleJsonChanges($args, $before, $after)
{
$args["old_value"] = null;
$args["new_value"] = null;
if ($before === null && $after === null)
{
return $args;
}
$beforeString = $this->jsonValueToString($before);
$afterString = $this->jsonValueToString($after);
if ($before !== null && $after !== null)
{
if ($beforeString !== $afterString)
{
$args["old_value"] = $beforeString;
$args["new_value"] = $afterString;
return $args;
}
}
else
{
// before or after is null, but not both
if ($before !== null)
{
$args["old_value"] = $beforeString;
}
if ($after !== null)
{
$args["new_value"] = $afterString;
}
return $args;
}
return $args;
}
/**
* Encodes a JSON column value as a plain string for comparison and logging.
*/
private function jsonValueToString(mixed $value): ?string
{
if ($value === null)
{
return null;
}
$encoded = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($encoded === false)
{
return (string) $value;
}
return $encoded;
}
public function handleBasicBoolChanges($args, $before, $after)
{
if ($before && !$after)
{
// $args["old_value"] = null;
$args["old_value"] = $this->translator->trans("yes");
$args["new_value"] = $this->translator->trans("no");
}
if (!$before && $after)
{
// $args["old_value"] = null;
$args["old_value"] = $this->translator->trans("no");
$args["new_value"] = $this->translator->trans("yes");
}
return $args;
}
public function handleLabelChanges($args, $before, $after)
{
$args["old_value"] = null;
$args["new_value"] = null;
if ($before !== null) $args["old_value"] = $before->getValue();
if ($after !== null) $args["new_value"] = $after->getValue();
return $args;
}
/**
* Maps Payment status integer codes to translated labels (payment_status_* keys).
*
* @param array<string, mixed> $args
*
* @return array<string, mixed>
*/
public function handlePaymentStatusChanges($args, $before, $after)
{
$args["old_value"] = $this->translatePaymentStatus($before);
$args["new_value"] = $this->translatePaymentStatus($after);
return $args;
}
/**
* Resolves a Payment status code via Payment::STATUS_* constants and rekapp_remix translations.
*/
private function translatePaymentStatus(mixed $status): ?string
{
if ($status === null)
{
return null;
}
$status = (int) $status;
foreach ((new \ReflectionClass(Payment::class))->getConstants() as $name => $value)
{
if (strpos($name, 'STATUS_') !== 0 || $name === 'STATUS_LIST')
{
continue;
}
if ($value === $status)
{
$key = 'payment_'.$this->logTools->camelToSnakeCase($name);
return $this->translator->trans($key, [], 'rekapp_remix');
}
}
return (string) $status;
}
/**
* Maps Payment type integer codes to translated labels (payment_type_* keys).
*
* @param array<string, mixed> $args
*
* @return array<string, mixed>
*/
public function handlePaymentTypeChanges($args, $before, $after)
{
$args["old_value"] = $this->translatePaymentType($before);
$args["new_value"] = $this->translatePaymentType($after);
return $args;
}
/**
* Resolves a Payment type code via Payment::TYPE_* constants and rekapp_remix translations.
*/
private function translatePaymentType(mixed $type): ?string
{
if ($type === null)
{
return null;
}
$type = (int) $type;
foreach ((new \ReflectionClass(Payment::class))->getConstants() as $name => $value)
{
if (strpos($name, 'TYPE_') !== 0 || $name === 'TYPE_LIST')
{
continue;
}
if ($value === $type)
{
$key = 'payment_'.$this->logTools->camelToSnakeCase($name);
return $this->translator->trans($key, [], 'rekapp_remix');
}
}
return (string) $type;
}
/**
* Maps Devis financial state keys to translated labels (devis_financial_state_* keys).
*
* The Devis::FINANCIAL_STATE_* values are already snake case, so the translation key is the
* value prefixed — a seventh state needs a translation entry and no code change.
*
* @param array<string, mixed> $args
*
* @return array<string, mixed>
*/
public function handleFinancialStateChanges($args, $before, $after)
{
$args["old_value"] = $this->translateFinancialState($before);
$args["new_value"] = $this->translateFinancialState($after);
return $args;
}
/**
* Resolves a Devis financial state key through the rekapp_remix translations.
*/
private function translateFinancialState(?string $state): ?string
{
if ($state === null)
{
return null;
}
return $this->translator->trans('devis_financial_state_'.$state, [], 'rekapp_remix');
}
public function handleObjectChanges($args, $before, $after)
{
$args["old_value"] = null;
$args["new_value"] = null;
if ($before !== null)
{
if (method_exists($before, "getLogLabel"))
{
$args["old_value"] = $before->getLogLabel();
}
else
{
$args["old_value"] = $before->display();
}
}
if ($after !== null)
{
if (method_exists($after, "getLogLabel"))
{
$args["new_value"] = $after->getLogLabel();
}
else
{
$args["new_value"] = $after->display();
}
}
return $args;
}
public function handleDateChanges($args, $before, $after, $displayHours = false)
{
$testFormat = "Ymd";
if ($displayHours) $testFormat = "YmdHi";
$displayFormat = "d/m/Y";
if ($displayHours) $displayFormat = "d/m/Y H:i";
$args["old_value"] = null;
$args["new_value"] = null;
if ($before === null && $after === null)
{
return $args;
}
if ($before !== null && $after !== null)
{
if ($before->format($testFormat) != $after->format($testFormat))
{
$args["old_value"] = $before->format($displayFormat);
$args["new_value"] = $after->format($displayFormat);
return $args;
}
}
else
{
// before or after is null, but not both
if ($before !== null)
{
$args["old_value"] = $before->format($displayFormat);
}
if ($after !== null)
{
$args["new_value"] = $after->format($displayFormat);
}
return $args;
}
return null;
}
public function handleNewSociety(Log $log)
{
if ($log->getObjectEntity() == "Society" && $log->getAction() == "society_add")
{
$society = $this->em->getRepository(Society::class)->findOneById($log->getObjectId());
if ($society !== null)
{
$accessApi = $society->getAccessApi();
if ($accessApi !== null)
{
$log->setAccessApiId($accessApi->getId());
$log->setAccessApiUsername($accessApi->getUsername());
}
}
}
}
public function handleMission(Log $log)
{
if ($log->getObjectEntity() == "Mission")
{
$mission = $this->em->getRepository(Mission::class)->findOneById($log->getObjectId());
if ($mission !== null)
{
$log->setMissionId($mission->getId());
$log->setMissionDisplay($mission->display());
}
}
}
/**
* Fills a log line's client columns once the client has an id. A client created inline
* by devis/add or invoice/add is persisted in the same flush as its document, so neither
* log line can know the client id when its args are built at prePersist.
*
* Only fills empty columns: a value set by initArgs() is never overwritten.
*/
public function handleClient(Log $log, object $object): void
{
if ($log->getClientId() !== null)
{
return;
}
$client = match (true)
{
$object instanceof Client => $object,
$object instanceof Devis, $object instanceof Invoice, $object instanceof Payment => $object->getClient(),
default => null,
};
if ($client === null || $client->getId() === null)
{
return;
}
$log->setClientId($client->getId());
$log->setClientDisplay($this->textTools->cleanTextWithoutHtmlentities($client->displayForListing()));
}
}