feat: record disposition deleted event for statistics
This commit is contained in:
@@ -17,6 +17,7 @@ enum StatisticsEventName: string
|
||||
case ASSIGNMENT_JOB_PROFILE_CHANGED = 'assignment.job_profile_changed';
|
||||
case ASSIGNMENT_CALLED_OFF = 'assignment.called_off';
|
||||
case DISPOSITION_CALLED_OFF = 'disposition.called_off';
|
||||
case DISPOSITION_DELETED = 'disposition.deleted';
|
||||
case APPLICATION_CREATED = 'application.created';
|
||||
|
||||
public function label(): string
|
||||
@@ -25,6 +26,7 @@ enum StatisticsEventName: string
|
||||
self::ASSIGNMENT_JOB_PROFILE_CHANGED => 'Tätigkeitsprofil geändert',
|
||||
self::ASSIGNMENT_CALLED_OFF => 'Reise abgesagt',
|
||||
self::DISPOSITION_CALLED_OFF => 'Einsatz abgesagt',
|
||||
self::DISPOSITION_DELETED => 'Einteilung gelöscht',
|
||||
self::APPLICATION_CREATED => 'Bewerbung eingegangen',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,6 +75,14 @@ class StatisticsChangeSetListener implements ResetInterface
|
||||
foreach ($pending as $event) {
|
||||
$subject = $event->subject;
|
||||
|
||||
// Dimensions the collector froze at onFlush time - the subject is gone or
|
||||
// detached by now, so there is nothing to resolve them from.
|
||||
if (null !== $event->dimensions) {
|
||||
$this->recorder->record($event->name, $event->dimensions, $event->payload);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($subject instanceof Assignment) {
|
||||
$this->recorder->recordForAssignment($event->name, $subject, $event->payload);
|
||||
|
||||
|
||||
@@ -12,16 +12,23 @@ use App\Enum\StatisticsEventName;
|
||||
*
|
||||
* Collectors run during onFlush, where the changeset still exists, but nothing may be
|
||||
* written until the transaction has committed. This is what travels between the two.
|
||||
*
|
||||
* Normally the recorder resolves the dimension columns from $subject in postFlush. A
|
||||
* collector that watches deletions cannot rely on that - the entity is detached and its id
|
||||
* nulled by then - so it snapshots $dimensions now, and the listener writes them verbatim.
|
||||
*/
|
||||
final class CollectedStatisticsEvent
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<string, int|string|null>|null $dimensions pre-resolved at collect time; when
|
||||
* set, written as-is instead of from $subject
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly StatisticsEventName $name,
|
||||
public readonly Assignment|Disposition|Application $subject,
|
||||
public readonly array $payload = [],
|
||||
public readonly ?array $dimensions = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Statistics\Collector;
|
||||
|
||||
use App\Entity\Disposition;
|
||||
use App\Enum\StatisticsEventName;
|
||||
use App\Service\Statistics\CollectedStatisticsEvent;
|
||||
use App\Service\Statistics\StatisticsDimensions;
|
||||
use App\Service\Statistics\StatisticsFlush;
|
||||
|
||||
/**
|
||||
* A disposition being hard deleted by an admin.
|
||||
*
|
||||
* Its own concern: a deletion shares nothing with the call-off metrics, which have to be
|
||||
* told apart together. Read from the flush rather than from the delete controller, so any
|
||||
* path that removes a disposition is counted, and because a hard delete leaves nothing
|
||||
* behind - this row is the only record the placement, its teamer, and the admin's
|
||||
* justification ever existed.
|
||||
*
|
||||
* Dimensions are resolved here, during onFlush: by postFlush the entity is detached and its
|
||||
* id nulled, so StatisticsRecorder could not resolve them from the subject.
|
||||
*/
|
||||
class DispositionDeletedCollector implements StatisticsCollectorInterface
|
||||
{
|
||||
public function collect(StatisticsFlush $flush): iterable
|
||||
{
|
||||
foreach ($flush->deletionsOf(Disposition::class) as $disposition) {
|
||||
yield new CollectedStatisticsEvent(
|
||||
StatisticsEventName::DISPOSITION_DELETED,
|
||||
$disposition,
|
||||
[
|
||||
'previous_status' => $disposition->getStatus(),
|
||||
// The reason typed on the delete form, bound onto the entity before the
|
||||
// remove(). Nowhere else survives the deletion.
|
||||
'remarks' => $disposition->getRemarks(),
|
||||
// Non-null only if the placement had already been called off first.
|
||||
'called_off_by' => $disposition->getCalledOffBy(),
|
||||
],
|
||||
StatisticsDimensions::forDisposition($disposition),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Statistics;
|
||||
|
||||
use App\Entity\Application;
|
||||
use App\Entity\Assignment;
|
||||
use App\Entity\Disposition;
|
||||
|
||||
/**
|
||||
* The dimension columns of a statistics_event, resolved from a live entity.
|
||||
*
|
||||
* One place, because the SER-stripping hotel-code rule is already reimplemented in three
|
||||
* spots (Destination, DispositionRepository, FeedbackRepository) and must not gain a fourth.
|
||||
* StatisticsRecorder resolves dimensions here in postFlush for inserts and updates; a
|
||||
* deletion collector calls the same methods during onFlush, while the entity is still whole.
|
||||
*/
|
||||
final class StatisticsDimensions
|
||||
{
|
||||
/**
|
||||
* @return array<string, int|string|null>
|
||||
*/
|
||||
public static function forAssignment(?Assignment $assignment): array
|
||||
{
|
||||
$destination = $assignment?->getDestination();
|
||||
|
||||
return [
|
||||
'assignment_id' => $assignment?->getId(),
|
||||
'destination_id' => $destination?->getId(),
|
||||
'job_profile_id' => $assignment?->getJobProfile()?->getId(),
|
||||
'hotel_code' => $destination?->getHotelCodeNormalized(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int|string|null>
|
||||
*/
|
||||
public static function forDisposition(Disposition $disposition): array
|
||||
{
|
||||
$dimensions = self::forAssignment($disposition->getAssignment());
|
||||
$dimensions['disposition_id'] = $disposition->getId();
|
||||
$dimensions['teamer_id'] = $disposition->getTeamer()?->getId();
|
||||
|
||||
return $dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int|string|null>
|
||||
*/
|
||||
public static function forApplication(Application $application): array
|
||||
{
|
||||
$dimensions = self::forAssignment($application->getAssignment());
|
||||
$dimensions['application_id'] = $application->getId();
|
||||
$dimensions['teamer_id'] = $application->getTeamer()?->getId();
|
||||
|
||||
return $dimensions;
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,34 @@ final class StatisticsFlush
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function deletions(): array
|
||||
{
|
||||
return $this->unitOfWork->getScheduledEntityDeletions();
|
||||
}
|
||||
|
||||
/**
|
||||
* A scheduled-for-deletion entity still has its id and its initialised relations here,
|
||||
* during onFlush. It does not in postFlush: Doctrine removes it from the identity map
|
||||
* and nulls its generated id once the delete commits, so a collector that cares about a
|
||||
* deletion has to read everything it needs off the entity now, not later.
|
||||
*
|
||||
* @template T of object
|
||||
*
|
||||
* @param class-string<T> $class
|
||||
*
|
||||
* @return array<int, T>
|
||||
*/
|
||||
public function deletionsOf(string $class): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->deletions(),
|
||||
static fn (object $entity): bool => $entity instanceof $class,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
||||
@@ -85,7 +85,7 @@ class StatisticsRecorder
|
||||
array $payload = [],
|
||||
?\DateTimeImmutable $occurredAt = null,
|
||||
): void {
|
||||
$this->record($name, $this->dimensionsFromAssignment($assignment), $payload, $occurredAt);
|
||||
$this->record($name, StatisticsDimensions::forAssignment($assignment), $payload, $occurredAt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,11 +97,7 @@ class StatisticsRecorder
|
||||
array $payload = [],
|
||||
?\DateTimeImmutable $occurredAt = null,
|
||||
): void {
|
||||
$dimensions = $this->dimensionsFromAssignment($disposition->getAssignment());
|
||||
$dimensions['disposition_id'] = $disposition->getId();
|
||||
$dimensions['teamer_id'] = $disposition->getTeamer()?->getId();
|
||||
|
||||
$this->record($name, $dimensions, $payload, $occurredAt);
|
||||
$this->record($name, StatisticsDimensions::forDisposition($disposition), $payload, $occurredAt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,11 +109,7 @@ class StatisticsRecorder
|
||||
array $payload = [],
|
||||
?\DateTimeImmutable $occurredAt = null,
|
||||
): void {
|
||||
$dimensions = $this->dimensionsFromAssignment($application->getAssignment());
|
||||
$dimensions['application_id'] = $application->getId();
|
||||
$dimensions['teamer_id'] = $application->getTeamer()?->getId();
|
||||
|
||||
$this->record($name, $dimensions, $payload, $occurredAt);
|
||||
$this->record($name, StatisticsDimensions::forApplication($application), $payload, $occurredAt);
|
||||
}
|
||||
|
||||
public function resolveActorRole(): StatisticsActorRole
|
||||
@@ -136,19 +128,4 @@ class StatisticsRecorder
|
||||
|
||||
return StatisticsActorRole::SYSTEM;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int|string|null>
|
||||
*/
|
||||
private function dimensionsFromAssignment(?Assignment $assignment): array
|
||||
{
|
||||
$destination = $assignment?->getDestination();
|
||||
|
||||
return [
|
||||
'assignment_id' => $assignment?->getId(),
|
||||
'destination_id' => $destination?->getId(),
|
||||
'job_profile_id' => $assignment?->getJobProfile()?->getId(),
|
||||
'hotel_code' => $destination?->getHotelCodeNormalized(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user