mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-14 14:24:24 +00:00
Serialization of tickets with history
This commit is contained in:
parent
670b8eb82b
commit
467bea7cde
@ -49,7 +49,6 @@ final readonly class ReplaceMotiveCommandHandler
|
||||
|
||||
if ($readyToAdd) {
|
||||
$history = new MotiveHistory($command->motive, $ticket, $this->clock->now());
|
||||
$ticket->addMotiveHistory($history);
|
||||
$this->entityManager->persist($history);
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ use Twig\Environment;
|
||||
class EditTicketController
|
||||
{
|
||||
public function __construct(
|
||||
private Environment $templating,
|
||||
private Environment $templating
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/ticket/ticket/{id}/edit', name: 'chill_ticket_ticket_edit')]
|
||||
@ -29,6 +29,9 @@ class EditTicketController
|
||||
return new Response(
|
||||
$this->templating->render(
|
||||
'@ChillTicket/Ticket/edit.html.twig',
|
||||
[
|
||||
'ticket' => $ticket,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -14,9 +14,11 @@ namespace Chill\TicketBundle\Entity;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'motives_history', schema: 'chill_ticket')]
|
||||
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_motive_history' => MotiveHistory::class])]
|
||||
class MotiveHistory implements TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
@ -24,21 +26,27 @@ class MotiveHistory implements TrackCreationInterface
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private ?\DateTimeImmutable $endDate = null;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: Motive::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private Motive $motive,
|
||||
#[ORM\ManyToOne(targetEntity: Ticket::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Ticket $ticket,
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private \DateTimeImmutable $startDate = new \DateTimeImmutable('now')
|
||||
) {}
|
||||
) {
|
||||
$ticket->addMotiveHistory($this);
|
||||
}
|
||||
|
||||
public function getEndDate(): ?\DateTimeImmutable
|
||||
{
|
||||
|
@ -16,9 +16,11 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'person_history', schema: 'chill_ticket')]
|
||||
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_person_history' => PersonHistory::class])]
|
||||
class PersonHistory implements TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
@ -26,22 +28,27 @@ class PersonHistory implements TrackCreationInterface
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private ?\DateTimeImmutable $endDate = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private ?User $removedBy = null;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: Person::class, fetch: 'EAGER')]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private Person $person,
|
||||
#[ORM\ManyToOne(targetEntity: Ticket::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Ticket $ticket,
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private \DateTimeImmutable $startDate,
|
||||
) {
|
||||
// keep ticket instance in sync with this
|
||||
|
@ -176,4 +176,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
return $this->motiveHistories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReadableCollection<int, PersonHistory>
|
||||
*/
|
||||
public function getPersonHistories(): ReadableCollection
|
||||
{
|
||||
return $this->personHistories;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,52 @@
|
||||
import {TranslatableString} from "../../../../ChillMainBundle/Resources/public/types";
|
||||
|
||||
export interface Ticket {
|
||||
id: number
|
||||
}
|
||||
import {DateTime, TranslatableString, User} from "../../../../ChillMainBundle/Resources/public/types";
|
||||
import {Person} from "../../../../ChillPersonBundle/Resources/public/types";
|
||||
|
||||
export interface Motive {
|
||||
type: "ticket_motive"
|
||||
id: number,
|
||||
active: boolean,
|
||||
label: TranslatableString
|
||||
}
|
||||
|
||||
interface TicketHistory<T extends string, D extends object> {
|
||||
event_type: T,
|
||||
at: DateTime,
|
||||
by: User,
|
||||
data: D
|
||||
}
|
||||
|
||||
interface PersonHistory {
|
||||
type: "ticket_person_history",
|
||||
id: number,
|
||||
startDate: DateTime,
|
||||
endDate: null|DateTime,
|
||||
person: Person,
|
||||
removedBy: null,
|
||||
createdBy: User|null,
|
||||
createdAt: DateTime|null
|
||||
}
|
||||
|
||||
interface MotiveHistory {
|
||||
type: "ticket_motive_history",
|
||||
id: number,
|
||||
startDate: null,
|
||||
endDate: null|DateTime,
|
||||
motive: Motive,
|
||||
createdBy: User|null,
|
||||
createdAt: DateTime|null,
|
||||
}
|
||||
|
||||
interface AddPersonEvent extends TicketHistory<"add_person", PersonHistory> {};
|
||||
interface SetMotiveEvent extends TicketHistory<"set_motive", MotiveHistory> {};
|
||||
|
||||
type TicketHistoryLine = AddPersonEvent | SetMotiveEvent;
|
||||
|
||||
export interface Ticket {
|
||||
type: "ticket_ticket"
|
||||
id: number
|
||||
externalRef: string
|
||||
currentPersons: Person[]
|
||||
currentMotive: null|Motive
|
||||
history: TicketHistoryLine[],
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,30 @@ import VueToast from 'vue-toast-notification';
|
||||
import 'vue-toast-notification/dist/theme-sugar.css';
|
||||
import {messages} from "./i18n/messages";
|
||||
import App from './App.vue';
|
||||
import {Ticket} from "../../types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
initialTicket: string
|
||||
}
|
||||
}
|
||||
|
||||
const i18n = _createI18n(messages)
|
||||
|
||||
// the initial ticket is serialized there:
|
||||
console.log(window.initialTicket);
|
||||
// to have js object
|
||||
const ticket = JSON.parse(window.initialTicket) as Ticket;
|
||||
console.log("the ticket for this app (at page loading)", ticket);
|
||||
|
||||
for (const eh of ticket.history) {
|
||||
if (eh.event_type === 'add_person') {
|
||||
console.log("add_person", eh.data.person);
|
||||
} else if (eh.event_type === 'set_motive') {
|
||||
console.log("set_motive", eh.data.motive);
|
||||
}
|
||||
}
|
||||
|
||||
const _app = createApp({
|
||||
template: '<app></app>',
|
||||
})
|
||||
|
@ -7,6 +7,9 @@
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
window.initialTicket = "{{ ticket|serialize('json', {'groups': 'read'})|escape('js') }}";
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_ticket_app') }}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\TicketBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\TicketBundle\Entity\MotiveHistory;
|
||||
use Chill\TicketBundle\Entity\PersonHistory;
|
||||
use Chill\TicketBundle\Entity\Ticket;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
final class TicketNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
public function normalize($object, ?string $format = null, array $context = [])
|
||||
{
|
||||
if (!$object instanceof Ticket) {
|
||||
throw new UnexpectedValueException();
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'ticket_ticket',
|
||||
'id' => $object->getId(),
|
||||
'externalRef' => $object->getExternalRef(),
|
||||
'currentPersons' => $this->normalizer->normalize($object->getPersons(), $format, [
|
||||
'groups' => 'read',
|
||||
]),
|
||||
'currentAddressees' => $this->normalizer->normalize($object->getCurrentAddressee(), $format, ['groups' => 'read']),
|
||||
'currentInputs' => $this->normalizer->normalize($object->getCurrentInputs(), $format, ['groups' => 'read']),
|
||||
'currentMotive' => $this->normalizer->normalize($object->getMotive(), $format, ['groups' => 'read']),
|
||||
'history' => array_values($this->serializeHistory($object, $format, ['groups' => 'read'])),
|
||||
];
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, ?string $format = null)
|
||||
{
|
||||
return 'json' === $format && $data instanceof Ticket;
|
||||
}
|
||||
|
||||
private function serializeHistory(Ticket $ticket, string $format, array $context): array
|
||||
{
|
||||
$events = [
|
||||
...array_map(
|
||||
fn (MotiveHistory $motiveHistory) => [
|
||||
'event_type' => 'set_motive',
|
||||
'at' => $motiveHistory->getStartDate(),
|
||||
'by' => $motiveHistory->getCreatedBy(),
|
||||
'data' => $motiveHistory,
|
||||
],
|
||||
$ticket->getMotiveHistories()->toArray()
|
||||
),
|
||||
...array_map(
|
||||
fn (PersonHistory $personHistory) => [
|
||||
'event_type' => 'add_person',
|
||||
'at' => $personHistory->getStartDate(),
|
||||
'by' => $personHistory->getCreatedBy(),
|
||||
'data' => $personHistory,
|
||||
],
|
||||
$ticket->getPersonHistories()->toArray(),
|
||||
),
|
||||
];
|
||||
|
||||
usort(
|
||||
$events,
|
||||
static function (array $a, array $b): int {
|
||||
return $a['at'] <=> $b['at'];
|
||||
}
|
||||
);
|
||||
|
||||
return array_map(
|
||||
fn ($data) => [
|
||||
'event_type' => $data['event_type'],
|
||||
'at' => $this->normalizer->normalize($data['at'], $format, $context),
|
||||
'by' => $this->normalizer->normalize($data['by'], $format, $context),
|
||||
'data' => $this->normalizer->normalize($data['data'], $format, $context),
|
||||
],
|
||||
$events
|
||||
);
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ services:
|
||||
Chill\TicketBundle\Action\Ticket\Handler\:
|
||||
resource: '../Action/Ticket/Handler/'
|
||||
|
||||
Chill\TicketBundle\Serializer\:
|
||||
resource: '../Serializer/'
|
||||
|
||||
Chill\TicketBundle\DataFixtures\:
|
||||
resource: '../DataFixtures/'
|
||||
|
@ -64,7 +64,7 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
|
||||
{
|
||||
$motive = new Motive();
|
||||
$ticket = new Ticket();
|
||||
$ticket->addMotiveHistory(new MotiveHistory(new Motive(), $ticket));
|
||||
$history = new MotiveHistory(new Motive(), $ticket);
|
||||
|
||||
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||
$entityManager->persist(Argument::that(static function ($arg) use ($motive): bool {
|
||||
@ -87,7 +87,7 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
|
||||
{
|
||||
$motive = new Motive();
|
||||
$ticket = new Ticket();
|
||||
$ticket->addMotiveHistory(new MotiveHistory($motive, $ticket));
|
||||
$history = new MotiveHistory($motive, $ticket);
|
||||
|
||||
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||
$entityManager->persist(Argument::that(static function ($arg) use ($motive): bool {
|
||||
|
@ -11,8 +11,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\TicketBundle\Tests\Entity;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\TicketBundle\Entity\Motive;
|
||||
use Chill\TicketBundle\Entity\MotiveHistory;
|
||||
use Chill\TicketBundle\Entity\PersonHistory;
|
||||
use Chill\TicketBundle\Entity\Ticket;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
@ -31,7 +33,6 @@ class TicketTest extends KernelTestCase
|
||||
self::assertNull($ticket->getMotive());
|
||||
|
||||
$history = new MotiveHistory($motive, $ticket);
|
||||
$ticket->addMotiveHistory($history);
|
||||
|
||||
self::assertSame($motive, $ticket->getMotive());
|
||||
self::assertCount(1, $ticket->getMotiveHistories());
|
||||
@ -39,9 +40,22 @@ class TicketTest extends KernelTestCase
|
||||
// replace motive
|
||||
$motive2 = new Motive();
|
||||
$history->setEndDate(new \DateTimeImmutable());
|
||||
$ticket->addMotiveHistory(new MotiveHistory($motive2, $ticket));
|
||||
$history2 = new MotiveHistory($motive2, $ticket);
|
||||
|
||||
self::assertCount(2, $ticket->getMotiveHistories());
|
||||
self::assertSame($motive2, $ticket->getMotive());
|
||||
}
|
||||
|
||||
public function testGetPerson(): void
|
||||
{
|
||||
$ticket = new Ticket();
|
||||
$person = new Person();
|
||||
|
||||
self::assertEquals([], $ticket->getPersons());
|
||||
|
||||
$history = new PersonHistory($person, $ticket, new \DateTimeImmutable('now'));
|
||||
|
||||
self::assertCount(1, $ticket->getPersons());
|
||||
self::assertSame($person, $ticket->getPersons()[0]);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\TicketBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\TicketBundle\Entity\Motive;
|
||||
use Chill\TicketBundle\Entity\MotiveHistory;
|
||||
use Chill\TicketBundle\Entity\PersonHistory;
|
||||
use Chill\TicketBundle\Entity\Ticket;
|
||||
use Chill\TicketBundle\Serializer\Normalizer\TicketNormalizer;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class TicketNormalizerTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @dataProvider provideTickets
|
||||
*/
|
||||
public function testNormalize(Ticket $ticket, array $expected): void
|
||||
{
|
||||
$actual = $this->buildNormalizer()->normalize($ticket, 'json', ['groups' => 'read']);
|
||||
|
||||
self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
|
||||
|
||||
foreach (array_keys($expected) as $k) {
|
||||
if ('history' === $k) {
|
||||
continue;
|
||||
}
|
||||
self::assertEqualsCanonicalizing($expected[$k], $actual[$k], sprintf("assert the content of the '%s' key", $k));
|
||||
}
|
||||
|
||||
self::assertArrayHasKey('history', $actual);
|
||||
self::assertIsArray($actual['history']);
|
||||
|
||||
foreach ($actual['history'] as $k => $eventType) {
|
||||
self::assertEquals($expected['history'][$k]['event_type'], $eventType['event_type']);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildNormalizer(): TicketNormalizer
|
||||
{
|
||||
$normalizer = $this->prophesize(NormalizerInterface::class);
|
||||
|
||||
// empty array
|
||||
$normalizer->normalize(
|
||||
Argument::that(fn ($arg) => is_array($arg) && 0 === count($arg)),
|
||||
'json',
|
||||
Argument::type('array')
|
||||
)->willReturn([]);
|
||||
|
||||
// array of mixed objects
|
||||
$normalizer->normalize(
|
||||
Argument::that(fn ($arg) => is_array($arg) && 0 < count($arg) && is_object($arg[0])),
|
||||
'json',
|
||||
Argument::type('array')
|
||||
)->will(function ($args) {
|
||||
return array_fill(0, count($args[0]), 'embedded');
|
||||
});
|
||||
|
||||
// array of event type
|
||||
$normalizer->normalize(
|
||||
Argument::that(fn ($arg) => is_array($arg) && 0 < count($arg) && is_array($arg[0]) && array_key_exists('event_type', $arg[0])),
|
||||
'json',
|
||||
Argument::type('array')
|
||||
)->will(function ($args): array {
|
||||
$events = [];
|
||||
|
||||
foreach ($args[0] as $event) {
|
||||
$events[] = $event['event_type'];
|
||||
}
|
||||
|
||||
return $events;
|
||||
});
|
||||
|
||||
// datetime
|
||||
$normalizer->normalize(Argument::type(\DateTimeImmutable::class), 'json', Argument::type('array'))
|
||||
->will(function ($args) { return $args[0]->getTimestamp(); });
|
||||
$normalizer->normalize(Argument::type(Motive::class), 'json', Argument::type('array'))->willReturn(['type' => 'motive', 'id' => 0]);
|
||||
$normalizer->normalize(Argument::type(PersonHistory::class), 'json', Argument::type('array'))
|
||||
->willReturn(['personHistory']);
|
||||
$normalizer->normalize(Argument::type(MotiveHistory::class), 'json', Argument::type('array'))
|
||||
->willReturn(['motiveHistory']);
|
||||
$normalizer->normalize(null, 'json', Argument::type('array'))->willReturn(null);
|
||||
|
||||
$ticketNormalizer = new TicketNormalizer();
|
||||
$ticketNormalizer->setNormalizer($normalizer->reveal());
|
||||
|
||||
return $ticketNormalizer;
|
||||
}
|
||||
|
||||
public static function provideTickets(): iterable
|
||||
{
|
||||
yield [
|
||||
new Ticket(),
|
||||
[
|
||||
'type' => 'ticket_ticket',
|
||||
'id' => null,
|
||||
'externalRef' => '',
|
||||
'currentPersons' => [],
|
||||
'currentAddressees' => [],
|
||||
'currentInputs' => [],
|
||||
'currentMotive' => null,
|
||||
'history' => [],
|
||||
],
|
||||
];
|
||||
|
||||
$ticket = new Ticket();
|
||||
$ticket->setExternalRef('2134');
|
||||
$personHistory = new PersonHistory(new Person(), $ticket, new \DateTimeImmutable('2024-04-01T12:00:00'));
|
||||
$ticketHistory = new MotiveHistory(new Motive(), $ticket, new \DateTimeImmutable('2024-04-01T12:02:00'));
|
||||
|
||||
yield [
|
||||
$ticket,
|
||||
[
|
||||
'type' => 'ticket_ticket',
|
||||
'id' => null,
|
||||
'externalRef' => '2134',
|
||||
'currentPersons' => ['embedded'],
|
||||
'currentAddressees' => [],
|
||||
'currentInputs' => [],
|
||||
'currentMotive' => ['type' => 'motive', 'id' => 0],
|
||||
'history' => [['event_type' => 'add_person'], ['event_type' => 'set_motive']],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user